OpenGL ES(WebGL)渲染许多小对象

时间:2016-01-04 06:38:09

标签: opengl-es webgl

我需要渲染很多小对象(大小为2到100个三角形),它们位于深层次结构中,每个对象都有自己的矩阵。为了渲染它们,我预先计算每个对象的实际矩阵,将对象放在一个列表中,我有两个调用来绘制每个对象:set matrix uniform和gl.drawElements()。

显然,这不是最快的方式。然后我有几千个对象的性能变得不可接受。我正在考虑的唯一解决方案是将多个对象批量放入单个缓冲区。但要做到这一点并不容易,因为每个对象都有自己的矩阵,并且要将对象放入共享缓冲区,我需要在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;
}

看起来它们将对象位置和旋转角度编码为4分量浮点纹理中的2个条目,添加属性以存储此纹理中每个顶点变换的位置,然后在顶点着色器中执行矩阵计算。

所以问题是这个着色器实际上是解决我的问题的有效方法,还是我应该更好地使用批处理或其他什么?

PS:可能更好的方法是存储四元数而不是角度并直接转换顶点?

2 个答案:

答案 0 :(得分:3)

我对此也很好奇所以我用4种不同的绘图技术进行了几次测试。

首先是你在大多数教程和书籍中找到的制服实例。对于每个模型,设置制服,然后绘制模型。

第二种是存储一个附加属性,即每个顶点上的矩阵变换,并在GPU上进行变换。在每次绘制时,gl.bufferSubData然后在每次绘制中绘制尽可能多的模型。

第三种方法是将多个矩阵变换上传到GPU,并在每个顶点上添加一个矩阵ID,以在GPU上选择正确的矩阵。这类似于第一个,除了它允许分批绘制模型。这也是它通常在骨架动画中实现的方式。在绘制时间,对于每个批处理,将批处理[index]中的模型上的矩阵上载到GPU中的矩阵数组[index]并绘制批处理。

最后的技术是通过纹理查找。我创建了一个大小为4096 * 256 * 4的Float32Array,它包含每个模型的世界矩阵(足够~256k型号)。每个模型都有一个modelIndex属性,用于从纹理中读取其矩阵。然后在每一帧,gl.texSubImage2D整个纹理,并在每次绘制调用中尽可能多地绘制。

不考虑硬件实例化,因为我认为要求绘制许多独特的模型,即使对于我的测试我只绘制每帧具有不同世界矩阵的立方体。

结果如下:(可以在60FPS绘制多少)

  1. 每个型号不同的制服:~2000
  2. 带有matrixId的批量制服:~20000
  3. 每个顶点存储转换:~40000(发现第一次实现的错误)
  4. 纹理查找:~160000
  5. 没有绘制,只有CPU时间来计算矩阵:~170000
  6. 我认为很明显,统一实例化并非如此。技术1失败只是因为它做了太多的绘制调用。批量制服应该应该处理绘制调用问题,但我发现太多的CPU时间用于从正确的模型获取矩阵数据并将其上传到GPU。众多的uniformMatrix4f调用也没有帮助。

    与计算动态对象的新世界矩阵所花费的时间相比,gl.texSubImage2D所花费的时间要少得多。在每个顶点上复制变换数据的效果比大多数人想象的要好,但它浪费了大量的内存带宽。在所有上述技术中,纹理查找方法可能对CPU最友好。进行4次纹理查找的速度似乎与进行统一数组查找类似。 (测试使用较大的复杂对象进行测试,其中我是GPU绑定的)。

    以下是使用纹理查找方法的其中一个测试的快照: enter image description here

    因此,总而言之,如果您的模型很小,您所追求的可能是将转换数据存储在每个顶点上,或者当您的模型很大时,可以使用纹理查找方法。

    评论中的问题答案:

    1. fillrate:我根本不受GPU的束缚。当我尝试使用大型复杂模型时,统一实例化实际上变得最快。我猜有一些GPU开销使用统一的批处理和纹理查找导致它们变慢。
    2. 存储四元数和翻译:在我的情况下并不重要,因为你可以看到texSubImage2D只花了9%的CPU时间,将其减少到4.5%并不重要。很难说它对GPU的影响,因为当你进行较少的纹理查找时,你必须将四元数和平移转换成矩阵。
    3. 交错:假如你的应用程序是顶点绑定的,那么这种技术可以提供大约5-10%的加速。但是,在我的测试中,我从未见过交错对我有所影响。所以我完全摆脱了它。
    4. 内存:除了每个顶点上的重复外,所有技术基本相同。所有其他3种技术都应该将相同数量的数据传递给GPU。 (你可以将翻译+四元数作为统一而不是矩阵传递)

答案 1 :(得分:2)

this可能会给你一些想法。

如果了解Rem的评论......

最简单的解决方案是存储某种每顶点变换数据。这实际上是上面的视频所做的。该解决方案的问题是,如果您有一个包含100个顶点的模型,则必须更新所有100个顶点的变换。

解决方案是通过纹理间接转换。对于每个模型存储中的每个顶点只有一个额外的浮点数,我们可以调用此浮点数" modelId"如在

attribute float modelId;

因此,第一个模型中的所有顶点都得到id = 0,第二个模型中的所有顶点都得到id = 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));

现在您可以使用translation和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