diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a7d8f05..cf87a09 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,4 +11,5 @@ add_subdirectory(core) add_subdirectory(data) add_subdirectory(net) +add_subdirectory(render) add_subdirectory(app) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 61a39a7..cd9cf42 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -24,6 +24,7 @@ target_link_libraries(geopro_desktop PRIVATE geopro_core # Phase 1:ColorScale 上色 geopro_data # Phase 2:本地样本仓储(对象树 / 网格 / 色阶) geopro_net # Phase 3:登录(验证码 + RSA + login2) + geopro_render # Phase 4:render 层(Scene / GridContourActor / 相机预设) ) vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES}) diff --git a/src/app/main.cpp b/src/app/main.cpp index ef6905a..7a6fe55 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -4,17 +4,21 @@ // 数据:docs/剖面网格数据的色阶数据2等文件/(真实样本,UTF-8 中文路径,经 QFile 读取)。 #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include @@ -28,106 +32,16 @@ #include "AuthService.hpp" #include "login/LoginWindow.hpp" +#include "CameraPreset.hpp" +#include "Scene.hpp" +#include "actors/GridContourActor.hpp" + #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include namespace { -// 把 core 模型(Grid + ColorScale)渲染为 banded contour(填充面 + 黑色等值线)。 -// 入参是已加载的 core 模型,不读文件(数据加载由 Repository 负责)。 -void renderGrid(vtkRenderer* ren, const geopro::core::Grid& g, const geopro::core::ColorScale& cs) -{ - ren->RemoveAllViewProps(); // 清旧 actor,支持重复切换数据集 - - const int nx = g.nx(), ny = g.ny(); - if (nx < 2 || ny < 2 || g.x.size() < 2 || g.y.size() < 2) { - ren->SetBackground(1, 1, 1); - ren->ResetCamera(); - return; - } - - vtkNew img; - img->SetDimensions(nx, ny, 1); - img->SetOrigin(g.x[0], g.y[0], 0.0); - img->SetSpacing(g.x[1] - g.x[0], g.y[1] - g.y[0], 1.0); - - vtkNew sc; - sc->SetName("v"); - sc->SetNumberOfTuples(static_cast(nx) * ny); - for (int j = 0; j < ny; ++j) - for (int i = 0; i < nx; ++i) - sc->SetValue(static_cast(j) * nx + i, g.valueAt(i, j)); // i 最快 - img->GetPointData()->SetScalars(sc); - - // vmin/vmax 来自 Grid;若退化(==)则用数据极值兜底,避免 LUT/contour 退化。 - double vmin = g.vmin, vmax = g.vmax; - if (vmin >= vmax) { - const auto& vals = g.values(); - vmin = vals.empty() ? 0.0 : vals.front(); - vmax = vmin; - for (double v : vals) { - if (v < vmin) vmin = v; - if (v > vmax) vmax = v; - } - if (vmin >= vmax) vmax = vmin + 1.0; - } - - // 256 级 LUT,按 ColorScale 阶梯取色。 - const int N = 256; - vtkNew lut; - lut->SetNumberOfTableValues(N); - lut->SetTableRange(vmin, vmax); - for (int t = 0; t < N; ++t) { - const double val = vmin + (vmax - vmin) * t / (N - 1); - const auto c = cs.colorAt(val); - lut->SetTableValue(t, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0); - } - lut->Build(); - - vtkNew surf; - surf->SetInputData(img); - - vtkNew banded; - banded->SetInputConnection(surf->GetOutputPort()); - banded->GenerateValues(20, vmin, vmax); - banded->GenerateContourEdgesOn(); - banded->SetScalarModeToValue(); - - vtkNew mapper; - mapper->SetInputConnection(banded->GetOutputPort()); - mapper->SetScalarModeToUseCellData(); - mapper->SetLookupTable(lut); - mapper->SetScalarRange(vmin, vmax); - vtkNew bands; - bands->SetMapper(mapper); - - vtkNew edgeMapper; - edgeMapper->SetInputConnection(banded->GetOutputPort(1)); // contour edges - edgeMapper->ScalarVisibilityOff(); - vtkNew edges; - edges->SetMapper(edgeMapper); - edges->GetProperty()->SetColor(0, 0, 0); - edges->GetProperty()->SetLineWidth(0.6); - - ren->AddActor(bands); - ren->AddActor(edges); - ren->SetBackground(1, 1, 1); - ren->GetActiveCamera()->ParallelProjectionOn(); - ren->ResetCamera(); -} - // 从对象结构树构建 QTreeWidget:GS → TM → DS 三层;DS 项在 UserRole 存 dsId。 void populateTree(QTreeWidget* tree, const std::vector& gss) { @@ -159,20 +73,69 @@ std::string readPem(const std::string& path) // 在给定 QMainWindow 上构建 M1 工作台:ADS 三栏 + 对象树 → 渲染联动 + 属性面板。 // repo 生命周期须覆盖到事件循环结束(由调用方保证)。 +// 相机模式:默认二维俯视。 +enum class CameraMode { Top2D, Free3D }; + void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo) { - // 中央 QVTK 视图(指针供联动回调使用)。 + // 中央 QVTK 视图(指针供联动回调使用)。renderer 由 render::Scene 持有。 + // Scene 需在 lambda 回调期间存活 ⇒ 堆分配,挂到 window 父链随窗口销毁。 + auto* scene = new geopro::render::Scene(); auto* vtkWidget = new QVTKOpenGLStereoWidget(); + // Scene 非 QObject:用 widget 销毁信号清理,避免泄漏(widget 随 window 销毁)。 + QObject::connect(vtkWidget, &QObject::destroyed, [scene]() { delete scene; }); vtkNew renderWindow; vtkWidget->setRenderWindow(renderWindow); - vtkNew renderer; - renderWindow->AddRenderer(renderer); + renderWindow->AddRenderer(scene->renderer()); + + // 当前相机模式(默认二维)。用 shared_ptr 让多个 lambda 共享同一状态。 + auto cameraMode = std::make_shared(CameraMode::Top2D); + vtkRenderer* rendererPtr = scene->renderer(); + vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get(); + auto applyCurrentCamera = [rendererPtr, cameraMode]() { + if (*cameraMode == CameraMode::Top2D) + geopro::render::applyTop2D(rendererPtr); + else + geopro::render::applyFree3D(rendererPtr); + }; auto* dockManager = new ads::CDockManager(&window); window.setCentralWidget(dockManager); - auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维剖面视图")); - vtkDock->setWidget(vtkWidget); + // 中央剖面容器:顶部 2D/3D 工具条 + 下方 QVTK 视图。 + auto* centerWidget = new QWidget(); + auto* centerLayout = new QVBoxLayout(centerWidget); + centerLayout->setContentsMargins(0, 0, 0, 0); + centerLayout->setSpacing(0); + + auto* viewToolBar = new QToolBar(); + auto* cameraGroup = new QActionGroup(viewToolBar); + cameraGroup->setExclusive(true); + auto* act2D = viewToolBar->addAction(QStringLiteral("二维")); + auto* act3D = viewToolBar->addAction(QStringLiteral("三维")); + act2D->setCheckable(true); + act3D->setCheckable(true); + cameraGroup->addAction(act2D); + cameraGroup->addAction(act3D); + act2D->setChecked(true); // 默认二维 + centerLayout->addWidget(viewToolBar); + centerLayout->addWidget(vtkWidget, 1); + + QObject::connect(act2D, &QAction::triggered, vtkWidget, + [cameraMode, rendererPtr, renderWindowPtr]() { + *cameraMode = CameraMode::Top2D; + geopro::render::applyTop2D(rendererPtr); + renderWindowPtr->Render(); + }); + QObject::connect(act3D, &QAction::triggered, vtkWidget, + [cameraMode, rendererPtr, renderWindowPtr]() { + *cameraMode = CameraMode::Free3D; + geopro::render::applyFree3D(rendererPtr); + renderWindowPtr->Render(); + }); + + auto* vtkDock = new ads::CDockWidget(QStringLiteral("剖面视图")); + vtkDock->setWidget(centerWidget); dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock); // 左 dock:对象树。 @@ -193,18 +156,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock); // 联动:点击 DS 项 → 加载 grid/colorScale → 渲染 + 更新属性。 - // VTK 对象(renderer/renderWindow)按【裸指针值】捕获:底层对象被 widget/renderWindow - // 引用计数持有(widget 父链挂到 window),生命周期覆盖事件循环;按值捕获避免 - // buildWorkbench 返回后 vtkNew 局部变量析构导致悬空引用。repo 由调用方保活。 - vtkRenderer* rendererPtr = renderer.Get(); - vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get(); - auto renderDataset = [&repo, rendererPtr, renderWindowPtr, propLabel](QTreeWidgetItem* item) { + // Scene/renderWindow 按【裸指针值】捕获:Scene 挂 window 父链、renderer 被 renderWindow + // 引用计数持有,生命周期覆盖事件循环。repo 由调用方保活。 + auto renderDataset = [&repo, scene, renderWindowPtr, applyCurrentCamera, propLabel]( + QTreeWidgetItem* item) { const QString id = item->data(0, Qt::UserRole).toString(); if (id.isEmpty()) return; // GS/TM 节点无 dsId,忽略 const std::string dsId = id.toStdString(); const auto g = repo.loadGrid(dsId); const auto cs = repo.loadColorScale(dsId); - renderGrid(rendererPtr, g, cs); + scene->clear(); + const auto actors = geopro::render::buildGridContour(g, cs); + scene->addActor(actors.bands); + scene->addActor(actors.edges); + applyCurrentCamera(); // 按当前 2D/3D 模式重设相机 renderWindowPtr->Render(); propLabel->setText(QStringLiteral("数据集: %1\n网格: %2 x %3\nvmin / vmax: %4 / %5") .arg(item->text(0)) diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt new file mode 100644 index 0000000..dffef63 --- /dev/null +++ b/src/render/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingOpenGL2 InteractionStyle) +add_library(geopro_render STATIC + Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp) +target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES}) +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/CameraPreset.cpp b/src/render/CameraPreset.cpp new file mode 100644 index 0000000..70b809c --- /dev/null +++ b/src/render/CameraPreset.cpp @@ -0,0 +1,40 @@ +#include "CameraPreset.hpp" + +#include + +namespace geopro::render { + +namespace { +// 三维斜视方位角 / 仰角。 +constexpr double kAzimuth = 30.0; +constexpr double kElevation = 25.0; +} // namespace + +void applyTop2D(vtkRenderer* r) +{ + if (!r) return; + auto* c = r->GetActiveCamera(); + c->ParallelProjectionOn(); + // 正对 XY 平面:position 在 +Z,focalpoint 在原点(ResetCamera 会重定位到场景中心),viewUp = +Y。 + c->SetFocalPoint(0, 0, 0); + c->SetPosition(0, 0, 1); + c->SetViewUp(0, 1, 0); + r->ResetCamera(); +} + +void applyFree3D(vtkRenderer* r) +{ + if (!r) return; + auto* c = r->GetActiveCamera(); + c->ParallelProjectionOff(); + // 先回到俯视基准,再叠加方位 / 仰角,得到稳定的斜视立体视角。 + c->SetFocalPoint(0, 0, 0); + c->SetPosition(0, 0, 1); + c->SetViewUp(0, 1, 0); + c->Azimuth(kAzimuth); + c->Elevation(kElevation); + c->OrthogonalizeViewUp(); + r->ResetCamera(); +} + +} // namespace geopro::render diff --git a/src/render/CameraPreset.hpp b/src/render/CameraPreset.hpp new file mode 100644 index 0000000..008ed30 --- /dev/null +++ b/src/render/CameraPreset.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +namespace geopro::render { + +// 俯视二维:正交投影,相机在 +Z 正对 XY 平面。 +void applyTop2D(vtkRenderer* r); + +// 自由三维:透视投影,斜视方位看到剖面立体。 +void applyFree3D(vtkRenderer* r); + +} // namespace geopro::render diff --git a/src/render/ColorLutBuilder.cpp b/src/render/ColorLutBuilder.cpp new file mode 100644 index 0000000..c4df643 --- /dev/null +++ b/src/render/ColorLutBuilder.cpp @@ -0,0 +1,20 @@ +#include "ColorLutBuilder.hpp" + +namespace geopro::render { + +vtkSmartPointer buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n) +{ + if (n < 2) n = 2; // 至少两级,避免 (n-1) 退化 + auto lut = vtkSmartPointer::New(); + lut->SetNumberOfTableValues(n); + lut->SetTableRange(vmin, vmax); + for (int t = 0; t < n; ++t) { + const double val = vmin + (vmax - vmin) * t / (n - 1); + const auto c = cs.colorAt(val); + lut->SetTableValue(t, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0); + } + lut->Build(); + return lut; +} + +} // namespace geopro::render diff --git a/src/render/ColorLutBuilder.hpp b/src/render/ColorLutBuilder.hpp new file mode 100644 index 0000000..5335a15 --- /dev/null +++ b/src/render/ColorLutBuilder.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include +#include "model/ColorScale.hpp" +namespace geopro::render { + +// 由 core 阶梯色阶构建 N 级 vtkLookupTable,区间 [vmin, vmax]。 +vtkSmartPointer buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n = 256); + +} // namespace geopro::render diff --git a/src/render/Scene.cpp b/src/render/Scene.cpp new file mode 100644 index 0000000..f1d28b5 --- /dev/null +++ b/src/render/Scene.cpp @@ -0,0 +1,20 @@ +#include "Scene.hpp" + +namespace geopro::render { + +Scene::Scene() : renderer_(vtkSmartPointer::New()) +{ + renderer_->SetBackground(1, 1, 1); // 白底 +} + +void Scene::clear() +{ + renderer_->RemoveAllViewProps(); +} + +void Scene::addActor(vtkActor* a) +{ + if (a) renderer_->AddActor(a); +} + +} // namespace geopro::render diff --git a/src/render/Scene.hpp b/src/render/Scene.hpp new file mode 100644 index 0000000..2cb49e3 --- /dev/null +++ b/src/render/Scene.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include +namespace geopro::render { + +// 单一渲染场景:持有 vtkRenderer(白底),统一管理 actor 的加入与清除。 +// 不持有 RenderWindow(由 app 的 QVTK widget 承载,把 renderer() 加入其 RenderWindow)。 +class Scene { +public: + Scene(); + + vtkRenderer* renderer() const { return renderer_.Get(); } + + void clear(); // 移除所有 view prop,支持重复切换数据集 + void addActor(vtkActor* a); // actor 由 renderer 引用计数保活 + +private: + vtkSmartPointer renderer_; +}; + +} // namespace geopro::render diff --git a/src/render/actors/GridContourActor.cpp b/src/render/actors/GridContourActor.cpp new file mode 100644 index 0000000..6ae4d1b --- /dev/null +++ b/src/render/actors/GridContourActor.cpp @@ -0,0 +1,92 @@ +#include "actors/GridContourActor.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ColorLutBuilder.hpp" + +namespace geopro::render { + +namespace { + +// banded contour 的色带级数(设计默认 20)。 +constexpr int kBandCount = 20; +// LUT 级数。 +constexpr int kLutLevels = 256; + +} // namespace + +GridActors buildGridContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs) +{ + const int nx = g.nx(), ny = g.ny(); + + // 退化网格:返回空 actor(调用方仍可安全 addActor,但 mapper 无输入则不绘制)。 + if (nx < 2 || ny < 2 || g.x.size() < 2 || g.y.size() < 2) { + return GridActors{}; + } + + vtkNew img; + img->SetDimensions(nx, ny, 1); + img->SetOrigin(g.x[0], g.y[0], 0.0); + img->SetSpacing(g.x[1] - g.x[0], g.y[1] - g.y[0], 1.0); + + vtkNew sc; + sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny); + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) + sc->SetValue(static_cast(j) * nx + i, g.valueAt(i, j)); // i 最快 + img->GetPointData()->SetScalars(sc); + + // vmin/vmax 来自 Grid;若退化(==)则用数据极值兜底,避免 LUT/contour 退化。 + double vmin = g.vmin, vmax = g.vmax; + if (vmin >= vmax) { + const auto& vals = g.values(); + vmin = vals.empty() ? 0.0 : vals.front(); + vmax = vmin; + for (double v : vals) { + if (v < vmin) vmin = v; + if (v > vmax) vmax = v; + } + if (vmin >= vmax) vmax = vmin + 1.0; + } + + auto lut = buildLut(cs, vmin, vmax, kLutLevels); + + vtkNew surf; + surf->SetInputData(img); + + vtkNew banded; + banded->SetInputConnection(surf->GetOutputPort()); + banded->GenerateValues(kBandCount, vmin, vmax); + banded->GenerateContourEdgesOn(); + banded->SetScalarModeToValue(); + + vtkNew mapper; + mapper->SetInputConnection(banded->GetOutputPort()); + mapper->SetScalarModeToUseCellData(); + mapper->SetLookupTable(lut); + mapper->SetScalarRange(vmin, vmax); + + auto bands = vtkSmartPointer::New(); + bands->SetMapper(mapper); + + vtkNew edgeMapper; + edgeMapper->SetInputConnection(banded->GetOutputPort(1)); // contour edges + edgeMapper->ScalarVisibilityOff(); + + auto edges = vtkSmartPointer::New(); + edges->SetMapper(edgeMapper); + edges->GetProperty()->SetColor(0, 0, 0); + edges->GetProperty()->SetLineWidth(0.6); + + return GridActors{bands, edges}; +} + +} // namespace geopro::render diff --git a/src/render/actors/GridContourActor.hpp b/src/render/actors/GridContourActor.hpp new file mode 100644 index 0000000..26e584a --- /dev/null +++ b/src/render/actors/GridContourActor.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +namespace geopro::render { + +// banded contour 的两个 actor:填充色带 + 黑色等值线。 +struct GridActors { + vtkSmartPointer bands; + vtkSmartPointer edges; +}; + +// 把 core 网格 + 色阶渲染为 banded contour actor(不操作 renderer,由调用方加入场景)。 +GridActors buildGridContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs); + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f6f9264..703dcdf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,4 +55,11 @@ if(WIN32) COMMAND_EXPAND_LISTS) endif() +# render 层:ColorLutBuilder(core ColorScale -> vtkLookupTable)。 +# 需 vtkLookupTable(VTK::CommonCore);geopro_render 已 PUBLIC 传递其余 VTK 组件。 +find_package(VTK REQUIRED COMPONENTS CommonCore) +target_sources(geopro_tests PRIVATE render/test_color_lut.cpp) +target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) +vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) + add_subdirectory(spike) # spike S3: banded contour 渲染验证 diff --git a/tests/render/test_color_lut.cpp b/tests/render/test_color_lut.cpp new file mode 100644 index 0000000..6140acf --- /dev/null +++ b/tests/render/test_color_lut.cpp @@ -0,0 +1,22 @@ +#include + +#include "ColorLutBuilder.hpp" +#include "model/ColorScale.hpp" + +using namespace geopro::core; + +// buildLut 对阶梯色阶取下界:val=2.0 落在 [0,10) -> 蓝档,蓝分量应大于红分量。 +TEST(ColorLut, BuildsSteppedLutFromColorScale) { + ColorScale cs; + cs.addStop(0.0, Rgba{0, 0, 255, 255}); // 蓝 + cs.addStop(10.0, Rgba{255, 0, 0, 255}); // 红 + + auto lut = geopro::render::buildLut(cs, 0.0, 10.0, 256); + ASSERT_NE(lut.GetPointer(), nullptr); + + double rgb[3]; + lut->GetColor(2.0, rgb); // [0,10) -> 蓝 + + EXPECT_GT(rgb[2], rgb[0]); // 蓝 > 红 + EXPECT_GT(rgb[2], 0.5); // 接近纯蓝 +}