296 lines
11 KiB
C++
296 lines
11 KiB
C++
// 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 <vtkImageData.h>
|
||
#include <vtkPointData.h>
|
||
#include <vtkShortArray.h>
|
||
|
||
#include <cmath>
|
||
#include <filesystem>
|
||
#include <gtest/gtest.h>
|
||
|
||
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<short>((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<double>(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<std::int16_t> 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<std::size_t>(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<vtkIdType>(lk) * d[1] + lj) * d[0] + li;
|
||
const std::int16_t got = arr->GetValue(id);
|
||
const std::int16_t want =
|
||
storeVoxelAt(static_cast<int>(gi), static_cast<int>(gj),
|
||
static_cast<int>(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<double>(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);
|
||
}
|