diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 9857aa6..5ac040c 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -3,9 +3,10 @@ find_package(GDAL CONFIG REQUIRED) add_library(geopro_render STATIC Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp - ground/TileMath.cpp) + ground/TileMath.cpp + source/WholeVolumeSource.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(geopro_render PUBLIC geopro_core ${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) set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES}) diff --git a/src/render/actors/VoxelActor.cpp b/src/render/actors/VoxelActor.cpp index 8c50e59..08156c6 100644 --- a/src/render/actors/VoxelActor.cpp +++ b/src/render/actors/VoxelActor.cpp @@ -48,6 +48,38 @@ vtkSmartPointer assembleVolume(vtkImageData* img, return volume; } +// int16 量化域传函组装:颜色对每量化级 qv 用 q.toPhys(qv) 反查 ColorScale; +// 不透明度 kBlank→0(透明)、[qmin,qmax] 线性递增到 kMaxOpacity。 +// 与 buildVoxelI16 内联逻辑一致(被 buildVoxelI16FromImage 复用)。 +vtkSmartPointer assembleVolumeI16(vtkImageData* img, + const geopro::core::Quant& q, + const geopro::core::ColorScale& cs, + double vminPhys, double vmaxPhys) +{ + if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0; + + const std::int16_t qmin = q.toQ(vminPhys); + const std::int16_t qmax = q.toQ(vmaxPhys); + const double qminD = static_cast(qmin); + const double qmaxD = static_cast(qmax); + + vtkNew color; + for (int t = 0; t < kTransferSamples; ++t) { + const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1); + const auto qvLevel = static_cast(std::lround(qd)); + const double phys = q.toPhys(qvLevel); + const auto c = cs.colorAt(phys); + color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0); + } + + vtkNew opacity; + opacity->AddPoint(static_cast(geopro::core::ScalarVolumeI16::kBlank), 0.0); + opacity->AddPoint(qminD, 0.0); + opacity->AddPoint(qmaxD, kMaxOpacity); + + return assembleVolume(img, color, opacity); +} + } // namespace vtkSmartPointer buildVoxel(const geopro::core::ScalarVolume& vol, @@ -167,4 +199,13 @@ vtkSmartPointer buildVoxel(const geopro::core::ScalarVolume& vol, return buildVoxel(vol, cs, ox, oy, oz, dx, dy, dz, vmin, vmax, ignored); } +vtkSmartPointer buildVoxelI16FromImage(vtkImageData* shortImg, + const geopro::core::Quant& q, + const geopro::core::ColorScale& cs, + double vminPhys, double vmaxPhys) +{ + // 图像由 source 预构建(VTK_SHORT,带 origin/spacing),直接成体;传函复用量化域逻辑。 + return assembleVolumeI16(shortImg, q, cs, vminPhys, vmaxPhys); +} + } // namespace geopro::render diff --git a/src/render/actors/VoxelActor.hpp b/src/render/actors/VoxelActor.hpp index d16c003..dbc873f 100644 --- a/src/render/actors/VoxelActor.hpp +++ b/src/render/actors/VoxelActor.hpp @@ -36,4 +36,13 @@ vtkSmartPointer buildVoxelI16(const geopro::core::ScalarVolumeI16& vo double vminPhys, double vmaxPhys, vtkSmartPointer& outImage); +// 预构建 VTK_SHORT vtkImageData → GPU 体绘制(供 IVolumeRenderSource 的整卷/工作集图像 +// 直接成体,不再从 ScalarVolumeI16 逐体素填)。传函/着色与 buildVoxelI16 一致: +// 在量化域取 qmin=q.toQ(vminPhys)、qmax=q.toQ(vmaxPhys);kBlank → 不透明度 0(透明); +// 颜色对每个量化级 qv 用 q.toPhys(qv) 反查 ColorScale。shortImg 标量须为 VTK_SHORT。 +vtkSmartPointer buildVoxelI16FromImage(vtkImageData* shortImg, + const geopro::core::Quant& q, + const geopro::core::ColorScale& cs, + double vminPhys, double vmaxPhys); + } // namespace geopro::render diff --git a/src/render/source/IVolumeRenderSource.hpp b/src/render/source/IVolumeRenderSource.hpp new file mode 100644 index 0000000..8d80188 --- /dev/null +++ b/src/render/source/IVolumeRenderSource.hpp @@ -0,0 +1,33 @@ +#pragma once +#include + +#include +#include + +#include "data/store/ChunkedVolumeStore.hpp" // geopro::data::StoreMeta + +class vtkCamera; + +namespace geopro::render { + +// B/C 共用的体渲染数据源接缝。上层(VtkSceneController/SliceTool)只认此接口, +// 运行时可在 WholeVolumeSource(B:整卷)与核外金字塔源(C:按相机选块/LOD)间切换。 +class IVolumeRenderSource { + public: + virtual ~IVolumeRenderSource() = default; + + // 体几何 + 量化 + 物理值域(来自分块存储 meta)。 + virtual const geopro::data::StoreMeta& meta() const = 0; + + // 按相机更新工作集。B:no-op;C:按相机选块/LOD。 + virtual void update(vtkCamera* cam) = 0; + + // 当前应渲染的 int16 体图像。B:1 个整卷;C:工作集多块。 + // 均为 VTK_SHORT,带正确 origin/spacing。 + virtual std::vector> currentImages() const = 0; + + // 供 SliceTool reslice 的图像。B:整卷;C:切面相交块拼的子体。 + virtual vtkImageData* sliceSource() const = 0; +}; + +} // namespace geopro::render diff --git a/src/render/source/WholeVolumeSource.cpp b/src/render/source/WholeVolumeSource.cpp new file mode 100644 index 0000000..ef2a814 --- /dev/null +++ b/src/render/source/WholeVolumeSource.cpp @@ -0,0 +1,65 @@ +#include "source/WholeVolumeSource.hpp" + +#include +#include +#include + +#include "data/store/ChunkedVolumeStore.hpp" + +namespace geopro::render { + +WholeVolumeSource::WholeVolumeSource(const std::string& storeDir) { + geopro::data::ChunkedVolumeStore store(storeDir); + meta_ = store.meta(); + + const int nx = meta_.nx, ny = meta_.ny, nz = meta_.nz; + + full_ = vtkSmartPointer::New(); + full_->SetDimensions(nx, ny, nz); + full_->SetOrigin(meta_.origin[0], meta_.origin[1], meta_.origin[2]); + full_->SetSpacing(meta_.spacing[0], meta_.spacing[1], meta_.spacing[2]); + + // 整卷标量数组(VTK_SHORT)。点序 i 最快、j 次之、k 最慢(匹配 vtkImageData)。 + // 全局体素数可 > 2^31,索引全程 vtkIdType(64 位)。 + vtkNew sc; + sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny * nz); + + const int brick = meta_.brick; + // 遍历所有 brick,按全局坐标把每块体素写回整卷对应位置。 + // 块内布局:i 最快、k 最慢(与 ChunkedVolumeStore::sliceBrick 一致)。 + for (int bz = 0; bz < store.bricksZ(); ++bz) { + for (int by = 0; by < store.bricksY(); ++by) { + for (int bx = 0; bx < store.bricksX(); ++bx) { + const std::vector raw = store.readBrick(bx, by, bz); + const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick; + // 边缘块尺寸由全局剩余推出(与存储侧 extent 一致)。 + const int bw = (nx - i0 < brick) ? (nx - i0) : brick; + const int bh = (ny - j0 < brick) ? (ny - j0) : brick; + const int bd = (nz - k0 < brick) ? (nz - k0) : brick; + + std::size_t w = 0; + for (int kk = 0; kk < bd; ++kk) { + const vtkIdType gk = static_cast(k0 + kk); + for (int jj = 0; jj < bh; ++jj) { + const vtkIdType gj = static_cast(j0 + jj); + // 行起始全局 id:((k*ny + j)*nx + i0)。 + vtkIdType id = (gk * ny + gj) * nx + i0; + for (int ii = 0; ii < bw; ++ii) { + sc->SetValue(id++, raw[w++]); + } + } + } + } + } + } + + full_->GetPointData()->SetScalars(sc); +} + +std::vector> WholeVolumeSource::currentImages() + const { + return {full_}; +} + +} // namespace geopro::render diff --git a/src/render/source/WholeVolumeSource.hpp b/src/render/source/WholeVolumeSource.hpp new file mode 100644 index 0000000..a71645d --- /dev/null +++ b/src/render/source/WholeVolumeSource.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +#include +#include + +#include "source/IVolumeRenderSource.hpp" + +namespace geopro::render { + +// B 实现:构造时读分块存储 → 重组为整卷 VTK_SHORT vtkImageData,常驻内存, +// 供整卷体绘制与切片 reslice。无核外/LOD(那是 C 的职责)。 +class WholeVolumeSource : public IVolumeRenderSource { + public: + // 读 store(meta + 所有 brick),重组整卷 VTK_SHORT image(Origin/Spacing 来自 meta)。 + explicit WholeVolumeSource(const std::string& storeDir); + + const geopro::data::StoreMeta& meta() const override { return meta_; } + void update(vtkCamera*) override {} // 整卷已载,no-op + std::vector> currentImages() const override; + vtkImageData* sliceSource() const override { return full_.Get(); } + + private: + geopro::data::StoreMeta meta_; + vtkSmartPointer full_; +}; + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ece1302..97462ab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -113,6 +113,8 @@ target_sources(geopro_tests PRIVATE render/test_axes.cpp) target_sources(geopro_tests PRIVATE render/test_tile_math.cpp) # SlicePlaneMath(P3):切面法向/滚轮平移+夹限/双击正视相机(含竖直兜底)/滚轮步长/最近切片——纯几何。 target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp) +# WholeVolumeSource(B):读分块存储→重组整卷 VTK_SHORT image,校验 dims/类型/边缘块重组位置。 +target_sources(geopro_tests PRIVATE render/test_whole_volume_source.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) diff --git a/tests/render/test_whole_volume_source.cpp b/tests/render/test_whole_volume_source.cpp new file mode 100644 index 0000000..6aa1a94 --- /dev/null +++ b/tests/render/test_whole_volume_source.cpp @@ -0,0 +1,31 @@ +#include "source/WholeVolumeSource.hpp" +#include "data/store/ChunkedVolumeStore.hpp" +#include "core/algo/GprVolumeBuilder.hpp" +#include +#include +#include +using namespace geopro; + +TEST(WholeVolumeSource, ReassemblesFullVolumeFromStore) { + auto dir = (std::filesystem::temp_directory_path() / "gpr_wvs_test").string(); + std::filesystem::remove_all(dir); + // 造一个可识别的体:值 = 全局 i+j+k(便于校验重组位置) + core::BuiltI16 b; b.vol = core::ScalarVolumeI16(100,40,30); + for (int k=0;k<30;k++)for(int j=0;j<40;j++)for(int i=0;i<100;i++) b.vol.at(i,j,k)=(short)((i+j+k)%1000); + b.quant={1.0,0.0}; b.origin={{1,2,3}}; b.spacing={{0.5,0.5,0.2}}; b.vminPhys=0; b.vmaxPhys=1000; + data::ChunkedVolumeStore::write(dir, b, 64); // 100/40/30 均非 64 整除→含边缘块 + render::WholeVolumeSource src(dir); + EXPECT_EQ(src.meta().nx, 100); + auto imgs = src.currentImages(); + ASSERT_EQ(imgs.size(), 1u); + auto* img = imgs[0].Get(); + EXPECT_EQ(img->GetScalarType(), VTK_SHORT); + int dims[3]; img->GetDimensions(dims); + EXPECT_EQ(dims[0],100); EXPECT_EQ(dims[1],40); EXPECT_EQ(dims[2],30); + // 关键:重组位置正确(边缘块也对) + EXPECT_EQ(*static_cast(img->GetScalarPointer(99,39,29)), (short)((99+39+29)%1000)); + EXPECT_EQ(*static_cast(img->GetScalarPointer(70,10,5)), (short)((70+10+5)%1000)); + EXPECT_EQ(src.sliceSource(), img); + src.update(nullptr); // no-op 安全 + std::filesystem::remove_all(dir); +}