fix(vtk): 消除地形先平面后逐块形变-改DEM到位直接铺起伏块(拉不到才降级平面)+DEM缓存(同祖先块复用)减少逐块卡顿

This commit is contained in:
gaozheng 2026-06-17 10:15:53 +08:00
parent 33e9949623
commit d99e5c61f4
2 changed files with 108 additions and 81 deletions

View File

@ -62,6 +62,25 @@ void unpackKey(long long key, int& z, int& x, int& y) {
y = static_cast<int>(key & 0x3FFFFF);
}
// QImage → vtkTexture转 RGBA + 垂直翻转,使纹理 v=0 对应瓦片南边(与 PlaneSource tcoord 一致)。
vtkSmartPointer<vtkTexture> makeTexture(const QImage& img) {
const QImage rgba = img.convertToFormat(QImage::Format_RGBA8888);
const int w = rgba.width(), h = rgba.height();
if (w <= 0 || h <= 0) return nullptr;
vtkNew<vtkImageData> vimg;
vimg->SetDimensions(w, h, 1);
vimg->AllocateScalars(VTK_UNSIGNED_CHAR, 4);
for (int row = 0; row < h; ++row) {
const uchar* src = rgba.scanLine(h - 1 - row);
auto* dst = static_cast<uchar*>(vimg->GetScalarPointer(0, row, 0));
std::memcpy(dst, src, static_cast<size_t>(w) * 4);
}
auto tex = vtkSmartPointer<vtkTexture>::New();
tex->SetInputData(vimg);
tex->InterpolateOn();
return tex;
}
// Terrarium 像素解码高程:(fx,fy)∈[0,1]fy=0 北/顶行。
double demElev(const QImage& dem, double fx, double fy) {
const int w = dem.width(), h = dem.height();
@ -112,7 +131,7 @@ void TileBasemap::show(Kind kind) {
for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second);
placed_.clear();
inFlight_.clear();
terrainInFlight_.clear();
demCache_.clear();
desired_.clear();
terrainProbed_ = false;
kind_ = kind;
@ -224,68 +243,98 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) {
QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url)));
connect(reply, &QNetworkReply::finished, this, [this, reply, key, z, x, y, gen]() {
reply->deleteLater();
inFlight_.erase(key);
if (gen != generation_) return; // 换源/隐藏后丢弃
if (kind_ == Hidden) return;
if (desired_.find(key) == desired_.end()) return; // 已移出视野
// inFlight 保持到瓦片最终落地(起伏/平面),使旧层在新块就位前不被清理 → 无空白闪烁。
QImage img;
if (!placed_.count(key) && reply->error() == QNetworkReply::NoError &&
img.loadFromData(reply->readAll())) {
placeTile(key, z, x, y, img); // 仅新瓦片落地;其余(失败/已存在)只推动清理
if (kind_ == Satellite) fetchTerrain(z, x, y, key); // 平面就位后拉 DEM 换起伏
const bool stale = (gen != generation_) || kind_ == Hidden ||
desired_.find(key) == desired_.end() || placed_.count(key);
const bool ok = !stale && reply->error() == QNetworkReply::NoError &&
img.loadFromData(reply->readAll());
if (!ok) {
inFlight_.erase(key);
purgeStale();
if (rw_) rw_->Render();
return;
}
auto tex = makeTexture(img);
if (kind_ == Satellite) {
fetchTerrain(z, x, y, key, tex); // 拉 DEM 后直接落地起伏块(inFlight 续到那时)
} else {
placeActor(key, buildFlat(z, x, y, tex)); // 街道图无地形 → 直接平面
inFlight_.erase(key);
purgeStale();
if (rw_) rw_->Render();
}
purgeStale(); // 本块已解决 → 若整轮落地则清理被顶替的旧层
});
}
void TileBasemap::placeTile(long long key, int z, int x, int y, const QImage& img) {
void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
if (!actor) return;
scene_.addActor(actor);
placed_[key] = actor;
}
vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
vtkSmartPointer<vtkTexture> tex) {
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 gz = kGroundZ + (z - kMinZoom) * kZEps; // 高层级略抬高,压在旧层之上防共面闪烁
// QImage → vtkImageData(RGBA),垂直翻转:使纹理 v=0 对应瓦片南边(与下方 PlaneSource tcoord 一致)。
const QImage rgba = img.convertToFormat(QImage::Format_RGBA8888);
const int w = rgba.width(), h = rgba.height();
if (w <= 0 || h <= 0) return;
vtkNew<vtkImageData> vimg;
vimg->SetDimensions(w, h, 1);
vimg->AllocateScalars(VTK_UNSIGNED_CHAR, 4);
for (int row = 0; row < h; ++row) {
const uchar* src = rgba.scanLine(h - 1 - row);
auto* dst = static_cast<uchar*>(vimg->GetScalarPointer(0, row, 0));
std::memcpy(dst, src, static_cast<size_t>(w) * 4);
}
vtkNew<vtkTexture> tex;
tex->SetInputData(vimg);
tex->InterpolateOn();
// PlaneSource 自动 tcoordorigin→(0,0)、point1→(1,0)、point2→(0,1)。
// origin=SW(西南)、point1=SE(东) → u 西→东point2=NW(北) → v 南→北。与翻转后纹理对齐。
// PlaneSource 自动 tcoordorigin=SW→u 西0东1、v 南0北1与翻转后纹理对齐
vtkNew<vtkPlaneSource> plane;
plane->SetOrigin(sw.x, sw.y, gz);
plane->SetPoint1(se.x, se.y, gz);
plane->SetPoint2(nw.x, nw.y, gz);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(plane->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
actor->SetTexture(tex);
actor->GetProperty()->LightingOff(); // 底图不受场景光照
scene_.addActor(actor);
placed_[key] = actor;
if (rw_) rw_->Render();
return actor;
}
void TileBasemap::fetchTerrain(int z, int x, int y, long long key) {
if (terrainInFlight_.count(key)) return;
void TileBasemap::fetchTerrain(int z, int x, int y, long long key, vtkSmartPointer<vtkTexture> tex) {
// Terrarium 数据约到 z15更高层级取覆盖本块的祖先 DEM 瓦片,按经纬采样其子区域。
const int dz = std::min(z, kDemMaxZoom);
const int shift = z - dz;
const int dx = x >> shift, dy = y >> shift;
const long long demKey = tileKey(dz, dx, dy);
// 落地一块瓦片DEM 有效→起伏,否则→平面兜底;并推进 inFlight/清理。
auto place = [this, key, z, x, y, dz, dx, dy, tex](const QImage* dem) {
if (dem && !dem->isNull()) {
if (!haveBaseline_) { // 基准高程取首块中心 → 地形整体绕 z=0 起伏
baseline_ = demElev(*dem, 0.5, 0.5);
haveBaseline_ = true;
double mn = 1e30, mx = -1e30;
for (int yy = 0; yy < dem->height(); yy += 16)
for (int xx = 0; xx < dem->width(); xx += 16) {
const double e = demElev(*dem, xx / double(dem->width() - 1),
yy / double(dem->height() - 1));
mn = std::min(mn, e);
mx = std::max(mx, e);
}
qInfo() << "[basemap] 地形启用 baseline=" << baseline_ << "m 起伏=" << (mx - mn)
<< "m";
}
placeActor(key, buildWarped(z, x, y, dz, dx, dy, tex, *dem));
} else {
placeActor(key, buildFlat(z, x, y, tex)); // DEM 拉不到 → 平面兜底
}
inFlight_.erase(key);
purgeStale();
if (rw_) rw_->Render();
};
// 命中缓存:同一祖先 DEM 块的多个瓦片瞬间起伏,免重复网络。
auto cached = demCache_.find(demKey);
if (cached != demCache_.end()) {
place(&cached->second);
return;
}
if (!terrainProbed_) {
terrainProbed_ = true;
qInfo() << "[basemap] 首次拉DEM 卫星z=" << z << " → DEMz=" << dz << "(" << dx << "," << dy
@ -298,53 +347,30 @@ void TileBasemap::fetchTerrain(int z, int x, int y, long long key) {
.arg(dx)
.arg(dy);
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, dz, dx, dy, gen]() {
connect(reply, &QNetworkReply::finished, this, [this, reply, key, demKey, gen, place]() {
reply->deleteLater();
terrainInFlight_.erase(key);
if (gen != generation_ || kind_ != Satellite) return;
if (placed_.find(key) == placed_.end()) return; // 平面块已被清理
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[basemap] DEM 拉取失败" << reply->url().toString() << reply->errorString();
if (gen != generation_ || kind_ != Satellite ||
desired_.find(key) == desired_.end() || placed_.count(key)) {
inFlight_.erase(key); // 过期/移出视野 → 不落地
purgeStale();
if (rw_) rw_->Render();
return;
}
QImage dem;
if (!dem.loadFromData(reply->readAll())) {
qWarning() << "[basemap] DEM 解码失败" << reply->url().toString();
return;
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[basemap] DEM 拉取失败(降级平面)" << reply->url().toString()
<< reply->errorString();
} else if (!dem.loadFromData(reply->readAll())) {
qWarning() << "[basemap] DEM 解码失败(降级平面)" << reply->url().toString();
dem = QImage();
} else {
demCache_[demKey] = dem; // 缓存供同祖先块复用
}
applyTerrain(key, z, x, y, dz, dx, dy, dem);
place(dem.isNull() ? nullptr : &dem);
});
}
void TileBasemap::applyTerrain(long long key, int sz, int sx, int sy, int dz, int dx, int dy,
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;
double mn = 1e30, mx = -1e30;
for (int yy = 0; yy < dem.height(); yy += 16)
for (int xx = 0; xx < dem.width(); xx += 16) {
const double e = demElev(dem, xx / double(dem.width() - 1),
yy / double(dem.height() - 1));
mn = std::min(mn, e);
mx = std::max(mx, e);
}
qInfo() << "[basemap] 地形启用 baseline=" << baseline_ << "m 首块高程范围[" << mn << ","
<< mx << "]m 起伏=" << (mx - mn) << "m";
}
auto warped = buildWarped(sz, sx, sy, dz, dx, dy, tex, dem);
scene_.renderer()->RemoveViewProp(it->second);
scene_.addActor(warped);
it->second = warped;
if (rw_) rw_->Render();
}
vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int dz, int dx, int dy,
vtkSmartPointer<vtkTexture> tex,
const QImage& dem) {

View File

@ -1,5 +1,6 @@
#pragma once
#include <QImage>
#include <QNetworkAccessManager>
#include <QObject>
@ -15,7 +16,6 @@ class vtkTexture;
class vtkRenderWindow;
class vtkInteractorObserver;
class vtkCallbackCommand;
class QImage;
namespace geopro::render { class Scene; }
namespace geopro::core { class GeoLocalFrame; }
@ -41,10 +41,11 @@ private:
void purgeStale(); // 本轮请求全部落地后再删旧层瓦片,避免缩放空白闪烁
bool computeView(double& centerLat, double& centerLon, int& zoom) const;
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 fetchTerrain(int z, int x, int y, long long key); // 拉覆盖该瓦片的 DEM(z>15 取祖先块)
void applyTerrain(long long key, int sz, int sx, int sy, int dz, int dx, int dy,
const QImage& dem); // 平面块换起伏块
void fetchTerrain(int z, int x, int y, long long key,
vtkSmartPointer<vtkTexture> tex); // 拉覆盖该瓦片的 DEM(z>15 取祖先块)后落地
void placeActor(long long key, vtkSmartPointer<vtkActor> actor);
vtkSmartPointer<vtkActor> buildFlat(int z, int x, int y,
vtkSmartPointer<vtkTexture> tex); // 平面瓦片(DEM 兜底)
vtkSmartPointer<vtkActor> buildWarped(int sz, int sx, int sy, int dz, int dx, int dy,
vtkSmartPointer<vtkTexture> tex,
const QImage& dem); // DEM 位移网格 + 卫星贴图
@ -58,8 +59,8 @@ private:
int generation_ = 0; // show/hide/换源 自增,丢弃过期回包
std::map<long long, vtkSmartPointer<vtkActor>> placed_; // 已贴瓦片key→actor
std::set<long long> desired_; // 当前视野应显示的瓦片 key
std::set<long long> inFlight_; // 卫星瓦片在途请求
std::set<long long> terrainInFlight_; // DEM 瓦片在途请求
std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地)
std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),避免重复拉
double baseline_ = 0.0; // 基准高程(首块中心),地形绕 z=0 起伏
bool haveBaseline_ = false;
bool terrainProbed_ = false; // 首次 fetchTerrain 打一行诊断日志