#include #include #include "lod/ViewAdaptiveLodPolicy.hpp" using geopro::render::CameraView; using geopro::render::LodSelection; using geopro::render::selectLod; using geopro::render::VolumeView; namespace { // 构造一个规整体:nx=ny=nz=512、brick=64(每轴 8 块)、level0 间距=1、levels=4(L0..L3)。 // 世界范围 [0,512]³(origin 0)。各 level 维度:512/256/128/64;间距 1/2/4/8。 VolumeView makeVol(int n = 512, int brick = 64, int levels = 4, double sp = 1.0) { VolumeView v{}; v.nx = v.ny = v.nz = n; v.brick = brick; v.levels = levels; v.origin[0] = v.origin[1] = v.origin[2] = 0.0; v.spacing[0] = v.spacing[1] = v.spacing[2] = sp; v.exagg = 1.0; return v; } // 相机:从 +X 方向看体中心。dist = 相机到中心距离;fov/viewportH 控分辨率密度。 CameraView lookFromX(const VolumeView& v, double dist, double fovYDeg = 30.0, int viewportH = 1080) { CameraView c{}; const double cx = v.origin[0] + 0.5 * v.nx * v.spacing[0]; const double cy = v.origin[1] + 0.5 * v.ny * v.spacing[1]; const double cz = v.origin[2] + 0.5 * v.nz * v.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 // ── empty:体完全在视锥外(相机背对体)→ empty ────────────────────────────── TEST(ViewAdaptiveLod, VolumeBehindCameraIsEmpty) { const VolumeView v = makeVol(); CameraView c = lookFromX(v, 1000.0); // 把焦点设到相机背后:相机仍在 +X 远处,但看向 +X 更远(体在身后)。 c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X,体在 -X 侧 → 视锥外 const LodSelection sel = selectLod(v, c); EXPECT_TRUE(sel.empty); } TEST(ViewAdaptiveLod, VolumeOffToSideIsEmpty) { const VolumeView v = makeVol(); // 相机看 +Z(体在 -X..+X、相机在远 +X,焦点正上方)→ 体偏出窄视锥。 CameraView c{}; c.pos[0] = 100000; c.pos[1] = 0; c.pos[2] = 0; c.focal[0] = 100000; c.focal[1] = 0; c.focal[2] = 100000; // 看 +Z c.up[0] = 1; c.up[1] = 0; c.up[2] = 0; c.fovYDeg = 5.0; c.aspect = 1.0; c.viewportH = 1080; const LodSelection sel = selectLod(v, c); EXPECT_TRUE(sel.empty); } // ── 远观整体 → 粗层、区间≈全体、各轴 ≤ maxTextureDim ─────────────────────── TEST(ViewAdaptiveLod, FarViewPicksCoarseLevelWholeVolume) { const VolumeView v = makeVol(); // 很远 + 适中 fov:整体入视锥,worldPerPixel 大 → 粗层。 const CameraView c = lookFromX(v, 8000.0); const LodSelection sel = selectLod(v, c); EXPECT_FALSE(sel.empty); EXPECT_GT(sel.level, 0); // 远 → 粗(非最细) // 区间≈全体:覆盖所有块(该 level 块数)。 // 该 level 每轴块数 = ceil(ceil(512/2^L)/64)。 const int dimL = (512 + (1 << sel.level) - 1) >> sel.level; const int bN = (dimL + v.brick - 1) / v.brick; EXPECT_EQ(sel.bx0, 0); EXPECT_EQ(sel.bx1, bN); EXPECT_EQ(sel.by0, 0); EXPECT_EQ(sel.by1, bN); EXPECT_EQ(sel.bz0, 0); EXPECT_EQ(sel.bz1, bN); } // ── 近观局部 → 细层、区间是视锥内小块、各轴 ≤ maxTextureDim ───────────────── TEST(ViewAdaptiveLod, NearViewPicksFineLevelSmallRegion) { const VolumeView v = makeVol(); // 贴近 + 窄 fov:只看到体内一小块,worldPerPixel 小 → 细层(0)、小区间。 const CameraView c = lookFromX(v, 60.0, 20.0, 1080); const LodSelection sel = selectLod(v, c); EXPECT_FALSE(sel.empty); EXPECT_EQ(sel.level, 0); // 近 → 最细 // 区间是子集(不是全 8 块):至少某一垂直于视线的轴被裁小。 const int bNfull = (512 + v.brick - 1) / v.brick; // 8 const bool yShrunk = (sel.by1 - sel.by0) < bNfull; const bool zShrunk = (sel.bz1 - sel.bz0) < bNfull; EXPECT_TRUE(yShrunk || zShrunk); } // 契约里的“重组单纹理某轴体素数”:起点对齐 b0*brick,终点取 // min(b1*brick, dimL, b0*brick + maxTextureDim)(含 brick>maxTextureDim 的体素级再裁)。 // 生产代码保证此值恒 ≤ maxTextureDim;测试用同一公式焊死不变量。 static int reconstructedAxisTex(int b0, int b1, int brick, int dimL, int maxTextureDim) { const int start = b0 * brick; const int end = std::min({b1 * brick, dimL, start + maxTextureDim}); return std::max(0, end - start); } static int dimAt(int n, int level) { return (n + (1 << level) - 1) >> level; } // ── 选定层重组区间各轴恒 ≤ maxTextureDim(小 maxTextureDim 强制升层/缩区间)── // brick=64、maxTextureDim=128(brick≠maxTextureDim,不靠 64=64 侥幸):允许 2 块/轴, // 多于此须按中心裁掉,真正走多块 clamp 算术(而非边界相等巧合)。 TEST(ViewAdaptiveLod, RespectsMaxTextureDimAlways) { const VolumeView v = makeVol(); // brick=64 const int kMax = 128; // ≠ brick;容 2 块,逼出多块中心裁剪 for (double dist : {60.0, 300.0, 1500.0, 8000.0, 40000.0}) { const CameraView c = lookFromX(v, dist); const LodSelection sel = selectLod(v, c, kMax); if (sel.empty) continue; const int dimL = dimAt(512, sel.level); EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); // 区间合法、非空。 EXPECT_LT(sel.bx0, sel.bx1); EXPECT_LT(sel.by0, sel.by1); EXPECT_LT(sel.bz0, sel.bz1); } } // ── brick > maxTextureDim:单块体素数已超限,仍必须保证重组 ≤ maxTextureDim ────── // brick=128、maxTextureDim=64:brick 粒度无法表达更小区间 → 返回单块 + C2 体素级再裁, // 重组实际体素数 = min(单块, maxTextureDim) = 64 ≤ 64。各视距各轴恒不超限。 TEST(ViewAdaptiveLod, BrickLargerThanMaxTextureDimStillClamped) { const VolumeView v = makeVol(/*n=*/512, /*brick=*/128, /*levels=*/4); const int kMax = 64; // < brick=128 for (double dist : {60.0, 300.0, 1500.0, 8000.0, 40000.0}) { const CameraView c = lookFromX(v, dist); const LodSelection sel = selectLod(v, c, kMax); if (sel.empty) continue; const int dimL = dimAt(512, sel.level); EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); EXPECT_LT(sel.bx0, sel.bx1); // 恒至少一块 } } // ── 最粗层整卷 > maxTextureDim:远观也必须裁成子区间,绝不突破硬上限 ──────────── // nx=156544、levels=3 → 最粗层 dim=ceil(156544/4)=39136 > 16384;旧实现在最粗层无条件 // 返回会突破上限。修复后远观应裁成 ≤16384 的中心子区间,empty=false。 TEST(ViewAdaptiveLod, CoarsestLevelOverflowIsClampedNotBreached) { VolumeView v{}; v.nx = v.ny = v.nz = 156544; v.brick = 64; v.levels = 3; // L0..L2;最粗层 dim = ceil(156544/4) = 39136 > 16384 v.origin[0] = v.origin[1] = v.origin[2] = 0.0; v.spacing[0] = v.spacing[1] = v.spacing[2] = 1.0; v.exagg = 1.0; // 极远观:worldPerPixel 大 → 选最粗层;整卷入视锥。 const CameraView c = lookFromX(v, 5.0e6, 30.0, 1080); const int kMax = 16384; const LodSelection sel = selectLod(v, c, kMax); ASSERT_FALSE(sel.empty); const int dimL = dimAt(v.nx, sel.level); EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); EXPECT_LT(sel.bx0, sel.bx1); EXPECT_LT(sel.by0, sel.by1); EXPECT_LT(sel.bz0, sel.bz1); } TEST(ViewAdaptiveLod, RespectsDefaultMaxTextureDim) { const VolumeView v = makeVol(/*n=*/4096, /*brick=*/256, /*levels=*/1); // levels=1(只有 level0)→ 不能升层,只能靠缩区间满足 16384。4096 < 16384 必满足。 const CameraView c = lookFromX(v, 100000.0); const LodSelection sel = selectLod(v, c, 16384); ASSERT_FALSE(sel.empty); EXPECT_EQ(sel.level, 0); const int texX = std::min((sel.bx1) * v.brick, 4096) - sel.bx0 * v.brick; EXPECT_LE(texX, 16384); } // ── 视距-层单调:拉近 level 不增 ──────────────────────────────────────────── TEST(ViewAdaptiveLod, LevelMonotonicWithDistance) { const VolumeView v = makeVol(); int prev = 1 << 30; for (double dist : {40000.0, 8000.0, 1500.0, 300.0, 60.0}) { // 由远到近 const CameraView c = lookFromX(v, dist); const LodSelection sel = selectLod(v, c); if (sel.empty) continue; EXPECT_LE(sel.level, prev) << "dist=" << dist; // 拉近 level 不增 prev = sel.level; } } // ── 区间半开且合法(b0> sel.level; const int bN = (dimL + v.brick - 1) / v.brick; EXPECT_GE(sel.bx0, 0); EXPECT_LT(sel.bx0, sel.bx1); EXPECT_LE(sel.bx1, bN); EXPECT_GE(sel.by0, 0); EXPECT_LT(sel.by0, sel.by1); EXPECT_LE(sel.by1, bN); EXPECT_GE(sel.bz0, 0); EXPECT_LT(sel.bz0, sel.bz1); EXPECT_LE(sel.bz1, bN); } // ── side 视角:从某一侧斜看,仍非空且区间合法 ───────────────────────────── TEST(ViewAdaptiveLod, ObliqueSideViewNotEmpty) { const VolumeView v = makeVol(); CameraView c{}; const double cx = 256, cy = 256, cz = 256; c.pos[0] = cx + 1200; c.pos[1] = cy + 1200; c.pos[2] = cz + 800; c.focal[0] = cx; c.focal[1] = cy; c.focal[2] = cz; c.up[0] = 0; c.up[1] = 0; c.up[2] = 1; c.fovYDeg = 45.0; c.aspect = 1.6; c.viewportH = 1080; const LodSelection sel = selectLod(v, c); EXPECT_FALSE(sel.empty); EXPECT_GE(sel.level, 0); EXPECT_LT(sel.bx0, sel.bx1); }