优化查找表的分支

时间:2018-04-03 22:20:53

标签: webgl

WebGL中的分支似乎类似于以下内容(从各种文章中解释):

着色器并行执行其代码,如果需要在继续之前评估条件是否为真(例如使用if语句),那么它必须发散并以某种方式与之通信其他主题是为了得出结论。

也许这有点偏离 - 但最终,似乎着色器中分支的问题是每个线程可能看到不同的数据。因此,仅使用制服进行分支通常是可以的,而动态数据上的分支则不然。

问题1:这是正确的吗?

问题2:这与那些相当可预测但不是统一的内容有关,例如循环中的索引?

具体来说,我有以下功能:

vec4 getMorph(int morphIndex) {
  /* doesn't work - can't access morphs via dynamic index
  vec4 morphs[8];
  morphs[0] = a_Morph_0;
  morphs[1] = a_Morph_1;
  ...
  morphs[7] = a_Morph_7;

  return morphs[morphIndex];
  */

  //need to do this:

  if(morphIndex == 0) {
     return a_Morph_0;
  } else if(morphIndex == 1) {
     return a_Morph_1;
  }
  ...
  else if(morphIndex == 7) {
     return a_Morph_7;
  }

}

我称之为:

for(int i = 0; i < 8; i++) {
  pos += weight * getMorph(i);
  normal += weight * getMorph(i);
  ...
}

从技术上讲,它运行正常 - 但我关心的是基于动态索引的所有if / else分支。在这样的情况下,这会减慢速度吗?

为了便于比较,虽然在这里用一些简洁的词语解释起来很棘手 - 但我有一个另类的想法是始终为每个属性运行所有计算。这将涉及每个顶点可能有24个多余的vec4 += float * vec4计算。通常情况下,这会比索引分支8次更好还是更差?

注意:在我的实际代码中,有更多级别的映射和间接,虽然它归结为同一个getMorph(i)问题,但我的用例涉及从两者获取该索引循环中的索引,以及在统一整数数组中查找该索引

1 个答案:

答案 0 :(得分:2)

我知道这不是你问题的直接答案,但是......为什么不只是不使用循环?

vec3 pos = weight[0] * a_Morph_0 + 
           weight[1] * a_Morph_1 + 
           weight[2] * a_Morph_2 ...

如果您想要通用代码(即可以设置变形数量的位置),那么可以使用#if#else#endif

获取广告素材
const numMorphs = ?
const shaderSource = `
...
#define NUM_MORPHS ${numMorphs}

vec3 pos = weight[0] * a_Morph_0
           #if NUM_MORPHS >= 1
           + weight[1] * a_Morph_1 
           #endif
           #if NUM_MORPHS >= 2
           + weight[2] * a_Morph_2 
           #endif
           ;
...
`;

或使用字符串操作在JavaScript中生成着色器。

 function createMorphShaderSource(numMorphs) {
   const morphStrs = [];
   for (i = 1; i < numMorphs; ++i) {
      morphStrs.push(`+ weight[${i}] * a_Morph_${i}`);
   }
   return `
     ..shader code..
     ${morphStrs.join('\n')}
     ..shader code..
   `;
 }

通过字符串操作生成着色器是正常的做法。你会发现所有主要的3D库都这样做(three.js,unreal,unity,pixi.js,playcanvas等...)

至于分支是否缓慢,它实际上取决于GPU,但一般规则是肯定的,无论它如何完成它都会变慢。

您通常可以通过编写自定义着色器而不是尝试通用来避免分支。

而不是

uniform bool haveTexture;

if (haveTexture) {
   ...
} else {
   ...
}

只需编写2个着色器。一个有纹理,一个没有。

避免分支的另一种方法是通过数学创造性。例如,假设我们想要支持顶点颜色或纹理

varying vec4 vertexColor;
uniform sampler2D textureColor;

...

vec4 tcolor = texture2D(textureColor, ...);
gl_FragColor = tcolor * vertexColor;

现在我们只想要一个顶点颜色集textureColor到1x1像素的白色纹理。当我们只想要一个纹理时,请关闭vertexColor的属性并将该属性设置为白色gl.vertexAttrib4f(vertexColorAttributeLocation, 1, 1, 1, 1);和 bonus!,我们可以通过提供纹理和顶点颜色来调整vertexColors的纹理。

类似地,我们可以传入0或1来将某些事物乘以0或1以消除它们的影响。在变形示例中,定位性能的3d引擎会为不同数量的变形生成着色器。不关心性能的3d引擎会有1个支持N个变形目标的着色器,只需将任何未使用目标的权重设置为0即可。

另一种避免分支的方法是step函数,定义为

step(edge, x) {
  return x < edge ? 0.0 : 1.0;
}

因此,您可以选择ab

v = mix(a, b, step(edge, x));