水波纹法线计算异常:正弦和近似实现中X/Z轴法线翻转排查
正弦波叠加水面法线X/Z轴翻转问题排查
我实现了NVIDIA GPU Gems文章《Effective Water Simulation from Physical Models》中的「正弦波叠加近似(The Sum of Sines Approximation)」方法,水面几何效果正常,但法线存在异常——X轴与Z轴数值翻转。只有手动对X、Z取反后法线才恢复正常,现寻求实现过程中的问题原因。
参考的法线计算公式如下:
- $n_x = -\sum_{i=1}^N k_i A_i d_{ix} \left( \frac{\sin(\theta_i)+1}{2} \right)^{p-1} \cos(\theta_i)$
- $n_z = -\sum_{i=1}^N k_i A_i d_{iz} \left( \frac{\sin(\theta_i)+1}{2} \right)^{p-1} \cos(\theta_i)$
- $n_y = 1 - \sum_{i=1}^N p k_i A_i \left( \frac{\sin(\theta_i)+1}{2} \right)^{p-1} \sin(\theta_i)$
其中$\theta_i = k_i (d_{ix}x + d_{iz}z) + \omega_i t$,$k_i$为频率,$A_i$为振幅,$d_i$为方向向量,$\omega_i$为速度,$p$为正弦指数。
当前着色器中的法线生成代码:
vec3 generateWaveSineSumNormal(sineParams _params[sineCount]) { vec2 pos = vec2(aPos.x, aPos.z); vec3 normal = vec3(0.0f, 1.0f, 0.0f); for(int i=0; i<sineCount; i++) { sinParams curParams = _params[i]; normal.x += sineExponent * curParams.direction.x * curParams.frequency * curParams.amplitude * pow((sin(dot(curParams.direction, pos) * curParams.frequency + curTime * curParams.speed)+1)/2, sineExponent-1) * cos(dot(curParams.direction, pos) * curParams.frequency + curTime * curParams.speed); normal.z += sineExponent * curParams.direction.y * curParams.frequency * curParams.amplitude * pow((sin(dot(curParams.direction, pos) * curParams.frequency + curTime * curParams.speed)+1)/2, sineExponent-1) * cos(dot(curParams.direction, pos) * curParams.frequency + curTime * curParams.speed); } return vec3(-normal.x, normal.y, -normal.z); }
问题原因分析
- 公式符号遗漏:GPU Gems中的法线公式明确在$n_x$和$n_z$分量前带有负号,这是因为水面法线的计算基于高度场的梯度取反——法线方向为$\vec{n} = \left( -\frac{\partial h}{\partial x}, 1, -\frac{\partial h}{\partial z} \right)$($h(x,z)$为水面高度函数)。你的代码在累加$normal.x$和$normal.z$时,直接计算的是梯度$\frac{\partial h}{\partial x}$和$\frac{\partial h}{\partial z}$的部分,没有添加公式要求的负号,因此必须手动取反才能得到正确的法线方向。
- 坐标系匹配问题:如果你的场景采用左手坐标系,而GPU Gems默认使用右手坐标系,也可能导致符号翻转,但从代码与公式的直接对比来看,核心问题是公式中的负号未在累加阶段体现。
修正后的代码示例
vec3 generateWaveSineSumNormal(sineParams _params[sineCount]) { vec2 pos = vec2(aPos.x, aPos.z); vec3 normal = vec3(0.0f, 1.0f, 0.0f); for(int i=0; i<sineCount; i++) { sineParams curParams = _params[i]; float theta = dot(curParams.direction, pos) * curParams.frequency + curTime * curParams.speed; float powTerm = pow((sin(theta) + 1.0) / 2.0, sineExponent - 1.0); float coeff = sineExponent * curParams.frequency * curParams.amplitude; // 按公式添加负号,直接计算正确的法线分量 normal.x -= coeff * curParams.direction.x * powTerm * cos(theta); normal.z -= coeff * curParams.direction.y * powTerm * cos(theta); // 补充y分量的修正(原代码未处理,物理上更准确) normal.y -= sineExponent * curParams.amplitude * curParams.frequency * powTerm * sin(theta); } // 无需再手动取反,直接返回归一化后的法线 return normalize(normal); }
内容的提问来源于stack exchange,提问作者user13310405
相关产品推荐
相关产品推荐

