我在 GLSL 中实现了梯度噪声函数,以便生成地形的高度图。我试图为高度图的每个像素获取法线向量,以便我可以计算来自“太阳”的定向照明。该法线向量将与地形高度一起存储在纹理中,因此我们可以假设平坦表面的法线为
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 空间中的表面法线所需的所有信息。我已经成功地通过采样靠近像素的 2 个点并仅根据噪声数据估计梯度来做到这一点,这在一定精度下给出了相当好的结果,但我想准确地计算它,因为我已经有了梯度数据可用。
这意味着我有估计版本可以与直接计算的结果进行比较,并且我已经尝试了很多方法来计算法线,但到目前为止,我所做的一切都与估计相一致。我不能确定这个估计绝对是更正确的版本,但与高度图相比,它看起来更有希望,并且给出了迄今为止我所见过的最佳视觉结果,所以我假设它可能是我管理过的最好的。
这里有两次从 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;
}
我可以通过将它们一起显示来比较结果。这些图像都显示(从左到右,然后从上到下):
[从此处编辑]
第一次正常计算:
第二次正常计算:
两个计算结果与估计结果有很大不同。
使用梯度->正常函数之一计算照明会产生没有意义的结果,因为我可以在地形高度/颜色细节中看到细节,而根据估计计算出的图像中的照明看起来是有意义的.
想象一下,您有一个 3d 函数,即表面上方的高度。
f(x, y, z) = z - surface_height(x, y)
如果你取该函数的梯度,你将得到 ℝ³ 而不是 ℝ² 中的向量。