高程+着色+矢量 设计方案一
可以,而且对“只有一整块高程 tif、不做影像、不做真实瓦片”的项目来说,这是更直接的设计。
也就是不要再用:
中心 tile
显示 5x5
缓存 7x7
tile key
tile bounds
而是改成:
当前 GPS / 当前视口中心
↓
以中心点为圆心或矩形中心
↓
读取固定半径范围内的高程
↓
生成一整块地形网格
↓
叠加矢量
↓
渲染
这完全可行。
1. 两种设计路线
路线 A:虚拟瓦片设计
一整块 DEM
↓
虚拟 tile grid
↓
显示 5x5,缓存 7x7
↓
每片 tile 一个 mesh
适合:
- 需要大范围漫游。
- 需要复用局部 mesh。
- 需要稳定缓存。
- 后续可能加多级 LOD。
- 后续可能加地图瓦片、影像、分块矢量。
路线 B:中心范围设计
一整块 DEM
↓
以当前 GPS 为中心取固定范围
↓
生成一个 terrain mesh
适合:
- 飞行器/车辆周围小范围实时显示。
- 只关心当前位置附近。
- 不需要地图瓦片体系。
- 想让项目简单。
- 不需要复杂缓存。
你现在问的就是路线 B。
2. 以 GPS 为中心读取多大范围可以吗?
可以。
比如:
中心:当前 GPS
范围:前后左右各 5km
总范围:10km x 10km
或者:
中心:当前 GPS
范围:前方 8km,后方 2km,左右各 5km
总范围:10km x 10km,但中心偏后
对于飞行视角,第二种通常更好,因为前方需要看得更远。
3. 推荐的范围表达方式
不要用 tile,而是定义一个 TerrainViewWindow:
struct TerrainViewWindow
{
double centerMapX = 0.0;
double centerMapY = 0.0;
double leftDistanceMeters = 5000.0;
double rightDistanceMeters = 5000.0;
double frontDistanceMeters = 8000.0;
double backDistanceMeters = 2000.0;
double headingDegrees = 0.0;
bool useHeadingAlignedWindow = true;
};
它描述:
当前 GPS 周围要取多大范围
4. 最简单方案:固定矩形范围
如果先不考虑航向,直接取一个正方形:
center = GPS
halfSize = 5000m
bounds = center ± 5000m
即:
10km x 10km
伪代码:
QRectF terrainBoundsFromCenter(double centerX,
double centerY,
double halfWidthMeters,
double halfHeightMeters)
{
return QRectF(centerX - halfWidthMeters,
centerY - halfHeightMeters,
halfWidthMeters * 2.0,
halfHeightMeters * 2.0);
}
如果 DEM 坐标系是米制投影坐标,这样非常简单。
5. 经纬度 DEM 怎么办?
如果你的 DEM 是经纬度坐标,GPS 也是经纬度,范围又想用米表示,需要换算。
近似换算:
1° 纬度 ≈ 111320 m
1° 经度 ≈ 111320 * cos(latitude) m
例如:
double metersPerDegreeLat = 111320.0;
double metersPerDegreeLon = 111320.0 * std::cos(centerLat * M_PI / 180.0);
double halfWidthDeg = halfWidthMeters / metersPerDegreeLon;
double halfHeightDeg = halfHeightMeters / metersPerDegreeLat;
QRectF bounds(centerLon - halfWidthDeg,
centerLat - halfHeightDeg,
halfWidthDeg * 2.0,
halfHeightDeg * 2.0);
注意 QRectF 的 y 方向你要和当前项目的坐标习惯统一,有些地方 top > bottom。
如果项目对精度要求高,最好把 GPS 经纬度投影到本地 ENU 或 WebMercator/UTM 后再处理。
6. 读取流程
中心范围设计的数据流是:
GPS lon/lat
↓
转换到 DEM 坐标 mapX/mapY
↓
计算 terrainBounds
↓
terrainBounds 与 DEM bounds 求交
↓
从 elevation.tif 裁剪读取这个范围
↓
生成 ElevationBlock
↓
用 ElevationBlock 生成 TerrainMesh
↓
读取/筛选 terrainBounds 内的矢量
↓
矢量采样高程并转 scene 坐标
↓
渲染
7. TerrainMesh 设计
不再是多个 tile mesh,而是一个或少数几个 mesh。
7.1 一个整块 mesh
struct TerrainMesh
{
QVector<float> positions;
QVector<float> normals;
QVector<float> colors;
QVector<unsigned int> indices;
QRectF mapBounds;
bool valid = false;
};
生成:
TerrainMesh buildTerrainMesh(const ElevationBlock &block,
const QRectF &mapBounds,
int gridColumns,
int gridRows);
例如:
gridColumns = 257;
gridRows = 257;
这相当于:
256 x 256 个格网单元
约 66k 顶点
约 131k 三角形
这个规模通常可以接受。
7.2 为什么不建议太大?
如果你取 10km x 10km,然后直接按 DEM 原始每个像素生成顶点,可能非常大。
例如 DEM 分辨率 5m:
10km / 5m = 2000
2000 x 2000 = 4,000,000 顶点
太大,不适合实时。
所以建议:
读取高程可以按原始窗口读
生成 mesh 时重采样到固定网格
比如:
| 模式 | 网格 |
|---|---|
| 交互 | 129 x 129 |
| 普通 | 257 x 257 |
| 高清 | 513 x 513 |
8. 推荐范围大小
范围大小取决于你的使用场景。
地面车辆
2km x 2km
或 5km x 5km
推荐:
halfWidth = 2500m;
halfHeight = 2500m;
低空飞行
10km x 10km
或 前方 15km、后方 5km
推荐:
left = 5000m;
right = 5000m;
front = 15000m;
back = 5000m;
高空飞行
30km x 30km
或 50km x 50km
但需要降低 mesh 密度,例如:
257 x 257 或 513 x 513
9. 更推荐:航向相关窗口
如果是飞机/GPS导航,建议不是以 GPS 为中心,而是:
以后方少一点、前方多一点的矩形
例如:
前方 12km
后方 3km
左右各 6km
并跟随航向旋转。
示意:
heading
↑
+-----------+
| |
| 前方12km |
| |
| GPS |
| 后方3km |
+-----------+
这样画面前方有更多地形,资源不会浪费在身后。
10. 航向窗口如何算?
先在局部平面坐标里定义四个角:
left = -6000
right = 6000
front = 12000
back = -3000
局部坐标:
x: 左右
y: 前后
根据 heading 旋转到地图坐标:
double headingRad = headingDegrees * M_PI / 180.0;
double forwardX = std::sin(headingRad);
double forwardY = std::cos(headingRad);
double rightX = std::cos(headingRad);
double rightY = -std::sin(headingRad);
任意局部点:
mapX = centerX + rightX * localX + forwardX * localY;
mapY = centerY + rightY * localX + forwardY * localY;
得到四个角后,取外包矩形去裁剪 DEM。
注意:如果 DEM 读取接口只支持轴对齐矩形,那么你实际读取的是旋转窗口的外接矩形。生成 mesh 时可以:
- 简化:直接生成外接矩形地形。
- 精细:只渲染旋转窗口内部区域。
第一版建议直接用外接矩形,简单稳定。
11. 是否需要缓存?
可以不做 5x5/7x7 那种瓦片缓存,但建议做一个简单的中心范围缓存。
否则 GPS 每移动一点就重读 DEM、重建 mesh,性能会不好。
简单策略
定义两个范围:
displayBounds:实际显示范围
loadBounds:比显示范围大一圈的加载范围
例如:
显示:10km x 10km
加载:14km x 14km
只要 GPS 还在加载范围的中间区域,就不重读。
这其实是非瓦片版的缓存。
12. 非瓦片缓存设计
struct TerrainSceneCache
{
QRectF loadedBounds; // 已读取高程范围
QRectF displayBounds; // 当前显示范围
ElevationBlock elevationBlock;
TerrainMesh mesh;
double centerMapX = 0.0;
double centerMapY = 0.0;
bool valid = false;
};
判断是否需要重建:
bool shouldReload(const TerrainSceneCache &cache,
double gpsX,
double gpsY,
double reloadMarginRatio)
{
if (!cache.valid) {
return true;
}
QRectF safeBounds = shrink(cache.loadedBounds, reloadMarginRatio);
return !safeBounds.contains(gpsX, gpsY);
}
比如:
loadedBounds = 14km x 14km
safeBounds = 中间 8km x 8km
GPS 还在中间 8km 内,就不重读。
GPS 离开中间区域,再重新以 GPS 为中心加载。
这比 tile 缓存简单很多。
13. 推荐参数
如果你是低空/导航场景,可以先用:
显示范围:
左右各 5km
前方 10km
后方 3km
加载范围:
左右各 7km
前方 14km
后方 5km
mesh 网格:
交互 129 x 129
最终 257 x 257
如果你只想简单测试:
显示范围:10km x 10km
加载范围:14km x 14km
mesh:257 x 257
14. 矢量数据怎么处理?
和地形范围一致。
读取/筛选 loadedBounds 内的矢量
↓
根据 elevationBlock 采样高程
↓
生成 scene 坐标
↓
渲染
如果矢量数据很少,可以每次全读后筛选。
如果矢量数据很大,需要建空间索引。
15. 和 5x5/7x7 的对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 5x5/7x7 虚拟瓦片 | 复用好、适合大范围漫游、容易多级 LOD | 架构复杂 |
| GPS 中心范围 | 简单、直接、适合当前位置导航 | 重建频率高,需要简单范围缓存 |
| GPS 航向窗口 | 更适合飞行/导航 | 坐标计算稍复杂 |
| 整块 DEM 全读全画 | 最简单 | 大 DEM 性能差,不推荐 |
16. 我建议你采用的最终方案
如果你现在想重建项目,并且只有一整块高程 tif,我建议:
不用 5x5/7x7 瓦片
使用 GPS 中心范围 + 扩大加载范围缓存
具体:
1. 启动时只读 elevation.tif 元数据
2. GPS 转 DEM map 坐标
3. 计算 displayBounds
4. 计算 loadedBounds,比 displayBounds 大 30%~50%
5. 如果 GPS 仍在 loadedBounds 的安全区,不重读
6. 如果超出安全区,重新裁剪 elevation.tif
7. 用裁剪块生成一个 TerrainMesh
8. 高程色带 + 法线光照渲染
9. 矢量按 loadedBounds 筛选并贴地
17. 最简数据流
GPS
↓
centerMapPoint
↓
displayBounds
↓
loadedBounds
↓
readElevationBlock(elevation.tif, loadedBounds)
↓
buildTerrainMesh(elevationBlock, displayBounds)
↓
readVectors(displayBounds or loadedBounds)
↓
projectVectorsToTerrain
↓
render terrain + vectors
18. 一句话结论
可以不采用 5x5 / 7x7 缓存。
如果只有一整块高程 tif,并且只做高程着色和矢量,完全可以设计成:
以当前 GPS 为中心,读取固定半径范围内的高程,生成一整块地形 mesh。
但不要每次 GPS 微小变化都重读,建议加一个:
显示范围 displayBounds + 扩大加载范围 loadedBounds
这样结构简单,性能也更稳定。

浙公网安备 33010602011771号