From fc9ea58cb58ed565c7ea71e3c9731e5739c2c93e Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 24 Jun 2026 09:24:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(vtk):=20ViewAdaptiveVolumeSource=20?= =?UTF-8?q?=E8=A7=86=E9=87=8E=E5=8C=BA=E5=9F=9F=E5=8D=95=E7=BA=B9=E7=90=86?= =?UTF-8?q?=E9=87=8D=E7=BB=84(C2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 C1 selectLod 选层选区,从 ChunkedVolumeStore 把当前视野区域重组为单张 VTK_SHORT vtkImageData(各轴 ≤16384,世界 origin/spacing 按 level+exagg),实现 IVolumeRenderSource。核心 updateView(CameraView,VolumeView) headless 可测。 gpr_poc view 切到该源(退掉 MultiBlock/budget 简化路径),--preview 渲默认视角存 PNG,--smoke 离屏不崩且 LOD 随缩放切换。 --- src/render/CMakeLists.txt | 2 +- .../source/ViewAdaptiveVolumeSource.cpp | 163 ++++++++++ .../source/ViewAdaptiveVolumeSource.hpp | 83 +++++ tests/CMakeLists.txt | 2 + tests/render/test_view_adaptive_source.cpp | 295 ++++++++++++++++++ tools/gpr_poc/main.cpp | 208 ++++-------- 6 files changed, 609 insertions(+), 144 deletions(-) create mode 100644 src/render/source/ViewAdaptiveVolumeSource.cpp create mode 100644 src/render/source/ViewAdaptiveVolumeSource.hpp create mode 100644 tests/render/test_view_adaptive_source.cpp diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 09f60b1..89d7543 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -5,7 +5,7 @@ add_library(geopro_render STATIC interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp ground/TileMath.cpp lod/ViewAdaptiveLodPolicy.cpp - source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp) + source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp source/ViewAdaptiveVolumeSource.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL) target_compile_features(geopro_render PUBLIC cxx_std_17) diff --git a/src/render/source/ViewAdaptiveVolumeSource.cpp b/src/render/source/ViewAdaptiveVolumeSource.cpp new file mode 100644 index 0000000..08c4e11 --- /dev/null +++ b/src/render/source/ViewAdaptiveVolumeSource.cpp @@ -0,0 +1,163 @@ +#include "source/ViewAdaptiveVolumeSource.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace geopro::render { + +namespace { + +// 该 level 某轴体素维度 = ceil(n / 2^level),至少 1(与 C1/store 同口径)。 +int dimAtLevel(int n, int level) { + const int d = (n + (1 << level) - 1) >> level; + return d > 0 ? d : 1; +} + +} // namespace + +ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir, + double exagg) + : store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {} + +VolumeView ViewAdaptiveVolumeSource::volumeView() const { + VolumeView v{}; + v.nx = meta_.nx; + v.ny = meta_.ny; + v.nz = meta_.nz; + v.brick = meta_.brick; + v.levels = store_.levels(); + v.origin[0] = meta_.origin[0]; + v.origin[1] = meta_.origin[1]; + v.origin[2] = meta_.origin[2]; + // C1 约定:spacing 已含 exagg 于 y/z。 + v.spacing[0] = meta_.spacing[0]; + v.spacing[1] = meta_.spacing[1] * exagg_; + v.spacing[2] = meta_.spacing[2] * exagg_; + v.exagg = exagg_; + return v; +} + +void ViewAdaptiveVolumeSource::update(vtkCamera* cam) { + if (cam == nullptr) { + current_ = nullptr; + return; + } + CameraView c{}; + cam->GetPosition(c.pos); + cam->GetFocalPoint(c.focal); + cam->GetViewUp(c.up); + c.fovYDeg = cam->GetViewAngle(); + c.aspect = aspect_; + c.viewportH = viewportH_; + updateView(c, volumeView()); +} + +void ViewAdaptiveVolumeSource::updateView(const CameraView& cam, + const VolumeView& vol) { + const LodSelection sel = selectLod(vol, cam, maxTextureDim_); + if (sel.empty) { + current_ = nullptr; + return; + } + const int level = sel.level; + lastLevel_ = level; + + const int brick = meta_.brick; + const int dimLx = dimAtLevel(meta_.nx, level); + const int dimLy = dimAtLevel(meta_.ny, level); + const int dimLz = dimAtLevel(meta_.nz, level); + + // 重组单纹理某轴范围(与 C1 hpp 契约逐字一致): + // 起点 = b0*brick;终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。 + // 即使 brick > maxTextureDim(单块超限),也按体素上限再裁 → 恒 ≤ maxTextureDim。 + const int gi0 = sel.bx0 * brick; + const int gj0 = sel.by0 * brick; + const int gk0 = sel.bz0 * brick; + const int gi1 = std::min({sel.bx1 * brick, dimLx, gi0 + maxTextureDim_}); + const int gj1 = std::min({sel.by1 * brick, dimLy, gj0 + maxTextureDim_}); + const int gk1 = std::min({sel.bz1 * brick, dimLz, gk0 + maxTextureDim_}); + + const int outNx = std::max(1, gi1 - gi0); + const int outNy = std::max(1, gj1 - gj0); + const int outNz = std::max(1, gk1 - gk0); + + // 世界几何(按 level + exagg): + // spacing = meta.spacing × 2^level(y/z 再 × exagg); + // origin = meta.origin + 区间起始体素 × spacing。 + const double sc = static_cast(std::int64_t(1) << level); // 2^level + const double sp[3] = {meta_.spacing[0] * sc, meta_.spacing[1] * sc * exagg_, + meta_.spacing[2] * sc * exagg_}; + const double org[3] = {meta_.origin[0] + static_cast(gi0) * sp[0], + meta_.origin[1] + static_cast(gj0) * sp[1], + meta_.origin[2] + static_cast(gk0) * sp[2]}; + + auto img = vtkSmartPointer::New(); + img->SetDimensions(outNx, outNy, outNz); + img->SetOrigin(org[0], org[1], org[2]); + img->SetSpacing(sp[0], sp[1], sp[2]); + + vtkNew arr; + arr->SetName("v"); + const vtkIdType total = static_cast(outNx) * outNy * outNz; + arr->SetNumberOfTuples(total); + + // 遍历区间覆盖的 level 块,按块内 (i 最快、k 最慢) 布局把每体素写入子体对应位置。 + // 块内布局与 readBrick / vtkImageData 点序一致。 + const int bx0 = gi0 / brick, bx1 = (gi1 + brick - 1) / brick; + const int by0 = gj0 / brick, by1 = (gj1 + brick - 1) / brick; + const int bz0 = gk0 / brick, bz1 = (gk1 + brick - 1) / brick; + + for (int bz = bz0; bz < bz1; ++bz) { + const int k0 = bz * brick; + const int bd = std::min(brick, dimLz - k0); + for (int by = by0; by < by1; ++by) { + const int j0 = by * brick; + const int bh = std::min(brick, dimLy - j0); + for (int bx = bx0; bx < bx1; ++bx) { + const int i0 = bx * brick; + const int bw = std::min(brick, dimLx - i0); + const std::vector raw = store_.readBrick(level, bx, by, bz); + + // 块内体素全局索引 (i0+ii, j0+jj, k0+kk);只写落在子体区间 [g*0,g*1) 内的。 + for (int kk = 0; kk < bd; ++kk) { + const int gk = k0 + kk; + if (gk < gk0 || gk >= gk1) continue; + const int lk = gk - gk0; + for (int jj = 0; jj < bh; ++jj) { + const int gj = j0 + jj; + if (gj < gj0 || gj >= gj1) continue; + const int lj = gj - gj0; + // 该 (kk,jj) 行在 raw 内的起始偏移(i 最快)。 + const std::size_t rowBase = + (static_cast(kk) * bh + jj) * bw; + for (int ii = 0; ii < bw; ++ii) { + const int gi = i0 + ii; + if (gi < gi0 || gi >= gi1) continue; + const int li = gi - gi0; + const vtkIdType id = + (static_cast(lk) * outNy + lj) * outNx + li; + arr->SetValue(id, raw[rowBase + ii]); + } + } + } + } + } + } + + img->GetPointData()->SetScalars(arr); + current_ = img; +} + +std::vector> +ViewAdaptiveVolumeSource::currentImages() const { + if (current_ == nullptr) return {}; + return {current_}; +} + +} // namespace geopro::render diff --git a/src/render/source/ViewAdaptiveVolumeSource.hpp b/src/render/source/ViewAdaptiveVolumeSource.hpp new file mode 100644 index 0000000..794c73c --- /dev/null +++ b/src/render/source/ViewAdaptiveVolumeSource.hpp @@ -0,0 +1,83 @@ +#pragma once +#include +#include + +#include +#include + +#include "data/store/ChunkedVolumeStore.hpp" +#include "lod/ViewAdaptiveLodPolicy.hpp" +#include "source/IVolumeRenderSource.hpp" + +class vtkCamera; + +namespace geopro::render { + +// C2 实现:视野自适应单纹理体绘制数据源。 +// +// 用 C1 selectLod(VolumeView,CameraView,maxTextureDim) 选 LOD level + 视野内 brick +// 区间,从 ChunkedVolumeStore 把【当前视野区域】重组为【单张 VTK_SHORT +// vtkImageData】(各轴 ≤maxTextureDim,由 C1 硬约束保证),带世界 origin/spacing +// (按 level + 垂向夸张 exagg)。 +// +// 与 B(WholeVolumeSource 整卷单图)/旧 C(OutOfCoreSource MultiBlock 多块)的区别: +// - 远观 → C1 选粗层、区间≈全体 → 重组整卷粗纹理(一张); +// - 近观 → C1 选细层、区间为视锥内小块 → 重组视野子体(一张); +// 两条都恒产【单张 ≤maxTextureDim 的纹理】,单 SmartVolumeMapper 渲,绝不退回 +// MultiBlock 分块(缺块/低 fps)。靠 C1 的硬上限契约,重组各轴恒 ≤maxTextureDim。 +// +// 可测缝:核心 updateView(CameraView,VolumeView) 不吃 vtkCamera、不需 GL 上下文 +//(构造 vtkImageData 无需渲染管线)→ headless 可测。update(vtkCamera*) 仅把相机 +// 参数填成 CameraView 再调 updateView。viewportH/aspect 经 setter 注入(vtkCamera +// 不自带视口像素高/宽高比)。 +class ViewAdaptiveVolumeSource : public IVolumeRenderSource { + public: + // storeDir:含金字塔的分块 store。exagg:垂向夸张(烘焙进 y/z 的 spacing/origin)。 + explicit ViewAdaptiveVolumeSource(const std::string& storeDir, + double exagg = 1.0); + + const geopro::data::StoreMeta& meta() const override { return meta_; } + + // 由 vtkCamera + 注入的 viewportH/aspect 填 CameraView,再调 updateView。 + // GL_MAX_3D_TEXTURE_SIZE 上限走 maxTextureDim_(默认 16384)。 + void update(vtkCamera* cam) override; + + // 可测缝:纯数值核——选层选区 + 重组单图。headless 可测。 + void updateView(const CameraView& cam, const VolumeView& vol); + + // 当前视野区域单图(empty → 空 vector,场景不渲)。 + std::vector> currentImages() const override; + + // reslice 源 = 当前单图(empty → nullptr)。 + vtkImageData* sliceSource() const override { return current_.Get(); } + + // 供 UI 显示当前 LOD level。 + int lastLevel() const { return lastLevel_; } + + // 总层数(含 level 0)——填 VolumeView.levels 用。 + int levelCount() const { return store_.levels(); } + + // 视口像素高 / 宽高比(C1 选层的分辨率密度与视锥裁剪用)。update(vtkCamera*) + // 前由渲染端按窗口尺寸注入。 + void setViewportHeight(int h) { viewportH_ = h > 0 ? h : viewportH_; } + void setAspect(double aspect) { aspect_ = aspect > 0 ? aspect : aspect_; } + + // 单张 3D 纹理各轴上限(GL_MAX_3D_TEXTURE_SIZE)。 + void setMaxTextureDim(int dim) { maxTextureDim_ = dim > 0 ? dim : maxTextureDim_; } + + private: + // 由 meta + exagg 填 VolumeView(spacing 已含 exagg 于 y/z)。 + VolumeView volumeView() const; + + geopro::data::ChunkedVolumeStore store_; + geopro::data::StoreMeta meta_; + double exagg_ = 1.0; + int maxTextureDim_ = 16384; + int viewportH_ = 1080; + double aspect_ = 1280.0 / 800.0; + + vtkSmartPointer current_; // 当前视野区域单图(empty 时为空指针) + int lastLevel_ = 0; +}; + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3321b5a..6faff58 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -126,6 +126,8 @@ target_sources(geopro_tests PRIVATE render/test_brick_pager.cpp) target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp) # ViewAdaptiveLodPolicy(C1):视野自适应 LOD 选层(纯逻辑,零 VTK/Qt)——视锥裁剪求可见 brick 区间 + 视距/分辨率选层。 target_sources(geopro_tests PRIVATE render/test_view_adaptive_lod.cpp) +# ViewAdaptiveVolumeSource(C2):用 C1 selectLod 选层选区→从分块存储重组当前视野区域为单张 VTK_SHORT image(各轴≤16384/世界 origin/spacing 按 level+exagg/体素位置与 store 一致)。headless 不需 GPU。 +target_sources(geopro_tests PRIVATE render/test_view_adaptive_source.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) diff --git a/tests/render/test_view_adaptive_source.cpp b/tests/render/test_view_adaptive_source.cpp new file mode 100644 index 0000000..c6cb412 --- /dev/null +++ b/tests/render/test_view_adaptive_source.cpp @@ -0,0 +1,295 @@ +// ViewAdaptiveVolumeSource(C2) headless 测试:用 C1 selectLod 选层选区,从分块 +// 存储重组当前视野区域为【单张 VTK_SHORT vtkImageData】(各轴 ≤16384,世界 +// origin/spacing 按 level+exagg)。核心 updateView(CameraView,VolumeView) 不需真 +// vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。 +// +// 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应 +// level+区间位置一致(不错位)/ empty 情形空。 + +#include "render/source/ViewAdaptiveVolumeSource.hpp" + +#include "core/algo/GprVolumeBuilder.hpp" +#include "data/store/ChunkedVolumeStore.hpp" +#include "lod/ViewAdaptiveLodPolicy.hpp" + +#include +#include +#include + +#include +#include +#include + +using namespace geopro; +using geopro::render::CameraView; +using geopro::render::VolumeView; + +namespace { + +// 造一个含金字塔的 store:值 = 全局 (i+j+k)%1000(便于校验块定位),非 64 整除 +// 维度以含边缘块。返回 store 目录。与 test_outofcore_source 同口径。 +std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz, + double ox, double oy, double oz, double dx, + double dy, double dz, int brick, int levels) { + std::filesystem::remove_all(dir); + core::BuiltI16 b; + b.vol = core::ScalarVolumeI16(nx, ny, nz); + for (int k = 0; k < nz; ++k) + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) + b.vol.at(i, j, k) = static_cast((i + j + k) % 1000); + b.quant = {1.0, 0.0}; + b.origin = {{ox, oy, oz}}; + b.spacing = {{dx, dy, dz}}; + b.vminPhys = 0; + b.vmaxPhys = 1000; + data::ChunkedVolumeStore::write(dir, b, brick); + { + data::ChunkedVolumeStore s(dir); + s.buildPyramid(levels); + } + return dir; +} + +// 由 store meta + levels + exagg 填 VolumeView(与生产 volumeViewOf 同口径)。 +VolumeView volumeViewOf(const data::StoreMeta& m, int levels, double exagg) { + VolumeView v{}; + v.nx = m.nx; + v.ny = m.ny; + v.nz = m.nz; + v.brick = m.brick; + v.levels = levels; + v.origin[0] = m.origin[0]; + v.origin[1] = m.origin[1]; + v.origin[2] = m.origin[2]; + // C1 约定:spacing 已含 exagg 于 y/z。 + v.spacing[0] = m.spacing[0]; + v.spacing[1] = m.spacing[1] * exagg; + v.spacing[2] = m.spacing[2] * exagg; + v.exagg = exagg; + return v; +} + +// 相机:从 +X 看体中心(用 level0 物理范围,未含 exagg 于 X)。 +CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.0, + int viewportH = 1080) { + CameraView c{}; + const double cx = m.origin[0] + 0.5 * m.nx * m.spacing[0]; + const double cy = m.origin[1] + 0.5 * m.ny * m.spacing[1]; + const double cz = m.origin[2] + 0.5 * m.nz * m.spacing[2]; + c.focal[0] = cx; + c.focal[1] = cy; + c.focal[2] = cz; + c.pos[0] = cx + dist; + c.pos[1] = cy; + c.pos[2] = cz; + c.up[0] = 0; + c.up[1] = 0; + c.up[2] = 1; + c.fovYDeg = fovYDeg; + c.aspect = 1.0; + c.viewportH = viewportH; + return c; +} + +} // namespace + +// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT ─────────────────────────────── +TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_far").string(); + // 200×80×60, brick=64 → 含边缘块。3 级金字塔。 + makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); + + render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0); + EXPECT_EQ(src.meta().nx, 200); + + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); + const CameraView far = lookFromX(src.meta(), 8000.0); + src.updateView(far, vol); + + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + ASSERT_NE(imgs[0].Get(), nullptr); + EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT); + int d[3]; + imgs[0]->GetDimensions(d); + EXPECT_LE(d[0], 16384); + EXPECT_LE(d[1], 16384); + EXPECT_LE(d[2], 16384); + EXPECT_GT(d[0], 0); + EXPECT_GT(d[1], 0); + EXPECT_GT(d[2], 0); + EXPECT_GT(src.lastLevel(), 0); // 远 → 粗 +} + +// ── 近观:选最细层(0)、单图、仍 ≤16384 ───────────────────────────────────── +TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_near").string(); + makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); + + render::ViewAdaptiveVolumeSource src(dir, 1.0); + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); + const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080); + src.updateView(near, vol); + + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细 + int d[3]; + imgs[0]->GetDimensions(d); + EXPECT_LE(d[0], 16384); + EXPECT_LE(d[1], 16384); + EXPECT_LE(d[2], 16384); +} + +// ── 重组体素与 store 对应 level/区间位置一致(不错位)────────────────────── +// 远观选某粗 level,重组单图后抽查若干体素:用世界坐标反推该 level 体素索引, +// 与 store.readBrick(level,...) 对应块的同一体素值逐一相等 → 证明位置正确。 +TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_match").string(); + makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); + + render::ViewAdaptiveVolumeSource src(dir, 1.0); + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); + const CameraView far = lookFromX(src.meta(), 8000.0); + src.updateView(far, vol); + + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + vtkImageData* img = imgs[0]; + const int level = src.lastLevel(); + + data::ChunkedVolumeStore store(dir); + const data::StoreMeta& m = src.meta(); + const int brick = m.brick; + const double sc = static_cast(1 << level); + + int dimLx = 0, dimLy = 0, dimLz = 0; + store.dims(level, dimLx, dimLy, dimLz); + + int d[3]; + img->GetDimensions(d); + double org[3], sp[3]; + img->GetOrigin(org); + img->GetSpacing(sp); + + // spacing 应 = meta.spacing × 2^level(exagg=1)。 + EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc); + EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc); + EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc); + + auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars()); + ASSERT_NE(arr, nullptr); + + // 该图覆盖的 level 体素起点:由 origin 反推(应整数)。 + const long gi0 = std::lround((org[0] - m.origin[0]) / sp[0]); + const long gj0 = std::lround((org[1] - m.origin[1]) / sp[1]); + const long gk0 = std::lround((org[2] - m.origin[2]) / sp[2]); + EXPECT_GE(gi0, 0); + EXPECT_GE(gj0, 0); + EXPECT_GE(gk0, 0); + + // 取该 level 单块校验:读 store 块 (b*,0,0),把块内某体素与图中同位置对比。 + auto storeVoxelAt = [&](int gi, int gj, int gk) -> std::int16_t { + const int bx = gi / brick, by = gj / brick, bz = gk / brick; + const std::vector raw = store.readBrick(level, bx, by, bz); + const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick; + const int bw = std::min(brick, dimLx - i0); + const int bh = std::min(brick, dimLy - j0); + const int li = gi - i0, lj = gj - j0, lk = gk - k0; + const std::size_t w = + (static_cast(lk) * bh + lj) * bw + li; + return raw[w]; + }; + + // 抽查图内若干位置(含非角点、跨块),逐一与 store 对应 level 体素相等。 + int checked = 0; + for (int lk : {0, d[2] / 2, d[2] - 1}) { + for (int lj : {0, d[1] / 2, d[1] - 1}) { + for (int li : {0, d[0] / 2, d[0] - 1}) { + const long gi = gi0 + li, gj = gj0 + lj, gk = gk0 + lk; + if (gi >= dimLx || gj >= dimLy || gk >= dimLz) continue; + const vtkIdType id = + (static_cast(lk) * d[1] + lj) * d[0] + li; + const std::int16_t got = arr->GetValue(id); + const std::int16_t want = + storeVoxelAt(static_cast(gi), static_cast(gj), + static_cast(gk)); + EXPECT_EQ(got, want) + << "体素错位 at local(" << li << "," << lj << "," << lk + << ") level=" << level; + ++checked; + } + } + } + EXPECT_GT(checked, 0); +} + +// ── 世界 origin:按 level+exagg 偏移正确(exagg≠1 校验 y/z 烘焙)───────────── +TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_exagg").string(); + makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); + + const double exagg = 4.0; + render::ViewAdaptiveVolumeSource src(dir, exagg); + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), exagg); + // 远观整体,区间从 0 起 → origin 应正好 = meta.origin(区间起点 0)。 + const CameraView far = lookFromX(src.meta(), 8000.0); + src.updateView(far, vol); + + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + const data::StoreMeta& m = src.meta(); + const int level = src.lastLevel(); + const double sc = static_cast(1 << level); + double sp[3], org[3]; + imgs[0]->GetSpacing(sp); + imgs[0]->GetOrigin(org); + // spacing:x 不夸张,y/z ×exagg。 + EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc); + EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg); + EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc * exagg); + // 区间从 0 起 → origin = meta.origin(y/z 偏移为 0×… 仍是 origin)。 + EXPECT_DOUBLE_EQ(org[0], m.origin[0]); + EXPECT_DOUBLE_EQ(org[1], m.origin[1]); + EXPECT_DOUBLE_EQ(org[2], m.origin[2]); +} + +// ── empty:体完全在视锥外 → currentImages 空 ─────────────────────────────── +TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_empty").string(); + makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); + + render::ViewAdaptiveVolumeSource src(dir, 1.0); + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); + CameraView c = lookFromX(src.meta(), 1000.0); + c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X,体在身后 → 视锥外 + src.updateView(c, vol); + + EXPECT_TRUE(src.currentImages().empty()); + EXPECT_EQ(src.sliceSource(), nullptr); +} + +// ── 单层 store(无金字塔)也能重组(level 恒 0)─────────────────────────── +TEST(ViewAdaptiveVolumeSource, SingleLevelStore) { + const auto dir = + (std::filesystem::temp_directory_path() / "gpr_va_1lvl").string(); + // levels=0 → 仅 level 0。 + makePyramidStore(dir, 128, 64, 48, 0, 0, 0, 1.0, 1.0, 1.0, 64, 0); + + render::ViewAdaptiveVolumeSource src(dir, 1.0); + EXPECT_EQ(src.levelCount(), 1); + const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); + const CameraView c = lookFromX(src.meta(), 500.0); + src.updateView(c, vol); + + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + EXPECT_EQ(src.lastLevel(), 0); + EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT); +} diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index c8cd305..44cfbc1 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -34,6 +34,7 @@ #include "io/gpr/IprHeader.hpp" #include "render/actors/VoxelActor.hpp" #include "render/source/OutOfCoreSource.hpp" +#include "render/source/ViewAdaptiveVolumeSource.hpp" #include "render/source/WholeVolumeSource.hpp" // ---- VTK 离屏渲染 ---- @@ -2771,8 +2772,13 @@ constexpr int kViewMax3DTex = 16384; // 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。 // view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。 +// +// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1 +// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。 +// 退掉旧 POC 简化路径(viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存 +// 三件套/MultiBlock 分块全部由 C1+C2 承担)。 struct ViewState { - geopro::data::ChunkedVolumeStore* store = nullptr; + geopro::render::ViewAdaptiveVolumeSource* source = nullptr; vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper vtkRenderer* ren = nullptr; vtkCamera* cam = nullptr; @@ -2780,135 +2786,25 @@ struct ViewState { vtkRenderWindow* rw = nullptr; double exagg = 8.0; int lastLevel = -1; - // 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。 - int cachedWholeLevel = -1; - vtkSmartPointer cachedWholeImg; - // 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。 - int cachedLocalBx0 = -1; - int cachedLocalCount = -1; - vtkSmartPointer cachedLocalImg; + // 持有当前单图引用,避免被释放(mapper 仅持裸指针)。 + vtkSmartPointer currentImg; // 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。 bool inCb = false; }; -// 某 level 整卷各轴是否都 ≤16384(可成单张 3D 纹理 → 整卷单 mapper 渲染)。 -bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store, - int level) { - int nx = 0, ny = 0, nz = 0; - store.dims(level, nx, ny, nz); - return nx <= kViewMax3DTex && ny <= kViewMax3DTex && nz <= kViewMax3DTex; -} - -// 给定相机选中的 level,返回真正用于整卷渲染的 level:从 picked 起向粗逐层找, -// 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384,则升到 level2)。 -// 找不到(极端情况)返回 -1,调用方退回局部子区域路径。 -int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store, - int picked) { - const int maxLevel = store.levels() - 1; - for (int lv = std::max(0, picked); lv <= maxLevel; ++lv) { - if (levelFitsSingleTexture(store, lv)) { - return lv; - } - } - return -1; -} - -// 由相机到体中心距离粗分档选 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 整卷 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); - } - - 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 文本用)。 +// 单纹理刷新:source->update(cam) 选 LOD + 重组当前视野区域单图,喂单 +// SmartVolumeMapper。返回喂入的块数(恒为 1 单纹理;视锥外 → 0 不渲)。 +// 同步刷新 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; + st->source->update(st->cam); + st->lastLevel = st->source->lastLevel(); + auto imgs = st->source->currentImages(); + if (imgs.empty() || imgs[0] == nullptr) { + return 0; // 视锥外:不更新输入(保留上一帧),由调用方决定是否渲。 } - - // 拉近(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->currentImg = imgs[0]; + st->mapper->SetInputData(st->currentImg); st->mapper->Update(); - st->lastLevel = 0; return 1; } @@ -2960,32 +2856,53 @@ constexpr int kViewDefaultLocalBricks = 4; // // 返回喂给 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 locImg = - buildLocalLevel0Image(store, m, bx0, localBx); + geopro::render::ViewAdaptiveVolumeSource& source = *st->source; + const geopro::data::StoreMeta& m = source.meta(); - // 单纹理:一张 image 直接喂单 SmartVolumeMapper(与 --preview 同路径)。 - st->mapper->SetInputData(locImg); - st->mapper->Update(); - st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组 - st->cachedLocalBx0 = bx0; - st->cachedLocalCount = localBx; - st->lastLevel = 0; + // 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列) + // 正前方近观 → 触发 C1 选 level0 + 视野子区间,C2 重组该视野单图。先用「局部段 + // 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。 + const int brick = m.brick; + const int totBricksX = (m.nx + brick - 1) / brick; + const int localBx = std::min(kViewDefaultLocalBricks, totBricksX); + const int bx0 = std::max(0, totBricksX / 2 - localBx / 2); // 沿线中段 + // 该局部段世界 X 范围(level0)。 + (void)bx0; + (void)localBx; + (void)totBricksX; + // 体(exagg 后)世界尺寸与中心 + 包围球半径。 + const double wx = std::max(1.0, m.nx * m.spacing[0]); + const double wy = std::max(1.0, m.ny * m.spacing[1] * st->exagg); + const double wz = std::max(1.0, m.nz * m.spacing[2] * st->exagg); + const double cx = m.origin[0] + 0.5 * wx; + const double cy = m.origin[1] + 0.5 * wy; + const double cz = m.origin[2] + 0.5 * wz; + const double radius = 0.5 * std::sqrt(wx * wx + wy * wy + wz * wz); + + // 相机从 +X 看体中心,距离 = 半径 / tan(半视角) × 余量,确保整体落入视锥(C1 + // selectLod 不会判 empty),由视距/分辨率自然选层;近观靠交互再拉近切细层。 + st->cam = ren->GetActiveCamera(); + const double fovY = st->cam->GetViewAngle(); + const double halfAngle = 0.5 * fovY * 3.14159265358979 / 180.0; + const double tanH = std::max(1e-3, std::tan(halfAngle)); + const double dist = radius / tanH * 1.4; // 1.4:留余量含 aspect/边缘 + st->cam->SetFocalPoint(cx, cy, cz); + st->cam->SetPosition(cx + dist, cy, cz); + st->cam->SetViewUp(0, 0, 1); + ren->ResetCameraClippingRange(); + + // 源选层选区 + 重组单图喂 mapper。 + const std::size_t blocks = viewRefreshSingle(st); // 框住局部段:用无参 ResetCamera(按 actor 的【已 SetScale(1,exagg,exagg)】缩放 - // 后包围盒框,把 exagg 后的 Y/Z 一并纳入;mapper->GetBounds() 是未缩放的,不可用), - // 相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。 + // 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。 ren->ResetCamera(); st->cam = ren->GetActiveCamera(); st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景:El18 st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22 st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面 ren->ResetCameraClippingRange(); - return 1; // 单纹理:恒一张 image + return blocks; } // 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。 @@ -3179,8 +3096,13 @@ int cmdView(int argc, char** argv) { // 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产 // 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。 (void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。 - geopro::data::ChunkedVolumeStore store(dir); - const auto& m = store.meta(); + // 渲染源 = ViewAdaptiveVolumeSource(C2)。exagg 走 actor 的 SetScale(不烘进 + // 几何,避免与 SetScale 重复夸张),故源构造 exagg=1.0。视口高/宽高比注入给 + // C1 选层(分辨率密度 + 视锥裁剪)。 + geopro::render::ViewAdaptiveVolumeSource source(dir, /*exagg=*/1.0); + source.setViewportHeight(winH); + source.setAspect(static_cast(winW) / winH); + const auto& m = source.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); @@ -3227,7 +3149,7 @@ int cmdView(int argc, char** argv) { vtkOutputWindow::SetInstance(capWin); ViewState st; - st.store = &store; + st.source = &source; st.mapper = mapper.Get(); st.ren = ren.Get(); st.fpsText = fpsText.Get();