feat(render): IVolumeRenderSource 接缝 + WholeVolumeSource(B) 整卷重组

新增 B/C 共用的体渲染数据源接口 IVolumeRenderSource(meta/update/
currentImages/sliceSource),并实现 B 的 WholeVolumeSource:构造时读
ChunkedVolumeStore,遍历所有 brick 按全局坐标(vtkIdType 64 位)重组为
整卷 VTK_SHORT vtkImageData(含边缘块),供整卷体绘制与切片 reslice。

VoxelActor 新增 buildVoxelI16FromImage 重载:直接以预构建 VTK_SHORT
图像成体,传函/着色复用量化域逻辑(抽出 assembleVolumeI16),不改动
Task 7 现有 buildVoxel/buildVoxelI16 行为。

geopro_render 链 geopro_store;新增 test_whole_volume_source 校验
dims/类型/边缘块重组位置。
This commit is contained in:
gaozheng 2026-06-23 11:23:37 +08:00
parent b362156364
commit cc3c5bf755
8 changed files with 213 additions and 2 deletions

View File

@ -3,9 +3,10 @@ find_package(GDAL CONFIG REQUIRED)
add_library(geopro_render STATIC 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 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 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_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) target_compile_features(geopro_render PUBLIC cxx_std_17)
set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES})

View File

@ -48,6 +48,38 @@ vtkSmartPointer<vtkVolume> assembleVolume(vtkImageData* img,
return volume; return volume;
} }
// int16 量化域传函组装:颜色对每量化级 qv 用 q.toPhys(qv) 反查 ColorScale
// 不透明度 kBlank→0(透明)、[qmin,qmax] 线性递增到 kMaxOpacity。
// 与 buildVoxelI16 内联逻辑一致(被 buildVoxelI16FromImage 复用)。
vtkSmartPointer<vtkVolume> 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<double>(qmin);
const double qmaxD = static_cast<double>(qmax);
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(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<vtkPiecewiseFunction> opacity;
opacity->AddPoint(static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, kMaxOpacity);
return assembleVolume(img, color, opacity);
}
} // namespace } // namespace
vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol, vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
@ -167,4 +199,13 @@ vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
return buildVoxel(vol, cs, ox, oy, oz, dx, dy, dz, vmin, vmax, ignored); return buildVoxel(vol, cs, ox, oy, oz, dx, dy, dz, vmin, vmax, ignored);
} }
vtkSmartPointer<vtkVolume> 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 } // namespace geopro::render

View File

@ -36,4 +36,13 @@ vtkSmartPointer<vtkVolume> buildVoxelI16(const geopro::core::ScalarVolumeI16& vo
double vminPhys, double vmaxPhys, double vminPhys, double vmaxPhys,
vtkSmartPointer<vtkImageData>& outImage); vtkSmartPointer<vtkImageData>& 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<vtkVolume> buildVoxelI16FromImage(vtkImageData* shortImg,
const geopro::core::Quant& q,
const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys);
} // namespace geopro::render } // namespace geopro::render

View File

@ -0,0 +1,33 @@
#pragma once
#include <vector>
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
#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<vtkSmartPointer<vtkImageData>> currentImages() const = 0;
// 供 SliceTool reslice 的图像。B:整卷;C:切面相交块拼的子体。
virtual vtkImageData* sliceSource() const = 0;
};
} // namespace geopro::render

View File

@ -0,0 +1,65 @@
#include "source/WholeVolumeSource.hpp"
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#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<vtkImageData>::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<vtkShortArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(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<std::int16_t> 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<vtkIdType>(k0 + kk);
for (int jj = 0; jj < bh; ++jj) {
const vtkIdType gj = static_cast<vtkIdType>(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<vtkSmartPointer<vtkImageData>> WholeVolumeSource::currentImages()
const {
return {full_};
}
} // namespace geopro::render

View File

@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <vector>
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
#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<vtkSmartPointer<vtkImageData>> currentImages() const override;
vtkImageData* sliceSource() const override { return full_.Get(); }
private:
geopro::data::StoreMeta meta_;
vtkSmartPointer<vtkImageData> full_;
};
} // namespace geopro::render

View File

@ -113,6 +113,8 @@ target_sources(geopro_tests PRIVATE render/test_axes.cpp)
target_sources(geopro_tests PRIVATE render/test_tile_math.cpp) target_sources(geopro_tests PRIVATE render/test_tile_math.cpp)
# SlicePlaneMath(P3)/+/双击正视相机(含竖直兜底)// # SlicePlaneMath(P3)/+/双击正视相机(含竖直兜底)//
target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp) 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}) 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})

View File

@ -0,0 +1,31 @@
#include "source/WholeVolumeSource.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include <vtkImageData.h>
#include <gtest/gtest.h>
#include <filesystem>
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<short*>(img->GetScalarPointer(99,39,29)), (short)((99+39+29)%1000));
EXPECT_EQ(*static_cast<short*>(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);
}