Heightmap数据读入C++需先用stbi_load加载单通道灰度图,归一化像素值后乘height_scale;确保图像尺寸为N+1×N+1以生成连续网格,按行优先填充顶点,用双层循环生成索引并注意绕序,法线用中心差分计算并妥善处理边界,大地图必须实现LOD避免性能崩溃。

Heightmap 数据怎么读进 C++ 程序
高度图本质是二维灰度图像(如 .png 或 .raw),每个像素值代表对应 (x, z) 位置的 y 坐标。别直接用 OpenCV 或 stb_image 做“图像处理”,重点是**按网格索引映射到顶点坐标**。
常见错误:把像素值当绝对高度,导致地形缩放失控;或忽略图像坐标系(左上为原点)与 OpenGL/DirectX 世界坐标的 Y/Z 轴方向差异。
- 用
stbi_load读.png时指定channels=1,确保拿到单通道灰度数据(unsigned char*) - 把像素值归一化到
[0.0f, 1.0f]后,再乘以你设定的height_scale(比如 100.0f),避免地形太平或太陡 - 图像宽高必须是
N+1(如 257×257),才能生成N×N个四边形——这是生成连续网格的硬性前提
如何组织顶点和索引缓冲区(OpenGL/Vulkan 场景)
一个 257×257 高度图应生成 256×256 个正方形,每个正方形拆成两个三角形,共 256×256×2 = 131072 个三角形 → 393216 个顶点索引。但顶点可复用,实际只需 257 * 257 个唯一顶点。
别用 std::vector<:vec3></:vec3> 动态 push —— 内存不连续、GPU 上传慢;也别手动写六重嵌套循环生成索引。
立即学习“C++免费学习笔记(深入)”;
- 顶点数组按行优先填充:
vertices[i * width + j] = glm::vec3(j * spacing, height_data[i * width + j], i * spacing) - 索引生成用双层循环:
for (int i = 0; i - 索引顺序必须匹配渲染 API 的 winding order(OpenGL 默认 CCW,Vulkan 默认 CW),否则背面剔除会吃掉整个地形
法线怎么算才不锯齿、不翻车
用中心差分(central difference)对高度图求偏导是最稳的:法线 ≈ (-∂h/∂x, 1, -∂h/∂z),再单位化。千万别用相邻三角面平均法线——在平坦区域会产生明显棱角,且边缘法线爆炸。
容易被忽略的是边界:第 0 行、最后一行、第 0 列、最后一列没有完整邻域,强行差分会越界读内存。
- 对内部点(i ∈ [1, h−2], j ∈ [1, w−2])用
dx = h[i][j+1] - h[i][j-1],dz = h[i+1][j] - h[i-1][j] - 边界点改用前向或后向差分(如第 0 行只用
h[1][j] - h[0][j]),比插值或复制更安全 - 法线计算必须在 CPU 完成后传入 VBO —— GPU 上用 textureGrad 算虽然快,但高度图采样精度低,法线噪声大
LOD(细节层次)不做的话,大地图直接卡死
2049×2049 高度图生成超 400 万个三角形,即使 GPU 能吞,CPU 每帧生成全部顶点也撑不住。LOD 不是“锦上添花”,是加载 >1km² 地形的生存线。
最简可行方案不是四叉树,而是基于视距的网格细分层级:离相机近用 64×64 子网格(高精度),远的用 16×16(粗略),再远的直接用 4×4 平面加位移贴图。
- 每个 LOD 级别预生成独立的
VBO和EBO,切换时 glBindBuffer,避免运行时重计算 - 高度图采样要配合 LOD 缩放:读
height_data[y * width + x]前,先做整数下采样(如 x /= 4)或双线性插值 - 接缝问题(相邻区块 LOD 不同)必须加 skirt(边缘向下延伸的三角形),否则出现裂缝——这个细节 90% 的入门实现会漏
高度图本身不带材质、光照或遮挡信息,生成完基础网格只是第一步;真正让地形“立得住”的,是后续的纹理混合、世界空间法线贴图、以及根据坡度/高度自动切分 grass/rock/snow 的着色逻辑——那些才是藏在 heightmap 背后的硬骨头。


















