You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

水波纹法线计算异常:正弦和近似实现中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);

}

问题原因分析

  1. 公式符号遗漏: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}$的部分,没有添加公式要求的负号,因此必须手动取反才能得到正确的法线方向。
  2. 坐标系匹配问题:如果你的场景采用左手坐标系,而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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 08:36:22