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

多八度分形布朗运动形变平面的正确法线计算问题排查

问题描述

我使用OpenGL,通过函数$y = A \cdot e^{\sin(k \cdot (x\cos\theta + z\sin\theta) + \omega t) - 1}$对水平平面的顶点进行垂直位移以模拟水波。随后通过该函数对x和z的偏导数计算每个顶点的法线向量,构造切线和副法线向量后取叉积得到最终法线向量,以此计算漫反射和高光光照。单八度分形布朗运动时效果良好,但使用多八度时,画面开始出现黑斑及类似六边形斑块的视觉伪影。

效果对比

  • 1八度光照效果
  • 32八度光照效果
  • 1八度法线贴图
  • 32八度法线贴图

核心代码

初始平面顶点与索引生成(src/Application.cpp)

struct Vertex{
    float x;
    float y;
    float z;
};

// Plane vertices
std::vector<Vertex> vertices;
float size = 10.0f;
int subdivisions = 100;
float tilewidth = size / static_cast<float>(subdivisions);

for (int z = 0; z < subdivisions + 1; z++) {
    for (int x = 0; x < subdivisions + 1; x++) {
        vertices.push_back(Vertex{x * tilewidth, 0.0f, z * tilewidth});
    }
}

// Plane indices
std::vector<unsigned int> indices;
for (int z = 0; z < subdivisions; z++) {
    for (int x = 0; x < subdivisions; x++) {
        int index = (z * (subdivisions + 1)) + x;   
        indices.insert(indices.end(), {
            index,
            index + (subdivisions + 1) + 1,
            index + (subdivisions + 1),

            index, 
            index + 1,
            index + (subdivisions + 1) + 1,
        });
    }
}

顶点着色器(res/ocean_simple.shader)

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float time;

out vec3 normal;
out vec3 FragPos;

const int NUM_OCTAVES = 32;

#define pi 3.141592653589793

// Rotate a vec2
vec2 rotate(vec2 vec, float rot)
{
    float s = sin(rot), c = cos(rot);
    return vec2(vec.x*c-vec.y*s, vec.x*s+vec.y*c);
}

void main()
{
    float y = 0.0;
    float xz = 0.0;
    vec2 direction = vec2(cos(0.5), sin(0.5));  
    vec2 derivative = vec2(0.0, 0.0);
    float frequency = 1.0;
    float amplitude = 1.0;
    float timeMultiplier = 1.0;

    // iterate over all octaves
    for (int i = 0; i < NUM_OCTAVES; i++) {
        // (x,z) position dotted with our wave direction gives us a scalar to feed into our e^(sin(x)-1), making the wave go in that direction
        xz = dot(aPos.xz, direction);   
        // increase y height 
        y += amplitude * exp(sin(xz * frequency + time * timeMultiplier) - 1.0);    
        // increase partial derivatives with respect to x and z (i think these are correct partial derivatives?)
        derivative += amplitude * frequency * direction * cos(xz * frequency + time * timeMultiplier) * exp(sin(xz * frequency + time * timeMultiplier) - 1.0);
        // change frequency and amplitude and wave direction for next octave - fractal brownian motion effect
        amplitude *= 0.5;
        frequency *= 2.0;
        direction = rotate(direction, 0.125*pi);
    }

    // Normal vector is cross of tangent and binomrial vector
    normal = normalize(cross(vec3(1.0, derivative.x, 0.0), vec3(0.0, derivative.y, -1.0)));

    gl_Position = projection * view * model * vec4(aPos.x, y, aPos.z, 1.0);
    FragPos = vec3(model * vec4(aPos.x, y, aPos.z, 1.0));
}

片段着色器(res/ocean_simple.shader)

#version 330 core
out vec4 FragColor;
in vec3 normal;
in vec3 FragPos;

uniform vec3 viewPos;

vec3 sunDirection = normalize(vec3(cos(0.5), 0.5, sin(0.5)));

void main()
{
    // diffuse is dot product of normal vector and sun direction - lamberts cosine law  
    float diffuse = max(dot(normal, sunDirection), 0.0);

    // specular highlights
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-sunDirection, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    
    FragColor = vec4(65, 107, 223, 225) / 255.0 * (diffuse + spec);
}

请问是什么原因导致法线向量出现此类六边形黑斑伪影?


问题分析与解决

核心原因:数值精度丢失+周期性方向叠加+法线构造逻辑瑕疵

  1. 高八度下的单精度浮点数精度崩溃
    当开启32个八度时,高频分量的frequency会达到$2^{31}$级别,结合平面顶点aPos.xz的取值范围(0~10),计算出的xz * frequency会远超单精度浮点数的有效精度范围(单精度有效位数仅约7位)。此时sin、cos函数的计算会出现严重的精度丢失,导数derivative值产生随机跳变或错误,直接导致法线向量方向突变,表现为画面中的黑斑。

  2. 方向向量旋转的周期性六边形叠加
    每个八度中你对方向向量固定旋转0.125π(22.5度),16次旋转后向量会回到初始方向。这种固定周期的旋转会让多八度的方向向量形成对称的六边形分布,导致导数分量在特定区域出现规律性的抵消或叠加,使得法线向量出现一致的错误方向,最终形成六边形斑块。

  3. 切线与副法线构造的逻辑问题
    对于函数$y = f(x,z)$,正确的x方向切线应为$\vec{T} = (1, \frac{\partial f}{\partial x}, 0)$,z方向副法线应为$\vec{B} = (0, \frac{\partial f}{\partial z}, 1)$,法线是$\vec{T} \times \vec{B}$的归一化结果。你当前的副法线z分量为-1,会导致法线方向反转;同时,当导数计算出现精度错误时,这种构造会进一步放大错误的影响。

解决建议

  • 限制八度数量:单精度环境下,八度数建议不超过8~10个,此时frequency最大值为512,xz*frequency的结果仍在单精度有效精度范围内。
  • 打破方向旋转周期性:改用随机角度或非整数倍的旋转角度,避免方向向量形成对称的六边形分布模式。
  • 修正法线计算逻辑:使用正确的切线、副法线构造方式,确保法线方向准确;也可以考虑在片段着色器中用中心差分法重新计算法线,规避顶点着色器的精度丢失传递。
  • 启用高精度计算:在顶点着色器中改用double类型进行计算(GLSL 330及以上支持),大幅提升高频率分量的计算精度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 08:14:52