我需要渲染很多小对象(大小为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:可能更好的方法是存储四元数而不是角度并直接转换顶点?
答案 0 :(得分:3)
我对此也很好奇所以我用4种不同的绘图技术进行了几次测试。
首先是你在大多数教程和书籍中找到的制服实例。对于每个模型,设置制服,然后绘制模型。
第二种是存储一个附加属性,即每个顶点上的矩阵变换,并在GPU上进行变换。在每次绘制时,gl.bufferSubData然后在每次绘制中绘制尽可能多的模型。
第三种方法是将多个矩阵变换上传到GPU,并在每个顶点上添加一个矩阵ID,以在GPU上选择正确的矩阵。这类似于第一个,除了它允许分批绘制模型。这也是它通常在骨架动画中实现的方式。在绘制时间,对于每个批处理,将批处理[index]中的模型上的矩阵上载到GPU中的矩阵数组[index]并绘制批处理。
最后的技术是通过纹理查找。我创建了一个大小为4096 * 256 * 4的Float32Array,它包含每个模型的世界矩阵(足够~256k型号)。每个模型都有一个modelIndex属性,用于从纹理中读取其矩阵。然后在每一帧,gl.texSubImage2D整个纹理,并在每次绘制调用中尽可能多地绘制。
不考虑硬件实例化,因为我认为要求绘制许多独特的模型,即使对于我的测试我只绘制每帧具有不同世界矩阵的立方体。
结果如下:(可以在60FPS绘制多少)
我认为很明显,统一实例化并非如此。技术1失败只是因为它做了太多的绘制调用。批量制服应该应该处理绘制调用问题,但我发现太多的CPU时间用于从正确的模型获取矩阵数据并将其上传到GPU。众多的uniformMatrix4f调用也没有帮助。
与计算动态对象的新世界矩阵所花费的时间相比,gl.texSubImage2D所花费的时间要少得多。在每个顶点上复制变换数据的效果比大多数人想象的要好,但它浪费了大量的内存带宽。在所有上述技术中,纹理查找方法可能对CPU最友好。进行4次纹理查找的速度似乎与进行统一数组查找类似。 (测试使用较大的复杂对象进行测试,其中我是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
。