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

顶点形变后如何计算法线?OpenGL着色器技术问询

动态水面法线计算方案

针对你遇到的顶点非线性形变后法线错误的问题,以下是两种可行的解决方法,均基于纯GLSL实现,无需依赖Unity等引擎:

方法1:片段着色器中用有限差分计算法线(推荐,无需额外顶点数据)

核心思路是对当前片段的位置,在X/Z方向取微小偏移,计算偏移后的形变位置,通过两个切线向量的叉积得到正确法线。

步骤1:提取可复用的形变函数

在顶点着色器和片段着色器中都定义完全相同的水面形变函数,确保顶点和片段的形变逻辑一致:

// 顶点/片段着色器中都添加此函数
vec4 deformWater(vec4 pos, float timeFactor) {
    vec3 temp = random2(vec3(pos.x, pos.y, pos.z));
    temp = (temp - 0.5) / 25.0;
    return vec4(
        mix(pos.x - temp.x, pos.x + temp.x, timeFactor),
        mix(pos.y - temp.y, pos.y + 3.0 * temp.y, timeFactor),
        mix(pos.z - temp.z, pos.z + temp.z, timeFactor),
        pos.w
    );
}

步骤2:修改顶点着色器调用形变函数

将原顶点着色器中water分支的代码替换为调用上述函数:

if (water) {
    float t = (cos(u_Time * 0.05) + 1.0) / 2.0;
    modelposition = deformWater(modelposition, t);
}

步骤3:片段着色器中计算法线

在片段着色器的water分支里,替换原Blinn-Phong部分的法线计算逻辑:

else if (water) {
    // ... 保留原纹理混合代码 ...

    // 计算水面法线
    float eps = 0.01; // 微小偏移量,根据场景缩放调整
    float t = (cos(u_Time * 0.05) + 1.0) / 2.0;

    // 获取当前点及偏移点的形变后位置
    vec4 p = deformWater(fs_Pos, t);
    vec4 pX = deformWater(vec4(fs_Pos.x + eps, fs_Pos.y, fs_Pos.z, fs_Pos.w), t);
    vec4 pZ = deformWater(vec4(fs_Pos.x, fs_Pos.y, fs_Pos.z + eps, fs_Pos.w), t);

    // 计算切线与副切线
    vec3 tangent = normalize(pX.xyz - p.xyz);
    vec3 bitangent = normalize(pZ.xyz - p.xyz);
    // 叉积得到法线,确保方向正确
    vec3 normal = normalize(cross(tangent, bitangent));
    // 与原始法线点乘修正方向,避免翻转
    normal *= sign(dot(normal, normalize(fs_Nor.xyz)));

    // Blinn-Phong光照计算(使用新计算的法线)
    vec4 lightDir = normalize(fs_LightVec - fs_Pos);
    vec4 viewDir = normalize(fs_CamPos - fs_Pos);
    vec4 halfVec = normalize(lightDir + viewDir);
    float shininess = 400.0;
    specularIntensity = max(pow(dot(halfVec, vec4(normal, 0.0)), shininess), 0.0);
}

方法2:基于切线/副切线的法线计算

如果你的模型网格带有切线数据,或可以在顶点着色器中预计算切线,可通过以下方式实现:

步骤1:顶点着色器传递切线/副切线

在顶点着色器中添加输出变量,并计算切线(以XZ平面为例,默认切线为(1,0,0),副切线为(0,0,1)):

// 顶点着色器输出
out vec3 fs_Tangent;
out vec3 fs_Bitangent;

void main() {
    // ... 保留原有代码 ...

    // 计算模型空间下的切线与副切线
    vec3 tangent = mat3(u_Model) * vec3(1.0, 0.0, 0.0);
    vec3 bitangent = mat3(u_Model) * vec3(0.0, 0.0, 1.0);
    fs_Tangent = tangent;
    fs_Bitangent = bitangent;
}

步骤2:片段着色器中更新切线并计算法线

在片段着色器中,对切线和副切线应用相同的形变偏移,再叉积得到法线:

// 片段着色器输入
in vec3 fs_Tangent;
in vec3 fs_Bitangent;

// 在water分支中
float eps = 0.01;
float t = (cos(u_Time * 0.05) + 1.0) / 2.0;

// 计算切线的形变后偏移
vec4 tangentPos = deformWater(vec4(fs_Pos.xyz + fs_Tangent * eps, 1.0), t);
vec3 deformedTangent = normalize(tangentPos.xyz - deformWater(fs_Pos, t).xyz);

// 计算副切线的形变后偏移
vec4 bitangentPos = deformWater(vec4(fs_Pos.xyz + fs_Bitangent * eps, 1.0), t);
vec3 deformedBitangent = normalize(bitangentPos.xyz - deformWater(fs_Pos, t).xyz);

// 叉积得到法线
vec3 normal = normalize(cross(deformedTangent, deformedBitangent));
normal *= sign(dot(normal, normalize(fs_Nor.xyz)));

注意事项

  • 确保顶点和片段的形变函数完全一致,否则法线会出现偏差
  • 偏移量eps需根据场景缩放调整,过大导致法线不平滑,过小会引入精度误差
  • 若水面朝向非Y轴,需调整偏移的轴向(比如Y/Z方向)

内容的提问来源于stack exchange,提问作者ngurnard

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 21:25:39