2016-01-04 3 views
2

Мне нужно отобразить много мелких объектов (размером 2 - 100 треугольников), которые лежат в глубокой иерархии, и каждый объект имеет свою собственную матрицу. Чтобы сделать их, я предварительно вычислил фактическую матрицу для каждого объекта, поместил объекты в один список, и у меня есть два вызова для рисования каждого объекта: установите равномерность матрицы и gl.drawElements().OpenGL ES (WebGL) рендеринг многих небольших объектов

Очевидно, что это не самый быстрый способ передвижения. Тогда у меня есть несколько тысяч объектов, которые становятся неприемлемыми. Единственное решение, о котором я думаю, - это пакетные несколько объектов в один буфер. Но делать это непросто, потому что каждый объект имеет свою собственную матрицу и помещает объект в общий буфер. Мне нужно преобразовать его вершины по матрице на CPU. Еще хуже то, что пользователь может перемещать любые объекты в любое время, и мне нужно снова пересчитать большие данные вершин (потому что пользователь может перемещать объект со множеством вложенных детей)

Итак, я ищу альтернативные подходы. А недавно обнаружили странные вершинные шейдеры onshape.com проекта:

uniform mat4 uMVMatrix; 
uniform mat3 uNMatrix; 
uniform mat4 uPMatrix; 
  
uniform vec3 uSpecular; 
uniform float uOpacity; 
uniform float uColorAmbientFactor;  //Determines how much of the vertex-specified color to use in the ambient term 
uniform float uColorDiffuseFactor;  //Determines how much of the vertex-specified color to use in the diffuse term 
  
uniform bool uApplyTranslucentAlphaToAll; 
uniform float uTranslucentPassAlpha; 
  
attribute vec3 aVertexPosition; 
attribute vec3 aVertexNormal; 
attribute vec2 aTextureCoordinate; 
attribute vec4 aVertexColor; 
  
varying vec3 vPosition; 
varying lowp vec3 vNormal; 
varying mediump vec2 vTextureCoordinate; 
varying lowp vec3 vAmbient; 
varying lowp vec3 vDiffuse; 
varying lowp vec3 vSpecular; 
varying lowp float vOpacity; 
  
attribute vec4 aOccurrenceId; 
  
float unpackOccurrenceId() { 
  return aOccurrenceId.g * 65536.0 + aOccurrenceId.b * 256.0 + aOccurrenceId.a; 
} 
  
float unpackHashedBodyId() { 
  return aOccurrenceId.r; 
} 
  
#define USE_OCCURRENCE_TEXTURE 1 
  
#ifdef USE_OCCURRENCE_TEXTURE 
  
uniform sampler2D uOccurrenceDataTexture; 
uniform float uOccurrenceTexelWidth; 
uniform float uOccurrenceTexelHeight; 
#define ELEMENTS_PER_OCCURRENCE 2.0 
  
void getOccurrenceData(out vec4 occurrenceData[2]) { 
  // We will extract the occurrence data from the occurrence texture by converting the occurrence id to texture coordinates 
  
  // Convert the packed occurrenceId into a single number 
  float occurrenceId = unpackOccurrenceId(); 
  
  // We first determine the row of the texture by dividing by the overall texture width.  Each occurrence 
  // has multiple rgba texture entries, so we need to account for each of those entries when determining the 
  // element's offset into the buffer. 
  float divided = (ELEMENTS_PER_OCCURRENCE * occurrenceId) * uOccurrenceTexelWidth; 
  float row = floor(divided); 
  vec2 coordinate; 
  // The actual coordinate lies between 0 and 1.  We need to take care that coordinate lies on the texel 
  // center by offsetting the coordinate by a half texel. 
  coordinate.t = (0.5 + row) * uOccurrenceTexelHeight; 
  // Figure out the width of one texel in texture space 
  // Since we've already done the texture width division, we can figure out the horizontal coordinate 
  // by adding a half-texel width to the remainder 
  coordinate.s = (divided - row) + 0.5 * uOccurrenceTexelWidth; 
  occurrenceData[0] = texture2D(uOccurrenceDataTexture, coordinate); 
  // The second piece of texture data will lie in the adjacent column 
  coordinate.s += uOccurrenceTexelWidth; 
  occurrenceData[1] = texture2D(uOccurrenceDataTexture, coordinate); 
} 
  
#else 
  
attribute vec4 aOccurrenceData0; 
attribute vec4 aOccurrenceData1; 
void getOccurrenceData(out vec4 occurrenceData[2]) { 
  occurrenceData[0] = aOccurrenceData0; 
  occurrenceData[1] = aOccurrenceData1; 
} 
  
#endif 
  
/** 
 * Create a model matrix from the given occurrence data. 
 * 
 * The method for deriving the rotation matrix from the euler angles is based on this publication: 
 * http://www.soi.city.ac.uk/~sbbh653/publications/euler.pdf 
 */ 
mat4 createModelTransformationFromOccurrenceData(vec4 occurrenceData[2]) { 
  float cx = cos(occurrenceData[0].x); 
  float sx = sin(occurrenceData[0].x); 
  float cy = cos(occurrenceData[0].y); 
  float sy = sin(occurrenceData[0].y); 
  float cz = cos(occurrenceData[0].z); 
  float sz = sin(occurrenceData[0].z); 
  
  mat4 modelMatrix = mat4(1.0); 
  
  float scale = occurrenceData[0][3]; 
  
  modelMatrix[0][0] = (cy * cz) * scale; 
  modelMatrix[0][1] = (cy * sz) * scale; 
  modelMatrix[0][2] = -sy * scale; 
  
  modelMatrix[1][0] = (sx * sy * cz - cx * sz) * scale; 
  modelMatrix[1][1] = (sx * sy * sz + cx * cz) * scale; 
  modelMatrix[1][2] = (sx * cy) * scale; 
  
  modelMatrix[2][0] = (cx * sy * cz + sx * sz) * scale; 
  modelMatrix[2][1] = (cx * sy * sz - sx * cz) * scale; 
  modelMatrix[2][2] = (cx * cy) * scale; 
  
  modelMatrix[3].xyz = occurrenceData[1].xyz; 
  
  return modelMatrix; 
} 
  
  
void main(void) { 
  vec4 occurrenceData[2]; 
  getOccurrenceData(occurrenceData); 
  mat4 modelMatrix = createModelTransformationFromOccurrenceData(occurrenceData); 
  mat3 normalMatrix = mat3(modelMatrix); 
  
  vec4 position = uMVMatrix * modelMatrix * vec4(aVertexPosition, 1.0); 
  vPosition = position.xyz; 
  vNormal = uNMatrix * normalMatrix * aVertexNormal; 
  vTextureCoordinate = aTextureCoordinate; 
  
  vAmbient = uColorAmbientFactor * aVertexColor.rgb; 
  vDiffuse = uColorDiffuseFactor * aVertexColor.rgb; 
  vSpecular = uSpecular; 
  vOpacity = uApplyTranslucentAlphaToAll ? (min(uTranslucentPassAlpha, aVertexColor.a)) : aVertexColor.a; 
  
  gl_Position = uPMatrix * position; 
} 

Похоже, что они кодируют позиции объекта и углов поворота, как 2 записи в 4-компонентной флоат текстуры, добавить атрибут, который хранит положение каждой вершины преобразования в этом текстуры, а затем выполнить вычисление матрицы в вершинном шейдере.

Итак, вопрос заключается в том, что этот шейдер действительно является эффективным решением для моей проблемы, или мне лучше использовать дозирование или что-то еще?

PS: Может быть, лучший подход заключается в том, чтобы хранить кватернион вместо углов и напрямую преобразовывать вершины?

ответ

2

Мне было любопытно об этом тоже, поэтому я провел пару тестов с использованием четырех различных методов рисования.

Первый - это инстанция через форму, которую вы нашли в большинстве учебников и книг. Для каждой модели задайте форму, затем нарисуйте модель.

Во-вторых, для хранения дополнительного атрибута, преобразования матрицы на каждой вершине и преобразования на графическом процессоре. В каждой ничьей сделайте gl.bufferSubData, затем нарисуйте как можно больше моделей в каждой ничьей.

Третий подход заключается в том, чтобы загрузить несколько матричных преобразований как единое целое для графического процессора и иметь дополнительную матрицуID на каждой вершине, чтобы выбрать правую матрицу на графическом процессоре. Это похоже на первое, за исключением того, что позволяет моделировать партии в партиях. Это также то, как это обычно реализуется в анимации скелета. По времени рисования для каждой партии загружайте матрицу из модели в пакетном [index] в матричный массив [index] в графическом процессоре и нарисуйте пакет.

Окончательный метод - поиск текстуры. Я создал Float32Array размером 4096 * 256 * 4, который содержит мировую матрицу для каждой модели (достаточно для моделей ~ 256 тыс.). Каждая модель имеет атрибут modelIndex, который используется для чтения его матрицы из текстуры. Затем в каждом кадре gl.texSubImage2D всю текстуру и рисуйте как можно больше в каждом обратном вызове.

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

Вот результаты: (сколько можно сделать в 60fps)

  1. Different равномерная на модель: ~ 2000
  2. дозируемой формы с matrixId: ~ 20000
  3. магазин трансформирует в вершине: ~ 40000 (нашел ошибку с первой реализацией)
  4. Texture поиск: ~ 160000
  5. Нет рисовать, только процессорное время для расчета матриц: ~ 170000

Я думаю, что его довольно очевидно, что единообразный инстанс - это не тот путь. Техника 1 терпит неудачу только из-за того, что она слишком много набирает призывы.Предположительно, чтобы упакованные униформы могли справиться с проблемой вызова draw, но я обнаружил, что слишком много времени процессора используется для получения матричных данных из правильной модели и загрузки ее на GPU. Многочисленные вызовы uniformMatrix4f тоже не помогли.

Время, затраченное на выполнение gl.texSubImage2D, значительно меньше времени, необходимого для вычисления новых мировых матриц для динамических объектов. Дублирование данных преобразования в каждой вершине работает лучше, чем то, что может подумать большинство людей, но это тратит много памяти на пропускную способность. Подход к поиску текстуры, вероятно, наиболее дружелюбен к процессору из всех вышеперечисленных методов. Скорость выполнения поиска 4 текстур, похоже, аналогична выполнению единообразного поиска массива. (результаты тестирования с более крупными сложными объектами, в которых я привязан к GPU).

Вот снимок одного из испытаний с использованием текстуры подстановок подхода: enter image description here

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

Ответы на вопросы в комментариях:

  1. закраски: Я не связан ГПУ вообще. Когда я пробовал с большими сложными моделями, единообразный инстанс фактически стал самым быстрым. Я думаю, что есть некоторые накладные расходы графического процессора с использованием равномерного пакетного и текстурного поиска, что привело к их замедлению.
  2. Магазин quaternion и перевод: в моем случае не имело бы большого значения, потому что, как вы можете видеть, texSubImage2D занял только 9% процессорного времени, уменьшив его до 4,5%. Сложно сказать о его влиянии на GPU, поскольку, пока вы делаете меньше текстур, но вам нужно преобразовать кватернион и перевод в матрицу.
  3. Перемеживание: возможно, эта техника может дать около 5-10% ускорения, если ваше приложение привязано к вершине. Тем не менее, я никогда не видел, чтобы чередование имело значение для меня в моих тестах. Поэтому я полностью избавился от него.
  4. Память: она практически одинакова для всех техник, за исключением дублирования на каждой вершине. Все остальные 3 метода должны передавать один и тот же объем данных на GPU. (вы можете передать в переводе + кватернион как единицу вместо матрицы)
+1

Большое спасибо за ваши исследования! Было бы даже лучше, если бы вы добавили информацию о своем оборудовании и ограничены ли ваши результаты? PS: вы пытались сохранить кватернион и положение, в результате получилось всего 2 текстуры?PPS: Вы экспериментировали с параметрами с чередованием и без перемежения? – Rem

+0

И добавьте требования к памяти для каждого метода, пожалуйста. Для новичков стало легко рассуждать о том, какой из них выбрать! – Rem

2

this, который может дать вам некоторые идеи.

Если понимать комментарии Рэма ...

Самое простое решение для хранения какой-то на вершине преобразования данных. Это то, что делает видео выше. Проблема с этим решением заключается в том, что если у вас есть модель с 100 вершинами, вам необходимо обновить преобразования для всех 100 вершин.

Решение заключается в косвенном преобразовании через текстуру. Для каждой вершины в каждой модели магазина только один дополнительный поплавок, мы можем назвать это поплавок «ModelID», как в

attribute float modelId; 

Таким образом, все вершины в первой модели получить ID = 0, все вершины в второй модели получить идентификатор = 1 и т. Д.

Затем вы сохраняете свои преобразования в текстуре. Например, вы можете сохранить перевод (x, y, z) + кватернион (x, y, z, w). Если ваша целевая платформа поддерживает текстуры с плавающей запятой, то это 2 пикселя RGBA для каждого преобразования.

Вы используете modelId, чтобы вычислить, где в текстуре вытащить данные преобразования.

float col = mod(modelId, halfTextureWidth) * 2.; 
float row = floor(modelId/halfTextureWidth); 
float oneHPixel = 1./textureWidth; 
vec2 uv = vec2((col + 0.5)/textureWidth, (row + 0.5)/textureHeight); 
vec4 translation = texture2D(transforms, uv); 
vec4 rotationQuat = texture2D(transform, uv + vec2(oneHPixel, 0)); 

Теперь вы можете использовать перевод и rotationQuat для создания матрицы в своем вершинном шейдере.

Почему halfTextureWidth? Потому что мы делаем 2 пикселя за трансформацию.

Почему + 0.5?См. https://stackoverflow.com/a/27439675/128511

Это означает, что вам нужно обновить только 1 преобразование на модель вместо 1 преобразования на вершину, что делает его минимальным объемом работы.

This example generates some matrices from quaternions. Это своего рода аналогичная идея, но поскольку она делает частицы, ей не нужна направленность текстуры.

Примечание: Вышеупомянутое предполагает, что все, что вам нужно, это перевод и вращение. Ничто не мешает вам хранить целые матрицы в текстуре, если это то, что вам нужно. Или что-то еще в этом отношении, как свойства материала, свойства освещения и т. Д.

AFAIK почти все текущие платформы поддерживают чтение данных из текстур с плавающей точкой. Вы должны включить эту функцию с помощью

var ext = gl.getExtension("OES_texture_float"); 
if (!ext) { 
    // no floating point textures for you! 
} 

Но имейте в виду, что не каждая платформа поддерживает фильтрацию текстур с плавающей запятой. Фильтрация не требуется для этого решения (и его необходимо будет отдельно включить). Обязательно установите свою фильтрацию на gl.NEAREST.

+0

В любом случае после тестирования я обнаружил, что преобразование даже 500 тысяч треугольников в JavaScript каждой рамки рисования в один буфер gl дает мне достойную производительность !!! Таким образом, я закончил разделение объектов между несколькими партиями (размером 16 тыс. Треугольников) и пересчитал целую партию, если какой-либо объект в ней был изменен или удален. – Rem

+0

@gman. Поскольку вы здесь, что ваши мысли являются относительной стоимостью между 'texSubImage2D' по сравнению с' uniformMatrix4fv'? Поскольку, если объекты полудинамичны, то я ожидаю, что вам нужно будет вызвать много небольших «texSubImage2D» или заменить всю текстуру на кадр. Я, кажется, помню, что изменения текстурных данных имеют значительно большие накладные расходы в webgl по сравнению с настольными, это правда? –

+0

@ Помните, что этот ответ принят, но решение, которое сработало для вас, принадлежит мне. Чувства были повреждены. JK в сторону Я надеюсь, что это отличное объяснение техники, которую вы опубликовали в OP, ясно, что вы не обновляете 1 float на объект, а скорее 8 поплавков в переводе + кватернион. Смещение не изменилось, но вам все равно придется обновлять данные, на которые указывает смещение. –

Смежные вопросы