【问题标题】:Generating a normal map from a height map?从高度图生成法线贴图?
【发布时间】:2011-03-12 07:26:05
【问题描述】:

我正在为一款视频游戏使用随机分形以程序方式生成泥土。我已经使用中点置换算法生成了一个高度图并将其保存到纹理中。对于如何将其转化为法线纹理,我有一些想法,但我们将不胜感激。

我的高度纹理目前是 257 x 257 灰度图像(高度值为可见性目的而缩放):

我的想法是图像的每个像素代表 256 x 256 网格中的一个格坐标(因此,为什么有 257 x 257 高度)。这意味着坐标 (i, j) 处的法线由 (i, j)、(i, j + 1)、(i + 1, j) 和 (i + 1, j + 1) 处的高度确定)(分别称为 A、B、C 和 D)。

因此,鉴于 A、B、C 和 D 的 3D 坐标,是否有意义:

  1. 将四个分成两个三角形:ABC 和 BCD
  2. 通过叉积计算这两个面的法线
  3. 分成两个三角形:ACD 和 ABD
  4. 计算这两个面的法线
  5. 平均四个法线

...或者有没有我想念的更简单的方法?

【问题讨论】:

  • 我会做类似的事情,只是我会使用 4 个点 (i, j+1), (i+1, j), (i, j-1) 和 (i-1, j) 计算法线,使 (i, j) 在它们的中心。无论如何,我认为你是在正确的方式:)
  • 对于“更简单”的方法,您需要知道什么函数描述 x,y 处的高度......类似于 h = f(x,y),然后您可以从那里导出法线函数x,y... 除非你有这个功能,否则你的方法是你能做的最好的;)

标签: c++ opengl graphics


【解决方案1】:

我的水面渲染着色器中的示例 GLSL 代码:

#version 130
uniform sampler2D unit_wave
noperspective in vec2 tex_coord;
const vec2 size = vec2(2.0,0.0);
const ivec3 off = ivec3(-1,0,1);

    vec4 wave = texture(unit_wave, tex_coord);
    float s11 = wave.x;
    float s01 = textureOffset(unit_wave, tex_coord, off.xy).x;
    float s21 = textureOffset(unit_wave, tex_coord, off.zy).x;
    float s10 = textureOffset(unit_wave, tex_coord, off.yx).x;
    float s12 = textureOffset(unit_wave, tex_coord, off.yz).x;
    vec3 va = normalize(vec3(size.xy,s21-s01));
    vec3 vb = normalize(vec3(size.yx,s12-s10));
    vec4 bump = vec4( cross(va,vb), s11 );

结果是一个凹凸向量:xyz=normal, a=height

【讨论】:

  • @Toolbox:这确实是一个非常好的方法。您不需要通过 CPU 计算法线,只需使用 GLSL 将其传递给 GPU。我相信这种将高度图用作法线的方法称为Bump mapping。所以我投票给@kvark!它与顶点和分裂成三角形无关。
  • 这是一个很好的解决方案,但我必须做出一些改变:vec3 va = normalize(vec3(size.x, s21-s01, size.y)); vec3 vb = normalize(vec3(size.y, s12-s10, -size.x)); 虽然切换 Y 和 Z 没什么大不了的,但我觉得有趣的是我必须减去 s21-s01 而不是s21-s11 如示例所示。我还不得不在vb 中否定size.x
  • 只是出于好奇,根据您的经验,仅从准备好的法线贴图计算凹凸贴图,还是从片段着色器中的高度图动态获取法线更有效?如果这种方式更快,我不会感到惊讶,因为您读取的纹理数据比使用法线贴图少 4 倍,而且您甚至不需要切线或副法线作为插值或顶点属性。另一方面,这种方法在片段阶段的 ALU 和 SF 上更重......
  • @bigD 传递切线/位切线与从高度图导出法线无关 - 这是高度/法线图解释的问题。高度图用于水,因为它是一些模拟算法的输出。对于其他情况,常规法线贴图更有效。
  • @kvark 是的,对不起,你是对的。您通常仍然需要传递切线/副法线,或四元数或其他东西。虽然您可以使用 glsl 内置的导数函数从纹理坐标(对于法线贴图或高度贴图)派生方向信息,但这实际上可能有一些严重的视图相关错误/不准确......
【解决方案2】:

我的想法是图像的每个像素代表 256 x 256 网格中的一个格坐标(因此,为什么有 257 x 257 高度)。这意味着坐标 (i, j) 处的法线由 (i, j)、(i, j + 1)、(i + 1, j) 和 (i + 1, j + 1) 处的高度确定)(分别称为 A、B、C 和 D)。

没有。图像的每个像素代表网格的一个顶点,所以直观地说,从对称性来看,它的法线是由相邻像素的高度决定的 (i-1,j), (i+1,j), (i,j-1) , (i,j+1)。

给定一个函数 f : ℝ2 → ℝ 描述 ℝ3 中的表面,(x,y) 处的法线单位为

v = (−∂f/∂x, −∂f/∂y, 1) 和 n = v/|v|。

可以证明,两个样本对 ∂f/∂x 的最佳近似是:

∂f/∂x(x,y) = (f(x+ε,y) − f(x−ε,y))/(2ε)

要获得更好的近似值,您至少需要使用四个点,因此添加第三个点(即 (x,y))不会改善结果。

您的高度图是常规网格上某个函数 f 的采样。取 ε=1 你得到:

2v = (f(x−1,y) − f(x+1,y), f(x,y−1) − f(x,y+1), 2)

将其放入代码如下所示:

// sample the height map:
float fx0 = f(x-1,y), fx1 = f(x+1,y);
float fy0 = f(x,y-1), fy1 = f(x,y+1);

// the spacing of the grid in same units as the height map
float eps = ... ;

// plug into the formulae above:
vec3 n = normalize(vec3((fx0 - fx1)/(2*eps), (fy0 - fy1)/(2*eps), 1));

【讨论】:

    【解决方案3】:

    一种常见的方法是使用Sobel 过滤器在每个方向上进行加权/平滑导数。

    首先对每个纹素周围 3x3 的高度区域进行采样(这里,[4] 是我们想要法线的像素)。

    [6][7][8]
    [3][4][5]
    [0][1][2]
    

    那么,

    //float s[9] contains above samples
    vec3 n;
    n.x = scale * -(s[2]-s[0]+2*(s[5]-s[3])+s[8]-s[6]);
    n.y = scale * -(s[6]-s[0]+2*(s[7]-s[1])+s[8]-s[2]);
    n.z = 1.0;
    n = normalize(n);
    

    可以调整scale 以匹配高度图真实世界的深度(相对于其大小)。

    【讨论】:

    • 谢谢。作为我职位的人的注意事项,我将其作为 MacOS 的核心图像过滤器来实现,并不断得到最奇怪的结果——每次我用相同的图像运行过滤器时,它们都是不同的。事实证明,有些人可能会认为这是一个新手错误——我是内核新手。它不喜欢公式中的数字文字。我做了一个常数const float d = 2.0 并用它代替了计算中的 2 和 bam,它工作得很好。
    • 不幸的是,这个算法制作了一个完全不同的图像,它检测到边缘但平坦区域是不同的颜色(在正常的法线贴图中,它主要是平坦的蓝色)我想留下评论,因为我浪费了移植时间
    • @Richard 尝试将结果从 [-1, 1] 范围缩放到 [0, 1],即n * 0.5 + 0.5
    【解决方案4】:

    如果您将每个像素视为顶点而不是面,则可以生成简单的三角形网格。

    +--+--+
    |\ |\ |
    | \| \|
    +--+--+
    |\ |\ |
    | \| \|
    +--+--+
    

    每个顶点都有一个 x 和 y 坐标,对应于地图中像素的 x 和 y。 z 坐标基于该位置的地图中的值。三角形可以通过它们在网格中的位置显式或隐式生成。

    你需要的是每个顶点的法线

    顶点法线可以通过对在该点相交的每个三角形的表面法线进行面积加权平均来计算。

    如果您有一个顶点为v0v1v2 的三角形,那么您可以使用向量叉积(位于三角形两侧的两个向量)来计算向量法线的方向并与三角形的面积成比例。

    Vector3 contribution = Cross(v1 - v0, v2 - v1);
    

    每个不在边上的顶点将由六个三角形共享。您可以遍历这些三角形,对contributions 求和,然后对向量和进行归一化。

    注意:您必须以一致的方式计算叉积,以确保法线都指向同一方向。始终以相同的顺序(顺时针或逆时针)选择两侧。如果您将其中一些混合在一起,这些贡献将指向相反的方向。

    对于边缘上的顶点,您最终会得到较短的循环和许多特殊情况。在您的假顶点网格周围创建一个边界,然后计算内部顶点的法线并丢弃假边界可能更容易。

    for each interior vertex V {
      Vector3 sum(0.0, 0.0, 0.0);
      for each of the six triangles T that share V {
        const Vector3 side1 = T.v1 - T.v0;
        const Vector3 side2 = T.v2 - T.v1;
        const Vector3 contribution = Cross(side1, side2);
        sum += contribution;
      }
      sum.Normalize();
      V.normal = sum;
    }
    

    如果您需要三角形上特定点的法线(除了其中一个顶点),您可以通过点的重心坐标对三个顶点的法线进行加权来进行插值。这就是图形光栅化器如何处理法线的阴影。它允许三角形网格看起来像光滑的曲面,而不是一堆相邻的平面三角形。

    提示:对于您的第一次测试,请使用完全平坦的网格并确保所有计算出的法线都指向正上方。

    【讨论】:

    • @Adian McCarthy - 不幸的是,我认为我买不起真正的网格,特别是因为它只是用于平坦的泥土。不过还是谢谢你的解释!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-17
    • 2023-03-11
    • 1970-01-01
    • 2012-04-22
    • 2011-01-23
    • 2010-10-08
    • 2023-02-19
    相关资源
    最近更新 更多