feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
6 changed files with 609 additions and 144 deletions
Showing only changes of commit fc9ea58cb5 - Show all commits

View File

@ -5,7 +5,7 @@ add_library(geopro_render STATIC
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)
source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp source/ViewAdaptiveVolumeSource.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)
target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -0,0 +1,163 @@
#include "source/ViewAdaptiveVolumeSource.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <vtkCamera.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
namespace geopro::render {
namespace {
// 该 level 某轴体素维度 = ceil(n / 2^level),至少 1与 C1/store 同口径)。
int dimAtLevel(int n, int level) {
const int d = (n + (1 << level) - 1) >> level;
return d > 0 ? d : 1;
}
} // namespace
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
double exagg)
: store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {}
VolumeView ViewAdaptiveVolumeSource::volumeView() const {
VolumeView v{};
v.nx = meta_.nx;
v.ny = meta_.ny;
v.nz = meta_.nz;
v.brick = meta_.brick;
v.levels = store_.levels();
v.origin[0] = meta_.origin[0];
v.origin[1] = meta_.origin[1];
v.origin[2] = meta_.origin[2];
// C1 约定spacing 已含 exagg 于 y/z。
v.spacing[0] = meta_.spacing[0];
v.spacing[1] = meta_.spacing[1] * exagg_;
v.spacing[2] = meta_.spacing[2] * exagg_;
v.exagg = exagg_;
return v;
}
void ViewAdaptiveVolumeSource::update(vtkCamera* cam) {
if (cam == nullptr) {
current_ = nullptr;
return;
}
CameraView c{};
cam->GetPosition(c.pos);
cam->GetFocalPoint(c.focal);
cam->GetViewUp(c.up);
c.fovYDeg = cam->GetViewAngle();
c.aspect = aspect_;
c.viewportH = viewportH_;
updateView(c, volumeView());
}
void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
const VolumeView& vol) {
const LodSelection sel = selectLod(vol, cam, maxTextureDim_);
if (sel.empty) {
current_ = nullptr;
return;
}
const int level = sel.level;
lastLevel_ = level;
const int brick = meta_.brick;
const int dimLx = dimAtLevel(meta_.nx, level);
const int dimLy = dimAtLevel(meta_.ny, level);
const int dimLz = dimAtLevel(meta_.nz, level);
// 重组单纹理某轴范围(与 C1 hpp 契约逐字一致):
// 起点 = b0*brick终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。
// 即使 brick > maxTextureDim单块超限也按体素上限再裁 → 恒 ≤ maxTextureDim。
const int gi0 = sel.bx0 * brick;
const int gj0 = sel.by0 * brick;
const int gk0 = sel.bz0 * brick;
const int gi1 = std::min({sel.bx1 * brick, dimLx, gi0 + maxTextureDim_});
const int gj1 = std::min({sel.by1 * brick, dimLy, gj0 + maxTextureDim_});
const int gk1 = std::min({sel.bz1 * brick, dimLz, gk0 + maxTextureDim_});
const int outNx = std::max(1, gi1 - gi0);
const int outNy = std::max(1, gj1 - gj0);
const int outNz = std::max(1, gk1 - gk0);
// 世界几何(按 level + exagg
// spacing = meta.spacing × 2^levely/z 再 × exagg
// origin = meta.origin + 区间起始体素 × spacing。
const double sc = static_cast<double>(std::int64_t(1) << level); // 2^level
const double sp[3] = {meta_.spacing[0] * sc, meta_.spacing[1] * sc * exagg_,
meta_.spacing[2] * sc * exagg_};
const double org[3] = {meta_.origin[0] + static_cast<double>(gi0) * sp[0],
meta_.origin[1] + static_cast<double>(gj0) * sp[1],
meta_.origin[2] + static_cast<double>(gk0) * sp[2]};
auto img = vtkSmartPointer<vtkImageData>::New();
img->SetDimensions(outNx, outNy, outNz);
img->SetOrigin(org[0], org[1], org[2]);
img->SetSpacing(sp[0], sp[1], sp[2]);
vtkNew<vtkShortArray> arr;
arr->SetName("v");
const vtkIdType total = static_cast<vtkIdType>(outNx) * outNy * outNz;
arr->SetNumberOfTuples(total);
// 遍历区间覆盖的 level 块,按块内 (i 最快、k 最慢) 布局把每体素写入子体对应位置。
// 块内布局与 readBrick / vtkImageData 点序一致。
const int bx0 = gi0 / brick, bx1 = (gi1 + brick - 1) / brick;
const int by0 = gj0 / brick, by1 = (gj1 + brick - 1) / brick;
const int bz0 = gk0 / brick, bz1 = (gk1 + brick - 1) / brick;
for (int bz = bz0; bz < bz1; ++bz) {
const int k0 = bz * brick;
const int bd = std::min(brick, dimLz - k0);
for (int by = by0; by < by1; ++by) {
const int j0 = by * brick;
const int bh = std::min(brick, dimLy - j0);
for (int bx = bx0; bx < bx1; ++bx) {
const int i0 = bx * brick;
const int bw = std::min(brick, dimLx - i0);
const std::vector<std::int16_t> raw = store_.readBrick(level, bx, by, bz);
// 块内体素全局索引 (i0+ii, j0+jj, k0+kk);只写落在子体区间 [g*0,g*1) 内的。
for (int kk = 0; kk < bd; ++kk) {
const int gk = k0 + kk;
if (gk < gk0 || gk >= gk1) continue;
const int lk = gk - gk0;
for (int jj = 0; jj < bh; ++jj) {
const int gj = j0 + jj;
if (gj < gj0 || gj >= gj1) continue;
const int lj = gj - gj0;
// 该 (kk,jj) 行在 raw 内的起始偏移i 最快)。
const std::size_t rowBase =
(static_cast<std::size_t>(kk) * bh + jj) * bw;
for (int ii = 0; ii < bw; ++ii) {
const int gi = i0 + ii;
if (gi < gi0 || gi >= gi1) continue;
const int li = gi - gi0;
const vtkIdType id =
(static_cast<vtkIdType>(lk) * outNy + lj) * outNx + li;
arr->SetValue(id, raw[rowBase + ii]);
}
}
}
}
}
}
img->GetPointData()->SetScalars(arr);
current_ = img;
}
std::vector<vtkSmartPointer<vtkImageData>>
ViewAdaptiveVolumeSource::currentImages() const {
if (current_ == nullptr) return {};
return {current_};
}
} // namespace geopro::render

View File

@ -0,0 +1,83 @@
#pragma once
#include <string>
#include <vector>
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
#include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp"
#include "source/IVolumeRenderSource.hpp"
class vtkCamera;
namespace geopro::render {
// C2 实现:视野自适应单纹理体绘制数据源。
//
// 用 C1 selectLod(VolumeView,CameraView,maxTextureDim) 选 LOD level + 视野内 brick
// 区间,从 ChunkedVolumeStore 把【当前视野区域】重组为【单张 VTK_SHORT
// vtkImageData】各轴 ≤maxTextureDim由 C1 硬约束保证),带世界 origin/spacing
// (按 level + 垂向夸张 exagg
//
// 与 B(WholeVolumeSource 整卷单图)/旧 C(OutOfCoreSource MultiBlock 多块)的区别:
// - 远观 → C1 选粗层、区间≈全体 → 重组整卷粗纹理(一张);
// - 近观 → C1 选细层、区间为视锥内小块 → 重组视野子体(一张);
// 两条都恒产【单张 ≤maxTextureDim 的纹理】,单 SmartVolumeMapper 渲,绝不退回
// MultiBlock 分块(缺块/低 fps。靠 C1 的硬上限契约,重组各轴恒 ≤maxTextureDim。
//
// 可测缝:核心 updateView(CameraView,VolumeView) 不吃 vtkCamera、不需 GL 上下文
//(构造 vtkImageData 无需渲染管线)→ headless 可测。update(vtkCamera*) 仅把相机
// 参数填成 CameraView 再调 updateView。viewportH/aspect 经 setter 注入vtkCamera
// 不自带视口像素高/宽高比)。
class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
public:
// storeDir含金字塔的分块 store。exagg垂向夸张烘焙进 y/z 的 spacing/origin
explicit ViewAdaptiveVolumeSource(const std::string& storeDir,
double exagg = 1.0);
const geopro::data::StoreMeta& meta() const override { return meta_; }
// 由 vtkCamera + 注入的 viewportH/aspect 填 CameraView再调 updateView。
// GL_MAX_3D_TEXTURE_SIZE 上限走 maxTextureDim_默认 16384
void update(vtkCamera* cam) override;
// 可测缝:纯数值核——选层选区 + 重组单图。headless 可测。
void updateView(const CameraView& cam, const VolumeView& vol);
// 当前视野区域单图empty → 空 vector场景不渲
std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override;
// reslice 源 = 当前单图empty → nullptr
vtkImageData* sliceSource() const override { return current_.Get(); }
// 供 UI 显示当前 LOD level。
int lastLevel() const { return lastLevel_; }
// 总层数(含 level 0——填 VolumeView.levels 用。
int levelCount() const { return store_.levels(); }
// 视口像素高 / 宽高比C1 选层的分辨率密度与视锥裁剪用。update(vtkCamera*)
// 前由渲染端按窗口尺寸注入。
void setViewportHeight(int h) { viewportH_ = h > 0 ? h : viewportH_; }
void setAspect(double aspect) { aspect_ = aspect > 0 ? aspect : aspect_; }
// 单张 3D 纹理各轴上限GL_MAX_3D_TEXTURE_SIZE
void setMaxTextureDim(int dim) { maxTextureDim_ = dim > 0 ? dim : maxTextureDim_; }
private:
// 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z
VolumeView volumeView() const;
geopro::data::ChunkedVolumeStore store_;
geopro::data::StoreMeta meta_;
double exagg_ = 1.0;
int maxTextureDim_ = 16384;
int viewportH_ = 1080;
double aspect_ = 1280.0 / 800.0;
vtkSmartPointer<vtkImageData> current_; // 当前视野区域单图empty 时为空指针)
int lastLevel_ = 0;
};
} // namespace geopro::render

View File

@ -126,6 +126,8 @@ 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)
# ViewAdaptiveVolumeSource(C2) C1 selectLod VTK_SHORT image(各轴≤16384/世界 origin/spacing level+exagg/体素位置与 store 一致)headless GPU
target_sources(geopro_tests PRIVATE render/test_view_adaptive_source.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})

View File

@ -0,0 +1,295 @@
// ViewAdaptiveVolumeSource(C2) headless 测试:用 C1 selectLod 选层选区,从分块
// 存储重组当前视野区域为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界
// origin/spacing 按 level+exagg。核心 updateView(CameraView,VolumeView) 不需真
// vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。
//
// 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应
// level+区间位置一致(不错位)/ empty 情形空。
#include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp"
#include <vtkImageData.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#include <cmath>
#include <filesystem>
#include <gtest/gtest.h>
using namespace geopro;
using geopro::render::CameraView;
using geopro::render::VolumeView;
namespace {
// 造一个含金字塔的 store值 = 全局 (i+j+k)%1000便于校验块定位非 64 整除
// 维度以含边缘块。返回 store 目录。与 test_outofcore_source 同口径。
std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz,
double ox, double oy, double oz, double dx,
double dy, double dz, int brick, int levels) {
std::filesystem::remove_all(dir);
core::BuiltI16 b;
b.vol = core::ScalarVolumeI16(nx, ny, nz);
for (int k = 0; k < nz; ++k)
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i)
b.vol.at(i, j, k) = static_cast<short>((i + j + k) % 1000);
b.quant = {1.0, 0.0};
b.origin = {{ox, oy, oz}};
b.spacing = {{dx, dy, dz}};
b.vminPhys = 0;
b.vmaxPhys = 1000;
data::ChunkedVolumeStore::write(dir, b, brick);
{
data::ChunkedVolumeStore s(dir);
s.buildPyramid(levels);
}
return dir;
}
// 由 store meta + levels + exagg 填 VolumeView与生产 volumeViewOf 同口径)。
VolumeView volumeViewOf(const data::StoreMeta& m, int levels, double exagg) {
VolumeView v{};
v.nx = m.nx;
v.ny = m.ny;
v.nz = m.nz;
v.brick = m.brick;
v.levels = levels;
v.origin[0] = m.origin[0];
v.origin[1] = m.origin[1];
v.origin[2] = m.origin[2];
// C1 约定spacing 已含 exagg 于 y/z。
v.spacing[0] = m.spacing[0];
v.spacing[1] = m.spacing[1] * exagg;
v.spacing[2] = m.spacing[2] * exagg;
v.exagg = exagg;
return v;
}
// 相机:从 +X 看体中心(用 level0 物理范围,未含 exagg 于 X
CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.0,
int viewportH = 1080) {
CameraView c{};
const double cx = m.origin[0] + 0.5 * m.nx * m.spacing[0];
const double cy = m.origin[1] + 0.5 * m.ny * m.spacing[1];
const double cz = m.origin[2] + 0.5 * m.nz * m.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
// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT ───────────────────────────────
TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_far").string();
// 200×80×60, brick=64 → 含边缘块。3 级金字塔。
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0);
EXPECT_EQ(src.meta().nx, 200);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
ASSERT_NE(imgs[0].Get(), nullptr);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
int d[3];
imgs[0]->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
EXPECT_GT(src.lastLevel(), 0); // 远 → 粗
}
// ── 近观:选最细层(0)、单图、仍 ≤16384 ─────────────────────────────────────
TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_near").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);
const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080);
src.updateView(near, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细
int d[3];
imgs[0]->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
}
// ── 重组体素与 store 对应 level/区间位置一致(不错位)──────────────────────
// 远观选某粗 level重组单图后抽查若干体素用世界坐标反推该 level 体素索引,
// 与 store.readBrick(level,...) 对应块的同一体素值逐一相等 → 证明位置正确。
TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_match").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);
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
vtkImageData* img = imgs[0];
const int level = src.lastLevel();
data::ChunkedVolumeStore store(dir);
const data::StoreMeta& m = src.meta();
const int brick = m.brick;
const double sc = static_cast<double>(1 << level);
int dimLx = 0, dimLy = 0, dimLz = 0;
store.dims(level, dimLx, dimLy, dimLz);
int d[3];
img->GetDimensions(d);
double org[3], sp[3];
img->GetOrigin(org);
img->GetSpacing(sp);
// spacing 应 = meta.spacing × 2^levelexagg=1
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc);
EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc);
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
ASSERT_NE(arr, nullptr);
// 该图覆盖的 level 体素起点:由 origin 反推(应整数)。
const long gi0 = std::lround((org[0] - m.origin[0]) / sp[0]);
const long gj0 = std::lround((org[1] - m.origin[1]) / sp[1]);
const long gk0 = std::lround((org[2] - m.origin[2]) / sp[2]);
EXPECT_GE(gi0, 0);
EXPECT_GE(gj0, 0);
EXPECT_GE(gk0, 0);
// 取该 level 单块校验:读 store 块 (b*,0,0),把块内某体素与图中同位置对比。
auto storeVoxelAt = [&](int gi, int gj, int gk) -> std::int16_t {
const int bx = gi / brick, by = gj / brick, bz = gk / brick;
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
const int bw = std::min(brick, dimLx - i0);
const int bh = std::min(brick, dimLy - j0);
const int li = gi - i0, lj = gj - j0, lk = gk - k0;
const std::size_t w =
(static_cast<std::size_t>(lk) * bh + lj) * bw + li;
return raw[w];
};
// 抽查图内若干位置(含非角点、跨块),逐一与 store 对应 level 体素相等。
int checked = 0;
for (int lk : {0, d[2] / 2, d[2] - 1}) {
for (int lj : {0, d[1] / 2, d[1] - 1}) {
for (int li : {0, d[0] / 2, d[0] - 1}) {
const long gi = gi0 + li, gj = gj0 + lj, gk = gk0 + lk;
if (gi >= dimLx || gj >= dimLy || gk >= dimLz) continue;
const vtkIdType id =
(static_cast<vtkIdType>(lk) * d[1] + lj) * d[0] + li;
const std::int16_t got = arr->GetValue(id);
const std::int16_t want =
storeVoxelAt(static_cast<int>(gi), static_cast<int>(gj),
static_cast<int>(gk));
EXPECT_EQ(got, want)
<< "体素错位 at local(" << li << "," << lj << "," << lk
<< ") level=" << level;
++checked;
}
}
}
EXPECT_GT(checked, 0);
}
// ── 世界 origin按 level+exagg 偏移正确exagg≠1 校验 y/z 烘焙)─────────────
TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_exagg").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
const double exagg = 4.0;
render::ViewAdaptiveVolumeSource src(dir, exagg);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), exagg);
// 远观整体,区间从 0 起 → origin 应正好 = meta.origin区间起点 0
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
const data::StoreMeta& m = src.meta();
const int level = src.lastLevel();
const double sc = static_cast<double>(1 << level);
double sp[3], org[3];
imgs[0]->GetSpacing(sp);
imgs[0]->GetOrigin(org);
// spacingx 不夸张y/z ×exagg。
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg);
EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc * exagg);
// 区间从 0 起 → origin = meta.originy/z 偏移为 0×… 仍是 origin
EXPECT_DOUBLE_EQ(org[0], m.origin[0]);
EXPECT_DOUBLE_EQ(org[1], m.origin[1]);
EXPECT_DOUBLE_EQ(org[2], m.origin[2]);
}
// ── empty体完全在视锥外 → currentImages 空 ───────────────────────────────
TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_empty").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);
CameraView c = lookFromX(src.meta(), 1000.0);
c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X体在身后 → 视锥外
src.updateView(c, vol);
EXPECT_TRUE(src.currentImages().empty());
EXPECT_EQ(src.sliceSource(), nullptr);
}
// ── 单层 store无金字塔也能重组level 恒 0───────────────────────────
TEST(ViewAdaptiveVolumeSource, SingleLevelStore) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_1lvl").string();
// levels=0 → 仅 level 0。
makePyramidStore(dir, 128, 64, 48, 0, 0, 0, 1.0, 1.0, 1.0, 64, 0);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
EXPECT_EQ(src.levelCount(), 1);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView c = lookFromX(src.meta(), 500.0);
src.updateView(c, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
EXPECT_EQ(src.lastLevel(), 0);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
}

View File

@ -34,6 +34,7 @@
#include "io/gpr/IprHeader.hpp"
#include "render/actors/VoxelActor.hpp"
#include "render/source/OutOfCoreSource.hpp"
#include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "render/source/WholeVolumeSource.hpp"
// ---- VTK 离屏渲染 ----
@ -2771,8 +2772,13 @@ constexpr int kViewMax3DTex = 16384;
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
//
// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1
// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。
// 退掉旧 POC 简化路径viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
struct ViewState {
geopro::data::ChunkedVolumeStore* store = nullptr;
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
@ -2780,135 +2786,25 @@ struct ViewState {
vtkRenderWindow* rw = nullptr;
double exagg = 8.0;
int lastLevel = -1;
// 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。
int cachedWholeLevel = -1;
vtkSmartPointer<vtkImageData> cachedWholeImg;
// 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。
int cachedLocalBx0 = -1;
int cachedLocalCount = -1;
vtkSmartPointer<vtkImageData> cachedLocalImg;
// 持有当前单图引用避免被释放mapper 仅持裸指针)。
vtkSmartPointer<vtkImageData> currentImg;
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
bool inCb = false;
};
// 某 level 整卷各轴是否都 ≤16384可成单张 3D 纹理 → 整卷单 mapper 渲染)。
bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store,
int level) {
int nx = 0, ny = 0, nz = 0;
store.dims(level, nx, ny, nz);
return nx <= kViewMax3DTex && ny <= kViewMax3DTex && nz <= kViewMax3DTex;
}
// 给定相机选中的 level返回真正用于整卷渲染的 level从 picked 起向粗逐层找,
// 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384则升到 level2
// 找不到(极端情况)返回 -1调用方退回局部子区域路径。
int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store,
int picked) {
const int maxLevel = store.levels() - 1;
for (int lv = std::max(0, picked); lv <= maxLevel; ++lv) {
if (levelFitsSingleTexture(store, lv)) {
return lv;
}
}
return -1;
}
// 由相机到体中心距离粗分档选 LOD level移植 OutOfCoreSource::pickLevel使交互
// view 不再依赖 OutOfCoreSource。0=最细。cam==nullptr 或单层 → 0。
int viewPickLevel(const geopro::data::ChunkedVolumeStore& store, vtkCamera* cam) {
const geopro::data::StoreMeta& m = store.meta();
const int maxLevel = store.levels() - 1;
if (cam == nullptr || maxLevel <= 0) return 0;
const double dx = m.nx * m.spacing[0];
const double dy = m.ny * m.spacing[1];
const double dz = m.nz * m.spacing[2];
const double diag = std::sqrt(dx * dx + dy * dy + dz * dz);
if (diag <= 0.0) return 0;
double pos[3];
cam->GetPosition(pos);
const double cx = m.origin[0] + 0.5 * dx;
const double cy = m.origin[1] + 0.5 * dy;
const double cz = m.origin[2] + 0.5 * dz;
const double ddx = pos[0] - cx, ddy = pos[1] - cy, ddz = pos[2] - cz;
const double dist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
const double ratio = dist / diag;
int level = 0;
if (ratio >= 1.0) level = 1;
if (ratio >= 2.0) level = 2;
if (ratio >= 4.0) level = 3;
return std::min(level, maxLevel);
}
// 拉近时level0 整卷 X(44476)>16384 无法成单纹理 → 只取相机视野覆盖的 X 段。
// 用相机视锥在 X 轴上的世界投影范围交体范围,换算成 level0 的 brick 列区间,并夹到
// 「段宽 ≤16384 体素」(=256 brick 列,远大于任何视野所需)。返回 [bx0, count)。
void viewLocalBrickRange(const geopro::data::ChunkedVolumeStore& store,
vtkCamera* cam, int& bx0, int& count) {
const geopro::data::StoreMeta& m = store.meta();
const int brick = m.brick;
const int totBx = store.bricksX(0);
const int maxBrickCols = kViewMax3DTex / brick; // 段宽上限(体素 ≤16384
// 默认:以体中段为中心取 kViewDefaultLocalBricks 列(无相机或退化时)。
int centerBx = totBx / 2;
int halfCols = std::max(2, maxBrickCols / 4); // 视野估不出时给个稳妥宽度
if (cam != nullptr) {
// 相机焦点 X 投影到 level0 brick 列;视野宽度由相机到焦点距离粗估。
double fp[3], pos[3];
cam->GetFocalPoint(fp);
cam->GetPosition(pos);
const double x0 = m.origin[0];
const double xspan = (m.nx > 1) ? (m.nx - 1) * m.spacing[0] : 1.0;
const double fx = (fp[0] - x0) / (xspan > 0 ? xspan : 1.0); // 0..1
centerBx = std::clamp(static_cast<int>(fx * totBx), 0, totBx - 1);
// 视野半宽(世界)≈ 视距 × tan(半视角),再换成 brick 列;夹到合理区间。
const double ddx = pos[0] - fp[0], ddy = pos[1] - fp[1],
ddz = pos[2] - fp[2];
const double viewDist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
const double halfAngle = 0.5 * cam->GetViewAngle() * 3.14159265 / 180.0;
const double halfWorld = viewDist * std::tan(halfAngle);
const double colWorld = brick * m.spacing[0];
halfCols = std::clamp(static_cast<int>(halfWorld / colWorld) + 1, 2,
maxBrickCols / 2);
}
bx0 = std::clamp(centerBx - halfCols, 0, std::max(0, totBx - 1));
count = std::min(2 * halfCols, totBx - bx0);
count = std::max(1, std::min(count, maxBrickCols));
}
// 单纹理刷新:按相机选 LOD产【一张 image】喂单 SmartVolumeMapper。返回喂入的块数
// (恒为 1单纹理)。同步刷新 st->lastLevelfps 文本用)。
// 单纹理刷新source->update(cam) 选 LOD + 重组当前视野区域单图,喂单
// SmartVolumeMapper。返回喂入的块数恒为 1 单纹理;视锥外 → 0 不渲)。
// 同步刷新 st->lastLevelfps 文本用)。
std::size_t viewRefreshSingle(ViewState* st) {
const int picked = viewPickLevel(*st->store, st->cam);
// 概览/中远:升到最细的「整卷 ≤16384」层整卷一张纹理缓存仅 level 变才重组)。
const int wlv = wholeVolumeLevelFor(*st->store, picked);
if (wlv >= 0 && picked >= 1) {
if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) {
st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta());
st->cachedWholeLevel = wlv;
st->source->update(st->cam);
st->lastLevel = st->source->lastLevel();
auto imgs = st->source->currentImages();
if (imgs.empty() || imgs[0] == nullptr) {
return 0; // 视锥外:不更新输入(保留上一帧),由调用方决定是否渲。
}
st->mapper->SetInputData(st->cachedWholeImg);
st->currentImg = imgs[0];
st->mapper->SetInputData(st->currentImg);
st->mapper->Update();
st->lastLevel = wlv;
return 1;
}
// 拉近picked==0要全分辨率取视野覆盖的 level0 X 子段,一张纹理。
int bx0 = 0, cnt = 1;
viewLocalBrickRange(*st->store, st->cam, bx0, cnt);
if (st->cachedLocalBx0 != bx0 || st->cachedLocalCount != cnt ||
st->cachedLocalImg == nullptr) {
st->cachedLocalImg =
buildLocalLevel0Image(*st->store, st->store->meta(), bx0, cnt);
st->cachedLocalBx0 = bx0;
st->cachedLocalCount = cnt;
}
st->mapper->SetInputData(st->cachedLocalImg);
st->mapper->Update();
st->lastLevel = 0;
return 1;
}
@ -2960,32 +2856,53 @@ constexpr int kViewDefaultLocalBricks = 4;
//
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0默认即全分辨率局部段
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
geopro::data::ChunkedVolumeStore& store = *st->store;
const geopro::data::StoreMeta& m = store.meta();
const int totBx = store.bricksX(0);
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 沿线中段
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
geopro::render::ViewAdaptiveVolumeSource& source = *st->source;
const geopro::data::StoreMeta& m = source.meta();
// 单纹理:一张 image 直接喂单 SmartVolumeMapper与 --preview 同路径)。
st->mapper->SetInputData(locImg);
st->mapper->Update();
st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组
st->cachedLocalBx0 = bx0;
st->cachedLocalCount = localBx;
st->lastLevel = 0;
// 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列)
// 正前方近观 → 触发 C1 选 level0 + 视野子区间C2 重组该视野单图。先用「局部段
// 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。
const int brick = m.brick;
const int totBricksX = (m.nx + brick - 1) / brick;
const int localBx = std::min(kViewDefaultLocalBricks, totBricksX);
const int bx0 = std::max(0, totBricksX / 2 - localBx / 2); // 沿线中段
// 该局部段世界 X 范围level0
(void)bx0;
(void)localBx;
(void)totBricksX;
// 体exagg 后)世界尺寸与中心 + 包围球半径。
const double wx = std::max(1.0, m.nx * m.spacing[0]);
const double wy = std::max(1.0, m.ny * m.spacing[1] * st->exagg);
const double wz = std::max(1.0, m.nz * m.spacing[2] * st->exagg);
const double cx = m.origin[0] + 0.5 * wx;
const double cy = m.origin[1] + 0.5 * wy;
const double cz = m.origin[2] + 0.5 * wz;
const double radius = 0.5 * std::sqrt(wx * wx + wy * wy + wz * wz);
// 相机从 +X 看体中心,距离 = 半径 / tan(半视角) × 余量确保整体落入视锥C1
// selectLod 不会判 empty由视距/分辨率自然选层;近观靠交互再拉近切细层。
st->cam = ren->GetActiveCamera();
const double fovY = st->cam->GetViewAngle();
const double halfAngle = 0.5 * fovY * 3.14159265358979 / 180.0;
const double tanH = std::max(1e-3, std::tan(halfAngle));
const double dist = radius / tanH * 1.4; // 1.4:留余量含 aspect/边缘
st->cam->SetFocalPoint(cx, cy, cz);
st->cam->SetPosition(cx + dist, cy, cz);
st->cam->SetViewUp(0, 0, 1);
ren->ResetCameraClippingRange();
// 源选层选区 + 重组单图喂 mapper。
const std::size_t blocks = viewRefreshSingle(st);
// 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放
// 后包围盒框,把 exagg 后的 Y/Z 一并纳入mapper->GetBounds() 是未缩放的,不可用),
// 相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
// 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
ren->ResetCamera();
st->cam = ren->GetActiveCamera();
st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景El18
st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景Az22
st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景Zoom2.0 填满画面
ren->ResetCameraClippingRange();
return 1; // 单纹理:恒一张 image
return blocks;
}
// 渲一组画廊变体并存 PNG报告 结构像素 / 平均亮度 / fps。返回 0=OK。
@ -3179,8 +3096,13 @@ int cmdView(int argc, char** argv) {
// 单纹理统一路径Task 12d-singletex交互 view 只用 ChunkedVolumeStore 产
// 单图 + 单 SmartVolumeMapper不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
geopro::data::ChunkedVolumeStore store(dir);
const auto& m = store.meta();
// 渲染源 = ViewAdaptiveVolumeSource(C2)。exagg 走 actor 的 SetScale不烘进
// 几何,避免与 SetScale 重复夸张),故源构造 exagg=1.0。视口高/宽高比注入给
// C1 选层(分辨率密度 + 视锥裁剪)。
geopro::render::ViewAdaptiveVolumeSource source(dir, /*exagg=*/1.0);
source.setViewportHeight(winH);
source.setAspect(static_cast<double>(winW) / winH);
const auto& m = source.meta();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
// 配色/不透明度包络取自 var4seismic + V 形实体包络(floor/mid + opacity 作峰值)。
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
@ -3227,7 +3149,7 @@ int cmdView(int argc, char** argv) {
vtkOutputWindow::SetInstance(capWin);
ViewState st;
st.store = &store;
st.source = &source;
st.mapper = mapper.Get();
st.ren = ren.Get();
st.fpsText = fpsText.Get();