fix(vtk): A-复刻原版垂直配准.剖面z用真实高程+g.y(原版data.y=高程,非深度);地形改真实高程(去基准减法)x同一VE;剖面与地形同系对齐,剖面顶≈地表露出地面

This commit is contained in:
gaozheng 2026-06-17 16:00:14 +08:00
parent 692ee057ab
commit c3f72fdc8d
4 changed files with 14 additions and 53 deletions

View File

@ -58,7 +58,6 @@ const char* kMapboxToken =
"pk.eyJ1IjoidGJ1c2FuIiwiYSI6ImNtZjY2emZneDBkY24ybXB4cmpvdmwzNWYifQ.h6tcQ380WN5AW6fZr08how"; "pk.eyJ1IjoidGJ1c2FuIiwiYSI6ImNtZjY2emZneDBkY24ybXB4cmpvdmwzNWYifQ.h6tcQ380WN5AW6fZr08how";
constexpr int kDemMaxZoom = 15; constexpr int kDemMaxZoom = 15;
constexpr int kTerrainGrid = 32; // 每瓦片网格分辨率(33x33 顶点) 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) {
@ -175,43 +174,19 @@ void TileBasemap::show(Kind kind) {
netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废) netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废)
desired_.clear(); desired_.clear();
// demCache_/texCache_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。 // demCache_/texCache_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。
haveBaseline_ = false; // 数据可能已换位置 → 重算基准高程
terrainProbed_ = false; terrainProbed_ = false;
kind_ = kind; kind_ = kind;
if (kind == Hidden) { if (kind == Hidden) {
requestRender(); requestRender();
return; return;
} }
primeBaselineThenRefresh(); // 先定基准高程再铺瓦片:避免先到的粗块用错基准把地形整体抬高 refresh(); // 四叉树覆盖:近细远粗一次铺满(地形按真实高程,与剖面同系)
} }
void TileBasemap::primeBaselineThenRefresh() { void TileBasemap::setVerticalExaggeration(double ve) {
if (haveBaseline_ || kind_ != Satellite) { refresh(); return; } if (ve <= 0.0 || ve == ve_) return;
// 先拉"含数据中心"的 DEM 块定基准(z15),确保任何瓦片 warp 前基准已就位、全场一致。 ve_ = ve;
const auto c = frame_->toLatLon(0.0, 0.0); if (kind_ != Hidden) show(kind_); // 重建地形(高程×新VE),与剖面 VE 保持一致
const int z = kDemMaxZoom;
const geopro::render::TileXY t = geopro::render::lonLatToTile(c.lon, c.lat, z);
const long long demKey = tileKey(z, t.x, t.y);
auto cached = demCache_.find(demKey);
if (cached != demCache_.end()) {
ensureBaseline(z, t.x, t.y, cached->second);
refresh();
return;
}
const int gen = generation_;
const QString url =
QStringLiteral("https://api.mapbox.com/v4/mapbox.terrain-rgb/%1/%2/%3.pngraw?access_token=%4")
.arg(z).arg(t.x).arg(t.y).arg(QString::fromLatin1(kMapboxToken));
enqueueGet(url, [this, z, t, demKey, gen](QNetworkReply* r) {
r->deleteLater();
if (gen != generation_) return;
QImage dem;
if (r->error() == QNetworkReply::NoError && dem.loadFromData(r->readAll())) {
demCache_[demKey] = dem;
ensureBaseline(z, t.x, t.y, dem);
}
refresh(); // 成功→基准就位失败→baseline=0 兜底,但仍全场一致
});
} }
void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) { void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) {
@ -392,21 +367,6 @@ void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
placed_[key] = actor; placed_[key] = actor;
} }
void TileBasemap::ensureBaseline(int z, int x, int y, const QImage& dem) {
if (haveBaseline_) return;
// 基准必须锚"数据中心"的地面高程(确定性):只有含数据中心的块来定,按中心点采样。
// 否则四叉树里随便哪块先到都定基准 → 地形整体偏移 → 三维剖面相对地面忽上忽下。
const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心经纬
const geopro::render::TileXY ct = geopro::render::lonLatToTile(c.lon, c.lat, z);
if (ct.x != x || ct.y != y) return; // 此块不含数据中心 → 不用它定基准
const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y);
const double fx = (c.lon - b.west) / (b.east - b.west); // 数据中心在块内的列比例
const double fy = (b.north - c.lat) / (b.north - b.south); // 行比例(顶=北)
baseline_ = demElev(dem, fx, fy);
haveBaseline_ = true;
qInfo() << "[basemap] 地形启用 baseline(数据中心)=" << baseline_ << "m z=" << z;
}
vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y, vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
vtkSmartPointer<vtkTexture> tex) { vtkSmartPointer<vtkTexture> tex) {
const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y);
@ -441,7 +401,6 @@ void TileBasemap::fetchTerrain(int z, int x, int y, long long key, vtkSmartPoint
// 落地一块瓦片DEM 有效→起伏,否则→平面兜底;并推进 inFlight/清理。 // 落地一块瓦片DEM 有效→起伏,否则→平面兜底;并推进 inFlight/清理。
auto place = [this, key, z, x, y, dz, dx, dy, tex](const QImage* dem) { auto place = [this, key, z, x, y, dz, dx, dy, tex](const QImage* dem) {
if (dem && !dem->isNull()) { if (dem && !dem->isNull()) {
ensureBaseline(dz, dx, dy, *dem); // 用含数据中心的 DEM 块定基准(确定性)
placeActor(key, buildWarped(z, x, y, dz, dx, dy, tex, *dem)); placeActor(key, buildWarped(z, x, y, dz, dx, dy, tex, *dem));
} else { } else {
placeActor(key, buildFlat(z, x, y, tex)); // DEM 拉不到 → 平面兜底 placeActor(key, buildFlat(z, x, y, tex)); // DEM 拉不到 → 平面兜底
@ -529,7 +488,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int d
const double elev = demElev(dem, fx, fy); const double elev = demElev(dem, fx, fy);
double p[3]; double p[3];
pts->GetPoint(id, p); pts->GetPoint(id, p);
p[2] = base + (elev - baseline_) * kTerrainExag; p[2] = base + elev * ve_; // 真实高程×垂直夸张:与剖面(同样真实高程×VE)同系对齐
pts->SetPoint(id, p); pts->SetPoint(id, p);
} }
pts->Modified(); pts->Modified();

View File

@ -38,12 +38,12 @@ public:
void show(Kind kind); // 显示某底图Hidden 等同 hide记住类型供 LOD 刷新复用 void show(Kind kind); // 显示某底图Hidden 等同 hide记住类型供 LOD 刷新复用
void hide(); // 移除全部瓦片 void hide(); // 移除全部瓦片
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调) void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
void setVerticalExaggeration(double ve); // 地形垂向夸张(须与剖面 VE 一致才对齐)
private: private:
static long long tileKey(int z, int x, int y); static long long tileKey(int z, int x, int y);
void ensureObserver(); // 首次显示时挂到交互样式的 EndInteractionEvent void ensureObserver(); // 首次显示时挂到交互样式的 EndInteractionEvent
void requestRender(); // 合并渲染:同一事件循环轮次多次请求只渲染一帧 void requestRender(); // 合并渲染:同一事件循环轮次多次请求只渲染一帧
void primeBaselineThenRefresh(); // 先定数据中心基准高程,再铺四叉树(保证所有瓦片同一基准)
void purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁 void purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁
// 四叉树细分:按瓦片投影屏幕尺寸递归(近细远粗),收集叶瓦片到 out。 // 四叉树细分:按瓦片投影屏幕尺寸递归(近细远粗),收集叶瓦片到 out。
void refineTile(int z, int x, int y, std::set<long long>& out, int& count); void refineTile(int z, int x, int y, std::set<long long>& out, int& count);
@ -51,7 +51,6 @@ private:
void fetchTerrain(int z, int x, int y, long long key, void fetchTerrain(int z, int x, int y, long long key,
vtkSmartPointer<vtkTexture> tex); // 拉覆盖该瓦片的 DEM(z>15 取祖先块)后落地 vtkSmartPointer<vtkTexture> tex); // 拉覆盖该瓦片的 DEM(z>15 取祖先块)后落地
void placeActor(long long key, vtkSmartPointer<vtkActor> actor); void placeActor(long long key, vtkSmartPointer<vtkActor> actor);
void ensureBaseline(int z, int x, int y, const QImage& dem); // 用含数据中心的块定基准(确定性)
vtkSmartPointer<vtkActor> buildFlat(int z, int x, int y, vtkSmartPointer<vtkActor> buildFlat(int z, int x, int y,
vtkSmartPointer<vtkTexture> tex); // 平面瓦片(DEM 兜底) vtkSmartPointer<vtkTexture> tex); // 平面瓦片(DEM 兜底)
vtkSmartPointer<vtkActor> buildWarped(int sz, int sx, int sy, int dz, int dx, int dy, vtkSmartPointer<vtkActor> buildWarped(int sz, int sx, int sy, int dz, int dx, int dy,
@ -73,6 +72,7 @@ private:
std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地) std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地)
std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用 std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用
std::map<long long, vtkSmartPointer<vtkTexture>> texCache_; // 影像纹理缓存,重选/缩放回看免重拉 std::map<long long, vtkSmartPointer<vtkTexture>> texCache_; // 影像纹理缓存,重选/缩放回看免重拉
double ve_ = 2.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐)
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。 // 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
double camX_ = 0, camY_ = 0, camZ_ = 0; double camX_ = 0, camY_ = 0, camZ_ = 0;
double projK_ = 1.0; double projK_ = 1.0;
@ -81,8 +81,6 @@ private:
struct PendingGet { QString url; std::function<void(QNetworkReply*)> cb; }; struct PendingGet { QString url; std::function<void(QNetworkReply*)> cb; };
std::deque<PendingGet> netQueue_; // 限并发请求队列(防瓦片暴发饱和卡死) std::deque<PendingGet> netQueue_; // 限并发请求队列(防瓦片暴发饱和卡死)
int netInFlight_ = 0; int netInFlight_ = 0;
double baseline_ = 0.0; // 基准高程(首块中心),地形绕 z=0 起伏
bool haveBaseline_ = false;
bool terrainProbed_ = false; // 首次 fetchTerrain 打一行诊断日志 bool terrainProbed_ = false; // 首次 fetchTerrain 打一行诊断日志
vtkSmartPointer<vtkInteractorObserver> styleObs_; // 持引用保证回调期有效 vtkSmartPointer<vtkInteractorObserver> styleObs_; // 持引用保证回调期有效
vtkSmartPointer<vtkCallbackCommand> observer_; vtkSmartPointer<vtkCallbackCommand> observer_;

View File

@ -414,6 +414,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}; };
// 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。 // 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。
sceneView->onCameraChanged = [basemap]() { basemap->refresh(); }; sceneView->onCameraChanged = [basemap]() { basemap->refresh(); };
// 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。
QObject::connect(drawer->col3D(), &geopro::app::Column3DDataset::verticalExaggerationChanged,
basemap, [basemap](double ve) { basemap->setVerticalExaggeration(ve); });
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。── // ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
// 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中; // 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中;

View File

@ -66,8 +66,9 @@ vtkSmartPointer<vtkActor> buildCurtain(const geopro::core::Grid& g,
py = 0.0; py = 0.0;
} }
const vtkIdType id = static_cast<vtkIdType>(j) * nx + i; const vtkIdType id = static_cast<vtkIdType>(j) * nx + i;
// g.y 是深度(越大越深)VTK Z 向上 → 取负,使深部在下、浅部在上(剖面不倒置)。 // g.y 是真实高程(米,越大越高,与原版 web data.y 同义):直接作世界 Z使剖面落在
points->SetPoint(id, px, py, -g.y[j]); // 真实海拔上,与同样按真实高程渲染的地形对齐(剖面顶≈地表→露出地面,复刻原版)。
points->SetPoint(id, px, py, g.y[j]);
const double val = g.valueAt(i, j); const double val = g.valueAt(i, j);
if (std::isfinite(val)) { if (std::isfinite(val)) {
sc->SetValue(id, val); sc->SetValue(id, val);