OpenGL - Как получилось, что спрайты спрайтов имеют такую ​​высокую производительность

Мне интересно, как рисование простой геометрии с текстурами может съесть столько производительности (ниже 60 кадров в секунду)? Даже моя хорошая графическая карта (GTX 960) может «только» нарисовать до 1000 спрайтов плавно. Текстуры, которые я использую, имеют мощность 2 текстуры и не превышают размер 512x512. Я даже фильтрую только GL_NEAREST.
Сами спрайты случайные генерируются по размеру. Таким образом, нет 1000 полноэкранных квадроциклов, которые не были бы реальным использованием.

Я рисую мои спрайты, имея в виду, что у меня есть один динамический буфер вершин и статический буфер индекса. Я обновляю буфер вершин каждый кадр с помощью glBufferSubData один раз, а затем рисую все с помощью `` glDrawElements`. У меня есть около 5 различных текстур, которые я связываю один раз для каждого кадра, что приводит к 5 обратным вызовам. Для рендеринга я использую только один шейдер, который привязан после запуска приложения.
Таким образом, у меня есть 5 привязок текстур, 5 вызовов рисования и одно обновление буфера вершин для каждого кадра, что на самом деле не так уж и много.

Вот пример с одной текстурой:

val shaderProgram = ShaderProgram("assets/default.vert", "assets/default.frag")
val texture = Texture("assets/logo.png")
val sprite = BufferSprite(texture)
val batch = BufferSpriteBatch()

val projView = Matrix4f().identity().ortho2D(0f, 640f, 0f, 480f)

fun setup() {
    glEnable(GL_TEXTURE)
    //glColorMask(true, true, true, true)
    //glDepthMask(false)

    glUseProgram(shaderProgram.program)
    texture.bind()

    batch.begin()
        for(i in 1..1000)
            batch.draw(sprite)
    batch.update()
}

fun render() {
    glClear(GL_COLOR_BUFFER_BIT)

    stackPush().use { stack ->
        val mat = stack.mallocFloat(16)
        projView.get(mat)
        val loc = glGetUniformLocation(shaderProgram.program, "u_projView")
        glUniformMatrix4fv(loc, false, mat)

        batch.flush()
    }

}

Метод batch.draw() помещает данные вершин спрайтов в боковый буфер процессора и batch.update() загружает все в gpu с помощью glBufferSubData. И настройка spritebatch выглядит следующим образом:

glBindBuffer(GL_ARRAY_BUFFER, tmpVbo)
            glBufferData(GL_ARRAY_BUFFER, vertexData, GL_STATIC_DRAW)
            glEnableVertexAttribArray(0)
            glEnableVertexAttribArray(1)
            glEnableVertexAttribArray(2)
            glVertexAttribPointer(0, 2, GL_FLOAT, false, 24 * sizeof(Float), 0)
            glVertexAttribPointer(1, 4, GL_FLOAT, false, 24 * sizeof(Float), 2.toLong() * sizeof(Float))
            glVertexAttribPointer(2, 2, GL_FLOAT, false, 24 * sizeof(Float), 6.toLong() * sizeof(Float))

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tmpEbo)
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)

Сначала я профилировал свою программу, но обновление буферов вершин и всей геометрии занимает около 10% от общего времени на кадр. Но замена буферов занимает остальную часть времени в 90%.

Итак, я спрашиваю, как могут такие большие игры AAA отображать свои сцены с миллионами вершин, если рисование пикселей - такая трудоемкая задача? Я знаю, что их много оптимизация в коде, но все же.

5 голосов | спросил mrdlink 3 MaramSat, 03 Mar 2018 04:29:48 +03002018-03-03T04:29:48+03:0004 2018, 04:29:48

2 ответа


13

Ваш графический процессор может, возможно, отображать даже справки 100 тыс. без проблем, но вам нужно сделать это умнее. Спрайты и другая геометрия должны поставляться на GPU партиями, сгруппированными по тем же текстурам, шейдерам и режиму наложения.

Большие игры AAA сводят к минимуму призывы к обращению, выданным на GPU. Навыки вызовов обычно дороги , так много подобных операций рисования группируются вместе и отправляются на GPU партиями. Каждый новый режим шейдера, текстуры или смешанного режима во время рендеринга приводит к отдельному обратному вызову. Кроме того, Текстуры текстуры используются для уменьшения вызовов рисования (много изображений на одной текстуре).

ответил HankMoody 3 MaramSat, 03 Mar 2018 04:57:15 +03002018-03-03T04:57:15+03:0004 2018, 04:57:15
1

То, как вы делаете дозирование ваших спрайтов, может быть субоптимальным. Если вы используете glDrawElements(), чтобы отобразить партию нескольких спрайтов, это может означать, что вы храните 4 вершины на квадрат в своем VBO (в противном случае я не вижу, как только glDrawElements() может отображать сразу несколько спрайтов. Возможно, я ошибаюсь, и в этом случае не стесняйтесь исправить мне).

Правильное решение также зависит от вашего варианта использования - это не обязательно одно и то же для системы частиц или общего 2D-рендеринга для игры.

Дело в том, что нам не нужны индексы, и нам не нужно хранить 4 вершинных позиции на квадрат.

Используя меньшую память, мы уменьшаем количество данных для обновления на каждый кадр и уменьшаем количество обращений к памяти, которые следует считать медленными.

Что бы я сделал, это Экранный рендеринг .
В принципе, ваша проблема может быть описана как рендеринг одиночного квадратного ячея, но в 1000 раз с разными настройками (включая преобразования и необходимую информацию для поиска текстур).
Кроме того, если вы знаете, что ваши квадроциклы всегда сталкиваются с экраном, вы даже можете позволить себе отправлять меньше информации на графический процессор (например, позиции как vec2, вращения в виде одного поплавка и т. Д.).

Вот очень грубый псевдокод для инстанционного рендеринга. Углубленные учебники и объяснения этого метода широко доступны, и я настоятельно рекомендую посмотреть некоторые из них.


// When setting up attrib pointers.
// See https://www.khronos.org/opengl/wiki/Vertex_Specification#Instanced_arrays
glVertexAttribDivisor(attribQuadCenter​, 1);
glVertexAttribDivisor(attribQuadScale​, 1);
glVertexAttribDivisor(attribTextureUnit​, 1);
glVertexAttribPointer(attribQuadCenter, ....);
glVertexAttribPointer(attribQuadScale, ....);
glVertexAttribIPointer(attribTextureUnit, .....);

glBufferSubData(...) // Supply all positions, scales and texture unit values.

// Rendering
for(int i=0 ; i<5 ; ++i) {
    glActiveTexture(GL_TEXTURE0 + i);
    glBindTexture(GL_TEXTURE_2D, textures[i]);
}
// Render absolutely all sprites in a single draw call.
glBindVertexArray(quad_vao);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 1000);

Еще одна техника, которую вы могли бы изучить, - Point Sprites .
Point Sprites включают «рисование» одной вершины в спрайте; каждая вершина затем расширяется в квадратный квадрат экрана, и вы можете настроить его внешний вид, используя шейдер фрагмента, учитывая нормализованные координаты внутри этого квадрата (например, выполнять поиск текстур).
Размер квадратного квадрата экрана можно записать в вершинный шейдер (где вы также можете разделить его на z), как описано в связанной статье.

Куча других вещей, чтобы попробовать и профиль:

  • Попробуйте переключить проверку глубины. Если вы можете позволить себе это, это даст вам толчок.
  • Рассмотрим отбор больших спрайтов, которые невозможно увидеть камерой. Тем не менее, я подозреваю, что это не стоит делать, если вы не работаете с более чем 10k спрайтами или около того.
  • Вызов glBufferSubData () каждый кадр для обновления данных для каждого спрайта, вероятно, будет медленным и масштабируется плохо; Передача памяти с CPU на GPU является дорогостоящей, поэтому у нас теперь есть вершинные буферы вместо старого конвейера с фиксированной функцией.
    Если ваш вариант использования подходит для него, вы можете использовать Compute Shader для обновления VBO напрямую с помощью GPU (это немного больше и есть отличные онлайн-ресурсы, которые объясняли бы это лучше, чем я мог).
ответил Yoan Lecoq 3 MarpmSat, 03 Mar 2018 15:54:10 +03002018-03-03T15:54:10+03:0003 2018, 15:54:10

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132