feat/vtk-3d-view #7
|
|
@ -4,6 +4,7 @@ add_library(geopro_render STATIC
|
|||
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp
|
||||
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
||||
ground/TileMath.cpp
|
||||
lod/ViewAdaptiveLodPolicy.cpp
|
||||
source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp)
|
||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
#include "lod/ViewAdaptiveLodPolicy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
using Vec3 = std::array<double, 3>;
|
||||
|
||||
Vec3 sub(const double a[3], const double b[3]) {
|
||||
return {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
|
||||
}
|
||||
double dot(const Vec3& a, const Vec3& b) {
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
Vec3 cross(const Vec3& a, const Vec3& b) {
|
||||
return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0]};
|
||||
}
|
||||
double norm(const Vec3& a) { return std::sqrt(dot(a, a)); }
|
||||
Vec3 normalize(const Vec3& a) {
|
||||
const double n = norm(a);
|
||||
if (n <= 0.0) return {0, 0, 1};
|
||||
return {a[0] / n, a[1] / n, a[2] / n};
|
||||
}
|
||||
|
||||
// 一个内侧法向平面:n·x + d ≥ 0 为内侧。
|
||||
struct Plane {
|
||||
Vec3 n;
|
||||
double d;
|
||||
};
|
||||
|
||||
// 由透视相机参数构造 5 个内侧法向平面(左/右/上/下/近;远平面省略,不参与视野裁剪)。
|
||||
//
|
||||
// 设 f=前向、r=右向、u=真上向(正交基);tanV=tan(fovY/2)、tanH=tanV*aspect。
|
||||
// 视空间内侧条件:|vx| ≤ tanH·vz、|vy| ≤ tanV·vz、vz ≥ near。
|
||||
// 将 vx=dot(rel,r)、vy=dot(rel,u)、vz=dot(rel,f)(rel=x-pos)代入得世界系线性平面:
|
||||
// 右:tanH·f - r;左:tanH·f + r;上:tanV·f - u;下:tanV·f + u(均过 pos)。
|
||||
// 近:f,过 pos+near·f。
|
||||
std::array<Plane, 5> buildFrustumPlanes(const CameraView& cam) {
|
||||
const Vec3 pos{cam.pos[0], cam.pos[1], cam.pos[2]};
|
||||
const Vec3 f = normalize(sub(cam.focal, cam.pos));
|
||||
const Vec3 upHint{cam.up[0], cam.up[1], cam.up[2]};
|
||||
Vec3 r = cross(f, upHint);
|
||||
if (norm(r) <= 1e-12) {
|
||||
// up 与视线共线:换一个参考轴。
|
||||
r = cross(f, Vec3{1, 0, 0});
|
||||
if (norm(r) <= 1e-12) r = cross(f, Vec3{0, 1, 0});
|
||||
}
|
||||
r = normalize(r);
|
||||
const Vec3 u = normalize(cross(r, f));
|
||||
|
||||
const double tanV = std::tan(0.5 * cam.fovYDeg * kPi / 180.0);
|
||||
const double tanH = tanV * (cam.aspect > 0 ? cam.aspect : 1.0);
|
||||
|
||||
auto makeThroughPos = [&](const Vec3& n) -> Plane {
|
||||
return Plane{n, -dot(n, pos)};
|
||||
};
|
||||
|
||||
std::array<Plane, 5> planes{};
|
||||
planes[0] = makeThroughPos({tanH * f[0] - r[0], tanH * f[1] - r[1],
|
||||
tanH * f[2] - r[2]}); // 右
|
||||
planes[1] = makeThroughPos({tanH * f[0] + r[0], tanH * f[1] + r[1],
|
||||
tanH * f[2] + r[2]}); // 左
|
||||
planes[2] = makeThroughPos({tanV * f[0] - u[0], tanV * f[1] - u[1],
|
||||
tanV * f[2] - u[2]}); // 上
|
||||
planes[3] = makeThroughPos({tanV * f[0] + u[0], tanV * f[1] + u[1],
|
||||
tanV * f[2] + u[2]}); // 下
|
||||
// 近平面:取很小的正 near,避免把贴脸的体裁掉;只防身后。
|
||||
const double nearD = 1e-6;
|
||||
planes[4] = Plane{f, -(dot(f, pos) + nearD)}; // 近(vz ≥ near)
|
||||
return planes;
|
||||
}
|
||||
|
||||
// AABB[bmin,bmax] 是否与视锥相交(保守:p-vertex 测试,与 OutOfCoreSource 一致)。
|
||||
bool aabbInFrustum(const Vec3& bmin, const Vec3& bmax,
|
||||
const std::array<Plane, 5>& planes) {
|
||||
for (const Plane& pl : planes) {
|
||||
const double px = (pl.n[0] >= 0) ? bmax[0] : bmin[0];
|
||||
const double py = (pl.n[1] >= 0) ? bmax[1] : bmin[1];
|
||||
const double pz = (pl.n[2] >= 0) ? bmax[2] : bmin[2];
|
||||
if (pl.n[0] * px + pl.n[1] * py + pl.n[2] * pz + pl.d < 0.0) {
|
||||
return false; // 最正顶点都在该面外 → 整盒在外。
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int ceilDiv(int a, int b) { return (a + b - 1) / b; }
|
||||
|
||||
// level L 各轴体素维度 = ceil(n / 2^L),至少 1。
|
||||
int dimAtLevel(int n, int level) {
|
||||
const int d = ceilDiv(n, 1 << level);
|
||||
return d > 0 ? d : 1;
|
||||
}
|
||||
|
||||
// 某 level 某轴块数。
|
||||
int bricksAtLevel(int n, int brick, int level) {
|
||||
return ceilDiv(dimAtLevel(n, level), brick);
|
||||
}
|
||||
|
||||
// 在给定 level 上做视锥裁剪,求可见 brick 区间(半开)。返回是否有可见块。
|
||||
bool visibleBrickRange(const VolumeView& vol, int level,
|
||||
const std::array<Plane, 5>& planes, int& bx0, int& bx1,
|
||||
int& by0, int& by1, int& bz0, int& bz1) {
|
||||
const int bxN = bricksAtLevel(vol.nx, vol.brick, level);
|
||||
const int byN = bricksAtLevel(vol.ny, vol.brick, level);
|
||||
const int bzN = bricksAtLevel(vol.nz, vol.brick, level);
|
||||
|
||||
const double scale = static_cast<double>(std::int64_t(1) << level); // 2^L
|
||||
const double sp[3] = {vol.spacing[0] * scale, vol.spacing[1] * scale,
|
||||
vol.spacing[2] * scale};
|
||||
const int dimX = dimAtLevel(vol.nx, level);
|
||||
const int dimY = dimAtLevel(vol.ny, level);
|
||||
const int dimZ = dimAtLevel(vol.nz, level);
|
||||
|
||||
bx0 = by0 = bz0 = 1 << 30;
|
||||
bx1 = by1 = bz1 = 0;
|
||||
bool any = false;
|
||||
|
||||
for (int bz = 0; bz < bzN; ++bz) {
|
||||
const int k0 = bz * vol.brick;
|
||||
const int kd = std::min(vol.brick, dimZ - k0);
|
||||
for (int by = 0; by < byN; ++by) {
|
||||
const int j0 = by * vol.brick;
|
||||
const int jd = std::min(vol.brick, dimY - j0);
|
||||
for (int bx = 0; bx < bxN; ++bx) {
|
||||
const int i0 = bx * vol.brick;
|
||||
const int id = std::min(vol.brick, dimX - i0);
|
||||
const Vec3 bmin{vol.origin[0] + i0 * sp[0], vol.origin[1] + j0 * sp[1],
|
||||
vol.origin[2] + k0 * sp[2]};
|
||||
const Vec3 bmax{vol.origin[0] + (i0 + id) * sp[0],
|
||||
vol.origin[1] + (j0 + jd) * sp[1],
|
||||
vol.origin[2] + (k0 + kd) * sp[2]};
|
||||
if (aabbInFrustum(bmin, bmax, planes)) {
|
||||
any = true;
|
||||
bx0 = std::min(bx0, bx);
|
||||
bx1 = std::max(bx1, bx + 1);
|
||||
by0 = std::min(by0, by);
|
||||
by1 = std::max(by1, by + 1);
|
||||
bz0 = std::min(bz0, bz);
|
||||
bz1 = std::max(bz1, bz + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
// 区间在该 level 重组单纹理的某轴体素数(块数 × brick,截到该 level 维度)。
|
||||
int axisTexture(int b0, int b1, int brick, int dimL) {
|
||||
const int start = b0 * brick;
|
||||
const int end = std::min(b1 * brick, dimL);
|
||||
return std::max(0, end - start);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LodSelection selectLod(const VolumeView& vol, const CameraView& cam,
|
||||
int maxTextureDim) {
|
||||
LodSelection out{};
|
||||
out.empty = true;
|
||||
|
||||
const int maxLevel = std::max(0, vol.levels - 1);
|
||||
const std::array<Plane, 5> planes = buildFrustumPlanes(cam);
|
||||
|
||||
// ── ② 选层第一步:定"最细的不过采样层" Lmin。 ───────────────────────────
|
||||
// worldPerPixel = 2·tanV·dist / viewportH(透视下屏幕一像素对应的世界尺度)。
|
||||
// level L 体素世界尺寸 = minSpacing × 2^L。不过采样 ⇔ 体素尺寸 ≥ worldPerPixel,
|
||||
// 即每体素投影 ≳1 像素。取满足该式的最细 L。
|
||||
const Vec3 f = normalize(sub(cam.focal, cam.pos));
|
||||
const Vec3 center{vol.origin[0] + 0.5 * vol.nx * vol.spacing[0],
|
||||
vol.origin[1] + 0.5 * vol.ny * vol.spacing[1],
|
||||
vol.origin[2] + 0.5 * vol.nz * vol.spacing[2]};
|
||||
const Vec3 toCenter = sub(center.data(), cam.pos);
|
||||
const double dist = std::max(1e-9, dot(toCenter, f)); // 沿视线深度(≥0)
|
||||
|
||||
const double tanV = std::tan(0.5 * cam.fovYDeg * kPi / 180.0);
|
||||
const int vph = cam.viewportH > 0 ? cam.viewportH : 1;
|
||||
const double worldPerPixel = 2.0 * tanV * dist / static_cast<double>(vph);
|
||||
|
||||
const double minSpacing =
|
||||
std::min({std::abs(vol.spacing[0]), std::abs(vol.spacing[1]),
|
||||
std::abs(vol.spacing[2])});
|
||||
|
||||
int lmin = 0;
|
||||
if (minSpacing > 0.0 && worldPerPixel > 0.0) {
|
||||
// 求最小 L 使 minSpacing·2^L ≥ worldPerPixel。
|
||||
const double ratio = worldPerPixel / minSpacing;
|
||||
if (ratio > 1.0) {
|
||||
lmin = static_cast<int>(std::ceil(std::log2(ratio)));
|
||||
}
|
||||
}
|
||||
lmin = std::clamp(lmin, 0, maxLevel);
|
||||
|
||||
// ── ③ 从 Lmin 起逐级变粗,直到可见区间重组各轴 ≤ maxTextureDim(硬约束 a)。 ─
|
||||
for (int level = lmin; level <= maxLevel; ++level) {
|
||||
int bx0, bx1, by0, by1, bz0, bz1;
|
||||
if (!visibleBrickRange(vol, level, planes, bx0, bx1, by0, by1, bz0, bz1)) {
|
||||
out.empty = true; // 该层全裁 → 体在视锥外(各 level 同一视锥,结论一致)。
|
||||
return out;
|
||||
}
|
||||
const int dimX = dimAtLevel(vol.nx, level);
|
||||
const int dimY = dimAtLevel(vol.ny, level);
|
||||
const int dimZ = dimAtLevel(vol.nz, level);
|
||||
const int tx = axisTexture(bx0, bx1, vol.brick, dimX);
|
||||
const int ty = axisTexture(by0, by1, vol.brick, dimY);
|
||||
const int tz = axisTexture(bz0, bz1, vol.brick, dimZ);
|
||||
|
||||
const bool fits =
|
||||
tx <= maxTextureDim && ty <= maxTextureDim && tz <= maxTextureDim;
|
||||
if (fits || level == maxLevel) {
|
||||
out.level = level;
|
||||
out.bx0 = bx0;
|
||||
out.bx1 = bx1;
|
||||
out.by0 = by0;
|
||||
out.by1 = by1;
|
||||
out.bz0 = bz0;
|
||||
out.bz1 = bz1;
|
||||
out.empty = false;
|
||||
return out;
|
||||
}
|
||||
// 不满足 maxTextureDim 且非最粗层 → 继续变粗。
|
||||
}
|
||||
|
||||
return out; // 理论不可达(循环必返回);保底 empty。
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// ── C1:视野自适应 LOD 选层(纯逻辑,headless 可测,零 VTK/Qt 依赖)────────────
|
||||
//
|
||||
// 把"渲视野内、远处自动粗、近处细且只取视野小块"的策略钉成纯数值函数:
|
||||
// 相机参数用平面结构 CameraView 传(渲染层从 vtkCamera 填),不直接吃 vtkCamera。
|
||||
// 输出:选定 LOD level + 该层要渲的 brick 区间(半开)+ 是否整体在视锥外。
|
||||
//
|
||||
// 与 OutOfCoreSource 的几何约定保持一致:
|
||||
// - 金字塔 level 0 = 最细;level L 维度 = ceil(n / 2^L);
|
||||
// - level L 世界间距 = 基础 spacing × 2^L;世界 origin 不随 level 变;
|
||||
// - 块布局:每 brick 个体素一块,边缘块更小;i 最快、k 最慢。
|
||||
|
||||
// 平面相机参数(透视)。渲染层从 vtkCamera 填:
|
||||
// pos/focal/up 世界系;fovYDeg/aspect 透视张角与宽高比;viewportH 像素高(定屏幕分辨率)。
|
||||
struct CameraView {
|
||||
double pos[3];
|
||||
double focal[3];
|
||||
double up[3];
|
||||
double fovYDeg; // 垂直全张角(度)
|
||||
double aspect; // 视口宽/高
|
||||
int viewportH; // 视口像素高(用于"体素投影 ≳1 像素/体素"判定)
|
||||
};
|
||||
|
||||
// 体的元信息(从 StoreMeta + 垂向夸张 exagg)。
|
||||
// nx/ny/nz:level 0 体素维度;brick:块边长;levels:总层数(含 level 0)。
|
||||
// origin/spacing:level 0 世界几何(spacing 已含 exagg 于 y/z);exagg:垂向夸张(仅记录)。
|
||||
struct VolumeView {
|
||||
int nx, ny, nz;
|
||||
int brick;
|
||||
int levels;
|
||||
double origin[3];
|
||||
double spacing[3];
|
||||
double exagg;
|
||||
};
|
||||
|
||||
// 选层结果。
|
||||
// level:选定 LOD 层(0=最细)。
|
||||
// [bx0,bx1) [by0,by1) [bz0,bz1):该 level 要渲的 brick 区间(半开;empty 时全 0)。
|
||||
// empty:体完全在视锥外(无块可渲)。
|
||||
struct LodSelection {
|
||||
int level;
|
||||
int bx0, bx1;
|
||||
int by0, by1;
|
||||
int bz0, bz1;
|
||||
bool empty;
|
||||
};
|
||||
|
||||
// 选层规则:
|
||||
// ① 视锥裁剪求可见体素 AABB → 该层 brick 区间。
|
||||
// ② 选层:
|
||||
// - 先按"体素投影 ≳1 像素/体素"定**最细的不过采样层** Lmin(近观 worldPerPixel 小
|
||||
// → Lmin 小 → 细;远观 → Lmin 大 → 粗);
|
||||
// - 再从 Lmin 起逐级**变粗**直到可见区间重组单纹理各轴 ≤ maxTextureDim(硬约束 a),
|
||||
// 或到达最粗层。
|
||||
// 这样:远观→粗层(区间≈全体)、近观→细层(区间是视锥内小块),且各轴恒 ≤ maxTextureDim。
|
||||
// 视距-层单调:相机拉近 → worldPerPixel 与视距均减 → Lmin 不增 → level 不增。
|
||||
// 体在视锥外 → empty=true。
|
||||
LodSelection selectLod(const VolumeView& vol, const CameraView& cam,
|
||||
int maxTextureDim = 16384);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -124,6 +124,8 @@ target_sources(geopro_tests PRIVATE render/test_whole_volume_source.cpp)
|
|||
# BrickPager(C):内存恒定的 brick LRU 分页器,驻留 ≤ budget 个解压块,证明超大体浏览内存不爆。
|
||||
target_sources(geopro_tests PRIVATE render/test_brick_pager.cpp)
|
||||
target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp)
|
||||
# ViewAdaptiveLodPolicy(C1):视野自适应 LOD 选层(纯逻辑,零 VTK/Qt)——视锥裁剪求可见 brick 区间 + 视距/分辨率选层。
|
||||
target_sources(geopro_tests PRIVATE render/test_view_adaptive_lod.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
#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);
|
||||
}
|
||||
|
||||
// ── 选定层重组区间各轴恒 ≤ maxTextureDim(小 maxTextureDim 强制升层/缩区间)──
|
||||
TEST(ViewAdaptiveLod, RespectsMaxTextureDimAlways) {
|
||||
const VolumeView v = makeVol();
|
||||
// 多组视距 × 极小 maxTextureDim,断言选定层重组单纹理各轴 ≤ maxTextureDim。
|
||||
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, /*maxTextureDim=*/64);
|
||||
if (sel.empty) continue;
|
||||
// 选定 level 下,区间重组单纹理各轴体素数 = (块数 × brick) 截到该 level 维度。
|
||||
const int dimL = (512 + (1 << sel.level) - 1) >> sel.level;
|
||||
auto axisTex = [&](int b0, int b1) {
|
||||
const int start = b0 * v.brick;
|
||||
const int end = std::min(b1 * v.brick, dimL);
|
||||
return end - start;
|
||||
};
|
||||
EXPECT_LE(axisTex(sel.bx0, sel.bx1), 64);
|
||||
EXPECT_LE(axisTex(sel.by0, sel.by1), 64);
|
||||
EXPECT_LE(axisTex(sel.bz0, sel.bz1), 64);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in New Issue