feat(vtk): 常驻粗底图+局部高清叠加(永不空白)
ViewAdaptiveVolumeSource 构造时一次性在主线程建整卷最粗「各轴≤16384」层 单纹理底图 baseImage(),永远持有、永不释放、绝不被异步路径触碰——任何视角/ 任何运动中底图都盖住整个体,拖动/缩放绝不空白。高清(currentImages)异步重组 当前视野后叠在底图之上局部覆盖,未就绪时只显底图。gpr_poc view 用两个 vtkVolume (底图先渲、高清叠加),新增 --preview --base 出整卷概览底图截图。为 morph(C3-4)/ 运动跟踪(C3-7)打两层结构。
This commit is contained in:
parent
a4db37735a
commit
fb944f706f
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
#include <vtkCamera.h>
|
#include <vtkCamera.h>
|
||||||
|
|
||||||
|
#include "source/RegionReorganizer.hpp"
|
||||||
|
|
||||||
namespace geopro::render {
|
namespace geopro::render {
|
||||||
|
|
||||||
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
|
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
|
||||||
|
|
@ -13,6 +15,57 @@ ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
|
||||||
exagg_(exagg > 0 ? exagg : 1.0),
|
exagg_(exagg > 0 ? exagg : 1.0),
|
||||||
builder_(storeDir) { // 后台 builder 用同一 storeDir(独立打开,线程独占)
|
builder_(storeDir) { // 后台 builder 用同一 storeDir(独立打开,线程独占)
|
||||||
builder_.setMaxTextureDim(maxTextureDim_);
|
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 区间(该 level),reorganizeRegion 内按 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 {
|
VolumeView ViewAdaptiveVolumeSource::volumeView() const {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,15 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
|
||||||
// 非阻塞:仅在 builder 锁内做指针移动。current_/lastLevel_ 为 mutable(懒取最新)。
|
// 非阻塞:仅在 builder 锁内做指针移动。current_/lastLevel_ 为 mutable(懒取最新)。
|
||||||
std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override;
|
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)。先拉一次最新就绪再返回。
|
// reslice 源 = 当前最新就绪单图(empty → nullptr)。先拉一次最新就绪再返回。
|
||||||
vtkImageData* sliceSource() const override;
|
vtkImageData* sliceSource() const override;
|
||||||
|
|
||||||
|
|
@ -89,6 +98,11 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
|
||||||
// 由 meta + exagg 填 VolumeView(spacing 已含 exagg 于 y/z)。
|
// 由 meta + exagg 填 VolumeView(spacing 已含 exagg 于 y/z)。
|
||||||
VolumeView volumeView() const;
|
VolumeView volumeView() const;
|
||||||
|
|
||||||
|
// C3-6:选整卷「各轴 ≤maxTextureDim」的最粗层 → 全卷 brick 区间 reorganizeRegion
|
||||||
|
// 重组成单张盖全底图。构造时调一次(主线程)。返回选定 level(写入 baseLevel_)。
|
||||||
|
// 找不到满足层(理论上最粗层必满足,否则金字塔层数不够)→ 退到最粗层并据上限裁中心。
|
||||||
|
int buildBaseImage();
|
||||||
|
|
||||||
// 从 builder 拉一次最新就绪结果:主目标 getReady 命中→用之并更新 current_/
|
// 从 builder 拉一次最新就绪结果:主目标 getReady 命中→用之并更新 current_/
|
||||||
// lastLevel_;否则沿用上一张(C3-2 行为)。const(仅刷新 mutable 缓存),供
|
// lastLevel_;否则沿用上一张(C3-2 行为)。const(仅刷新 mutable 缓存),供
|
||||||
// currentImages/sliceSource 共用(DRY)。
|
// currentImages/sliceSource 共用(DRY)。
|
||||||
|
|
@ -117,6 +131,12 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
|
||||||
// 当前主目标(updateView 设;pullLatest 用它向 builder getReady)。
|
// 当前主目标(updateView 设;pullLatest 用它向 builder getReady)。
|
||||||
RegionTarget mainTarget_{};
|
RegionTarget mainTarget_{};
|
||||||
bool hasMainTarget_ = false;
|
bool hasMainTarget_ = false;
|
||||||
|
|
||||||
|
// C3-6 常驻粗底图:构造时一次性主线程建成、永远持有、永不释放(盖全整卷的最粗
|
||||||
|
// ≤maxTextureDim 层单纹理)。与异步高清(current_)完全分离——底图绝不被异步路径
|
||||||
|
// 触碰,故永不空。声明在 builder_ 之后(构造体中先建 builder 再建底图,复用 store_)。
|
||||||
|
vtkSmartPointer<vtkImageData> baseImage_;
|
||||||
|
int baseLevel_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::render
|
} // namespace geopro::render
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,119 @@ TEST(ViewAdaptiveVolumeSource, UpdateDoesNotBlock) {
|
||||||
ASSERT_NE(img.Get(), nullptr);
|
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 ──
|
// ── C3-2 维度取自 store.dims(单一真源):奇数维 store 验重组 dims == store.dims ──
|
||||||
// 全卷请求某粗 level,重组单图各轴 == store.dims(level)(而非自算公式)。奇数维
|
// 全卷请求某粗 level,重组单图各轴 == store.dims(level)(而非自算公式)。奇数维
|
||||||
// 多级降采样下,store.dims 是唯一权威;本测试钉死「重组维度跟随 store.dims」。
|
// 多级降采样下,store.dims 是唯一权威;本测试钉死「重组维度跟随 store.dims」。
|
||||||
|
|
|
||||||
|
|
@ -2781,14 +2781,14 @@ constexpr int kViewMax3DTex = 16384;
|
||||||
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
|
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
|
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
|
||||||
vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper
|
vtkSmartVolumeMapper* mapper = nullptr; // 高清层:单 SmartVolumeMapper(叠在底图上)
|
||||||
vtkRenderer* ren = nullptr;
|
vtkRenderer* ren = nullptr;
|
||||||
vtkCamera* cam = nullptr;
|
vtkCamera* cam = nullptr;
|
||||||
vtkTextActor* fpsText = nullptr;
|
vtkTextActor* fpsText = nullptr;
|
||||||
vtkRenderWindow* rw = nullptr;
|
vtkRenderWindow* rw = nullptr;
|
||||||
double exagg = 8.0;
|
double exagg = 8.0;
|
||||||
int lastLevel = -1;
|
int lastLevel = -1;
|
||||||
// 持有当前单图引用,避免被释放(mapper 仅持裸指针)。
|
// 持有当前高清单图引用,避免被释放(mapper 仅持裸指针)。
|
||||||
vtkSmartPointer<vtkImageData> currentImg;
|
vtkSmartPointer<vtkImageData> currentImg;
|
||||||
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
||||||
bool inCb = false;
|
bool inCb = false;
|
||||||
|
|
@ -3075,7 +3075,7 @@ int cmdView(int argc, char** argv) {
|
||||||
const Args a = parseArgs(argc, argv, 2);
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
if (a.positional.empty()) {
|
if (a.positional.empty()) {
|
||||||
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
|
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";
|
"[--gallery] [--frames 90]\n";
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
@ -3099,6 +3099,9 @@ int cmdView(int argc, char** argv) {
|
||||||
};
|
};
|
||||||
const bool smoke = hasFlag("smoke");
|
const bool smoke = hasFlag("smoke");
|
||||||
const bool preview = hasFlag("preview");
|
const bool preview = hasFlag("preview");
|
||||||
|
// C3-6 底图预览:--preview --base(或 --base)模拟「交互态:只渲常驻粗底图」——
|
||||||
|
// 隐去高清叠加层、只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、绝不空白。
|
||||||
|
const bool basePreview = hasFlag("base");
|
||||||
// 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口
|
// 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口
|
||||||
// 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read
|
// 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read
|
||||||
// 验证「拉近后」单图非空、完整、fps 高。
|
// 验证「拉近后」单图非空、完整、fps 高。
|
||||||
|
|
@ -3163,8 +3166,27 @@ int cmdView(int argc, char** argv) {
|
||||||
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
|
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
|
||||||
rw->AddRenderer(ren);
|
rw->AddRenderer(ren);
|
||||||
|
|
||||||
// 单纹理:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview /
|
// C3-6 常驻粗底图层(底图永不空白的命脉):第一个 vtkVolume = 整卷最粗
|
||||||
// gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。
|
// 「各轴 ≤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); // 先加底图 → 底层常渲
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高清叠加层:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview /
|
||||||
|
// gallery 同一 mapper 类型。叠在底图之上:currentImages 就绪后摆到对应世界位置局部
|
||||||
|
// 覆盖底图;没就绪则无输入(只显底图,不空)。运动中高清滞后由底图兜底,绝不空白。
|
||||||
vtkNew<vtkSmartVolumeMapper> mapper;
|
vtkNew<vtkSmartVolumeMapper> mapper;
|
||||||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||||||
// C3-5:交互式采样距离自适应(修长板填屏 ray-march 慢的关键)。POC 当初为离屏
|
// C3-5:交互式采样距离自适应(修长板填屏 ray-march 慢的关键)。POC 当初为离屏
|
||||||
|
|
@ -3179,7 +3201,7 @@ int cmdView(int argc, char** argv) {
|
||||||
volume->SetMapper(mapper);
|
volume->SetMapper(mapper);
|
||||||
volume->SetProperty(prop);
|
volume->SetProperty(prop);
|
||||||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(默认 var4 exagg)
|
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(默认 var4 exagg)
|
||||||
ren->AddVolume(volume);
|
ren->AddVolume(volume); // 后加高清 → 叠在底图上
|
||||||
|
|
||||||
// 屏幕左上角实时 fps 文本。
|
// 屏幕左上角实时 fps 文本。
|
||||||
vtkNew<vtkTextActor> fpsText;
|
vtkNew<vtkTextActor> fpsText;
|
||||||
|
|
@ -3207,6 +3229,66 @@ int cmdView(int argc, char** argv) {
|
||||||
std::size_t warm = viewSetupDefaultFrame(&st, ren);
|
std::size_t warm = viewSetupDefaultFrame(&st, ren);
|
||||||
rw->Render();
|
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 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
|
// 单纹理选区路径,level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
|
||||||
if (nearPreview) {
|
if (nearPreview) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue