GLSL矩阵/逆矩阵乘法精度问题

8
我正在尝试使用GPU进行布料模拟,但在不同硬件上遇到了一些问题。我使用threejs作为框架,但我认为这与我遇到的问题无关。
基本上,我上传一个矩阵和该矩阵的逆矩阵,以便将点从局部坐标转换为世界坐标系,对世界坐标系中的点进行一些数学计算(如碰撞检测),然后将它们转换回局部坐标系。当我在笔记本电脑上使用浮点纹理时,这非常有效,但是我注意到我的手机上存在一些奇怪的伪影。
经过一些调试,我将问题缩小到两个问题。两者都与十进制精度有关。由于约束(和约束期间的精度问题)而导致顶点崩溃,并且在使用矩阵乘法和逆矩阵时失去精度。
我相信问题与精度有关,因为如果我使用浮点纹理,则在我的计算机上可以正常工作,但是如果我使用半精度,我会遇到相同的问题。我的手机支持浮点纹理,这是我感到困惑的原因之一。我将问题缩小,以便所有布料模拟都被禁用,如果我在计算机上使用半精度纹理运行应用程序,没有重力但有转换和逆转,则平面会以奇怪的方式闪烁。
如果禁用转换和逆变,则看起来正常。
我已经想不出如何处理这个问题,也不确定是否正确。我相信半精度纹理具有有限的十进制精度,但我不明白为什么这会导致我的问题,因为它只应影响着色器的输出,而不是着色器中正在进行的数学运算。
着色器的代码如下:
    '   vec2 cellSize  = 1.0 / res;',
    '   vec4 pos = texture2D(vertexPositions, vuv.xy );',


    '   vec2 newUV;',
    '   if(type == 0.0){',
        '   float px = floor(vuv.x * res.x );',
        '   float spacingx = px- (2.0 * floor(px/2.0));',
        '   float py = floor(vuv.y * res.y );',
        '   float spacingy = py- (2.0 * floor(py/2.0));',
        '   float total = spacingx + spacingy;',
        '   total = total- (2.0 * floor(total/2.0));',

        '   if(total == 0.0){',
        '       newUV = vuv + (direction * cellSize);',
        '   }',
        '   else{',
        '       newUV = vuv - (direction * cellSize);',
        '   }',
    '   }',
    '   if(type == 1.0){',
        '   float px = floor(vuv.x * res.x );',
        '   float spacingx = px- (2.0 * floor(px/2.0));',

        '   float total = spacingx;',


        '   if(total == 0.0){',
        '       newUV = vuv + (direction * cellSize);',
        '   }',
        '   else{',
        '       newUV = vuv - (direction * cellSize);',
        '   }',
    '   }',






    '   vec4 totalDisplacement = vec4(0.0);',

    '           if(newUV.x > 0.0 && newUV.x < 1.0 && newUV.y > 0.0 && newUV.y < 1.0){ ',
    '               vec4 posOld = texture2D(vertexPositionsStart, vuv);' ,
    '               vec4 posOld2 = texture2D(vertexPositionsStart, newUV);' ,

    '               float targetDistance = length(posOld - posOld2);',
    '               vec4 newPos =  texture2D(vertexPositions, newUV);',
    '               float dx = pos.x - newPos.x;',
    '               float dy = pos.y - newPos.y;',
    '               float dz = pos.z - newPos.z;',
    '               float distance = sqrt(dx * dx + dy * dy + dz * dz);',
    '               float difference = targetDistance- distance;',
    '               float percent = difference / distance / 2.0;',
    '               float offsetX = dx * percent * rigid;',
    '               float offsetY = dy * percent * rigid;',
    '               float offsetZ = dz * percent * rigid;',
    '               totalDisplacement.x += offsetX;',
    '               totalDisplacement.y += offsetY;',
    '               totalDisplacement.z += offsetZ;',
    '           }',
    '       }',
    '   }',

    '   pos += totalDisplacement;',
    '   if(  vuv.x  > 1.0 - cellSize.x  && topConstrain == 1 ){',
    '       pos =transformation *  texture2D(vertexPositionsStart, vuv.xy );',
    '   }',

    '   if(  vuv.x  < cellSize.x  && bottomConstrain == 1 ){',
    '       pos =transformation *  texture2D(vertexPositionsStart, vuv.xy );',
    '   }',

    '   if(  vuv.y  < cellSize.y  && leftConstrain == 1 ){',
    '       pos =transformation *  texture2D(vertexPositionsStart, vuv.xy );',
    '   }',


    '   if(  vuv.y  > 1.0 - cellSize.y && rightConstrain == 1 ){',
    '       pos =transformation *  texture2D(vertexPositionsStart, vuv.xy );',
    '   }',




    '   gl_FragColor = vec4( pos.xyz , 1.0 );',

1
GLES的精度要求比桌面GL低得多(特别是当您使用GLES2时)。如果您的着色器ALU仍然使用较低的精度,即使您使用完整的fp32纹理也没有帮助。 - derhass
我明白了,你认为问题在于我的手机着色器ALU不支持足够的精度。但我不明白为什么如果我在电脑上使用半浮点纹理,这个问题仍然会发生。尽管如此,这似乎是一个合理的解释。 - Luple
1
尝试使用相对坐标系,使得转换后的顶点不会离矩阵原点太远。计算完成后再将其转回到原始坐标系。这样可以避免使用高幂次向量与矩阵相乘,从而避免精度问题。更多信息请参见光线和椭球体交点精度改进 - Spektre
谢谢你的建议,Spektre。我打算采用一种避免使用变换矩阵的方法。不过,我仍然在精度方面遇到了一些问题(我认为是精度问题)。在手机上,顶点会缓慢地向中心移动,而在电脑上则表现正常。虽然它们都应该支持高精度浮点数。 - Luple
所以最简单的方法是“尝试提高精度”。更好的方法是“寻找更好的(数值稳定等)算法”。 - user202729
1个回答

1
为了确保你的着色器创建高精度浮点计算变量,应该在顶点着色器的开头添加以下内容:
precision highp float;
precision highp int;

在片元着色器中,浮点数变量声明应该如下所示:
precision highp float;

浮点数误差在计算中被放大,如果您在计算中使用存储为浮点数的先前计算结果作为值。这些被称为中间值。
为了最小化这些误差,您应该限制着色器中执行的中间计算次数。例如,您可以完全展开对“newUV”的计算:
newUV = vuv + ( direction * ( 1.0 / res ) );

您还可以逐步完全展开totalDisplacement的计算,首先像这样替换offsetX

totalDisplacement.x += ( dx * percent * rigid )

现在将每个变量dxpercent代入上述公式:
totalDisplacement.x += ( ( pos.x - newPos.x ) * ( difference / distance / 2.0 ) * rigid )

现在您可以看到方程式可以进一步扩展,像这样替换 difference:
totalDisplacement.x += ( ( pos.x - newPos.x ) * ( ( targetDistance- distance ) / ( distance * 2.0 ) ) * rigid );

此时,您可以进行一些代数运算以简化并消除一些变量(除以distance)。通过简化上述方程,我们现在得到以下结果:

totalDisplacement.x += ( ( pos.x - newPos.x ) * ( ( targetDistance / ( distance * 2.0 ) - 0.5 ) * rigid );

最后,我们可以这样替换targetDistance的公式:
totalDisplacement.x += ( ( pos.x - newPos.x ) * ( ( length(posOld - posOld2) / ( distance * 2.0 ) - 0.5 ) * rigid );

而对于其他坐标,则分别如下:

totalDisplacement.y += ( ( pos.y - newPos.y ) * ( ( length(posOld - posOld2) / ( distance * 2.0 ) - 0.5 ) * rigid );
totalDisplacement.z += ( ( pos.z - newPos.z ) * ( ( length(posOld - posOld2) / ( distance * 2.0 ) - 0.5 ) * rigid );

显然,你可以继续操作,即通过替换 posOldposOld2newPos 的值来实现。请注意,到目前为止,为了得到上述方程,我们已经消除了需要在浮点变量中存储5个中间值的需求。此外,通过简化方程式(除以 distance),distance 变量仅在计算中使用一次。与最初的实现相比,distance 被用于计算 differencepercent。将所有内容合并到一个方程式中,可以简化并减少使用相同浮点值的次数。因此,还可以减少总的浮点误差。这里的权衡是生成的方程式不太易读。如果您好奇,还可以通过调用 glGetShaderPrecisionFormat 来检查给定着色器编译器的精度水平:
int range[2], precision;
glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_HIGH_FLOAT, range, &precision);

如果你检查你的应用程序的桌面端移动端版本的结果,你就可以比较这两个版本的精度差异。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接