diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 9aa47ff..d2dcad7 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -3984,61 +3986,209 @@ int cmdViewGallery(const std::string& dir, int frames, // // --preview 离屏出俯视(top) + 斜视(oblique)两张图展示 20 条并排成测区;否则开真窗口可转可缩。 -// 一条线在世界中的摆放(刚体变换 + 该线已加载的整卷体)。 -struct LinePlacement { - std::string name; // 明星路_NNN - vtkSmartPointer img; // 该线整卷 VTK_SHORT(线局部坐标) - geopro::data::StoreMeta meta; // 量化/几何 - double startX = 0, startY = 0; // 起点局部米(相对公共原点) - double headingDeg = 0; // 航向角(度,相对 +X 东向,逆时针) - double spreadX = 0, spreadY = 0; // 可选横向铺开偏移(垂直航向,--spread>0 时) +// 一条线在世界中的摆放(刚体变换 + 该线的视野自适应引擎源)。 +// +// P11:每条线不再整卷加载固定层,而是各持一个 ViewAdaptiveVolumeSource(LOD+视锥 +// 裁剪+异步重组引擎,与单条 view 完全同款)。引擎 exagg=1.0(不烘几何),垂向夸张/ +// 航向/平移全由世界变换 T 承担(与单条 view 把 exagg 放 actor SetScale 同理)。每帧 +// 把世界相机逆变换到该线局部帧喂引擎 → selectLod 选层选区(视锥外→引擎不提交)。 +struct PlacedSource { + std::string name; // 明星路_NNN + std::unique_ptr source; + geopro::data::StoreMeta meta; // 量化/几何 + double startX = 0, startY = 0; // 起点局部米(相对公共原点) + double headingDeg = 0; // 航向角(度,相对 +X 东向) + double spreadX = 0, spreadY = 0; // 可选横向铺开偏移 + + vtkSmartPointer world; // T:Scale(1,1,exagg)→RotateZ→Translate + vtkSmartPointer worldInv; // T⁻¹(相机逆变换到局部帧) + vtkSmartPointer prop; // 逐线 2/98 分位标定的传函(底图+高清共用) + vtkSmartPointer baseVolume; // 常驻粗底图(永在场,套 T) + vtkSmartPointer baseMapper; + vtkSmartPointer hiresVolume; // 高清叠加(就绪后局部覆盖,套 T) + vtkSmartPointer hiresMapper; + vtkSmartPointer currentImg; // 持当前高清单图引用(mapper 仅持裸指针) + + double worldBounds[6] = {0, 0, 0, 0, 0, 0}; // 该线(含 T+底图盒)的世界 AABB(视锥裁剪用) + bool culled = false; // 本帧是否被视锥裁掉(两层皆隐 → 真跳过) }; -// 由该线整卷体 + 公共世界原点 + 航向 + Z 夸张 → vtkVolume(套刚体变换 + 传函)。 -// 变换合成(VTK:先加先内层,作用于点的顺序为 后加先施):Translate→RotateZ→Scale(1,1,exagg), -// 即点先按 exagg 拉伸 Z(局部),再绕 Z 旋到真实航向,再平移到世界起点。 -vtkSmartPointer makePlacedVolume(const LinePlacement& lp, double exagg, - double& vminOut, double& vmaxOut) { - const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版(var4) - const geopro::data::StoreMeta& m = lp.meta; - - // 逐体 2/98 分位标定(裁离群,自适应该体值域;退化则回退全量化域)。 - double vmin = m.vminPhys, vmax = m.vmaxPhys; - const ScalarPercentiles pc = - sampleScalarPercentiles(lp.img.Get(), m.quant, 0.02, 0.98); - if (pc.samples > 0) { - vmin = pc.lo; - vmax = pc.hi; - } - vminOut = vmin; - vmaxOut = vmax; - const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax); - - GradStats gs; - if (v.useGradientOpacity) gs = sampleGradientMagnitude(lp.img.Get()); - vtkSmartPointer prop = makeVariantProperty( - v, m.quant, cs, vmin, vmax, v.maxOpacity, - v.useGradientOpacity ? &gs : nullptr); - - vtkNew mapper; - mapper->SetInputData(lp.img.Get()); - mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); - mapper->SetAutoAdjustSampleDistances(0); - mapper->SetInteractiveAdjustSampleDistances(0); - - auto volume = vtkSmartPointer::New(); - volume->SetMapper(mapper); - volume->SetProperty(prop); - - // 刚体摆放 + Z 夸张(一并烘进 UserTransform)。 +// 由 起点+航向+Z 夸张 → 世界刚体变换 T。 +// 合成顺序(VTK PostMultiply:先加先施于点):Scale(1,1,exagg)→RotateZ→Translate, +// 即点先按 exagg 拉伸 Z(局部深度),再绕竖直轴转真实航向,再平移到世界起点。 +vtkSmartPointer makeLineTransform(double startX, double startY, + double headingDeg, + double spreadX, double spreadY, + double exagg) { auto xf = vtkSmartPointer::New(); xf->PostMultiply(); - xf->Scale(1.0, 1.0, exagg); // 只 Z 夸张(局部深度) - xf->RotateZ(lp.headingDeg); // 绕竖直轴转航向 - // 平移到世界起点(+ 可选横向铺开偏移,垂直航向,让重叠的同路多趟可分辨)。 - xf->Translate(lp.startX + lp.spreadX, lp.startY + lp.spreadY, 0.0); - volume->SetUserTransform(xf); - return volume; + xf->Scale(1.0, 1.0, exagg); + xf->RotateZ(headingDeg); + xf->Translate(startX + spreadX, startY + spreadY, 0.0); + return xf; +} + +// 逐线传函:从该线常驻底图(整卷代表)实测 2/98 分位标定色阶/不透明度端点 + 梯度门, +// 与单条 view 的传函标定同口径(底图非空恒可标定;退化回退全量化域)。 +vtkSmartPointer buildLineProperty( + const geopro::data::StoreMeta& m, vtkImageData* basis) { + const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版(var4) + double vmin = m.vminPhys, vmax = m.vmaxPhys; + if (basis != nullptr) { + const ScalarPercentiles pc = + sampleScalarPercentiles(basis, m.quant, 0.02, 0.98); + if (pc.samples > 0) { + vmin = pc.lo; + vmax = pc.hi; + } + } + const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax); + GradStats gs; + if (v.useGradientOpacity && basis != nullptr) gs = sampleGradientMagnitude(basis); + return makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity, + (v.useGradientOpacity && gs.samples > 0) ? &gs + : nullptr); +} + +// 把世界相机参数逆变换到某线局部帧(T⁻¹):pos/focal 是点(含平移逆),up 是方向 +// (仅旋转逆,TransformVector 不含平移)。再调引擎 updateView 选层选区(视锥外→引擎 +// 内部 selectLod 判 empty → 不提交,保留上一就绪/无图)。 +void viewAllSubmitOneLine(PlacedSource& ps, vtkCamera* worldCam, + double aspect, int viewportH) { + if (ps.culled || worldCam == nullptr) return; + double wp[3], wf[3], wu[3]; + worldCam->GetPosition(wp); + worldCam->GetFocalPoint(wf); + worldCam->GetViewUp(wu); + + geopro::render::CameraView c{}; + ps.worldInv->TransformPoint(wp, c.pos); + ps.worldInv->TransformPoint(wf, c.focal); + ps.worldInv->TransformVector(wu, c.up); + c.fovYDeg = worldCam->GetViewAngle(); + c.aspect = aspect; + c.viewportH = viewportH; + + // 引擎 exagg=1.0:局部帧几何无夸张(夸张在 T 里),故喂引擎 volumeView 默认即可。 + ps.source->setAspect(aspect); + ps.source->setViewportHeight(viewportH); + ps.source->updateView(c, geopro::render::VolumeView{ + ps.meta.nx, ps.meta.ny, ps.meta.nz, + ps.meta.brick, ps.source->levelCount(), + {ps.meta.origin[0], ps.meta.origin[1], + ps.meta.origin[2]}, + {ps.meta.spacing[0], ps.meta.spacing[1], + ps.meta.spacing[2]}, + 1.0}); +} + +// 非阻塞拉取该线后台已就绪的高清单图,喂高清 mapper(无新结果→沿用上一帧)。 +// 返回 1=换上新图。 +int viewAllPickOneLine(PlacedSource& ps) { + if (ps.culled) return 0; + auto imgs = ps.source->currentImages(); // 内部 takeLatest(非阻塞) + if (imgs.empty() || imgs[0] == nullptr) return 0; + if (imgs[0] == ps.currentImg) return 0; + ps.currentImg = imgs[0]; + ps.hiresMapper->SetInputData(ps.currentImg); + ps.hiresMapper->Update(); + ps.hiresVolume->SetVisibility(1); + return 1; +} + +// 视锥裁剪(#2):把该线世界 AABB 与相机 6 个视锥面比对,整盒在任一面外侧 → 裁掉 +// (base+hires 两层皆隐 → 该线本帧完全不渲,省下解压/重组/ray-march)。盒 8 角全在 +// 某面负半空间才裁(保守,绝不误裁部分可见的线)。 +bool aabbOutsideFrustum(const double b[6], const double planes[24]) { + for (int p = 0; p < 6; ++p) { + const double a = planes[p * 4 + 0], bb = planes[p * 4 + 1], + cc = planes[p * 4 + 2], dd = planes[p * 4 + 3]; + bool allOut = true; + for (int cx = 0; cx < 2 && allOut; ++cx) + for (int cy = 0; cy < 2 && allOut; ++cy) + for (int cz = 0; cz < 2 && allOut; ++cz) { + const double x = b[cx], y = b[2 + cy], z = b[4 + cz]; + if (a * x + bb * y + cc * z + dd >= 0.0) allOut = false; // 该角在面内 + } + if (allOut) return true; // 全 8 角在该面外 → 整盒在视锥外 + } + return false; +} + +// view-all 每帧驱动共享状态(挂 interactor 回调)。 +struct ViewAllState { + std::vector* lines = nullptr; + vtkRenderer* ren = nullptr; + vtkCamera* cam = nullptr; + vtkRenderWindow* rw = nullptr; + vtkTextActor* fpsText = nullptr; + double aspect = 1400.0 / 900.0; + int viewportH = 900; + bool inCb = false; +}; + +// 重算各线视锥可见性(裁屏外线)+ 对可见线提交引擎目标(非阻塞)。culled 线两层皆隐。 +void viewAllRefreshFrustum(ViewAllState* st) { + double planes[24]; + st->cam->GetFrustumPlanes(st->aspect, planes); + for (PlacedSource& ps : *st->lines) { + const bool outside = aabbOutsideFrustum(ps.worldBounds, planes); + ps.culled = outside; + ps.baseVolume->SetVisibility(outside ? 0 : 1); + if (outside) { + ps.hiresVolume->SetVisibility(0); + } else { + // 可见:提交引擎目标(局部帧),高清可见性由 pick 决定(有就绪图才显)。 + viewAllSubmitOneLine(ps, st->cam, st->aspect, st->viewportH); + ps.hiresVolume->SetVisibility(ps.currentImg != nullptr ? 1 : 0); + } + } +} + +// 交互进行中:只重算视锥可见性 + 提交目标(非阻塞),主线程立即继续响应输入。 +void viewAllOnInteracting(vtkObject*, unsigned long, void* clientData, void*) { + auto* st = static_cast(clientData); + viewAllRefreshFrustum(st); +} + +// 定时器:非阻塞拉取各可见线后台已就绪的新高清纹理换上 → 有新图才重渲。 +void viewAllOnTimer(vtkObject*, unsigned long, void* clientData, void*) { + auto* st = static_cast(clientData); + if (st->inCb) return; + int changed = 0; + for (PlacedSource& ps : *st->lines) changed += viewAllPickOneLine(ps); + if (changed > 0) { + st->ren->ResetCameraClippingRange(); + st->rw->Render(); + } +} + +// 交互结束:重算视锥 + 提交 + 拉取 + 刷新 fps(仅松手触发一次)。 +void viewAllOnInteract(vtkObject*, unsigned long, void* clientData, void*) { + auto* st = static_cast(clientData); + if (st->inCb) return; + st->inCb = true; + viewAllRefreshFrustum(st); + int culledN = 0, visN = 0; + for (PlacedSource& ps : *st->lines) { + viewAllPickOneLine(ps); + if (ps.culled) ++culledN; else ++visN; + } + st->ren->ResetCameraClippingRange(); + + constexpr int kFpsProbeFrames = 3; + Stopwatch swR; + for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render(); + const double fps = swR.elapsedMs() > 0 + ? 1000.0 * kFpsProbeFrames / swR.elapsedMs() + : 0.0; + char buf[256]; + std::snprintf(buf, sizeof(buf), + "fps: %.1f | visible lines: %d | culled: %d", fps, visN, + culledN); + if (st->fpsText) st->fpsText->SetInput(buf); + st->rw->Render(); + st->inCb = false; } int cmdViewAll(int argc, char** argv) { @@ -4159,12 +4309,15 @@ int cmdViewAll(int argc, char** argv) { "如需把各趟铺开成可分辨的并排测区,加 --spread 60。\n"; } - // 3) 逐线:算起点局部米 + 航向 + 加载整卷体 → LinePlacement。 - std::vector placements; + // 3) 逐线:算起点局部米 + 航向 + 建 ViewAdaptiveVolumeSource 引擎 → PlacedSource。 + // P11:不再整卷固定层加载(撞 GL 16384 纹理墙),改为每线一个视野自适应引擎, + // 引擎恒产 ≤16384 单纹理(LOD+视野选区),exagg/航向/平移由世界变换 T 承担。 + const int winW = 1400, winH = 900; + const double aspect = static_cast(winW) / winH; + std::vector lines; int lineIdx = 0; for (const LineGps& lg : gpsList) { - // 轨迹 → CGCS2000 局部米(投影到公共带号后减公共原点)。XY 约定 x=东、y=北, - // 与下游航向 atan2(hy,hx) / 刚体平移 Translate(x,y) 一致。 + // 轨迹 → CGCS2000 局部米(投影到公共带号后减公共原点)。XY 约定 x=东、y=北。 std::vector trackM; trackM.reserve(lg.track.pts.size()); for (const auto& p : lg.track.pts) { @@ -4175,11 +4328,9 @@ int cmdViewAll(int argc, char** argv) { } const geopro::io::gpr::XY& start = trackM.front(); - // 航向:起→止主方向(直接首尾向量,稳健于逐点抖动)。 const geopro::io::gpr::XY& end = trackM.back(); const double hx = end.x - start.x, hy = end.y - start.y; const double headingDeg = std::atan2(hy, hx) * 180.0 / 3.14159265358979323846; - // 垂直航向的左法向单位向量 (-hy,hx)/|h|,用于 --spread 横向铺开。 const double hlen = std::hypot(hx, hy); double spreadX = 0, spreadY = 0; if (spread > 0.0 && hlen > 0.0) { @@ -4187,34 +4338,95 @@ int cmdViewAll(int argc, char** argv) { spreadX = (-hy / hlen) * off; spreadY = (hx / hlen) * off; } - - // 加载该线整卷体(按 --level;夹到该 store 实际层数)。 - const std::string storePath = (fs::path(storesDir) / lg.name).string(); - geopro::data::ChunkedVolumeStore store(storePath); - const int lv = std::max(0, std::min(level, store.levels() - 1)); - vtkSmartPointer img = - buildLevelImage(store, lv, store.meta()); - - LinePlacement lp; - lp.name = lg.name; - lp.img = img; - lp.meta = store.meta(); - lp.startX = start.x; - lp.startY = start.y; - lp.headingDeg = headingDeg; - lp.spreadX = spreadX; - lp.spreadY = spreadY; ++lineIdx; - std::cout << "[view-all] " << lg.name << " level=" << lv << " 维度=" - << img->GetDimensions()[0] << "x" << img->GetDimensions()[1] - << "x" << img->GetDimensions()[2] << " 起点局部米=(" << start.x - << ", " << start.y << ") 航向=" << headingDeg << "°\n"; - placements.push_back(std::move(lp)); - } - std::cout << "[view-all] 加载并定位线数=" << placements.size() << "\n"; - // 4) 同一 renderer 加全部 placed volume。 - const int winW = 1400, winH = 900; + const std::string storePath = (fs::path(storesDir) / lg.name).string(); + + PlacedSource ps; + ps.name = lg.name; + // 引擎 exagg=1.0:垂向夸张放进世界变换 T(与单条 view 把 exagg 放 actor 同理)。 + ps.source = std::make_unique( + storePath, /*exagg=*/1.0); + ps.source->setAspect(aspect); + ps.source->setViewportHeight(winH); + ps.meta = ps.source->meta(); + ps.startX = start.x; + ps.startY = start.y; + ps.headingDeg = headingDeg; + ps.spreadX = spreadX; + ps.spreadY = spreadY; + + // 世界变换 T + 逆变换(相机逆变换到局部帧)。 + ps.world = makeLineTransform(start.x, start.y, headingDeg, spreadX, spreadY, + exagg); + ps.worldInv = vtkSmartPointer::New(); + ps.worldInv->DeepCopy(ps.world); + ps.worldInv->Inverse(); + + // 逐线传函(从常驻底图标定)+ 底图层 + 高清层,两层皆套世界变换 T。 + ps.prop = buildLineProperty(ps.meta, ps.source->baseImage()); + + ps.baseMapper = vtkSmartPointer::New(); + ps.baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); + ps.baseMapper->SetAutoAdjustSampleDistances(1); + ps.baseMapper->SetInteractiveAdjustSampleDistances(1); + ps.baseVolume = vtkSmartPointer::New(); + if (ps.source->baseImage() != nullptr) { + ps.baseMapper->SetInputData(ps.source->baseImage()); + ps.baseMapper->Update(); + } + ps.baseVolume->SetMapper(ps.baseMapper); + ps.baseVolume->SetProperty(ps.prop); + ps.baseVolume->SetUserTransform(ps.world); + + ps.hiresMapper = vtkSmartPointer::New(); + ps.hiresMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); + // #1 拖动降采样:交互式采样距离自适应(拖动→大步长降采样跟手,松手→全质量)。 + ps.hiresMapper->SetAutoAdjustSampleDistances(1); + ps.hiresMapper->SetInteractiveAdjustSampleDistances(1); + ps.hiresVolume = vtkSmartPointer::New(); + ps.hiresVolume->SetMapper(ps.hiresMapper); + ps.hiresVolume->SetProperty(ps.prop); + ps.hiresVolume->SetUserTransform(ps.world); + ps.hiresVolume->SetVisibility(0); // 无就绪高清前不显(底图兜底) + + // 该线世界 AABB(底图模型盒经 T 变换的 8 角包络)→ 视锥裁剪用。 + if (ps.source->baseImage() != nullptr) { + double mb[6]; + ps.source->baseImage()->GetBounds(mb); + double lo[3] = {std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity()}; + double hi[3] = {-std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + for (int cx = 0; cx < 2; ++cx) + for (int cy = 0; cy < 2; ++cy) + for (int cz = 0; cz < 2; ++cz) { + double in[3] = {mb[cx], mb[2 + cy], mb[4 + cz]}, out[3]; + ps.world->TransformPoint(in, out); + for (int d = 0; d < 3; ++d) { + lo[d] = std::min(lo[d], out[d]); + hi[d] = std::max(hi[d], out[d]); + } + } + ps.worldBounds[0] = lo[0]; ps.worldBounds[1] = hi[0]; + ps.worldBounds[2] = lo[1]; ps.worldBounds[3] = hi[1]; + ps.worldBounds[4] = lo[2]; ps.worldBounds[5] = hi[2]; + } + + int bd[3] = {0, 0, 0}; + if (ps.source->baseImage()) ps.source->baseImage()->GetDimensions(bd); + std::cout << "[view-all] " << lg.name << " 引擎底图 level=" + << ps.source->baseLevel() << " 底图维度=" << bd[0] << "x" << bd[1] + << "x" << bd[2] << " 起点局部米=(" << start.x << ", " << start.y + << ") 航向=" << headingDeg << "°\n"; + lines.push_back(std::move(ps)); + } + std::cout << "[view-all] 加载并定位线数=" << lines.size() + << "(每线一个 ViewAdaptiveVolumeSource 引擎)\n"; + + // 4) 同一 renderer 加全部线的底图层 + 高清层。 auto rw = preview ? makeOffscreenWindow(winW, winH) : vtkSmartPointer::New(); if (!preview) rw->SetSize(winW, winH); @@ -4226,30 +4438,47 @@ int cmdViewAll(int argc, char** argv) { auto capWin = vtkSmartPointer::New(); vtkOutputWindow::SetInstance(capWin); - int placed = 0; - for (const LinePlacement& lp : placements) { - double vmin = 0, vmax = 0; - vtkSmartPointer vol = makePlacedVolume(lp, exagg, vmin, vmax); - ren->AddVolume(vol); - ++placed; + for (PlacedSource& ps : lines) { + ren->AddVolume(ps.baseVolume); // 先加底图 → 底层常渲 + ren->AddVolume(ps.hiresVolume); // 后加高清 → 叠在底图上 } - std::cout << "[view-all] 已加入场景体数=" << placed << "\n"; + std::cout << "[view-all] 已加入场景线数=" << lines.size() + << "(底图常驻 + 高清叠加,各 ≤16384 单纹理,绝不撞 GL 纹理墙)\n"; - // 渲一帧 → 验非空 + 闸门纹理错。 + ViewAllState st; + st.lines = &lines; + st.ren = ren.Get(); + st.rw = rw.Get(); + st.aspect = aspect; + st.viewportH = winH; + + // 首帧:ResetCamera 框全测区 → 概览(各线选粗 LOD 底图)。提交引擎目标 + 阻塞拉首图。 ren->ResetCamera(); + st.cam = ren->GetActiveCamera(); + viewAllRefreshFrustum(&st); + // 概览阻塞拉一次(保证首帧高清就绪,离屏/真窗口都从有图起步)。 + for (PlacedSource& ps : lines) { + if (ps.culled) continue; + for (int tries = 0; tries < 200; ++tries) { + if (viewAllPickOneLine(ps)) break; + if (ps.currentImg != nullptr) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + } rw->Render(); if (capWin->textureError()) { - std::cerr << "[view-all] 警告: 检测到 3D 纹理维度错误(某体超 GL 上限)," - "可调小 --level 增大 level 取更粗层。\n"; + std::cerr << "[view-all] 警告: 仍检测到 3D 纹理维度错误(不应发生,引擎契约 " + "≤16384)。\n"; } if (preview) { fs::create_directories(shotDir); - // 俯视(top):相机沿 -Z 俯看 XY 平面(看 20 条在测区平面如何并排铺开)。 + + // (A) 概览俯视(top):相机沿 -Z 俯看 XY 平面(看 20 条在测区平面铺开)。 { ren->ResetCamera(); vtkCamera* cam = ren->GetActiveCamera(); - double fp[3], pos[3]; + double fp[3]; cam->GetFocalPoint(fp); const double* b = ren->ComputeVisiblePropBounds(); const double span = std::max({b[1] - b[0], b[3] - b[2], b[5] - b[4]}); @@ -4257,67 +4486,188 @@ int cmdViewAll(int argc, char** argv) { cam->SetFocalPoint(fp[0], fp[1], fp[2]); cam->SetViewUp(0, 1, 0); ren->ResetCameraClippingRange(); + st.cam = cam; + viewAllRefreshFrustum(&st); + for (PlacedSource& ps : lines) + for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) { + if (viewAllPickOneLine(ps)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } rw->Render(); - const std::string p = (fs::path(shotDir) / "view-all-top.png").string(); - savePng(rw.Get(), p); - std::cout << "[view-all] 俯视图存: " << p << "\n"; + savePng(rw.Get(), (fs::path(shotDir) / "view-all-top.png").string()); + std::cout << "[view-all] 俯视图存: " + << (fs::path(shotDir) / "view-all-top.png").string() << "\n"; } - // 斜视(oblique):默认 var4 取景(El45/Az30 风格),看测区三维起伏。 + + // (B) 概览斜视(oblique):var4 取景 + 概览 fps(全部可见、各选粗 LOD)。 + int ovVisible = 0, ovCulled = 0; + double fpsOverview = 0.0; { ren->ResetCamera(); vtkCamera* cam = ren->GetActiveCamera(); cam->Elevation(35.0); cam->Azimuth(30.0); ren->ResetCameraClippingRange(); + st.cam = cam; + viewAllRefreshFrustum(&st); + for (PlacedSource& ps : lines) { + for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) { + if (viewAllPickOneLine(ps)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + if (ps.culled) ++ovCulled; else ++ovVisible; + } rw->Render(); - const std::string p = - (fs::path(shotDir) / "view-all-oblique.png").string(); - savePng(rw.Get(), p); - std::cout << "[view-all] 斜视图存: " << p << "\n"; + savePng(rw.Get(), + (fs::path(shotDir) / "view-all-oblique.png").string()); + std::cout << "[view-all] 斜视图存: " + << (fs::path(shotDir) / "view-all-oblique.png").string() << "\n"; + + rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样 + rw->Render(); + Stopwatch sw; + const int frames = 60; + for (int f = 0; f < frames; ++f) { + cam->Azimuth(360.0 / frames); + rw->Render(); + } + fpsOverview = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0; } - // fps:斜视取景预热后连渲计时(真实多体共场景 fps,不编造)。 - rw->Render(); - Stopwatch sw; - const int frames = 60; - for (int f = 0; f < frames; ++f) rw->Render(); - const double ms = sw.elapsedMs(); - const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0; - const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH); + // (C) 拉近一段:把焦点对准测区中段一条线的世界中心、近距正对该段 → 大部分线出 + // 视锥被裁掉,当前线只渲可见段的合适 LOD。报拉近 fps(与概览对比)。 + // fps 探针用小幅 azimuth 摆动(±6°,模拟拉近态轻微转动观察),而非整周 orbit + // ——整周 orbit 会把全测区转回视野使裁剪失效,不代表真实"拉近看一段"的交互。 + int nearVisible = 0, nearCulled = 0; + double fpsNear = 0.0; + { + // 选中段一条线(取参与线的中位)作拉近目标,焦点置其世界 AABB 中心。 + const PlacedSource& tgt = lines[lines.size() / 2]; + const double cx = 0.5 * (tgt.worldBounds[0] + tgt.worldBounds[1]); + const double cy = 0.5 * (tgt.worldBounds[2] + tgt.worldBounds[3]); + const double cz = 0.5 * (tgt.worldBounds[4] + tgt.worldBounds[5]); + // 该段(含 exagg 后)世界跨度 → 近距视距(贴该段,使其充满视野、邻线出视锥)。 + const double segLen = std::max({tgt.worldBounds[1] - tgt.worldBounds[0], + tgt.worldBounds[3] - tgt.worldBounds[2], + tgt.worldBounds[5] - tgt.worldBounds[4]}); + vtkCamera* cam = ren->GetActiveCamera(); + const double fovY = cam->GetViewAngle(); + const double tanH = + std::max(1e-3, std::tan(0.5 * fovY * 3.14159265358979 / 180.0)); + // 近距:只贴该段横截面尺度(取较短轴 ~该段 Y/Z 跨度的 1/4),使邻行线出视锥。 + const double shortSpan = std::max( + 1.0, std::min(tgt.worldBounds[3] - tgt.worldBounds[2], + tgt.worldBounds[5] - tgt.worldBounds[4])); + const double dist = (0.25 * shortSpan) / tanH * 1.4; + cam->SetFocalPoint(cx, cy, cz); + cam->SetPosition(cx, cy + dist, cz + 0.3 * dist); // 斜上方近观该段 + cam->SetViewUp(0, 0, 1); + ren->ResetCameraClippingRange(); + st.cam = cam; + (void)segLen; + viewAllRefreshFrustum(&st); + for (PlacedSource& ps : lines) { + for (int t = 0; t < 120 && !ps.culled && ps.currentImg == nullptr; ++t) { + if (viewAllPickOneLine(ps)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + if (ps.culled) ++nearCulled; else ++nearVisible; + } + rw->Render(); + savePng(rw.Get(), (fs::path(shotDir) / "view-all-near.png").string()); + std::cout << "[view-all] 拉近图存: " + << (fs::path(shotDir) / "view-all-near.png").string() << "\n"; - std::cout << "\n=== view-all --preview 测区全貌(多体共场景)===\n"; - std::cout << "参与线数 : " << placements.size() << "\n"; - std::cout << "level : " << level << "\n"; - std::cout << "exagg(Z) : " << exagg << "\n"; - std::cout << "spread(横向铺开): " << spread << " m (0=纯真实位置)\n"; - std::cout << "非黑像素 : " << nb << " / " << (winW * winH) << "\n"; - std::cout << "fps(" << frames << "帧连渲) : " << fps << "\n"; - std::cout << "俯视图 : " << (fs::path(shotDir) / "view-all-top.png").string() + rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样 + rw->Render(); + Stopwatch sw; + const int frames = 60; + for (int f = 0; f < frames; ++f) { + cam->Azimuth(f % 2 == 0 ? 0.4 : -0.4); // 小幅摆动(不转回全测区) + viewAllRefreshFrustum(&st); // 拖动中持续重算视锥裁剪 + rw->Render(); + } + fpsNear = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0; + rw->SetDesiredUpdateRate(0.5); + } + + const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH); + const bool texErr = capWin->textureError(); + vtkOutputWindow::SetInstance(nullptr); + + std::cout << "\n=== view-all --preview 测区全貌(多体共场景,引擎 LOD+视锥裁剪)===\n"; + std::cout << "参与线数 : " << lines.size() << "\n"; + std::cout << "exagg(Z) : " << exagg << "\n"; + std::cout << "spread(横向铺开) : " << spread << " m (0=纯真实位置)\n"; + std::cout << "纹理维度错误 : " << (texErr ? "是(!!)" : "否(引擎契约 ≤16384)") << "\n"; - std::cout << "斜视图 : " + std::cout << "概览可见/裁剪线 : " << ovVisible << " / " << ovCulled << "\n"; + std::cout << "概览 fps(60帧旋) : " << fpsOverview << "\n"; + std::cout << "拉近可见/裁剪线 : " << nearVisible << " / " << nearCulled + << " (视锥裁剪生效:裁掉屏外线)\n"; + std::cout << "拉近 fps(60帧旋) : " << fpsNear << "\n"; + const double speedup = fpsOverview > 0 ? fpsNear / fpsOverview : 0.0; + std::cout << "拉近/概览 fps 比 : " << speedup << "x\n"; + std::cout << "末帧非黑像素 : " << nb << " / " << (winW * winH) << "\n"; + std::cout << "俯视图 : " + << (fs::path(shotDir) / "view-all-top.png").string() << "\n"; + std::cout << "斜视图 : " << (fs::path(shotDir) / "view-all-oblique.png").string() << "\n"; + std::cout << "拉近图 : " + << (fs::path(shotDir) / "view-all-near.png").string() << "\n"; writeMetricLine( - "view-all,lines=" + std::to_string(placements.size()) + - ",level=" + std::to_string(level) + ",exagg=" + std::to_string(exagg) + - ",spread=" + std::to_string(spread) + - ",nonBlack=" + std::to_string(nb) + ",fps=" + std::to_string(fps)); - return nb > 0 ? 0 : 1; + "view-all,lines=" + std::to_string(lines.size()) + + ",exagg=" + std::to_string(exagg) + ",spread=" + std::to_string(spread) + + ",ovVisible=" + std::to_string(ovVisible) + + ",ovCulled=" + std::to_string(ovCulled) + + ",nearVisible=" + std::to_string(nearVisible) + + ",nearCulled=" + std::to_string(nearCulled) + + ",fpsOverview=" + std::to_string(fpsOverview) + + ",fpsNear=" + std::to_string(fpsNear) + + ",nonBlack=" + std::to_string(nb) + + ",texErr=" + std::to_string(texErr ? 1 : 0)); + return (nb > 0 && !texErr) ? 0 : 1; } - // 真窗口:可旋转/缩放。 - rw->SetWindowName("gpr_poc view-all —— 20 条独立体按真实 GPS 摆成测区"); + // 真窗口:可旋转/缩放(每线引擎 LOD + 视锥裁剪 + 拖动降采样)。 + vtkOutputWindow::SetInstance(nullptr); + rw->SetWindowName("gpr_poc view-all —— 20 条独立体引擎LOD/视锥裁剪/拖动降采样"); + + // 屏幕左上角 fps + 可见/裁剪线数文本。 + vtkNew fpsText; + fpsText->SetInput("fps: -- | visible lines: -- | culled: --"); + fpsText->GetTextProperty()->SetFontSize(20); + fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4); + fpsText->SetDisplayPosition(12, winH - 30); + ren->AddViewProp(fpsText); + st.fpsText = fpsText.Get(); + vtkNew iren; iren->SetRenderWindow(rw); vtkNew style; iren->SetInteractorStyle(style); - ren->ResetCamera(); - vtkCamera* cam = ren->GetActiveCamera(); - cam->Elevation(35.0); - cam->Azimuth(30.0); - ren->ResetCameraClippingRange(); + iren->SetDesiredUpdateRate(15.0); // 拖动态:mapper 降采样 + iren->SetStillUpdateRate(0.5); // 静止态:全质量 + + vtkNew cbInteract; + cbInteract->SetCallback(viewAllOnInteracting); + cbInteract->SetClientData(&st); + iren->AddObserver(vtkCommand::InteractionEvent, cbInteract); + + vtkNew cbEnd; + cbEnd->SetCallback(viewAllOnInteract); + cbEnd->SetClientData(&st); + iren->AddObserver(vtkCommand::EndInteractionEvent, cbEnd); + + vtkNew cbTimer; + cbTimer->SetCallback(viewAllOnTimer); + cbTimer->SetClientData(&st); + iren->AddObserver(vtkCommand::TimerEvent, cbTimer); + std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n"; iren->Initialize(); + iren->CreateRepeatingTimer(33); // ~30Hz 非阻塞拉取后台就绪纹理 rw->Render(); iren->Start(); std::cout << "[view-all] 窗口关闭,退出。\n";