diff --git a/docs/superpowers/plans/poc-lod-shots/view-default.png b/docs/superpowers/plans/poc-lod-shots/view-default.png index 06d3ca6..93de6f6 100644 Binary files a/docs/superpowers/plans/poc-lod-shots/view-default.png and b/docs/superpowers/plans/poc-lod-shots/view-default.png differ diff --git a/docs/superpowers/plans/poc-lod-shots/view-near.png b/docs/superpowers/plans/poc-lod-shots/view-near.png new file mode 100644 index 0000000..ff1feb8 Binary files /dev/null and b/docs/superpowers/plans/poc-lod-shots/view-near.png differ diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 59aec7d..333f5a6 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -100,9 +100,13 @@ Args parseArgs(int argc, char** argv, int start) { Args a; for (int i = start; i < argc; ++i) { std::string tok = argv[i]; - if (tok.rfind("--", 0) == 0 && i + 1 < argc) { - a.kv[tok.substr(2)] = argv[i + 1]; - ++i; + if (tok.rfind("--", 0) == 0) { + // 下一个 token 也是 --xxx(或本 token 是末尾)→ 本 token 是无值布尔旗标, + // 存空串(避免把后面的旗标误当成本旗标的值,如 --preview --near)。 + const bool hasValue = + (i + 1 < argc) && std::string(argv[i + 1]).rfind("--", 0) != 0; + a.kv[tok.substr(2)] = hasValue ? argv[i + 1] : ""; + if (hasValue) ++i; } else { a.positional.push_back(tok); } @@ -2390,11 +2394,24 @@ vtkSmartPointer makeVariantProperty( // 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE)。 constexpr int kViewMax3DTex = 16384; -// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction/Timer/Render 上)。 +// 单纹理统一渲染(Task 12d-singletex): +// +// 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps)。 +// 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】,与 --preview +// 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps(与预览 184fps 同档)。 +// +// LOD 选层规则(拉近变细、拉远变粗): +// - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层(本数据 L2:11119、 +// L3:5560),整卷重组成一张纹理,任何缩放都显示完整体,绝不缺块。 +// - 拉近(相机选中 level0/1,X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的 +// X 子区域(沿线裁一段,使子体各轴 ≤16384)重组一张纹理。 +// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。 + +// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。 struct ViewState { - geopro::render::OutOfCoreSource* src = nullptr; - geopro::data::ChunkedVolumeStore* store = nullptr; // 整卷粗层渲染走它 - vtkMultiBlockVolumeMapper* mapper = nullptr; + geopro::data::ChunkedVolumeStore* store = nullptr; + vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper + vtkRenderer* ren = nullptr; vtkCamera* cam = nullptr; vtkTextActor* fpsText = nullptr; vtkRenderWindow* rw = nullptr; @@ -2403,6 +2420,10 @@ struct ViewState { // 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。 int cachedWholeLevel = -1; vtkSmartPointer cachedWholeImg; + // 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。 + int cachedLocalBx0 = -1; + int cachedLocalCount = -1; + vtkSmartPointer cachedLocalImg; // 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。 bool inCb = false; }; @@ -2417,7 +2438,7 @@ bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store, // 给定相机选中的 level,返回真正用于整卷渲染的 level:从 picked 起向粗逐层找, // 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384,则升到 level2)。 -// 找不到(极端情况)返回 -1,调用方退回分块路径。 +// 找不到(极端情况)返回 -1,调用方退回局部子区域路径。 int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store, int picked) { const int maxLevel = store.levels() - 1; @@ -2429,43 +2450,103 @@ int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store, return -1; } -// 用 source 当前工作集刷新 mapper 输入。返回喂给 mapper 的块数。 -// -// 策略(同 12c renderLOD 已验,修概览空窗): -// - 概览/中远视角(相机选中粗层)→ 升到最细的“整卷各轴 ≤16384”层,整卷重组成 -// 单张 image 单块喂 mapper(忽略 budget,粗层本就小),任何缩放都显示完整体, -// 不再是 budget 砍后 9% 稀疏。本数据 level0/1 的 X(44476/22238)>16384 → 升 level2。 -// - 只有拉近到要全分辨率(相机选中 level0,X=44476 无法成单纹理)→ 退回分块 + -// budget 路径(核外 LRU 驻留,只渲视野内子区域)。 -std::size_t viewRefreshBlocks(ViewState* st) { - // 先让 source 按相机选好 LOD(同时刷新 lastLevel/视野统计供 fps 文本用)。 - st->src->update(st->cam); - const int picked = st->src->lastLevel(); +// 由相机到体中心距离粗分档选 LOD level(移植 OutOfCoreSource::pickLevel,使交互 +// view 不再依赖 OutOfCoreSource)。0=最细。cam==nullptr 或单层 → 0。 +int viewPickLevel(const geopro::data::ChunkedVolumeStore& store, vtkCamera* cam) { + const geopro::data::StoreMeta& m = store.meta(); + const int maxLevel = store.levels() - 1; + if (cam == nullptr || maxLevel <= 0) return 0; + const double dx = m.nx * m.spacing[0]; + const double dy = m.ny * m.spacing[1]; + const double dz = m.nz * m.spacing[2]; + const double diag = std::sqrt(dx * dx + dy * dy + dz * dz); + if (diag <= 0.0) return 0; + double pos[3]; + cam->GetPosition(pos); + const double cx = m.origin[0] + 0.5 * dx; + const double cy = m.origin[1] + 0.5 * dy; + const double cz = m.origin[2] + 0.5 * dz; + const double ddx = pos[0] - cx, ddy = pos[1] - cy, ddz = pos[2] - cz; + const double dist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz); + const double ratio = dist / diag; + int level = 0; + if (ratio >= 1.0) level = 1; + if (ratio >= 2.0) level = 2; + if (ratio >= 4.0) level = 3; + return std::min(level, maxLevel); +} - // 仅当相机选中 level0(最近、要全分辨率)才分块;其余(概览/中远)整卷渲染。 - if (st->store != nullptr && picked > 0) { - const int wlv = wholeVolumeLevelFor(*st->store, picked); - if (wlv >= 0) { - // 整卷粗层:按 level 缓存整卷 image,仅在 level 变化时重组。 - if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) { - st->cachedWholeImg = - buildLevelImage(*st->store, wlv, st->store->meta()); - st->cachedWholeLevel = wlv; - } - std::vector> one{st->cachedWholeImg}; - auto mb = makeMultiBlock(one); - st->mapper->SetInputDataObject(mb); - st->mapper->Update(); - return one.size(); - } +// 拉近时,level0 整卷 X(44476)>16384 无法成单纹理 → 只取相机视野覆盖的 X 段。 +// 用相机视锥在 X 轴上的世界投影范围交体范围,换算成 level0 的 brick 列区间,并夹到 +// 「段宽 ≤16384 体素」(=256 brick 列,远大于任何视野所需)。返回 [bx0, count)。 +void viewLocalBrickRange(const geopro::data::ChunkedVolumeStore& store, + vtkCamera* cam, int& bx0, int& count) { + const geopro::data::StoreMeta& m = store.meta(); + const int brick = m.brick; + const int totBx = store.bricksX(0); + const int maxBrickCols = kViewMax3DTex / brick; // 段宽上限(体素 ≤16384) + + // 默认:以体中段为中心取 kViewDefaultLocalBricks 列(无相机或退化时)。 + int centerBx = totBx / 2; + int halfCols = std::max(2, maxBrickCols / 4); // 视野估不出时给个稳妥宽度 + + if (cam != nullptr) { + // 相机焦点 X 投影到 level0 brick 列;视野宽度由相机到焦点距离粗估。 + double fp[3], pos[3]; + cam->GetFocalPoint(fp); + cam->GetPosition(pos); + const double x0 = m.origin[0]; + const double xspan = (m.nx > 1) ? (m.nx - 1) * m.spacing[0] : 1.0; + const double fx = (fp[0] - x0) / (xspan > 0 ? xspan : 1.0); // 0..1 + centerBx = std::clamp(static_cast(fx * totBx), 0, totBx - 1); + // 视野半宽(世界)≈ 视距 × tan(半视角),再换成 brick 列;夹到合理区间。 + const double ddx = pos[0] - fp[0], ddy = pos[1] - fp[1], + ddz = pos[2] - fp[2]; + const double viewDist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz); + const double halfAngle = 0.5 * cam->GetViewAngle() * 3.14159265 / 180.0; + const double halfWorld = viewDist * std::tan(halfAngle); + const double colWorld = brick * m.spacing[0]; + halfCols = std::clamp(static_cast(halfWorld / colWorld) + 1, 2, + maxBrickCols / 2); } - // 全分辨率长线:分块 + budget。 - auto imgs = st->src->currentImages(); - auto mb = makeMultiBlock(imgs); - st->mapper->SetInputDataObject(mb); + bx0 = std::clamp(centerBx - halfCols, 0, std::max(0, totBx - 1)); + count = std::min(2 * halfCols, totBx - bx0); + count = std::max(1, std::min(count, maxBrickCols)); +} + +// 单纹理刷新:按相机选 LOD,产【一张 image】喂单 SmartVolumeMapper。返回喂入的块数 +// (恒为 1,单纹理)。同步刷新 st->lastLevel(fps 文本用)。 +std::size_t viewRefreshSingle(ViewState* st) { + const int picked = viewPickLevel(*st->store, st->cam); + + // 概览/中远:升到最细的「整卷 ≤16384」层,整卷一张纹理(缓存,仅 level 变才重组)。 + const int wlv = wholeVolumeLevelFor(*st->store, picked); + if (wlv >= 0 && picked >= 1) { + if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) { + st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta()); + st->cachedWholeLevel = wlv; + } + st->mapper->SetInputData(st->cachedWholeImg); + st->mapper->Update(); + st->lastLevel = wlv; + return 1; + } + + // 拉近(picked==0,要全分辨率):取视野覆盖的 level0 X 子段,一张纹理。 + int bx0 = 0, cnt = 1; + viewLocalBrickRange(*st->store, st->cam, bx0, cnt); + if (st->cachedLocalBx0 != bx0 || st->cachedLocalCount != cnt || + st->cachedLocalImg == nullptr) { + st->cachedLocalImg = + buildLocalLevel0Image(*st->store, st->store->meta(), bx0, cnt); + st->cachedLocalBx0 = bx0; + st->cachedLocalCount = cnt; + } + st->mapper->SetInputData(st->cachedLocalImg); st->mapper->Update(); - return imgs.size(); + st->lastLevel = 0; + return 1; } // interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。 @@ -2479,8 +2560,10 @@ void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) { // 将无限递归。已在回调中则直接返回(双保险)。 if (st->inCb) return; st->inCb = true; - const std::size_t blocks = viewRefreshBlocks(st); - const int lvl = st->src->lastLevel(); + // EndInteraction 时重选 LOD + 重组单图(仅松手触发一次,避免拖动中卡)。 + const std::size_t blocks = viewRefreshSingle(st); + st->ren->ResetCameraClippingRange(); + const int lvl = st->lastLevel; // 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后 // 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。 @@ -2522,12 +2605,12 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) { vtkSmartPointer locImg = buildLocalLevel0Image(store, m, bx0, localBx); - std::vector> one{locImg}; - auto mb = makeMultiBlock(one); - st->mapper->SetInputDataObject(mb); + // 单纹理:一张 image 直接喂单 SmartVolumeMapper(与 --preview 同路径)。 + st->mapper->SetInputData(locImg); st->mapper->Update(); - st->cachedWholeImg = locImg; // 持有引用,避免被释放 - st->cachedWholeLevel = 0; + st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组 + st->cachedLocalBx0 = bx0; + st->cachedLocalCount = localBx; st->lastLevel = 0; // 框住局部段:用无参 ResetCamera(按 actor 的【已 SetScale(1,exagg,exagg)】缩放 @@ -2539,7 +2622,7 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) { st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22 st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面 ren->ResetCameraClippingRange(); - return one.size(); + return 1; // 单纹理:恒一张 image } // 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。 @@ -2696,13 +2779,18 @@ int cmdView(int argc, char** argv) { }; const bool smoke = hasFlag("smoke"); const bool preview = hasFlag("preview"); + // 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口 + // 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read + // 验证「拉近后」单图非空、完整、fps 高。 + const bool nearPreview = + hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near"); // 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。 if (hasFlag("gallery")) { return cmdViewGallery(dir, frames); } - // 单变体:view --preview --variant N(N=1..4),只渲第 N 组。 - if (preview && a.kv.count("variant")) { + // 单变体:view --preview --variant N(N=1..4),只渲第 N 组(near 不走此路)。 + if (preview && a.kv.count("variant") && !nearPreview) { const int vi = std::stoi(a.get("variant", "1")); const int n = static_cast(sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0])); @@ -2725,12 +2813,11 @@ int cmdView(int argc, char** argv) { const bool offscreen = smoke || preview; const int winW = 1280, winH = 800; - // 核外源(读 meta + 建 pager,不载整卷)。 - geopro::render::OutOfCoreSource src(dir, budget); - // 整卷粗层渲染另开 store(粗层各轴 ≤16384 时整卷单 mapper 渲,绕过 budget 稀疏)。 + // 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产 + // 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。 + (void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。 geopro::data::ChunkedVolumeStore store(dir); - const auto& m = src.meta(); - src.setAspect(static_cast(winW) / winH); + const auto& m = store.meta(); const double vmin = m.vminPhys, vmax = m.vmaxPhys; // 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。 const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax); @@ -2751,8 +2838,12 @@ int cmdView(int argc, char** argv) { ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景 rw->AddRenderer(ren); - vtkNew mapper; + // 单纹理:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview / + // gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。 + vtkNew mapper; mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); + mapper->SetAutoAdjustSampleDistances(0); + mapper->SetInteractiveAdjustSampleDistances(0); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); @@ -2773,9 +2864,9 @@ int cmdView(int argc, char** argv) { vtkOutputWindow::SetInstance(capWin); ViewState st; - st.src = &src; st.store = &store; st.mapper = mapper.Get(); + st.ren = ren.Get(); st.fpsText = fpsText.Get(); st.rw = rw.Get(); st.exagg = exagg; @@ -2783,11 +2874,20 @@ int cmdView(int argc, char** argv) { // 相机初始定向(修复 1):默认框「局部段」而非整卷。整线横截面 1:34,框整卷 // 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口 // 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。 - const std::size_t warm = viewSetupDefaultFrame(&st, ren); + std::size_t warm = viewSetupDefaultFrame(&st, ren); rw->Render(); - std::cout << "[view] 预热(默认局部段): level=" << st.lastLevel - << " 渲染块=" << warm << "\n"; + // 拉近预览:在默认取景基础上拉近相机,再走 viewRefreshSingle(与真窗口缩放后 + // 完全相同的单纹理路径,选 level0 局部子区域),验证「拉近后」单图非空、完整。 + if (nearPreview) { + st.cam->Dolly(2.5); // 拉近 + ren->ResetCameraClippingRange(); + warm = viewRefreshSingle(&st); + rw->Render(); + } + + std::cout << "[view] 预热(" << (nearPreview ? "拉近局部段" : "默认局部段") + << "): level=" << st.lastLevel << " 渲染块=" << warm << "\n"; const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH); const bool textureErr = capWin->textureError(); @@ -2799,7 +2899,9 @@ int cmdView(int argc, char** argv) { const fs::path shotDir = fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots"; fs::create_directories(shotDir); - const std::string pngPath = (shotDir / "view-default.png").string(); + const std::string pngPath = + (shotDir / (nearPreview ? "view-near.png" : "view-default.png")) + .string(); savePng(rw.Get(), pngPath); // 结构像素计数:背景为深蓝灰(R/G≈10,B≈20),countNonBlackPixels(>10) 会把整屏 // 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素 @@ -2859,17 +2961,17 @@ int cmdView(int argc, char** argv) { } if (smoke) { - // 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩。 - const int lvlNear = src.lastLevel(); - st.cam->Dolly(0.2); // 拉远 → 期望切粗 LOD + // 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩(单纹理路径)。 + const int lvlNear = st.lastLevel; + st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD(整卷粗层单纹理) ren->ResetCameraClippingRange(); - const std::size_t blocksFar = viewRefreshBlocks(&st); - const int lvlFar = src.lastLevel(); + const std::size_t blocksFar = viewRefreshSingle(&st); + const int lvlFar = st.lastLevel; rw->Render(); - st.cam->Dolly(8.0); // 拉近 → 期望切细 LOD + st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LOD(level0 局部子区域) ren->ResetCameraClippingRange(); - viewRefreshBlocks(&st); - const int lvlNear2 = src.lastLevel(); + viewRefreshSingle(&st); + const int lvlNear2 = st.lastLevel; rw->Render(); const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH); @@ -2933,7 +3035,8 @@ void usage() { " gpr_poc fps-budget [--frames 90] " "[--bricks 4,16,64,128,256]\n" " gpr_poc view [--exagg 8] [--opacity 0.5] " - "[--budget 64] [--smoke] [--preview] [--frames 90]\n"; + "[--smoke] [--preview] [--near] [--variant N] [--gallery] " + "[--frames 90]\n"; } } // namespace