feat(vtk): 常驻粗底图+局部高清叠加(永不空白)

ViewAdaptiveVolumeSource 构造时一次性在主线程建整卷最粗「各轴≤16384」层
单纹理底图 baseImage(),永远持有、永不释放、绝不被异步路径触碰——任何视角/
任何运动中底图都盖住整个体,拖动/缩放绝不空白。高清(currentImages)异步重组
当前视野后叠在底图之上局部覆盖,未就绪时只显底图。gpr_poc view 用两个 vtkVolume
(底图先渲、高清叠加),新增 --preview --base 出整卷概览底图截图。为 morph(C3-4)/
运动跟踪(C3-7)打两层结构。
This commit is contained in:
gaozheng 2026-06-24 12:03:10 +08:00
parent a4db37735a
commit fb944f706f
4 changed files with 274 additions and 6 deletions

View File

@ -4,6 +4,8 @@
#include <vtkCamera.h>
#include "source/RegionReorganizer.hpp"
namespace geopro::render {
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
@ -13,6 +15,57 @@ ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
exagg_(exagg > 0 ? exagg : 1.0),
builder_(storeDir) { // 后台 builder 用同一 storeDir独立打开线程独占
builder_.setMaxTextureDim(maxTextureDim_);
// C3-6构造时一次性在主线程建常驻粗底图盖全整卷。永远持有、永不释放。
baseLevel_ = buildBaseImage();
}
int ViewAdaptiveVolumeSource::buildBaseImage() {
// 选整卷「各轴 ≤maxTextureDim」的最粗层从最粗层(levels-1)往细找第一层使全卷各轴
// ≤maxTextureDim。金字塔每升一级各轴约减半最粗层多半已满足万一仍超(单条超长
// 整线)则停在最粗层reorganizeRegion 会按体素上限裁中心 → 仍 ≤maxTextureDim(盖全
// 中心段,绝不超限、绝不空)。从最粗往细可拿到「满足上限的最细一层」=信息最多的底图。
const int levels = store_.levels() > 0 ? store_.levels() : 1; // 防 0/负
int chosen = levels - 1; // 退路:最粗层
for (int L = levels - 1; L >= 0; --L) {
int dx = 0, dy = 0, dz = 0;
store_.dims(L, dx, dy, dz);
if (dx <= maxTextureDim_ && dy <= maxTextureDim_ && dz <= maxTextureDim_) {
chosen = L; // 满足上限——继续往细找更细的满足层
} else {
break; // 该层已超限;更细层只会更大 → 上一(更粗)层即最细满足层
}
}
// 全卷 brick 区间(该 levelreorganizeRegion 内按 store.dims+maxTextureDim 裁。
int dx = 0, dy = 0, dz = 0;
store_.dims(chosen, dx, dy, dz);
const int brick = meta_.brick > 0 ? meta_.brick : 64;
RegionTarget t{};
t.level = chosen;
t.bx0 = 0;
t.bx1 = (dx + brick - 1) / brick;
t.by0 = 0;
t.by1 = (dy + brick - 1) / brick;
t.bz0 = 0;
t.bz1 = (dz + brick - 1) / brick;
t.exagg = exagg_;
baseImage_ = reorganizeRegion(store_, t, maxTextureDim_);
// 防御reorganizeRegion 空区间会返回 nullptr退化 store。底图契约是「永不空」
// 故退到最粗层全卷再试一次最粗层各轴最小reorganizeRegion 内裁后仍 ≥1 体素,
// 只要 dims>0 必非空)。这样 baseImage() 在任何非退化 store 上都非空。
if (baseImage_ == nullptr && chosen != levels - 1) {
int cdx = 0, cdy = 0, cdz = 0;
store_.dims(levels - 1, cdx, cdy, cdz);
RegionTarget c{};
c.level = levels - 1;
c.bx1 = (cdx + brick - 1) / brick;
c.by1 = (cdy + brick - 1) / brick;
c.bz1 = (cdz + brick - 1) / brick;
c.exagg = exagg_;
baseImage_ = reorganizeRegion(store_, c, maxTextureDim_);
if (baseImage_ != nullptr) return levels - 1;
}
return chosen;
}
VolumeView ViewAdaptiveVolumeSource::volumeView() const {

View File

@ -62,6 +62,15 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// 非阻塞:仅在 builder 锁内做指针移动。current_/lastLevel_ 为 mutable懒取最新
std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override;
// C3-6 常驻粗底图:整卷最粗「各轴 ≤maxTextureDim」层重组成的单张 VTK_SHORT 纹理,
// 盖住整个体。构造时一次性在主线程建成、永远持有、永不释放 → 任何视角/任何运动中
// 底图都在场 → 绝不空白。view 把它当底层 vtkVolume 常渲,高清(currentImages)叠其上。
// 返回裸指针(本类持有所有权,生命周期 == 本对象);理论上永不为空(构造保证)。
vtkImageData* baseImage() const { return baseImage_.Get(); }
// 底图对应的 LOD level整卷最粗 ≤maxTextureDim 层)。供 UI/测试。
int baseLevel() const { return baseLevel_; }
// reslice 源 = 当前最新就绪单图empty → nullptr。先拉一次最新就绪再返回。
vtkImageData* sliceSource() const override;
@ -89,6 +98,11 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z
VolumeView volumeView() const;
// C3-6选整卷「各轴 ≤maxTextureDim」的最粗层 → 全卷 brick 区间 reorganizeRegion
// 重组成单张盖全底图。构造时调一次(主线程)。返回选定 level写入 baseLevel_
// 找不到满足层(理论上最粗层必满足,否则金字塔层数不够)→ 退到最粗层并据上限裁中心。
int buildBaseImage();
// 从 builder 拉一次最新就绪结果:主目标 getReady 命中→用之并更新 current_/
// lastLevel_否则沿用上一张C3-2 行为。const仅刷新 mutable 缓存),供
// currentImages/sliceSource 共用DRY
@ -117,6 +131,12 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// 当前主目标updateView 设pullLatest 用它向 builder getReady
RegionTarget mainTarget_{};
bool hasMainTarget_ = false;
// C3-6 常驻粗底图:构造时一次性主线程建成、永远持有、永不释放(盖全整卷的最粗
// ≤maxTextureDim 层单纹理)。与异步高清(current_)完全分离——底图绝不被异步路径
// 触碰,故永不空。声明在 builder_ 之后(构造体中先建 builder 再建底图,复用 store_
vtkSmartPointer<vtkImageData> baseImage_;
int baseLevel_ = 0;
};
} // namespace geopro::render

View File

@ -405,6 +405,119 @@ TEST(ViewAdaptiveVolumeSource, UpdateDoesNotBlock) {
ASSERT_NE(img.Get(), nullptr);
}
// ── C3-6 常驻粗底图baseImage() 非空 / 整卷最粗各轴 ≤16384 / VTK_SHORT / 盖全 ──
// 底图 = 整卷「各轴 ≤16384」最粗层单纹理。构造时即建好不需 updateView/不需异步)。
TEST(ViewAdaptiveVolumeSource, BaseImageWholeVolumeCoarsest) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_base").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0);
// 构造后即非空(不调 updateView→ 永不空白的底图常驻。
vtkImageData* base = src.baseImage();
ASSERT_NE(base, nullptr);
EXPECT_EQ(base->GetScalarType(), VTK_SHORT);
int d[3];
base->GetDimensions(d);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
// 底图层 = 该 store「各轴 ≤16384」的最细满足层本 store 各层都 ≤16384 → level 0。
const int bl = src.baseLevel();
int sdx = 0, sdy = 0, sdz = 0;
data::ChunkedVolumeStore store(dir);
store.dims(bl, sdx, sdy, sdz);
EXPECT_LE(sdx, 16384);
EXPECT_LE(sdy, 16384);
EXPECT_LE(sdz, 16384);
// 盖全:底图各轴 == 该 level 全卷 store.dims全卷区间未被 maxTextureDim 裁切)。
EXPECT_EQ(d[0], sdx) << "底图未盖全 x";
EXPECT_EQ(d[1], sdy) << "底图未盖全 y";
EXPECT_EQ(d[2], sdz) << "底图未盖全 z";
// 世界范围盖全整卷origin == meta.origin全卷从 0 起);
// 跨度 ≈ 整卷世界跨度spacing×dims == meta.spacing×meta.dims同一物理范围
const data::StoreMeta& m = src.meta();
double org[3], sp[3];
base->GetOrigin(org);
base->GetSpacing(sp);
EXPECT_DOUBLE_EQ(org[0], m.origin[0]);
EXPECT_DOUBLE_EQ(org[1], m.origin[1]);
EXPECT_DOUBLE_EQ(org[2], m.origin[2]);
// 底图世界跨度应覆盖整卷 level0 世界跨度(粗层 spacing×dims ≥ 细层覆盖范围)。
EXPECT_GE(sp[0] * d[0], m.spacing[0] * m.nx - sp[0]);
EXPECT_GE(sp[1] * d[1], m.spacing[1] * m.ny - sp[1]);
EXPECT_GE(sp[2] * d[2], m.spacing[2] * m.nz - sp[2]);
}
// ── C3-6 底图永不空updateView 出界(empty)、高清从未就绪时baseImage 仍非空盖全 ──
// 这是「拖动/缩放绝不空白」的核:高清异步路径(current_)与底图(baseImage_)完全分离,
// 高清空不影响底图。
TEST(ViewAdaptiveVolumeSource, BaseImagePersistsWhenHiResEmpty) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_basepersist").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);
// 视锥外:体在相机身后 → 高清不提交、currentImages 恒空。
CameraView c = lookFromX(src.meta(), 1000.0);
c.focal[0] = c.pos[0] + 1000.0;
src.updateView(c, vol);
// 高清空(验空)但底图始终非空、盖全。
for (int i = 0; i < 10; ++i) {
EXPECT_TRUE(src.currentImages().empty());
ASSERT_NE(src.baseImage(), nullptr); // 底图永不空
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
int d[3];
src.baseImage()->GetDimensions(d);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
}
// ── C3-6 超长 X 整卷:最粗 ≤16384 层选取正确X 远超 16384 须升到能盖全的层)──────
// 模拟全路段长条体X 极长):底图 level 必须升到 store.dims(level).x ≤16384 的最细层,
// 且底图各轴恒 ≤16384盖全整卷、绝不撞纹理墙
TEST(ViewAdaptiveVolumeSource, BaseImageLongVolumePicksCoarseEnough) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_baselong").string();
// X=40000>16384多级金字塔 → L0:40000, L1:20000(>16384), L2:10000(≤16384)。
// 底图须选 L2最细的「各轴 ≤16384」满足层
makePyramidStore(dir, 40000, 64, 48, 0, 0, 0, 0.05, 0.05, 0.05, 64, 4);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
vtkImageData* base = src.baseImage();
ASSERT_NE(base, nullptr);
int d[3];
base->GetDimensions(d);
EXPECT_LE(d[0], 16384) << "底图 x 超纹理上限";
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
EXPECT_GT(d[0], 0);
// 选定层各轴 store.dims 必 ≤16384且更细一层(level-1)的 x 必 >16384确认是最细满足层
const int bl = src.baseLevel();
data::ChunkedVolumeStore store(dir);
int sdx = 0, sdy = 0, sdz = 0;
store.dims(bl, sdx, sdy, sdz);
EXPECT_LE(sdx, 16384);
EXPECT_EQ(d[0], sdx) << "盖全整卷(全卷区间 ≤16384未被裁";
if (bl > 0) {
int fdx = 0, fdy = 0, fdz = 0;
store.dims(bl - 1, fdx, fdy, fdz);
EXPECT_GT(fdx, 16384) << "更细一层 x 应 >16384故选了这层";
}
}
// ── C3-2 维度取自 store.dims单一真源奇数维 store 验重组 dims == store.dims ──
// 全卷请求某粗 level重组单图各轴 == store.dims(level)(而非自算公式)。奇数维
// 多级降采样下store.dims 是唯一权威;本测试钉死「重组维度跟随 store.dims」。

View File

@ -2781,14 +2781,14 @@ constexpr int kViewMax3DTex = 16384;
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
struct ViewState {
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper
vtkSmartVolumeMapper* mapper = nullptr; // 高清层:单 SmartVolumeMapper叠在底图上
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr;
double exagg = 8.0;
int lastLevel = -1;
// 持有当前单图引用避免被释放mapper 仅持裸指针)。
// 持有当前高清单图引用避免被释放mapper 仅持裸指针)。
vtkSmartPointer<vtkImageData> currentImg;
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
bool inCb = false;
@ -3075,7 +3075,7 @@ int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke] [--preview] [--variant N] "
"[--budget 64] [--smoke] [--preview] [--base] [--variant N] "
"[--gallery] [--frames 90]\n";
return 2;
}
@ -3099,6 +3099,9 @@ int cmdView(int argc, char** argv) {
};
const bool smoke = hasFlag("smoke");
const bool preview = hasFlag("preview");
// C3-6 底图预览:--preview --base或 --base模拟「交互态:只渲常驻粗底图」——
// 隐去高清叠加层、只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、绝不空白。
const bool basePreview = hasFlag("base");
// 拉近预览Task 12d-singletex--preview --variant near或 --near走与真窗口
// 完全相同的单纹理拉近路径viewRefreshSingle 选 level0 局部子区域),供控制方 Read
// 验证「拉近后」单图非空、完整、fps 高。
@ -3163,8 +3166,27 @@ int cmdView(int argc, char** argv) {
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
rw->AddRenderer(ren);
// 单纹理:单 vtkSmartVolumeMapperGPU 光线投射,整张 3D 纹理),与 --preview /
// gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。
// C3-6 常驻粗底图层(底图永不空白的命脉):第一个 vtkVolume = 整卷最粗
// 「各轴 ≤16384」层单纹理source.baseImage(),构造时主线程一次建成、永远持有)。
// 它【永远在场、永远渲染、绝不释放、绝不被异步路径触碰】→ 任何相机/任何运动中都
// 盖住整个体 → 拖动/缩放绝不空白。高清层(下方 mapper)叠在其上、就绪后局部覆盖。
vtkNew<vtkSmartVolumeMapper> baseMapper;
baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
baseMapper->SetAutoAdjustSampleDistances(1);
baseMapper->SetInteractiveAdjustSampleDistances(1);
auto baseVolume = vtkSmartPointer<vtkVolume>::New();
if (source.baseImage() != nullptr) { // 退化 store 防护(理论恒非空)
baseMapper->SetInputData(source.baseImage()); // 常驻输入,永不改
baseMapper->Update();
baseVolume->SetMapper(baseMapper);
baseVolume->SetProperty(prop); // 与高清层共用传函(同配色/不透明度)
baseVolume->SetScale(1.0, exagg, exagg); // 同垂向夸张 → 与高清层空间对齐
ren->AddVolume(baseVolume); // 先加底图 → 底层常渲
}
// 高清叠加层:单 vtkSmartVolumeMapperGPU 光线投射,整张 3D 纹理),与 --preview /
// gallery 同一 mapper 类型。叠在底图之上currentImages 就绪后摆到对应世界位置局部
// 覆盖底图;没就绪则无输入(只显底图,不空)。运动中高清滞后由底图兜底,绝不空白。
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
// C3-5交互式采样距离自适应修长板填屏 ray-march 慢的关键。POC 当初为离屏
@ -3179,7 +3201,7 @@ int cmdView(int argc, char** argv) {
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(默认 var4 exagg
ren->AddVolume(volume);
ren->AddVolume(volume); // 后加高清 → 叠在底图上
// 屏幕左上角实时 fps 文本。
vtkNew<vtkTextActor> fpsText;
@ -3207,6 +3229,66 @@ int cmdView(int argc, char** argv) {
std::size_t warm = viewSetupDefaultFrame(&st, ren);
rw->Render();
// C3-6 底图预览(模拟「交互态:只渲常驻粗底图」):隐去高清叠加层、把相机框到整卷
// 包围盒exagg 后),只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、非空。证明
// 拖动/缩放时即使高清全部缺位,底图也独立盖住整个体(永不空白的命脉)。
if (preview && basePreview) {
volume->SetVisibility(0); // 隐去高清层 → 只剩常驻底图
// 框整卷(无参 ResetCamera 按场景中可见 actor 包围盒;高清隐了 → 框底图全卷)。
ren->ResetCamera();
st.cam = ren->GetActiveCamera();
st.cam->Elevation(kViewDefaultVariant.elevation);
st.cam->Azimuth(kViewDefaultVariant.azimuth);
ren->ResetCameraClippingRange();
rw->Render();
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const std::string pngPath = (shotDir / "view-base.png").string();
savePng(rw.Get(), pngPath);
auto countStructPx = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType n = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
px->GetComponent(i, 2) > 50) {
++n;
}
}
return n;
};
const vtkIdType baseStruct = countStructPx();
int bd[3] = {0, 0, 0};
if (source.baseImage()) source.baseImage()->GetDimensions(bd);
const bool texErrB = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool okB = !texErrB && source.baseImage() != nullptr && baseStruct > 0;
std::cout << "\n=== view --preview --base 常驻粗底图验证(整卷概览)===\n";
std::cout << "底图 level : " << source.baseLevel()
<< "(整卷最粗 ≤16384 层)\n";
std::cout << "底图维度 : " << bd[0] << " x " << bd[1] << " x " << bd[2]
<< "\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "结构像素(>50) : " << baseStruct << " / " << (winW * winH)
<< " (" << (100.0 * baseStruct / (winW * winH)) << "%)\n";
std::cout << "纹理维度错误 : " << (texErrB ? "是(!!)" : "") << "\n";
std::cout << "base 结果 : "
<< (okB ? "OK ✔ 底图盖全非空" : "FAIL ✘") << "\n";
writeMetricLine(
"view-base,dir=" + dir + ",baseLevel=" +
std::to_string(source.baseLevel()) + ",bx=" + std::to_string(bd[0]) +
",by=" + std::to_string(bd[1]) + ",bz=" + std::to_string(bd[2]) +
",structPixels=" + std::to_string(baseStruct) +
",ok=" + std::to_string(okB ? 1 : 0) + ",png=" + pngPath);
return okB ? 0 : 1;
}
// 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
// 单纹理选区路径level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
if (nearPreview) {