fix(gpr_poc): view 交互窗口统一走单纹理快路,去 MultiBlock 分块

交互 view 任何相机位置都只渲一张 vtkImageData + 单 vtkSmartVolumeMapper,
与 --preview 走完全同一条产单图 + 同一 mapper 的路径:

- LOD 选层:远观/中景升到最细的整卷各轴<=16384 层(L2/L3)整卷一张纹理;
  拉近取 level0 视野覆盖的 X 子区域重组一张纹理。两条都用 buildLevelImage /
  buildLocalLevel0Image 产单图。
- 去掉交互路径的 OutOfCoreSource/BrickPager/MultiBlock + budget 分块渲染
  (renderC 等 POC 子命令仍保留)。
- LOD 随缩放真切换,只在 EndInteraction 重组一次。
- 新增 view --preview --near 拉近预览(view-near.png),与真窗口同路径。
- parseArgs 支持无值布尔旗标(--preview --near 不再误吞)。

修复:开窗+拉近无缺块(整张纹理),fps 从个位数升到几十~上百。
This commit is contained in:
gaozheng 2026-06-23 20:15:28 +08:00
parent f4922dd6e2
commit fb175d6d3d
3 changed files with 174 additions and 71 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -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<vtkVolumeProperty> 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/1X 超 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<vtkImageData> cachedWholeImg;
// 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。
int cachedLocalBx0 = -1;
int cachedLocalCount = -1;
vtkSmartPointer<vtkImageData> 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。
// - 只有拉近到要全分辨率(相机选中 level0X=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) {
// 拉近时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<int>(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<int>(halfWorld / colWorld) + 1, 2,
maxBrickCols / 2);
}
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->lastLevelfps 文本用)。
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) {
// 整卷粗层:按 level 缓存整卷 image仅在 level 变化时重组。
if (wlv >= 0 && picked >= 1) {
if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) {
st->cachedWholeImg =
buildLevelImage(*st->store, wlv, st->store->meta());
st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta());
st->cachedWholeLevel = wlv;
}
std::vector<vtkSmartPointer<vtkImageData>> one{st->cachedWholeImg};
auto mb = makeMultiBlock(one);
st->mapper->SetInputDataObject(mb);
st->mapper->SetInputData(st->cachedWholeImg);
st->mapper->Update();
return one.size();
}
st->lastLevel = wlv;
return 1;
}
// 全分辨率长线:分块 + budget。
auto imgs = st->src->currentImages();
auto mb = makeMultiBlock(imgs);
st->mapper->SetInputDataObject(mb);
// 拉近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<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
std::vector<vtkSmartPointer<vtkImageData>> 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 NN=1..4),只渲第 N 组
if (preview && a.kv.count("variant")) {
// 单变体view --preview --variant NN=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<int>(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<double>(winW) / winH);
const auto& m = store.meta();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
// 配色/不透明度包络取自 var4seismic + 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<vtkMultiBlockVolumeMapper> mapper;
// 单纹理:单 vtkSmartVolumeMapperGPU 光线投射,整张 3D 纹理),与 --preview /
// gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto volume = vtkSmartPointer<vtkVolume>::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); // 拉近回来 → 期望切回细 LODlevel0 局部子区域)
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 <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n"
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke] [--preview] [--frames 90]\n";
"[--smoke] [--preview] [--near] [--variant N] [--gallery] "
"[--frames 90]\n";
}
} // namespace