feat(vtk): ViewAdaptiveVolumeSource 视野区域单纹理重组(C2)
用 C1 selectLod 选层选区,从 ChunkedVolumeStore 把当前视野区域重组为单张 VTK_SHORT vtkImageData(各轴 ≤16384,世界 origin/spacing 按 level+exagg),实现 IVolumeRenderSource。核心 updateView(CameraView,VolumeView) headless 可测。 gpr_poc view 切到该源(退掉 MultiBlock/budget 简化路径),--preview 渲默认视角存 PNG,--smoke 离屏不崩且 LOD 随缩放切换。
This commit is contained in:
parent
0da5accebe
commit
fc9ea58cb5
|
|
@ -5,7 +5,7 @@ add_library(geopro_render STATIC
|
||||||
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
||||||
ground/TileMath.cpp
|
ground/TileMath.cpp
|
||||||
lod/ViewAdaptiveLodPolicy.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_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
|
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
|
||||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||||
|
|
|
||||||
|
|
@ -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^level(y/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
|
||||||
|
|
@ -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 填 VolumeView(spacing 已含 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
|
||||||
|
|
@ -126,6 +126,8 @@ target_sources(geopro_tests PRIVATE render/test_brick_pager.cpp)
|
||||||
target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp)
|
target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp)
|
||||||
# ViewAdaptiveLodPolicy(C1):视野自适应 LOD 选层(纯逻辑,零 VTK/Qt)——视锥裁剪求可见 brick 区间 + 视距/分辨率选层。
|
# ViewAdaptiveLodPolicy(C1):视野自适应 LOD 选层(纯逻辑,零 VTK/Qt)——视锥裁剪求可见 brick 区间 + 视距/分辨率选层。
|
||||||
target_sources(geopro_tests PRIVATE render/test_view_adaptive_lod.cpp)
|
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})
|
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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^level(exagg=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);
|
||||||
|
// spacing:x 不夸张,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.origin(y/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);
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
#include "io/gpr/IprHeader.hpp"
|
#include "io/gpr/IprHeader.hpp"
|
||||||
#include "render/actors/VoxelActor.hpp"
|
#include "render/actors/VoxelActor.hpp"
|
||||||
#include "render/source/OutOfCoreSource.hpp"
|
#include "render/source/OutOfCoreSource.hpp"
|
||||||
|
#include "render/source/ViewAdaptiveVolumeSource.hpp"
|
||||||
#include "render/source/WholeVolumeSource.hpp"
|
#include "render/source/WholeVolumeSource.hpp"
|
||||||
|
|
||||||
// ---- VTK 离屏渲染 ----
|
// ---- VTK 离屏渲染 ----
|
||||||
|
|
@ -2771,8 +2772,13 @@ constexpr int kViewMax3DTex = 16384;
|
||||||
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
|
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
|
||||||
|
|
||||||
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
|
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
|
||||||
|
//
|
||||||
|
// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1
|
||||||
|
// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。
|
||||||
|
// 退掉旧 POC 简化路径(viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存
|
||||||
|
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
geopro::data::ChunkedVolumeStore* store = 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;
|
||||||
|
|
@ -2780,135 +2786,25 @@ struct ViewState {
|
||||||
vtkRenderWindow* rw = nullptr;
|
vtkRenderWindow* rw = nullptr;
|
||||||
double exagg = 8.0;
|
double exagg = 8.0;
|
||||||
int lastLevel = -1;
|
int lastLevel = -1;
|
||||||
// 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。
|
// 持有当前单图引用,避免被释放(mapper 仅持裸指针)。
|
||||||
int cachedWholeLevel = -1;
|
vtkSmartPointer<vtkImageData> currentImg;
|
||||||
vtkSmartPointer<vtkImageData> cachedWholeImg;
|
|
||||||
// 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。
|
|
||||||
int cachedLocalBx0 = -1;
|
|
||||||
int cachedLocalCount = -1;
|
|
||||||
vtkSmartPointer<vtkImageData> cachedLocalImg;
|
|
||||||
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
||||||
bool inCb = false;
|
bool inCb = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 某 level 整卷各轴是否都 ≤16384(可成单张 3D 纹理 → 整卷单 mapper 渲染)。
|
// 单纹理刷新:source->update(cam) 选 LOD + 重组当前视野区域单图,喂单
|
||||||
bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store,
|
// SmartVolumeMapper。返回喂入的块数(恒为 1 单纹理;视锥外 → 0 不渲)。
|
||||||
int level) {
|
// 同步刷新 st->lastLevel(fps 文本用)。
|
||||||
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->lastLevel(fps 文本用)。
|
|
||||||
std::size_t viewRefreshSingle(ViewState* st) {
|
std::size_t viewRefreshSingle(ViewState* st) {
|
||||||
const int picked = viewPickLevel(*st->store, st->cam);
|
st->source->update(st->cam);
|
||||||
|
st->lastLevel = st->source->lastLevel();
|
||||||
// 概览/中远:升到最细的「整卷 ≤16384」层,整卷一张纹理(缓存,仅 level 变才重组)。
|
auto imgs = st->source->currentImages();
|
||||||
const int wlv = wholeVolumeLevelFor(*st->store, picked);
|
if (imgs.empty() || imgs[0] == nullptr) {
|
||||||
if (wlv >= 0 && picked >= 1) {
|
return 0; // 视锥外:不更新输入(保留上一帧),由调用方决定是否渲。
|
||||||
if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) {
|
|
||||||
st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta());
|
|
||||||
st->cachedWholeLevel = wlv;
|
|
||||||
}
|
|
||||||
st->mapper->SetInputData(st->cachedWholeImg);
|
|
||||||
st->mapper->Update();
|
|
||||||
st->lastLevel = wlv;
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
st->currentImg = imgs[0];
|
||||||
// 拉近(picked==0,要全分辨率):取视野覆盖的 level0 X 子段,一张纹理。
|
st->mapper->SetInputData(st->currentImg);
|
||||||
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->mapper->Update();
|
||||||
st->lastLevel = 0;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2960,32 +2856,53 @@ constexpr int kViewDefaultLocalBricks = 4;
|
||||||
//
|
//
|
||||||
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。
|
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。
|
||||||
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
|
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
|
||||||
geopro::data::ChunkedVolumeStore& store = *st->store;
|
geopro::render::ViewAdaptiveVolumeSource& source = *st->source;
|
||||||
const geopro::data::StoreMeta& m = store.meta();
|
const geopro::data::StoreMeta& m = source.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);
|
|
||||||
|
|
||||||
// 单纹理:一张 image 直接喂单 SmartVolumeMapper(与 --preview 同路径)。
|
// 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列)
|
||||||
st->mapper->SetInputData(locImg);
|
// 正前方近观 → 触发 C1 选 level0 + 视野子区间,C2 重组该视野单图。先用「局部段
|
||||||
st->mapper->Update();
|
// 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。
|
||||||
st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组
|
const int brick = m.brick;
|
||||||
st->cachedLocalBx0 = bx0;
|
const int totBricksX = (m.nx + brick - 1) / brick;
|
||||||
st->cachedLocalCount = localBx;
|
const int localBx = std::min(kViewDefaultLocalBricks, totBricksX);
|
||||||
st->lastLevel = 0;
|
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)】缩放
|
// 框住局部段:用无参 ResetCamera(按 actor 的【已 SetScale(1,exagg,exagg)】缩放
|
||||||
// 后包围盒框,把 exagg 后的 Y/Z 一并纳入;mapper->GetBounds() 是未缩放的,不可用),
|
// 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。
|
||||||
// 相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。
|
|
||||||
ren->ResetCamera();
|
ren->ResetCamera();
|
||||||
st->cam = ren->GetActiveCamera();
|
st->cam = ren->GetActiveCamera();
|
||||||
st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景:El18
|
st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景:El18
|
||||||
st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22
|
st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22
|
||||||
st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面
|
st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面
|
||||||
ren->ResetCameraClippingRange();
|
ren->ResetCameraClippingRange();
|
||||||
return 1; // 单纹理:恒一张 image
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。
|
// 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。
|
||||||
|
|
@ -3179,8 +3096,13 @@ int cmdView(int argc, char** argv) {
|
||||||
// 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产
|
// 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产
|
||||||
// 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
|
// 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
|
||||||
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
|
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
|
||||||
geopro::data::ChunkedVolumeStore store(dir);
|
// 渲染源 = ViewAdaptiveVolumeSource(C2)。exagg 走 actor 的 SetScale(不烘进
|
||||||
const auto& m = store.meta();
|
// 几何,避免与 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;
|
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||||
// 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。
|
// 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。
|
||||||
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
|
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
|
||||||
|
|
@ -3227,7 +3149,7 @@ int cmdView(int argc, char** argv) {
|
||||||
vtkOutputWindow::SetInstance(capWin);
|
vtkOutputWindow::SetInstance(capWin);
|
||||||
|
|
||||||
ViewState st;
|
ViewState st;
|
||||||
st.store = &store;
|
st.source = &source;
|
||||||
st.mapper = mapper.Get();
|
st.mapper = mapper.Get();
|
||||||
st.ren = ren.Get();
|
st.ren = ren.Get();
|
||||||
st.fpsText = fpsText.Get();
|
st.fpsText = fpsText.Get();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue