// 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); }