高程+着色+矢量 设计方案一

可以,而且对“只有一整块高程 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

这样结构简单,性能也更稳定。

posted @ 2026-05-29 15:44  twjy  阅读(5)  评论(0)    收藏  举报