feat(render): render 层(Scene/ColorLut/GridContourActor/相机预设) + 2D/3D 切换

This commit is contained in:
gaozheng 2026-06-07 21:42:55 +08:00
parent 1f55763a8a
commit cdf49020af
14 changed files with 342 additions and 106 deletions

View File

@ -11,4 +11,5 @@
add_subdirectory(core)
add_subdirectory(data)
add_subdirectory(net)
add_subdirectory(render)
add_subdirectory(app)

View File

@ -24,6 +24,7 @@ target_link_libraries(geopro_desktop PRIVATE
geopro_core # Phase 1ColorScale
geopro_data # Phase 2 / /
geopro_net # Phase 3 + RSA + login2
geopro_render # Phase 4render Scene / GridContourActor /
)
vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES})

View File

@ -4,17 +4,21 @@
// 数据docs/剖面网格数据的色阶数据2等文件/真实样本UTF-8 中文路径,经 QFile 读取)。
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <QActionGroup>
#include <QApplication>
#include <QDialog>
#include <QFormLayout>
#include <QLabel>
#include <QMainWindow>
#include <QSurfaceFormat>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <DockManager.h>
@ -28,106 +32,16 @@
#include "AuthService.hpp"
#include "login/LoginWindow.hpp"
#include "CameraPreset.hpp"
#include "Scene.hpp"
#include "actors/GridContourActor.hpp"
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkBandedPolyDataContourFilter.h>
#include <vtkCamera.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkDoubleArray.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImageData.h>
#include <vtkLookupTable.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderer.h>
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<vtkImageData> 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<vtkDoubleArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny);
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i)
sc->SetValue(static_cast<vtkIdType>(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<vtkLookupTable> 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<vtkDataSetSurfaceFilter> surf;
surf->SetInputData(img);
vtkNew<vtkBandedPolyDataContourFilter> banded;
banded->SetInputConnection(surf->GetOutputPort());
banded->GenerateValues(20, vmin, vmax);
banded->GenerateContourEdgesOn();
banded->SetScalarModeToValue();
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(banded->GetOutputPort());
mapper->SetScalarModeToUseCellData();
mapper->SetLookupTable(lut);
mapper->SetScalarRange(vmin, vmax);
vtkNew<vtkActor> bands;
bands->SetMapper(mapper);
vtkNew<vtkPolyDataMapper> edgeMapper;
edgeMapper->SetInputConnection(banded->GetOutputPort(1)); // contour edges
edgeMapper->ScalarVisibilityOff();
vtkNew<vtkActor> 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();
}
// 从对象结构树构建 QTreeWidgetGS → TM → DS 三层DS 项在 UserRole 存 dsId。
void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& 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<vtkGenericOpenGLRenderWindow> renderWindow;
vtkWidget->setRenderWindow(renderWindow);
vtkNew<vtkRenderer> renderer;
renderWindow->AddRenderer(renderer);
renderWindow->AddRenderer(scene->renderer());
// 当前相机模式(默认二维)。用 shared_ptr 让多个 lambda 共享同一状态。
auto cameraMode = std::make_shared<CameraMode>(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))

View File

@ -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})

View File

@ -0,0 +1,40 @@
#include "CameraPreset.hpp"
#include <vtkCamera.h>
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 在 +Zfocalpoint 在原点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

View File

@ -0,0 +1,11 @@
#pragma once
#include <vtkRenderer.h>
namespace geopro::render {
// 俯视二维:正交投影,相机在 +Z 正对 XY 平面。
void applyTop2D(vtkRenderer* r);
// 自由三维:透视投影,斜视方位看到剖面立体。
void applyFree3D(vtkRenderer* r);
} // namespace geopro::render

View File

@ -0,0 +1,20 @@
#include "ColorLutBuilder.hpp"
namespace geopro::render {
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n)
{
if (n < 2) n = 2; // 至少两级,避免 (n-1) 退化
auto lut = vtkSmartPointer<vtkLookupTable>::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

View File

@ -0,0 +1,10 @@
#pragma once
#include <vtkSmartPointer.h>
#include <vtkLookupTable.h>
#include "model/ColorScale.hpp"
namespace geopro::render {
// 由 core 阶梯色阶构建 N 级 vtkLookupTable区间 [vmin, vmax]。
vtkSmartPointer<vtkLookupTable> buildLut(const geopro::core::ColorScale& cs, double vmin, double vmax, int n = 256);
} // namespace geopro::render

20
src/render/Scene.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "Scene.hpp"
namespace geopro::render {
Scene::Scene() : renderer_(vtkSmartPointer<vtkRenderer>::New())
{
renderer_->SetBackground(1, 1, 1); // 白底
}
void Scene::clear()
{
renderer_->RemoveAllViewProps();
}
void Scene::addActor(vtkActor* a)
{
if (a) renderer_->AddActor(a);
}
} // namespace geopro::render

22
src/render/Scene.hpp Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkActor.h>
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<vtkRenderer> renderer_;
};
} // namespace geopro::render

View File

@ -0,0 +1,92 @@
#include "actors/GridContourActor.hpp"
#include <vtkBandedPolyDataContourFilter.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkDoubleArray.h>
#include <vtkImageData.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#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<vtkImageData> 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<vtkDoubleArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny);
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i)
sc->SetValue(static_cast<vtkIdType>(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<vtkDataSetSurfaceFilter> surf;
surf->SetInputData(img);
vtkNew<vtkBandedPolyDataContourFilter> banded;
banded->SetInputConnection(surf->GetOutputPort());
banded->GenerateValues(kBandCount, vmin, vmax);
banded->GenerateContourEdgesOn();
banded->SetScalarModeToValue();
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(banded->GetOutputPort());
mapper->SetScalarModeToUseCellData();
mapper->SetLookupTable(lut);
mapper->SetScalarRange(vmin, vmax);
auto bands = vtkSmartPointer<vtkActor>::New();
bands->SetMapper(mapper);
vtkNew<vtkPolyDataMapper> edgeMapper;
edgeMapper->SetInputConnection(banded->GetOutputPort(1)); // contour edges
edgeMapper->ScalarVisibilityOff();
auto edges = vtkSmartPointer<vtkActor>::New();
edges->SetMapper(edgeMapper);
edges->GetProperty()->SetColor(0, 0, 0);
edges->GetProperty()->SetLineWidth(0.6);
return GridActors{bands, edges};
}
} // namespace geopro::render

View File

@ -0,0 +1,17 @@
#pragma once
#include <vtkSmartPointer.h>
#include <vtkActor.h>
#include "model/Field.hpp"
#include "model/ColorScale.hpp"
namespace geopro::render {
// banded contour 的两个 actor填充色带 + 黑色等值线。
struct GridActors {
vtkSmartPointer<vtkActor> bands;
vtkSmartPointer<vtkActor> edges;
};
// 把 core 网格 + 色阶渲染为 banded contour actor不操作 renderer由调用方加入场景
GridActors buildGridContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs);
} // namespace geopro::render

View File

@ -55,4 +55,11 @@ if(WIN32)
COMMAND_EXPAND_LISTS)
endif()
# render ColorLutBuildercore ColorScale -> vtkLookupTable
# vtkLookupTableVTK::CommonCoregeopro_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

View File

@ -0,0 +1,22 @@
#include <gtest/gtest.h>
#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); // 接近纯蓝
}