feat(vtk): 底图地面起伏-AWS Terrarium DEM瓦片位移网格+卫星贴图(平面先铺,DEM到位换起伏,拉不到则降级平面)

This commit is contained in:
gaozheng 2026-06-17 09:47:16 +08:00
parent 29ea44560d
commit ef8a9da254
2 changed files with 113 additions and 1 deletions

View File

@ -14,11 +14,15 @@
#include <vtkCallbackCommand.h> #include <vtkCallbackCommand.h>
#include <vtkCamera.h> #include <vtkCamera.h>
#include <vtkCommand.h> #include <vtkCommand.h>
#include <vtkDataArray.h>
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkInteractorObserver.h> #include <vtkInteractorObserver.h>
#include <vtkNew.h> #include <vtkNew.h>
#include <vtkPlaneSource.h> #include <vtkPlaneSource.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h> #include <vtkPolyDataMapper.h>
#include <vtkPoints.h>
#include <vtkProperty.h> #include <vtkProperty.h>
#include <vtkRenderWindow.h> #include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h> #include <vtkRenderWindowInteractor.h>
@ -44,12 +48,28 @@ constexpr double kPi = 3.14159265358979323846;
constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸 constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸
constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级) constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级)
// 地面起伏AWS Terrarium DEM 瓦片(免费/无 tokenXYZ Web-Mercator 与卫星瓦片对齐)。
// elev(米) = R*256 + G + B/256 - 32768。数据到约 z15更高层级无 DEM → 保持平面。
constexpr int kDemMaxZoom = 15;
constexpr int kTerrainGrid = 32; // 每瓦片网格分辨率(33x33 顶点)
constexpr double kTerrainExag = 1.0; // 地形垂向夸张(1=真实高程)
// key 打包z<<44 | x<<22 | yz≤18, x/y<2^18 < 2^22 // key 打包z<<44 | x<<22 | yz≤18, x/y<2^18 < 2^22
void unpackKey(long long key, int& z, int& x, int& y) { void unpackKey(long long key, int& z, int& x, int& y) {
z = static_cast<int>(key >> 44); z = static_cast<int>(key >> 44);
x = static_cast<int>((key >> 22) & 0x3FFFFF); x = static_cast<int>((key >> 22) & 0x3FFFFF);
y = static_cast<int>(key & 0x3FFFFF); y = static_cast<int>(key & 0x3FFFFF);
} }
// Terrarium 像素解码高程:(fx,fy)∈[0,1]fy=0 北/顶行。
double demElev(const QImage& dem, double fx, double fy) {
const int w = dem.width(), h = dem.height();
if (w <= 0 || h <= 0) return 0.0;
const int px = std::clamp(static_cast<int>(std::lround(fx * (w - 1))), 0, w - 1);
const int py = std::clamp(static_cast<int>(std::lround(fy * (h - 1))), 0, h - 1);
const QRgb c = dem.pixel(px, py);
return qRed(c) * 256.0 + qGreen(c) + qBlue(c) / 256.0 - 32768.0;
}
} // namespace } // namespace
long long TileBasemap::tileKey(int z, int x, int y) { long long TileBasemap::tileKey(int z, int x, int y) {
@ -91,6 +111,7 @@ void TileBasemap::show(Kind kind) {
for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second); for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second);
placed_.clear(); placed_.clear();
inFlight_.clear(); inFlight_.clear();
terrainInFlight_.clear();
desired_.clear(); desired_.clear();
kind_ = kind; kind_ = kind;
if (kind == Hidden) { if (kind == Hidden) {
@ -209,6 +230,7 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) {
if (!placed_.count(key) && reply->error() == QNetworkReply::NoError && if (!placed_.count(key) && reply->error() == QNetworkReply::NoError &&
img.loadFromData(reply->readAll())) { img.loadFromData(reply->readAll())) {
placeTile(key, z, x, y, img); // 仅新瓦片落地;其余(失败/已存在)只推动清理 placeTile(key, z, x, y, img); // 仅新瓦片落地;其余(失败/已存在)只推动清理
if (kind_ == Satellite) fetchTerrain(z, x, y, key); // 平面就位后拉 DEM 换起伏
} }
purgeStale(); // 本块已解决 → 若整轮落地则清理被顶替的旧层 purgeStale(); // 本块已解决 → 若整轮落地则清理被顶替的旧层
}); });
@ -256,4 +278,86 @@ void TileBasemap::placeTile(long long key, int z, int x, int y, const QImage& im
if (rw_) rw_->Render(); if (rw_) rw_->Render();
} }
void TileBasemap::fetchTerrain(int z, int x, int y, long long key) {
if (z > kDemMaxZoom) return; // 高层级无 DEM → 维持平面
if (terrainInFlight_.count(key)) return;
const QString url =
QStringLiteral("http://s3.amazonaws.com/elevation-tiles-prod/terrarium/%1/%2/%3.png")
.arg(z)
.arg(x)
.arg(y);
const int gen = generation_;
terrainInFlight_.insert(key);
QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url)));
connect(reply, &QNetworkReply::finished, this, [this, reply, key, z, x, y, gen]() {
reply->deleteLater();
terrainInFlight_.erase(key);
if (gen != generation_ || kind_ != Satellite) return;
if (placed_.find(key) == placed_.end()) return; // 平面块已被清理
if (reply->error() != QNetworkReply::NoError) return;
QImage dem;
if (!dem.loadFromData(reply->readAll())) return;
applyTerrain(key, z, x, y, dem);
});
}
void TileBasemap::applyTerrain(long long key, int z, int x, int y, const QImage& dem) {
auto it = placed_.find(key);
if (it == placed_.end()) return;
vtkSmartPointer<vtkTexture> tex = it->second->GetTexture(); // 复用已贴卫星纹理
if (!tex) return;
if (!haveBaseline_) { // 基准高程取首块中心 → 地形整体绕 z=0 起伏
baseline_ = demElev(dem, 0.5, 0.5);
haveBaseline_ = true;
}
auto warped = buildWarped(z, x, y, tex, dem);
scene_.renderer()->RemoveViewProp(it->second);
scene_.addActor(warped);
it->second = warped;
if (rw_) rw_->Render();
}
vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int z, int x, int y,
vtkSmartPointer<vtkTexture> tex,
const QImage& dem) {
const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y);
const auto sw = frame_->toLocal(b.south, b.west);
const auto se = frame_->toLocal(b.south, b.east);
const auto nw = frame_->toLocal(b.north, b.west);
const double base = kGroundZ + (z - kMinZoom) * kZEps;
// PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord再按高程位移每点 Z。
vtkNew<vtkPlaneSource> plane;
plane->SetOrigin(sw.x, sw.y, base);
plane->SetPoint1(se.x, se.y, base);
plane->SetPoint2(nw.x, nw.y, base);
plane->SetResolution(kTerrainGrid, kTerrainGrid);
plane->Update();
auto warped = vtkSmartPointer<vtkPolyData>::New();
warped->DeepCopy(plane->GetOutput());
vtkDataArray* tc = warped->GetPointData()->GetTCoords();
vtkPoints* pts = warped->GetPoints();
const vtkIdType n = pts->GetNumberOfPoints();
for (vtkIdType id = 0; id < n; ++id) {
double t[2];
tc->GetTuple(id, t);
const double fx = t[0], fy = 1.0 - t[1]; // tcoord v: 南0北1 → fy: 北0南1(对齐 DEM 顶行=北)
const double elev = demElev(dem, fx, fy);
double p[3];
pts->GetPoint(id, p);
p[2] = base + (elev - baseline_) * kTerrainExag;
pts->SetPoint(id, p);
}
pts->Modified();
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputData(warped);
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
actor->SetTexture(tex);
actor->GetProperty()->LightingOff();
return actor;
}
} // namespace geopro::app } // namespace geopro::app

View File

@ -11,6 +11,7 @@
class vtkActor; class vtkActor;
class vtkObject; class vtkObject;
class vtkTexture;
class vtkRenderWindow; class vtkRenderWindow;
class vtkInteractorObserver; class vtkInteractorObserver;
class vtkCallbackCommand; class vtkCallbackCommand;
@ -41,6 +42,10 @@ private:
bool computeView(double& centerLat, double& centerLon, int& zoom) const; bool computeView(double& centerLat, double& centerLon, int& zoom) const;
void fetchTile(int z, int x, int y, long long key); void fetchTile(int z, int x, int y, long long key);
void placeTile(long long key, int z, int x, int y, const QImage& img); void placeTile(long long key, int z, int x, int y, const QImage& img);
void fetchTerrain(int z, int x, int y, long long key); // 拉对应 DEM 瓦片(卫星模式)
void applyTerrain(long long key, int z, int x, int y, const QImage& dem); // 平面块换起伏块
vtkSmartPointer<vtkActor> buildWarped(int z, int x, int y, vtkSmartPointer<vtkTexture> tex,
const QImage& dem); // DEM 位移网格 + 卫星贴图
static void onInteractionEnd(vtkObject*, unsigned long, void* clientData, void*); static void onInteractionEnd(vtkObject*, unsigned long, void* clientData, void*);
geopro::render::Scene& scene_; geopro::render::Scene& scene_;
@ -51,7 +56,10 @@ private:
int generation_ = 0; // show/hide/换源 自增,丢弃过期回包 int generation_ = 0; // show/hide/换源 自增,丢弃过期回包
std::map<long long, vtkSmartPointer<vtkActor>> placed_; // 已贴瓦片key→actor std::map<long long, vtkSmartPointer<vtkActor>> placed_; // 已贴瓦片key→actor
std::set<long long> desired_; // 当前视野应显示的瓦片 key std::set<long long> desired_; // 当前视野应显示的瓦片 key
std::set<long long> inFlight_; // 在途请求,避免重复拉 std::set<long long> inFlight_; // 卫星瓦片在途请求
std::set<long long> terrainInFlight_; // DEM 瓦片在途请求
double baseline_ = 0.0; // 基准高程(首块中心),地形绕 z=0 起伏
bool haveBaseline_ = false;
vtkSmartPointer<vtkInteractorObserver> styleObs_; // 持引用保证回调期有效 vtkSmartPointer<vtkInteractorObserver> styleObs_; // 持引用保证回调期有效
vtkSmartPointer<vtkCallbackCommand> observer_; vtkSmartPointer<vtkCallbackCommand> observer_;
bool refreshing_ = false; bool refreshing_ = false;