Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Стоимость OpenGL команд. (2 стр)

Стоимость OpenGL команд. (2 стр)

Автор:

Инстансинг

Инстансинг придуман для быстрого рендера одинаковой геометрии с разными параметрами. Каждому объекту соответствует свой индекс, по которому можно выбрать соответствующие параметры из буфера, варьировать какие то переменные и т.д. Главное преимущества от использования инстансинга — можно сильно сократить количество дипов.

Можно сложить все параметры объектов в 1 буфер, переслать на GPU и выполнить один дип. Хранение данных в буферах само по себе является неплохой оптимизацией — экономим на том, что не надо постоянно менять параметры шейдера. К тому же, если данные инстансов не меняются (например, мы точно знаем, что эта геометрия статическая), то можно не пересылать их на гпу постоянно. В целом, для оптимального рендеринга стоит сперва упаковать все инстанс данные в один буфер и передать на GPU одной командой. Для каждого дипа передавать только смещение по которому находятся его данные. Используя индекс инстанса (gl_InstanceID) можно добраться до данных конкретного объекта.

Вариантов хранения данных в OpenGL достаточно много: vertex buffer (VBO), uniform buffer (UBO), texture buffer (TBO), shader storage buffer (SSBO), textures. Все зависит в каком буфере хранятся данные. Есть различные особенности, которые и рассмотрим.

Текстурный инстансинг

Все данные хранятся в текстуре. Для эффективного обновления текстур лучше использовать специальные структуры Pixel Buffer Object (PBO), которые позволяют асинхронно передавать данные на GPU. CPU не ждет пока данные передадутся и продолжает работу.

Код создания:

//создаем 2 буфера.
//Пока один используется для передачи уже подготовленных данных в непосредственно в текстуру,
//второй буфер мы используем для заполнения новыми данными.
GLuint textureInstancingPBO[2], textureInstancingDataTex;
glGenBuffersARB(2, textureInstancingPBO);
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, textureInstancingPBO[0]);
//GL_STREAM_DRAW_ARB означает что мы будем менять данные часто, каждый кадр
glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, INSTANCES_DATA_SIZE, 0, GL_STREAM_DRAW_ARB);
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, textureInstancingPBO[1]);
glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, INSTANCES_DATA_SIZE, 0, GL_STREAM_DRAW_ARB);
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);

//создаем текстуру, в которой будем хранить непосредственно данные инстансов
glGenTextures(1, &textureInstancingDataTex);
glBindTexture(GL_TEXTURE_2D, textureInstancingDataTex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_REPEAT);

//в каждой строке храним данные NUM_INSTANCES_PER_LINE объектов. 128 в нашем случае
//Для каждого объекта храним PER_INSTANCE_DATA_VECTORS данных-векторов. 2 в данном примере
//GL_RGBA32F — у нас float32 данные
// complex_mesh_instances_data исходные данные инстансов, если мы не собираемся обновлять данные в текстуре
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F,
       NUM_INSTANCES_PER_LINE * PER_INSTANCE_DATA_VECTORS, MAX_INSTANCES / NUM_INSTANCES_PER_LINE, 0,
       GL_RGBA, GL_FLOAT, &complex_mesh_instances_data[0]);
glBindTexture(GL_TEXTURE_2D, 0);

Обновление текстуры:

glBindTexture(GL_TEXTURE_2D, textureInstancingDataTex);
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER, textureInstancingPBO[current_frame_index]);

//копируем пиксели из PBO в текстуру
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
       NUM_INSTANCES_PER_LINE * PER_INSTANCE_DATA_VECTORS, MAX_INSTANCES / NUM_INSTANCES_PER_LINE,
       GL_RGBA, GL_FLOAT, 0);

//устанавливаем PBO в который будем записывать новые данные
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER, textureInstancingPBO[next_frame_index]);

//вызываем glBufferDataARB() с NULL указателем, чтобы не было 'синхронизации' с гпу
glBufferData(GL_PIXEL_UNPACK_BUFFER, INSTANCES_DATA_SIZE, 0, GL_STREAM_DRAW_ARB);

gpu_data = (float*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY_ARB);
if (gpu_data)
{
  //скопировать данные на gpu
  memcpy(gpu_data, &complex_mesh_instances_data[0], INSTANCES_DATA_SIZE);
  
  //говорим, что закончили пересылку данных
  glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}

Код рендера через текстурный инстансинг:

//устанавливаем текстуру с данными инстансов
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureInstancingDataTex);
glBindSampler(0, Sampler_nearest);

//что рисуем, геометрия
glBindVertexArray(geometry_vao_id);

//шейдер которым рисуем
tex_instancing_shader.bind();

static GLint location = glGetUniformLocation(tex_instancing_shader.programm_id, "s_texture_0");
if (location >= 0)
  glUniform1i(location, 0);

//отрисовываем группу объектов, дип
glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, CURRENT_NUM_INSTANCES);

Вершинный шейдер для доступа к данным:

#version 150 core

//вершинные атрибуты
in vec3 s_pos;
in vec3 s_normal;
in vec2 s_uv;

//матрица трансформаций камеры
uniform mat4 ModelViewProjectionMatrix;

uniform sampler2D s_texture_0; //текстура в которой хранятся данные инстансов

out vec2 uv;
out vec3 instance_color;

void main()
{
  const vec2 texel_size = vec2(1.0 / 256.0, 1.0 / 16.0);
  const int objects_per_row = 128;
  const vec2 half_texel = vec2(0.5, 0.5); //Opengl texel расположен в центре клетки

  //вычисляем текстурные координаты по которым находятся данные инстансов
  //gl_InstanceID % objects_per_row — номер объекта в строке
  //умножаем на 2, т. к. у каждого объекта по 2 вектора с данными
  //gl_InstanceID / objects_per_row — в какой строке находятся данные объекта
  //умножение на  texel_size переводит целочисленный индекс текселя в интервал 0..1 для семплирования
  vec2 texel_uv =
     (vec2((gl_InstanceID % objects_per_row) * 2, floor(gl_InstanceID / objects_per_row)) + half_texel) * texel_size;
  
  //собственно семплирование данных из текстуры, 2 текселя подряд
  vec4 instance_pos = textureLod(s_texture_0, texel_uv, 0);
  instance_color = textureLod(s_texture_0, texel_uv + vec2(texel_size.x, 0.0), 0).xyz;

  uv = s_uv;
  gl_Position = ModelViewProjectionMatrix * vec4(s_pos + instance_pos.xyz, 1.0);
}

Инстансинг через вершинный буфер

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

Код создания самого буфера опустим, он выглядит тривиально. Нашей задачей является модифицирование информации о вершине для шейдера (vertex declaration, vdecl).

//...код создания основного vdecl
glBindVertexArray(geometry_vao_vbo_instancing_id); //продолжаем модифицировать vdecl

//устанавливаем буфер с инстанс данными
glBindBuffer(GL_ARRAY_BUFFER, all_instances_data_vbo);

//размер инстанс данных для одного объекта
const int per_instance_data_size = sizeof(vec4) * PER_INSTANCE_DATA_VECTORS;

//устанавливаем 4й атрибут вершины, в котором 4 float'а, находится по 0 смещению
glEnableVertexAttribArray(4);
glVertexAttribPointer((GLuint)4, 4, GL_FLOAT, GL_FALSE, per_instance_data_size, (GLvoid*)(0));
glVertexAttribDivisor(4, 1);

//устанавливаем 5й атрибут вершины, в котором 4 float'а, находится по sizeof(vec4) смещению
glEnableVertexAttribArray(5);
glVertexAttribPointer((GLuint)5, 4, GL_FLOAT, GL_FALSE, per_instance_data_size, (GLvoid*)(sizeof(vec4)));
glVertexAttribDivisor(5, 1);

glBindVertexArray(0);

Код рендера:

vbo_instancing_shader.bind();
//устанавливаем наш вершинный буфер с модифицированным vertex declaration (vdecl)
glBindVertexArray(geometry_vao_vbo_instancing_id);
glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, CURRENT_NUM_INSTANCES);

Вершинный шейдер для доступа к данным:

#version 150 core

//вершинные атрибуты
in vec3 s_pos;
in vec3 s_normal;
in vec2 s_uv;
in vec4 s_attribute_3; //some_data;

in vec4 s_attribute_4; //instance pos !
in vec4 s_attribute_5; //instance color !

//матрица трансформаций камеры
uniform mat4 ModelViewProjectionMatrix;

out vec3 instance_color;

void main()
{
  instance_color = s_attribute_5.xyz;
  gl_Position = ModelViewProjectionMatrix * vec4(s_pos + s_attribute_4.xyz, 1.0);
}

Uniform buffer instancing, Texture buffer instancing, SSBO buffer instancing

3 метода в целом очень похожи друг на друга, различаются только типом создаваемого буфера.
- Uniform buffer (UBO) отличается небольшим размером, но теоретически должен быть быстрее остальных.
- Texture buffer (TBO) имеет очень большой размер. В нем можно уместить данные всех объектов сцены, данные скелетной трансформации.
- Shader Storage Buffer (SSBO) теоретически обладает обоими свойствами — быстрый с большим размером. К тому же в него можно писать данные. Единственное, это новое расширение и старым железом не поддерживается.

Uniform buffer

Код создания:

glGenBuffers(1, &dips_uniform_buffer);
glBindBuffer(GL_UNIFORM_BUFFER, dips_uniform_buffer);
glBufferData(GL_UNIFORM_BUFFER, INSTANCES_DATA_SIZE, &complex_mesh_instances_data[0], GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

//юниформ буфер нужно привязать к шейдеру специальной командой
GLint instanceData_UBO_location = glGetUniformLocation(ubo_instancing_shader.programm_id, "instance_data");
glUniformBufferEXT(ubo_instancing_shader.programm_id, iinstanceData_UBO_location, dips_uniform_buffer);

Рендер:

ubo_instancing_shader.bind();
glBindVertexArray(geometry_vao_id);
glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, CURRENT_NUM_INSTANCES);

Вершинный шейдер:

#version 150 core
#extension GL_EXT_bindable_uniform : enable
#extension GL_EXT_gpu_shader4 : enable

in vec3 s_pos;
in vec3 s_normal;
in vec2 s_uv;

uniform mat4 ModelViewProjectionMatrix;
bindable uniform vec4 instance_data[4096]; //наш uniform буфер с инстанс данными

out vec3 instance_color;

void main()
{
  vec4 instance_pos = instance_data[gl_InstanceID*2];
  instance_color = instance_data[gl_InstanceID*2+1].xyz;
  gl_Position = ModelViewProjectionMatrix * vec4(s_pos + instance_pos.xyz, 1.0);
}

TBO

Код создания:

glGenBuffers(1, &dips_texture_buffer);
glBindBuffer(GL_TEXTURE_BUFFER, dips_texture_buffer);
glBufferData(GL_TEXTURE_BUFFER, INSTANCES_DATA_SIZE, &complex_mesh_instances_data[0], GL_STATIC_DRAW);
glGenTextures(1, &dips_texture_buffer_tex);
glBindBuffer(GL_TEXTURE_BUFFER, 0);

Рендер:

glBindVertexArray(geometry_vao_id);
tbo_instancing_shader.bind();

//устанавливаем tbo в шейдер, как 'специальную' текстуру
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_BUFFER, dips_texture_buffer_tex);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, dips_texture_buffer);

glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, CURRENT_NUM_INSTANCES);

Вершинный шейдер:

#version 150 core

in vec3 s_pos;
in vec3 s_normal;
in vec2 s_uv;

uniform mat4 ModelViewProjectionMatrix;
uniform samplerBuffer s_texture_0; //наш tbo с инстанс данными

out vec3 instance_color;

void main()
{
  //семплирование данных из буфера
  vec4 instance_pos = texelFetch(s_texture_0, gl_InstanceID*2);
  instance_color = texelFetch(s_texture_0, gl_InstanceID*2+1).xyz;
  gl_Position = ModelViewProjectionMatrix * vec4(s_pos + instance_pos.xyz, 1.0);
}

SSBO

Код создания:

glGenBuffers(1, &ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, INSTANCES_DATA_SIZE, &complex_mesh_instances_data[0], GL_STATIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // unbind

Рендер:

glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
//привязываем к точке с индексом 0, в шейдере это также будет отражено
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);

ssbo_instancing_shader.bind();
glBindVertexArray(geometry_vao_id);
glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, CURRENT_NUM_INSTANCES);
glBindVertexArray(0);

Вершинный шейдер:

#version 430
#extension GL_ARB_shader_storage_buffer_object : require

in vec3 s_pos;
in vec3 s_normal;
in vec2 s_uv;

uniform mat4 ModelViewProjectionMatrix;

//наш ssbo, мы ранее привязывали его к 0 'токе'
layout(std430, binding = 0) buffer ssboData
{
    vec4 instance_data[4096];
};

out vec3 instance_color;

void main()
{
  vec4 instance_pos = instance_data[gl_InstanceID*2];
  instance_color = instance_data[gl_InstanceID*2+1].xyz;
  gl_Position = ModelViewProjectionMatrix * vec4(s_pos + instance_pos.xyz, 1.0);
}

Uniforms instancing

Достаточно простой. Имеем возможность передать через glUniform* некоторое  количество векторов с данными. Максимальное количество зависит от видеокарты. Получить максимальное количесто можно при помощи вызова glGetIntegerv с параметром GL_MAX_VERTEX_UNIFORM_VECTORS. Для R9 380 вернет 4096. Минимальное значение 256.

uniforms_instancing_shader.bind();
glBindVertexArray(geometry_vao_id);

//получаем расположение шейдерной переменной-массива, в которую и будем писать данные
static int uniformsInstancing_data_varLocation =
  glGetUniformLocation(uniforms_instancing_shader.programm_id, "instance_data");

//инстанс данные можно залить одной командой если хватает векторов
//для наглядности разделим на группы, потому-что, как правило, данных сильно больше.
for (int i = 0; i < UNIFORMS_INSTANCING_NUM_GROUPS; i++)
{
  //записываем порцию данных
  glUniform4fv(uniformsInstancing_data_varLocation, UNIFORMS_INSTANCING_MAX_CONSTANTS_FOR_INSTANCING,
        &complex_mesh_instances_data[i*UNIFORMS_INSTANCING_MAX_CONSTANTS_FOR_INSTANCING].x);
  //собственно отрисовка текущей группы
  glDrawElementsInstanced(GL_TRIANGLES, BOX_NUM_INDICES, GL_UNSIGNED_INT, NULL, UNIFORMS_INSTANCING_OBJECTS_PER_DIP);
}

Multi draw indirect

Отдельно рассмотрим команду, которая позволяет рисовать огромное количество дипов за один вызов. Это очень полезная команда, которая позволяет рендерить группы инстансов с разной геометрией. Ей передается массив, который описывает параметры дипов: количество индексов, смещение данных в вершинном буфере, количество инстансов и т. д. Ограничения в том, что вся выводимая геометрия должна храниться в одном VBO и рендериться одним шейдером. Дополнительным плюсом является то, что этот массив с информацией о дипах можно сформировать на стороне GPU, что очень удобно для GPU кулинга объектов, например.

//заполнить indirect buffers
for (int i = 0; i < CURRENT_NUM_INSTANCES; i++)
{
  multi_draw_indirect_buffer[i].vertexCount = BOX_NUM_INDICES;
  multi_draw_indirect_buffer[i].instanceCount = 1;
  multi_draw_indirect_buffer[i].firstVertex = i*BOX_NUM_INDICES;
  multi_draw_indirect_buffer[i].baseVertex = 0;
  multi_draw_indirect_buffer[i].baseInstance = 0;
}

glBindVertexArray(ws_complex_geometry_vao_id);
simple_geometry_shader.bind();

//собственно вызов
glMultiDrawElementsIndirect(GL_TRIANGLES,
  GL_UNSIGNED_INT,
  (GLvoid*)&multi_draw_indirect_buffer[0],
  CURRENT_NUM_INSTANCES,
  0);

По сути, данная команда выполняет несколько glDrawElementsInstancedIndirect за один вызов. Есть, правда, неприятная особенность в поведении. Что каждый такой glDrawElementsInstanced будет иметь независимый gl_InstanceID, то есть каждый раз сбрасываться в 0 при новом Draw*. Что усложняет доступ к данным соответствующего инстанса. Обходится эта проблема модифицированием vdecl каждого типа объектов, посылаемых на рендер. Можно почитать Surviving without gl_DrawID.

Стоит заметить, что glMultiDrawElementsIndirect выполнил сразу огромное количество дипов (для наглядности), одной командой.  Для этого и предназначен. Не стоит сравнивать его скорость с остальными типами инстансинга.

Сравнение типов инстансинга о скорости

Таблица 8. Стоимость типов инстансинга. Количество итераций = 100. Сверху - количество инстансов. Время потраченное CPU (время потраченное GPU) в ms.

Тип инстансинга 50 100 200
UBO_INSTANCING 0.35 (0.10) 0.37 (0.13) 0.36 (0.24)
TBO_INSTANCING 0.72 (0.11) 0.73 (0.13) 0.73 (0.25)
SSBO_INSTANCING 0.37 (0.09) 0.40 (0.13) 0.38 (0.24)
VBO_INSTANCING 0.36 (0.09) 0.37 (0.12) 0.37 (0.24)
TEXTURE_INSTANCING 0.38 (0.10) 0.39 (0.13) 0.39 (0.24)
UNIFORMS_INSTANCING 0.41 (0.13) 0.52 (0.27) 0.74 (0.51)
MULTI_DRAW_INDIRECT 0.63 (0.53) 1.17 (1.01) 2.10 (1.93)

Нужно было убедиться, что производительность не упирается в GPU. Поэтому в таблице, в скобочках также приводится время потраченное видеокартой.

UBO, VBO, SSBO, TEXTURE типы инстансинга примерно одинаковы по скорости и имеют 'хороший' тайминг.

TBO можно хранить большие объемы информации, но он проигрывает по скорости большинству других типов. Если есть такая возможность, то стоит использовать SSBO для хранения данных, т.к. он и быстрый и обладает большим размером.

Текстурный инстансинг тоже хорошая альтернатива UBO. Поддерживается «старым железом», можно хранить огромное количество информации. Немного неудобно обновлять.
Передавать данные каждый раз через шейдерные переменные, очевидно, оказалось самым медленным.

glMultiDrawElementsIndirect в тестах выполнил 5к, 10к и 20к дипов! Правда мы тестировали просто повторения теста. Такое количество дипов можно было бы сделать и одной командой. Единственное, стоит заметить, что при таком количестве дипов сам масив с их описанием будет достаточно большим. Так что лучше использовать GPU для генерации информации о дипах.

Рекомендации по оптимизации и выводы

В данной статье провели анализ стоимости API вызовов, оценили по скорости различные типы инстансинга геометрии. В целом, чем меньше переключений стейтов тем лучше. Следует использовать новые фичи последних версий API по максимуму: текстурные массивы, SSBO, Draw Indirect, мапинг буферов с флагами GL_MAP_PERSISTENT_BIT и GL_MAP_COHERENT_BIT для быстрой передачи данных и др фичи.

Рекомендации:
    - Чем меньше переключений стейтов, тем лучше. Стоит группировать объекты по материалам.
    - Можно сделать обертку на смену состояний (текстур, буферов, шейдеров, других стейтов). Проверять, действительно ли менялся ресурс прежде чем вызывать его смену на стороне gl, API вызов которого гораздо дороже чем проверка индекса.
    - Объединять геометрию в один буфер.
    - Использовать текстурные массивы.
    - Хранить данные в буферах и текстурах.
    - Использовать как можно меньше шейдеров. Но слишком сложный, универсальный шейдер с большим количеством ветвлений очевидно станет проблемой. Тем более на старых видео картах, где ветвления обходятся дороже.
    - Использовать инстансинг.
    - Если возможно, использовать Draw Indirect и формировать информацию о дипах на стороне GPU.

Несколько общих советов:
    - Нужно вычислять узкие места и в первую очередь оптимизировать их.
    - Нужно знать во что упирается производительность – CPU или GPU.
    - Не делать работу дважды, переиспользывать результаты работы алгоритма с предыдущего кадра.
    - Сложные вычисления можно предрассчитывать.
    - Лучшая оптимизация производительности - вообще не делать работу.
    - Использовать параллельные вычисления: разбивать работу на части и выполнять в параллельных потоках.

Исходный код всех примеров

Ссылки:
    1. Beyond Porting
    2. OpenGL documentation
    3. Instancing in OpenGL
    4. OpenGL Pixel Buffer Object (PBO)
    5. Buffer Object
    6. Drawing with OpenGL
    7. Shader Storage Buffer Object
    8. Shader Storage Buffers Objects
    9. hardware caps, stats (for GL_MAX_VERTEX_UNIFORM_COMPONENTS)
    10. Array Texture
    11. Textures Array example
    12. MultiDrawIndirect example
    13. The Road to One Million Draws
    14. MultiDrawIndirect, Surviving without gl_DrawID
    15. Humus demo about Instancing

Страницы: 1 2

8 февраля 2017

#instancing, #OpenGL


Обновление: 4 мая 2017

2001—2018 © GameDev.ru — Разработка игр