From ca847f5a77e529e8b1364439f6699f2b64fc926e Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 17 Jun 2026 14:03:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(vtk):=20=E5=BA=95=E5=9B=BE=E6=94=B9?= =?UTF-8?q?=E7=9C=9F=E6=AD=A3=E5=9B=9B=E5=8F=89=E6=A0=91=E5=A4=9A=E7=BA=A7?= =?UTF-8?q?LOD(=E6=8C=89=E5=B1=8F=E5=B9=95=E8=AF=AF=E5=B7=AE=E7=BB=86?= =?UTF-8?q?=E5=88=86,=E8=BF=91=E7=BB=86=E8=BF=9C=E7=B2=97=E9=93=BA?= =?UTF-8?q?=E6=BB=A1=E8=A7=86=E9=87=8E)=E5=8F=96=E4=BB=A3=E5=8D=95?= =?UTF-8?q?=E5=B1=82=E7=BA=A7+=E8=A7=86=E9=87=8E=E7=9B=92+=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=B2=97=E5=BA=95,=E6=A0=B9=E6=B2=BB=E5=80=BE?= =?UTF-8?q?=E6=96=9C=E6=A8=A1=E7=B3=8A/=E9=BB=91=E8=BE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/TileBasemap.cpp | 233 +++++++++------------------------------- src/app/TileBasemap.hpp | 15 ++- 2 files changed, 55 insertions(+), 193 deletions(-) diff --git a/src/app/TileBasemap.cpp b/src/app/TileBasemap.cpp index 1bcd6f5..636c15b 100644 --- a/src/app/TileBasemap.cpp +++ b/src/app/TileBasemap.cpp @@ -40,10 +40,9 @@ namespace geopro::app { namespace { // 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。 const char* kTk = "aca91d8c9f59a4f779f39061b8a07737"; -constexpr int kRadius = 4; // 回退用:算不出可视范围时中心 ±4 -constexpr int kMaxTilesPerSide = 13; // 单边瓦片上限(防倾斜时可视范围过大拉爆请求) -constexpr int kBaseZoom = 13; // 远处粗底层级(单块~4.9km) -constexpr int kBaseRadius = 3; // 粗底半径 ±3 → 7x7 覆盖~34km,填到天边 +constexpr int kRootZoom = 9; // 四叉树根层级(单块~78km,±1 覆盖~234km 到天边) +constexpr double kTargetPx = 384.0; // 瓦片屏幕像素阈值:超过则细分(越小越清晰但块更多) +constexpr int kMaxLeaves = 200; // 一次覆盖的叶瓦片上限(安全兜底,防细分爆炸) constexpr int kMaxConcurrent = 8; // 瓦片请求最大并发(防暴发饱和单域名连接) constexpr int kMinZoom = 3; constexpr int kMaxZoom = 18; @@ -51,8 +50,6 @@ constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面 constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存 constexpr double kPi = 3.14159265358979323846; -constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸 -constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级) // 地面起伏:Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN,比 AWS Terrarium 快)。 // 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15,更高层级取祖先块。 @@ -161,9 +158,6 @@ void TileBasemap::show(Kind kind) { ++generation_; // 旧回包(含换源前的层)按 generation 丢弃 for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second); placed_.clear(); - for (auto& a : baseTiles_) scene_.renderer()->RemoveViewProp(a); - baseTiles_.clear(); - baseLoaded_ = false; inFlight_.clear(); netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废) desired_.clear(); @@ -175,126 +169,67 @@ void TileBasemap::show(Kind kind) { if (rw_) rw_->Render(); return; } - refresh(); // 先排近处精细(优先加载,用户最先看到) - ensureBaseLayer(); // 再排远处粗底(背景,排队在后) + refresh(); // 四叉树覆盖:近细远粗一次铺满(含远处粗块,无需单独粗底层) } -bool TileBasemap::computeView(double& centerLat, double& centerLon, int& zoom) const { - auto* ren = scene_.renderer(); - if (!ren) return false; - auto* cam = ren->GetActiveCamera(); - if (!cam) return false; +void TileBasemap::refineTile(int z, int x, int y, std::set& out, int& count) { + if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分 + const int n = 1 << z; + if (x < 0 || y < 0 || x >= n || y >= n) return; - double fp[3]; - cam->GetFocalPoint(fp); - const auto c = frame_->toLatLon(fp[0], fp[1]); // 焦点局部米(x East,y North) → 经纬 - centerLat = c.lat; - centerLon = c.lon; + const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); + const auto sw = frame_->toLocal(b.south, b.west); + const auto ne = frame_->toLocal(b.north, b.east); + const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米) + const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米) - // 视野在地面的近似跨度(米):透视用 2·dist·tan(半 FOV),平行投影用 2·parallelScale。 - double span; - if (cam->GetParallelProjection()) { - span = 2.0 * cam->GetParallelScale(); + // 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。 + double screenPx; + if (projParallel_) { + screenPx = g * projK_; // 平行投影:projK_ = H/(2·parallelScale) } else { - const double halfAngle = cam->GetViewAngle() * 0.5 * kPi / 180.0; - span = 2.0 * cam->GetDistance() * std::tan(halfAngle); + const double dx = cx - camX_, dy = cy - camY_, dz = -camZ_; // 相对相机(瓦片 z≈0) + 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); - - const double cosLat = std::max(0.01, std::cos(centerLat * kPi / 180.0)); - const double tileMeters = span / kTilesAcross; - int z = static_cast(std::lround(std::log2(kEarthCirc * cosLat / tileMeters))); - zoom = std::clamp(z, kMinZoom, kMaxZoom); - return true; -} - -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 (screenPx > kTargetPx && z < kMaxZoom) { + refineTile(z + 1, 2 * x, 2 * y, out, count); + refineTile(z + 1, 2 * x + 1, 2 * y, out, count); + refineTile(z + 1, 2 * x, 2 * y + 1, out, count); + refineTile(z + 1, 2 * x + 1, 2 * y + 1, out, count); + } else { + out.insert(tileKey(z, x, y)); + ++count; } - 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() { if (kind_ == Hidden || refreshing_) return; refreshing_ = true; - double clat = 0, clon = 0; - int zoom = 0; - if (!computeView(clat, clon, zoom)) { - refreshing_ = false; - return; - } + auto* ren = scene_.renderer(); + auto* cam = ren ? ren->GetActiveCamera() : nullptr; + if (!cam) { refreshing_ = false; return; } - const int n = 1 << zoom; - const geopro::render::TileXY center = geopro::render::lonLatToTile(clon, clat, zoom); - int x0 = center.x - kRadius, x1 = center.x + kRadius; - int y0 = center.y - kRadius, y1 = center.y + kRadius; - - // 优先按相机可视范围覆盖(治倾斜/旋转黑边);算不出则用上面的中心±半径回退。 - double w, s, e, nn; - if (visibleGroundBox(w, s, e, nn)) { - 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; - } - } + const int* sz = ren->GetSize(); + const double H = (sz && sz[1] > 0) ? sz[1] : 800.0; + double camPos[3]; + cam->GetPosition(camPos); + camX_ = camPos[0]; camY_ = camPos[1]; camZ_ = camPos[2]; + projParallel_ = cam->GetParallelProjection(); + projK_ = projParallel_ ? H / (2.0 * std::max(1.0, cam->GetParallelScale())) + : H / (2.0 * std::tan(cam->GetViewAngle() * 0.5 * kPi / 180.0)); + // 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。 desired_.clear(); - for (int ty = y0; ty <= y1; ++ty) - for (int tx = x0; tx <= x1; ++tx) { - if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue; - desired_.insert(tileKey(zoom, tx, ty)); - } + int count = 0; + const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心 + const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom); + 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_) { if (placed_.count(key) || inFlight_.count(key)) continue; int z, x, y; @@ -302,7 +237,7 @@ void TileBasemap::refresh() { fetchTile(z, x, y, key); } - purgeStale(); // 若无需新拉(inFlight 空)则立即清理旧层 + purgeStale(); if (rw_) rw_->Render(); refreshing_ = false; } @@ -405,78 +340,6 @@ void TileBasemap::ensureBaseline(const QImage& dem) { 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 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 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 TileBasemap::buildFlat(int z, int x, int y, vtkSmartPointer tex) { const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); diff --git a/src/app/TileBasemap.hpp b/src/app/TileBasemap.hpp index 50540dc..05862d7 100644 --- a/src/app/TileBasemap.hpp +++ b/src/app/TileBasemap.hpp @@ -43,16 +43,13 @@ private: static long long tileKey(int z, int x, int y); void ensureObserver(); // 首次显示时挂到交互样式的 EndInteractionEvent void purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁 - bool computeView(double& centerLat, double& centerLon, int& zoom) const; - // 相机视锥 ∩ z=0 地面 → 可视经纬范围(治倾斜/旋转黑边);算不出(看向地平线)返回 false。 - bool visibleGroundBox(double& west, double& south, double& east, double& north) const; + // 四叉树细分:按瓦片投影屏幕尺寸递归(近细远粗),收集叶瓦片到 out。 + void refineTile(int z, int x, int y, std::set& out, int& count); void fetchTile(int z, int x, int y, long long key); void fetchTerrain(int z, int x, int y, long long key, vtkSmartPointer tex); // 拉覆盖该瓦片的 DEM(z>15 取祖先块)后落地 void placeActor(long long key, vtkSmartPointer actor); - void ensureBaseline(const QImage& dem); // 首块 DEM 定基准高程(base/detail 共用→地形连续) - void ensureBaseLayer(); // 远处粗底图层(填到天边,治倾斜露黑边) - void fetchBaseTile(int z, int x, int y); // 单块粗底(sat+DEM→warp,持久不purge) + void ensureBaseline(const QImage& dem); // 首块 DEM 定基准高程(各层级共用→地形连续) vtkSmartPointer buildFlat(int z, int x, int y, vtkSmartPointer tex); // 平面瓦片(DEM 兜底) vtkSmartPointer buildWarped(int sz, int sx, int sy, int dz, int dx, int dy, @@ -74,8 +71,10 @@ private: std::set inFlight_; // 在途瓦片(续到起伏/平面最终落地) std::map demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用 std::map> texCache_; // 影像纹理缓存,重选/缩放回看免重拉 - std::vector> baseTiles_; // 远处粗底层(持久,不随相机purge) - bool baseLoaded_ = false; + // 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数。 + double camX_ = 0, camY_ = 0, camZ_ = 0; + double projK_ = 1.0; + bool projParallel_ = false; struct PendingGet { QString url; std::function cb; }; std::deque netQueue_; // 限并发请求队列(防瓦片暴发饱和卡死) int netInFlight_ = 0;