feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
2 changed files with 144 additions and 35 deletions
Showing only changes of commit b1a8d1365d - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -2224,7 +2224,6 @@ struct ViewState {
vtkCamera* cam = nullptr; vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr; vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr; vtkRenderWindow* rw = nullptr;
Stopwatch frameTimer;
double exagg = 8.0; double exagg = 8.0;
int lastLevel = -1; int lastLevel = -1;
// 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。 // 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。
@ -2296,16 +2295,26 @@ std::size_t viewRefreshBlocks(ViewState* st) {
} }
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。 // interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
//
// fps 修复Task 12d-fix3之前用 frameTimer上次回调到本次的墙钟算 fps
// 用户思考/不动的空闲时间也算进去,显示的是「空闲间隔」(如 0.2fps),不可信。改为
// 松手时连渲 kFpsProbeFrames 帧、累计「实际 Render 耗时」取均值,得到真实渲染帧率。
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) { void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData); auto* st = static_cast<ViewState*>(clientData);
// 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调 // 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调
// 将无限递归。已在回调中则直接返回(双保险)。 // 将无限递归。已在回调中则直接返回(双保险)。
if (st->inCb) return; if (st->inCb) return;
st->inCb = true; st->inCb = true;
const double frameMs = st->frameTimer.elapsedMs();
const std::size_t blocks = viewRefreshBlocks(st); const std::size_t blocks = viewRefreshBlocks(st);
const int lvl = st->src->lastLevel(); const int lvl = st->src->lastLevel();
const double fps = frameMs > 0 ? 1000.0 / frameMs : 0.0;
// 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后
// 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。
constexpr int kFpsProbeFrames = 3;
Stopwatch swR;
for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
const double renderMs = swR.elapsedMs() / kFpsProbeFrames;
const double fps = renderMs > 0 ? 1000.0 / renderMs : 0.0;
char buf[256]; char buf[256];
std::snprintf(buf, sizeof(buf), std::snprintf(buf, sizeof(buf),
@ -2313,16 +2322,58 @@ void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
fps, lvl, blocks, st->exagg); fps, lvl, blocks, st->exagg);
st->fpsText->SetInput(buf); st->fpsText->SetInput(buf);
st->lastLevel = lvl; st->lastLevel = lvl;
st->rw->Render(); st->rw->Render(); // 末帧带上更新后的 fps 文本
st->frameTimer.reset();
st->inCb = false; st->inCb = false;
} }
// 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面
// 相对长度 1:34框整卷只会看到一条隐形细带框这个局部段层状结构才充满视野
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
// 越填不满画面256 道是 ① cmdTune 出 lod-tuned-local.png有清晰层状结构的取景
// 沿用之以保证首帧同等可读。
constexpr int kViewDefaultLocalBricks = 4;
// ResetCamera 后再 Zoom 拉近填满画面(同 ① cmdTune 的 1.7×:默认框得偏松,留白多)。
constexpr double kViewDefaultZoom = 1.7;
// 建立 view 的「默认取景」:把 level0 一段局部体(沿线中段)整卷单块喂 mapper
// ResetCamera 到该局部段(actor 已 SetScale(1,exagg,exagg)),置相机为能看出层状
// 结构的角度。真窗口 / --smoke / --preview 三条路径共用此函数 → 渲的是同一画面。
//
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0默认即全分辨率局部段
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
geopro::data::ChunkedVolumeStore& store = *st->store;
const geopro::data::StoreMeta& m = store.meta();
const int totBx = store.bricksX(0);
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 沿线中段
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
auto mb = makeMultiBlock(one);
st->mapper->SetInputDataObject(mb);
st->mapper->Update();
st->cachedWholeImg = locImg; // 持有引用,避免被释放
st->cachedWholeLevel = 0;
st->lastLevel = 0;
// 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放
// 后包围盒框,把 exagg 后的 Y/Z 一并纳入mapper->GetBounds() 是未缩放的,不可用),
// 相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
ren->ResetCamera();
st->cam = ren->GetActiveCamera();
st->cam->Elevation(28.0); // 同 ① cmdTune 的取景角度
st->cam->Azimuth(30.0);
st->cam->Zoom(kViewDefaultZoom); // 拉近填满画面,避免局部段缩在角落
ren->ResetCameraClippingRange();
return one.size();
}
int cmdView(int argc, char** argv) { int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] " std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n"; "[--budget 64] [--smoke] [--preview] [--frames 90]\n";
return 2; return 2;
} }
const std::string dir = a.positional[0]; const std::string dir = a.positional[0];
@ -2330,13 +2381,22 @@ int cmdView(int argc, char** argv) {
const double opacity = std::stod(a.get("opacity", "0.5")); const double opacity = std::stod(a.get("opacity", "0.5"));
const std::size_t budget = const std::size_t budget =
static_cast<std::size_t>(std::stoul(a.get("budget", "64"))); static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
const bool smoke = a.kv.count("smoke") > 0 || const int frames = std::stoi(a.get("frames", "90"));
auto hasFlag = [&](const char* name) {
return a.kv.count(name) > 0 ||
std::find(a.positional.begin(), a.positional.end(), std::find(a.positional.begin(), a.positional.end(),
"--smoke") != a.positional.end(); std::string("--") + name) != a.positional.end();
};
const bool smoke = hasFlag("smoke");
const bool preview = hasFlag("preview");
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
<< " opacity=" << opacity << " budget=" << budget << " opacity=" << opacity << " budget=" << budget
<< (smoke ? " [SMOKE 离屏]" : " [真窗口交互]") << "\n"; << (preview ? " [PREVIEW 离屏存图+测fps]"
: (smoke ? " [SMOKE 离屏]" : " [真窗口交互]"))
<< "\n";
// preview/smoke 走离屏。
const bool offscreen = smoke || preview;
const int winW = 1280, winH = 800; const int winW = 1280, winH = 800;
// 核外源(读 meta + 建 pager不载整卷 // 核外源(读 meta + 建 pager不载整卷
@ -2350,9 +2410,9 @@ int cmdView(int argc, char** argv) {
vtkSmartPointer<vtkVolumeProperty> prop = vtkSmartPointer<vtkVolumeProperty> prop =
makeTunedVolumeProperty(m.quant, cs, vmin, vmax, opacity); makeTunedVolumeProperty(m.quant, cs, vmin, vmax, opacity);
// 渲染窗口:smoke 走离屏,否则真窗口。 // 渲染窗口:preview/smoke 走离屏,否则真窗口。
vtkSmartPointer<vtkRenderWindow> rw; vtkSmartPointer<vtkRenderWindow> rw;
if (smoke) { if (offscreen) {
rw = makeOffscreenWindow(winW, winH); rw = makeOffscreenWindow(winW, winH);
} else { } else {
rw = vtkSmartPointer<vtkRenderWindow>::New(); rw = vtkSmartPointer<vtkRenderWindow>::New();
@ -2393,34 +2453,84 @@ int cmdView(int argc, char** argv) {
st.rw = rw.Get(); st.rw = rw.Get();
st.exagg = exagg; st.exagg = exagg;
// 相机初始定向:先框整体选出工作集,再 ResetCamera 到工作集包围盒(同 renderC // 相机初始定向(修复 1默认框「局部段」而非整卷。整线横截面 1:34框整卷
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0], // 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
m.origin[1], m.origin[1] + m.ny * m.spacing[1] * exagg, // 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
m.origin[2], m.origin[2] + m.nz * m.spacing[2] * exagg); const std::size_t warm = viewSetupDefaultFrame(&st, ren);
st.cam = ren->GetActiveCamera();
const std::size_t warm = viewRefreshBlocks(&st);
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
// 工作集包围盒需按 exagg 缩放后再框actor 已 SetScale
ren->ResetCamera();
}
}
st.cam->Elevation(25.0);
st.cam->Azimuth(25.0);
ren->ResetCameraClippingRange();
rw->Render(); rw->Render();
std::cout << "[view] 预热: level=" << src.lastLevel() << " 视野块=" std::cout << "[view] 预热(默认局部段): level=" << st.lastLevel
<< src.lastVisibleCount() << "/" << src.lastLevelBrickTotal() << " 渲染块=" << warm << "\n";
<< " 驻留=" << src.residentCount() << " 渲染块=" << warm << "\n";
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH); const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
const bool textureErr = capWin->textureError(); const bool textureErr = capWin->textureError();
const bool renderedOk = !textureErr && nonBlack > 0; const bool renderedOk = !textureErr && nonBlack > 0;
if (preview) {
// 修复 2用与真窗口完全相同的默认相机/source/exagg/传函viewSetupDefaultFrame
// 已建好),离屏渲一帧存图 → 控制方先 Read 确认开窗默认画面非空、有结构。
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();
savePng(rw.Get(), pngPath);
// 结构像素计数:背景为深蓝灰(R/G≈10,B≈20)countNonBlackPixels(>10) 会把整屏
// 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素
// (任一通道 >50),作为「确有渲出的体结构」的诚实判据。
auto countStructPixels = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType n = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
px->GetComponent(i, 2) > 50) {
++n;
}
}
return n;
};
const vtkIdType defStruct = countStructPixels();
// 旋相机 N 帧测真实 fps非首帧首帧含纹理上传/shader 编译已在预热完成)。
rw->Render(); // 再预热一帧,确保管线热
Stopwatch sw;
for (int f = 0; f < frames; ++f) {
st.cam->Azimuth(360.0 / frames);
rw->Render();
}
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const bool texErr2 = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool ok = !texErr2 && defStruct > 0;
std::cout << "\n=== view --preview 离屏默认视角验证 ===\n";
std::cout << "默认局部段维度 : " << kViewDefaultLocalBricks
<< " brick 列(沿线中段) level0\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "结构像素(>50) : " << defStruct << " / " << (winW * winH)
<< " (" << (100.0 * defStruct / (winW * winH))
<< "%, 已排除深蓝灰背景)\n";
std::cout << "纹理维度错误 : " << (texErr2 ? "是(!!)" : "") << "\n";
std::cout << "真实渲染 fps : " << (ok ? std::to_string(fps) : "INVALID")
<< " (" << frames << " 帧旋相机, 非首帧)\n";
std::cout << "preview 结果 : "
<< (ok ? "OK ✔ 默认视角有结构" : "FAIL ✘") << "\n";
writeMetricLine(
"view-preview,dir=" + dir + ",exagg=" + std::to_string(exagg) +
",opacity=" + std::to_string(opacity) +
",localBricks=" + std::to_string(kViewDefaultLocalBricks) +
",structPixels=" + std::to_string(defStruct) +
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
",textureErr=" + std::to_string(texErr2 ? 1 : 0) +
",ok=" + std::to_string(ok ? 1 : 0) +
",png=" + pngPath);
return ok ? 0 : 1;
}
if (smoke) { if (smoke) {
// 离屏 smoke模拟一次缩放 → 验 LOD 切换 + 不崩。 // 离屏 smoke模拟一次缩放 → 验 LOD 切换 + 不崩。
const int lvlNear = src.lastLevel(); const int lvlNear = src.lastLevel();
@ -2472,7 +2582,6 @@ int cmdView(int argc, char** argv) {
iren->AddObserver(vtkCommand::EndInteractionEvent, cb); iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n"; std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
st.frameTimer.reset();
iren->Initialize(); iren->Initialize();
rw->Render(); rw->Render();
iren->Start(); iren->Start();
@ -2497,7 +2606,7 @@ void usage() {
" gpr_poc fps-budget <storeDir> [--frames 90] " " gpr_poc fps-budget <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n" "[--bricks 4,16,64,128,256]\n"
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] " " gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n"; "[--budget 64] [--smoke] [--preview] [--frames 90]\n";
} }
} // namespace } // namespace