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

如何从Perlin梯度噪声的2D梯度计算3D表面法向量

GLSL梯度噪声生成地形高度图的3D法向量计算问题

需求背景

我在GLSL中实现了梯度噪声函数,用于生成地形高度图。需要获取高度图每个像素的3D法向量,以此计算来自“太阳”的方向光。法向量会与地形高度一同存储在纹理中,平坦表面的法向量为vec3(0, 0, -1);(z轴方向)。

梯度噪声实现代码

梯度本身是2D向量,目前先使用单八度噪声测试,每个像素的噪声和梯度生成代码如下:

vec3 random_pcg3d(uvec3 v)
{
    v = v * 1664525u + 1013904223u;
    v.x += v.y*v.z; v.y += v.z*v.x; v.z += v.x*v.y;
    v ^= v >> 16u;
    v.x += v.y*v.z; v.y += v.z*v.x; v.z += v.x*v.y;
    return vec3(v) * (1.0/float(0xffffffffu));
}

vec2 random_gradient(uvec3 p)
{
    vec3 uv = random_pcg3d(p);
    float r = sqrt(uv[0]);
    float phi = 2.0 * M_PI * uv[1];
    return vec2(r * cos(phi), r * sin(phi));
}

vec3 gradient_noise(vec2 pos, float gridSize)
{
    vec2 gridPos = pos * gridSize;
    uvec2 i = uvec2(gridPos);
    vec2 f = fract(gridPos);

    vec2 g11 = random_gradient(uvec3(i.x, i.y, 1));
    vec2 g12 = random_gradient(uvec3(i.x + 1u, i.y, 1));
    vec2 g21 = random_gradient(uvec3(i.x, i.y + 1u, 1));
    vec2 g22 = random_gradient(uvec3(i.x + 1u, i.y + 1u, 1));

    float d11 = dot(g11, f);
    float d12 = dot(g12, f - vec2(1.0, 0.0));
    float d21 = dot(g21, f - vec2(0.0, 1.0));
    float d22 = dot(g22, f - vec2(1.0, 1.0));

    f = smoothstep(0.0, 1.0, f);

    vec2 gradient_lerp_x0 = mix(g11, g12, f.x);
    vec2 gradient_lerp_x1 = mix(g21, g22, f.x);
    vec2 gradient  = mix(gradient_lerp_x0, gradient_lerp_x1, f.y);

    float q1 = mix(d11, d12, f.x);
    float q2 = mix(d21, d22, f.x);
    float noise = mix(q1, q2, f.y);

    return vec3(noise, gradient.x, gradient.y);
}

这段代码是对一个未包含梯度输出的YouTube教程修改而来,我已查证资料,确认梯度可以像噪声值一样进行插值。

问题核心

我确定现有梯度数据足够用来精确计算3D表面法向量。目前通过采样像素附近两点、仅用噪声数据估计梯度的方法,得到了匹配度更高、光照效果更合理的法向量结果。但尝试直接用生成噪声时得到的梯度计算法向量,结果都与估计值相差甚远,对应的光照效果也和地形高度/颜色细节不匹配。

两次法向量计算尝试代码

以下是两次从2D梯度计算3D法向量的尝试,第一次包含几个注释掉的3D梯度方向计算尝试,问题可能出在此处:

vec3 calculate_surface_normal(float height, vec2 gradient)
{
    vec2 tangent_xy = normalize(gradient);
//    vec2 tangent_xy = vec2(1.0) / gradient;
    float tangent_z = length(gradient);
//    float tangent_z = 1;
//    float tangent_z = sqrt(1.0 - dot(tangent_xy, tangent_xy));

    vec3 gradient_forward = normalize(vec3(tangent_xy, tangent_z));
    vec3 gradient_right = vec3(rotate_by_phase(normalize(gradient), 0.25), 0);
    vec3 gradient_up = cross(gradient_right, gradient_forward);

    return gradient_up;
}

vec3 calculate_surface_normal(float height, vec2 gradient)
{
    // Compute the partial derivatives using finite differencing
    float dz_dx = gradient.x;
    float dz_dy = gradient.y;

    // Compute the surface normal using cross product
    vec3 tangent = normalize(vec3(1.0, 0.0, dz_dx));
    vec3 bitangent = normalize(vec3(0.0, 1.0, dz_dy));
    vec3 normal = normalize(cross(bitangent, tangent));

    // Negate z as up is simply -z
    normal.z *= -1.f;

    return normal;
}

结果对比

可通过图片对比验证,图片从左到右、从上到下依次为:

  • 噪声值对应的高度图
  • 从噪声生成时的梯度计算出的法向量(法向量轴映射为RGB)
  • 通过对比附近区域高度值估计出的法向量(法向量轴映射为RGB)
  • 根据高度着色并使用估计法向量计算光照的地形(高于水位时)

第一次法向量计算结果:
First normal calculation

第二次法向量计算结果:
Second normal calculation

两次计算结果都与估计值差异很大,用梯度转法向量函数算出的光照效果和地形细节不匹配,而估计值的效果更合理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 07:20:09