259 lines
10 KiB
C++
259 lines
10 KiB
C++
#include <gtest/gtest.h>
|
||
|
||
#include <cmath>
|
||
|
||
#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<b1,落在块数范围内)────────────────────────────────
|
||
TEST(ViewAdaptiveLod, IntervalsAreValidHalfOpen) {
|
||
const VolumeView v = makeVol();
|
||
const CameraView c = lookFromX(v, 1500.0);
|
||
const LodSelection sel = selectLod(v, c);
|
||
ASSERT_FALSE(sel.empty);
|
||
const int dimL = (512 + (1 << sel.level) - 1) >> 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);
|
||
}
|