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

从体素高度图生成正则网格法线的技术问题求助

高度图体素场景法线计算问题修正指南

问题背景

开发体素游戏时,以高度图构建关卡,为预计算光线追踪环境遮挡,生成了存储遮挡项的3D正则网格(X/Z范围覆盖整个关卡,Y范围从0到体素最大高度)。为计算每个点的遮挡项,需先得到表面法线,以确定发射光线的半球空间。目前法线计算函数存在错误,部分结果不符合预期。

当前实现与问题

现有代码

// 输入为x、y(高度值)、z
const byte left_x = (x == 0) ? 0 : (x - 1), top_z = (z == 0) ? 0 : (z - 1);
    
#define SIGN_DIFF(a, b) sign_of((GLfloat) (a) - (GLfloat) (b))

/*
采样邻域:
| a | b |
| c | d |
*/

const byte
    a = sample_heightmap(heightmap, left_x, top_z),
    b = sample_heightmap(heightmap, x, top_z),
    c = sample_heightmap(heightmap, left_x, z),
    d = sample_heightmap(heightmap, x, z);

vec3 normal = {
    SIGN_DIFF(c, d),
    y == a || y == b || y == c || y == d,
    SIGN_DIFF(b, d)
};

normalize(normal);

#undef SIGN_DIFF

现有逻辑

  1. 取当前点与左、上邻点的高度差符号,作为法线X、Z分量;
  2. 若当前点Y值等于任意采样邻点高度,法线Y分量设为1,否则设为0;
  3. 最后归一化法线。

错误表现

部分法线方向异常:

  • 下方错误法线指向-x、+y、z=0方向;
  • 上方错误法线指向-x、y=0、z=0方向。

核心错误分析

  1. 梯度信息缺失:仅用单方向(左、上)的高度差符号作为分量,完全忽略高度差的幅度,也没有考虑右、下方向的地形变化,导致坡度变化和边缘处的法线计算失真。
  2. Y分量判定逻辑错误:用“当前Y是否等于邻点高度”来决定Y分量是1还是0,完全违背地形法线的物理意义——倾斜地形的表面法线Y分量必然小于1,而空空间的点直接设为0会导致水平法线,不符合实际需求。
  3. 边界处理粗糙:X/Z为0时直接用0代替邻点,没有做镜像或扩展采样,导致边缘区域法线计算错误。

修正方案

标准高度图法线计算方法

高度图的法线应通过计算地形梯度向量推导,步骤如下:

  1. 3x3邻域采样:对每个点,采样周围3x3范围内的高度(边界点用镜像采样避免信息缺失);
  2. 计算梯度:用中心差分或索贝尔算子计算X、Z方向的高度梯度,保留高度变化的幅度信息;
  3. 推导法线:通过梯度向量的垂直向量得到法线,保证方向正确指向地形外侧;
  4. 归一化:对法线向量做归一化处理。

修正后代码示例

// 定义网格Y轴缩放系数(根据高度图实际高度比例调整)
#define HEIGHT_SCALE 1.0f

// 边界采样处理:镜像采样,避免边缘信息缺失
int clamp_sample(int coord, int max_coord) {
    if (coord < 0) return -coord;
    if (coord > max_coord) return max_coord - (coord - max_coord);
    return coord;
}

vec3 calculate_normal(byte* heightmap, int x, int z, int max_x, int max_z) {
    // 采样3x3邻域的高度值
    int x_left = clamp_sample(x-1, max_x);
    int x_right = clamp_sample(x+1, max_x);
    int z_top = clamp_sample(z+1, max_z);
    int z_bottom = clamp_sample(z-1, max_z);

    float nw = (float)sample_heightmap(heightmap, x_left, z_top);
    float n = (float)sample_heightmap(heightmap, x, z_top);
    float ne = (float)sample_heightmap(heightmap, x_right, z_top);
    float w = (float)sample_heightmap(heightmap, x_left, z);
    float e = (float)sample_heightmap(heightmap, x_right, z);
    float sw = (float)sample_heightmap(heightmap, x_left, z_bottom);
    float s = (float)sample_heightmap(heightmap, x, z_bottom);
    float se = (float)sample_heightmap(heightmap, x_right, z_bottom);

    // 用索贝尔算子计算X、Z方向梯度
    float dx = (ne + 2*e + se) - (nw + 2*w + sw);
    float dz = (nw + 2*n + ne) - (sw + 2*s + se);

    // 构造法线向量:梯度的垂直向量,平衡Y轴权重
    vec3 normal = {
        -dx * HEIGHT_SCALE,
        2.0f,
        -dz * HEIGHT_SCALE
    };

    // 归一化法线
    normalize(normal);

    return normal;
}

额外处理建议

  • 仅计算表面点法线:只对高度图表面的点(Y等于对应地形高度)计算法线,空空间的点无需存储法线,节省内存;
  • 法线平滑:对相邻表面点的法线进行加权平均,可减少光照的锯齿感;
  • 边缘点特殊处理:除了镜像采样,也可以采用重复采样或边缘梯度修正,进一步提升边缘法线的准确性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 12:36:25