feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
2 changed files with 55 additions and 193 deletions
Showing only changes of commit ca847f5a77 - Show all commits

View File

@ -40,10 +40,9 @@ namespace geopro::app {
namespace { namespace {
// 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。 // 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。
const char* kTk = "aca91d8c9f59a4f779f39061b8a07737"; const char* kTk = "aca91d8c9f59a4f779f39061b8a07737";
constexpr int kRadius = 4; // 回退用:算不出可视范围时中心 ±4 constexpr int kRootZoom = 9; // 四叉树根层级(单块~78km±1 覆盖~234km 到天边)
constexpr int kMaxTilesPerSide = 13; // 单边瓦片上限(防倾斜时可视范围过大拉爆请求) constexpr double kTargetPx = 384.0; // 瓦片屏幕像素阈值:超过则细分(越小越清晰但块更多)
constexpr int kBaseZoom = 13; // 远处粗底层级(单块~4.9km) constexpr int kMaxLeaves = 200; // 一次覆盖的叶瓦片上限(安全兜底,防细分爆炸)
constexpr int kBaseRadius = 3; // 粗底半径 ±3 → 7x7 覆盖~34km填到天边
constexpr int kMaxConcurrent = 8; // 瓦片请求最大并发(防暴发饱和单域名连接) constexpr int kMaxConcurrent = 8; // 瓦片请求最大并发(防暴发饱和单域名连接)
constexpr int kMinZoom = 3; constexpr int kMinZoom = 3;
constexpr int kMaxZoom = 18; constexpr int kMaxZoom = 18;
@ -51,8 +50,6 @@ constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面
constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting
constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存 constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存
constexpr double kPi = 3.14159265358979323846; constexpr double kPi = 3.14159265358979323846;
constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸
constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级)
// 地面起伏Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN比 AWS Terrarium 快)。 // 地面起伏Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN比 AWS Terrarium 快)。
// 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15更高层级取祖先块。 // 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15更高层级取祖先块。
@ -161,9 +158,6 @@ void TileBasemap::show(Kind kind) {
++generation_; // 旧回包(含换源前的层)按 generation 丢弃 ++generation_; // 旧回包(含换源前的层)按 generation 丢弃
for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second); for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second);
placed_.clear(); placed_.clear();
for (auto& a : baseTiles_) scene_.renderer()->RemoveViewProp(a);
baseTiles_.clear();
baseLoaded_ = false;
inFlight_.clear(); inFlight_.clear();
netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废) netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废)
desired_.clear(); desired_.clear();
@ -175,126 +169,67 @@ void TileBasemap::show(Kind kind) {
if (rw_) rw_->Render(); if (rw_) rw_->Render();
return; return;
} }
refresh(); // 先排近处精细(优先加载,用户最先看到) refresh(); // 四叉树覆盖:近细远粗一次铺满(含远处粗块,无需单独粗底层)
ensureBaseLayer(); // 再排远处粗底(背景,排队在后)
} }
bool TileBasemap::computeView(double& centerLat, double& centerLon, int& zoom) const { void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) {
auto* ren = scene_.renderer(); if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分
if (!ren) return false; const int n = 1 << z;
auto* cam = ren->GetActiveCamera(); if (x < 0 || y < 0 || x >= n || y >= n) return;
if (!cam) return false;
double fp[3]; const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y);
cam->GetFocalPoint(fp); const auto sw = frame_->toLocal(b.south, b.west);
const auto c = frame_->toLatLon(fp[0], fp[1]); // 焦点局部米(x East,y North) → 经纬 const auto ne = frame_->toLocal(b.north, b.east);
centerLat = c.lat; const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米)
centerLon = c.lon; const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米)
// 视野在地面的近似跨度(米):透视用 2·dist·tan(半 FOV),平行投影用 2·parallelScale // 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)
double span; double screenPx;
if (cam->GetParallelProjection()) { if (projParallel_) {
span = 2.0 * cam->GetParallelScale(); screenPx = g * projK_; // 平行投影projK_ = H/(2·parallelScale)
} else { } else {
const double halfAngle = cam->GetViewAngle() * 0.5 * kPi / 180.0; const double dx = cx - camX_, dy = cy - camY_, dz = -camZ_; // 相对相机(瓦片 z≈0)
span = 2.0 * cam->GetDistance() * std::tan(halfAngle); const double dist = std::max(1.0, std::sqrt(dx * dx + dy * dy + dz * dz));
screenPx = g * projK_ / dist; // 透视projK_ = H/(2·tan(vfov/2))
} }
span = std::clamp(span, 1.0, 4.0e7); if (screenPx > kTargetPx && z < kMaxZoom) {
refineTile(z + 1, 2 * x, 2 * y, out, count);
const double cosLat = std::max(0.01, std::cos(centerLat * kPi / 180.0)); refineTile(z + 1, 2 * x + 1, 2 * y, out, count);
const double tileMeters = span / kTilesAcross; refineTile(z + 1, 2 * x, 2 * y + 1, out, count);
int z = static_cast<int>(std::lround(std::log2(kEarthCirc * cosLat / tileMeters))); refineTile(z + 1, 2 * x + 1, 2 * y + 1, out, count);
zoom = std::clamp(z, kMinZoom, kMaxZoom); } else {
return true; out.insert(tileKey(z, x, y));
} ++count;
bool TileBasemap::visibleGroundBox(double& west, double& south, double& east, double& north) const {
auto* ren = scene_.renderer();
if (!ren) return false;
const int* sz = ren->GetSize();
if (!sz || sz[0] <= 0 || sz[1] <= 0) return false;
const double W = sz[0], H = sz[1];
const double corners[4][2] = {{0, 0}, {W, 0}, {0, H}, {W, H}};
double minX = 1e30, minY = 1e30, maxX = -1e30, maxY = -1e30;
int valid = 0;
for (auto& c : corners) {
double nearP[4], farP[4];
ren->SetDisplayPoint(c[0], c[1], 0.0);
ren->DisplayToWorld();
ren->GetWorldPoint(nearP);
ren->SetDisplayPoint(c[0], c[1], 1.0);
ren->DisplayToWorld();
ren->GetWorldPoint(farP);
if (nearP[3] == 0.0 || farP[3] == 0.0) continue;
for (int k = 0; k < 3; ++k) {
nearP[k] /= nearP[3];
farP[k] /= farP[3];
} }
const double dz = farP[2] - nearP[2];
if (std::abs(dz) < 1e-9) continue;
const double t = (kGroundZ - nearP[2]) / dz; // 射线交 z=0 地面
if (t < 0.0 || t > 1.5) continue; // 该角看向地平线之外/反向 → 跳过
const double gx = nearP[0] + t * (farP[0] - nearP[0]);
const double gy = nearP[1] + t * (farP[1] - nearP[1]);
minX = std::min(minX, gx); maxX = std::max(maxX, gx);
minY = std::min(minY, gy); maxY = std::max(maxY, gy);
++valid;
}
if (valid < 2) return false; // 看不到地面(地平线视角) → 让调用方回退
const auto sw = frame_->toLatLon(minX, minY); // 局部米 → 经纬
const auto ne = frame_->toLatLon(maxX, maxY);
west = sw.lon; south = sw.lat; east = ne.lon; north = ne.lat;
return true;
} }
void TileBasemap::refresh() { void TileBasemap::refresh() {
if (kind_ == Hidden || refreshing_) return; if (kind_ == Hidden || refreshing_) return;
refreshing_ = true; refreshing_ = true;
double clat = 0, clon = 0; auto* ren = scene_.renderer();
int zoom = 0; auto* cam = ren ? ren->GetActiveCamera() : nullptr;
if (!computeView(clat, clon, zoom)) { if (!cam) { refreshing_ = false; return; }
refreshing_ = false;
return;
}
const int n = 1 << zoom; const int* sz = ren->GetSize();
const geopro::render::TileXY center = geopro::render::lonLatToTile(clon, clat, zoom); const double H = (sz && sz[1] > 0) ? sz[1] : 800.0;
int x0 = center.x - kRadius, x1 = center.x + kRadius; double camPos[3];
int y0 = center.y - kRadius, y1 = center.y + kRadius; cam->GetPosition(camPos);
camX_ = camPos[0]; camY_ = camPos[1]; camZ_ = camPos[2];
// 优先按相机可视范围覆盖(治倾斜/旋转黑边);算不出则用上面的中心±半径回退。 projParallel_ = cam->GetParallelProjection();
double w, s, e, nn; projK_ = projParallel_ ? H / (2.0 * std::max(1.0, cam->GetParallelScale()))
if (visibleGroundBox(w, s, e, nn)) { : H / (2.0 * std::tan(cam->GetViewAngle() * 0.5 * kPi / 180.0));
const geopro::render::TileXY tnw = geopro::render::lonLatToTile(w, nn, zoom); // 西北
const geopro::render::TileXY tse = geopro::render::lonLatToTile(e, s, zoom); // 东南
x0 = std::min(tnw.x, tse.x) - 1; // 各留 1 圈余量
x1 = std::max(tnw.x, tse.x) + 1;
y0 = std::min(tnw.y, tse.y) - 1;
y1 = std::max(tnw.y, tse.y) + 1;
// 上限保护:单边超 kMaxTilesPerSide 则以中心截断(倾斜看地平线时范围会爆)。
if (x1 - x0 + 1 > kMaxTilesPerSide) {
const int cx = (x0 + x1) / 2;
x0 = cx - kMaxTilesPerSide / 2;
x1 = cx + kMaxTilesPerSide / 2;
}
if (y1 - y0 + 1 > kMaxTilesPerSide) {
const int cy = (y0 + y1) / 2;
y0 = cy - kMaxTilesPerSide / 2;
y1 = cy + kMaxTilesPerSide / 2;
}
}
// 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。
desired_.clear(); desired_.clear();
for (int ty = y0; ty <= y1; ++ty) int count = 0;
for (int tx = x0; tx <= x1; ++tx) { const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心
if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue; const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom);
desired_.insert(tileKey(zoom, tx, ty)); for (int dy = -1; dy <= 1; ++dy)
} for (int dx = -1; dx <= 1; ++dx)
refineTile(kRootZoom, root.x + dx, root.y + dy, desired_, count);
// 拉取缺失瓦片(旧层暂不删,留作回退;新层落地后由 purgeStale 清理)。 // 拉取缺失瓦片(旧层暂不删,落地后由 purgeStale 清理)。
for (long long key : desired_) { for (long long key : desired_) {
if (placed_.count(key) || inFlight_.count(key)) continue; if (placed_.count(key) || inFlight_.count(key)) continue;
int z, x, y; int z, x, y;
@ -302,7 +237,7 @@ void TileBasemap::refresh() {
fetchTile(z, x, y, key); fetchTile(z, x, y, key);
} }
purgeStale(); // 若无需新拉(inFlight 空)则立即清理旧层 purgeStale();
if (rw_) rw_->Render(); if (rw_) rw_->Render();
refreshing_ = false; refreshing_ = false;
} }
@ -405,78 +340,6 @@ void TileBasemap::ensureBaseline(const QImage& dem) {
qInfo() << "[basemap] 地形启用 baseline=" << baseline_ << "m 起伏=" << (mx - mn) << "m"; qInfo() << "[basemap] 地形启用 baseline=" << baseline_ << "m 起伏=" << (mx - mn) << "m";
} }
void TileBasemap::ensureBaseLayer() {
if (baseLoaded_ || kind_ != Satellite) return;
baseLoaded_ = true;
const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心(show 在重锚后调 → 已对准数据)
const geopro::render::TileXY ct = geopro::render::lonLatToTile(c.lon, c.lat, kBaseZoom);
const int n = 1 << kBaseZoom;
for (int dy = -kBaseRadius; dy <= kBaseRadius; ++dy)
for (int dx = -kBaseRadius; dx <= kBaseRadius; ++dx) {
const int tx = ct.x + dx, ty = ct.y + dy;
if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue;
fetchBaseTile(kBaseZoom, tx, ty);
}
}
void TileBasemap::fetchBaseTile(int z, int x, int y) {
const int gen = generation_;
const long long key = tileKey(z, x, y);
// 有了卫星纹理后 → 取 DEM(base zoom ≤ kDemMaxZoom, 同级) → warp 成持久粗底块。
auto onTex = [this, z, x, y, gen](vtkSmartPointer<vtkTexture> tex) {
const long long demKey = tileKey(z, x, y);
auto placeBase = [this, z, x, y, gen, tex](const QImage* dem) {
if (gen != generation_ || kind_ != Satellite) return; // 已隐藏/换源 → 丢弃
vtkSmartPointer<vtkActor> a;
if (dem && !dem->isNull()) {
ensureBaseline(*dem);
a = buildWarped(z, x, y, z, x, y, tex, *dem);
} else {
a = buildFlat(z, x, y, tex);
}
a->SetUseBounds(false); // 粗底不参与包围盒/取景
scene_.addActor(a);
baseTiles_.push_back(a);
if (rw_) rw_->Render();
};
auto dc = demCache_.find(demKey);
if (dc != demCache_.end()) { placeBase(&dc->second); return; }
const QString durl =
QStringLiteral("https://api.mapbox.com/v4/mapbox.terrain-rgb/%1/%2/%3.pngraw?access_token=%4")
.arg(z).arg(x).arg(y).arg(QString::fromLatin1(kMapboxToken));
enqueueGet(durl, [this, demKey, gen, placeBase](QNetworkReply* dr) {
dr->deleteLater();
if (gen != generation_) return;
QImage dem;
if (dr->error() == QNetworkReply::NoError && dem.loadFromData(dr->readAll())) {
demCache_[demKey] = dem;
placeBase(&dem);
} else {
placeBase(nullptr);
}
});
};
auto tc = texCache_.find(key);
if (tc != texCache_.end()) { onTex(tc->second); return; }
const int sub = (x + y) % 8;
const QString url =
QStringLiteral("http://t%1.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile"
"&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix=%2&TileRow=%3"
"&TileCol=%4&style=default&format=tiles&tk=%5")
.arg(sub).arg(z).arg(y).arg(x).arg(QString::fromLatin1(kTk));
enqueueGet(url, [this, key, gen, onTex](QNetworkReply* reply) {
reply->deleteLater();
if (gen != generation_ || kind_ != Satellite) return;
QImage img;
if (reply->error() != QNetworkReply::NoError || !img.loadFromData(reply->readAll())) return;
auto tex = makeTexture(img);
texCache_[key] = tex;
onTex(tex);
});
}
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);

View File

@ -43,16 +43,13 @@ 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 purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁 void purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁
bool computeView(double& centerLat, double& centerLon, int& zoom) const; // 四叉树细分:按瓦片投影屏幕尺寸递归(近细远粗),收集叶瓦片到 out。
// 相机视锥 ∩ z=0 地面 → 可视经纬范围(治倾斜/旋转黑边);算不出(看向地平线)返回 false。 void refineTile(int z, int x, int y, std::set<long long>& out, int& count);
bool visibleGroundBox(double& west, double& south, double& east, double& north) const;
void fetchTile(int z, int x, int y, long long key); void fetchTile(int z, int x, int y, long long key);
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(const QImage& dem); // 首块 DEM 定基准高程(base/detail 共用→地形连续) void ensureBaseline(const QImage& dem); // 首块 DEM 定基准高程(各层级共用→地形连续)
void ensureBaseLayer(); // 远处粗底图层(填到天边,治倾斜露黑边)
void fetchBaseTile(int z, int x, int y); // 单块粗底(sat+DEM→warp,持久不purge)
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,
@ -74,8 +71,10 @@ 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_; // 影像纹理缓存,重选/缩放回看免重拉
std::vector<vtkSmartPointer<vtkActor>> baseTiles_; // 远处粗底层(持久,不随相机purge) // 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数。
bool baseLoaded_ = false; double camX_ = 0, camY_ = 0, camZ_ = 0;
double projK_ = 1.0;
bool projParallel_ = false;
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;