geopro/tests/render/test_view_adaptive_lod.cpp

259 lines
10 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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=4L0..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=128brick≠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=64brick 粒度无法表达更小区间 → 返回单块 + 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);
}