From 48b8e582ef463f6de6e4d094c881a98161b722e2 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 1 Jul 2026 13:52:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(vtk):=20=E5=AF=BC=E8=88=AAgizmo=E6=94=B9?= =?UTF-8?q?=E4=B8=9A=E7=95=8C=E8=BD=B4=E7=90=83=E9=A3=8E(=E5=B9=B3?= =?UTF-8?q?=E6=B6=82=E7=9B=98+=E7=99=BD=E5=AD=97=E6=A0=87=E7=AD=BE+?= =?UTF-8?q?=E8=B4=9F=E8=BD=B4=E6=B7=A1=E7=8E=AF+hover=E9=AB=98=E4=BA=AE+?= =?UTF-8?q?=E6=8A=97=E9=94=AF=E9=BD=BF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 方向球 LightingOff 平涂实心盘(theta/phi=48),非高光3D球 - 柔和调色板 X(0.90,0.30,0.36)/Y(0.55,0.78,0.33)/Z(0.28,0.45,0.90) - 正向球稍大+白色XYZ公告板字标(推到球前避遮挡),负向球同色×0.42更暗更小无字标(仍可拾取) - 细轴线(1.8)连正向球、同色、不可拾取 - 新增 MouseMove hover 观察者(不abort):角落内拾取→提亮+就地放大1.18×,离开复原 - 整窗 MSAA(8x,有守卫) + 视口上抬避开沿线条 + parallelScale 按视口长宽比自适应不裁切 - CMake 增补 RenderingFreeType 供白字字形运行时渲染 --- CMakeLists.txt | 4 + src/app/VtkSceneView.cpp | 171 ++++++++++++++++++++++++++++++--------- src/app/VtkSceneView.hpp | 16 ++++ 3 files changed, 155 insertions(+), 36 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ac9ea1a..7e1305b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent) find_package(VTK REQUIRED COMPONENTS GUISupportQt RenderingOpenGL2 + RenderingFreeType # 导航 gizmo 轴标签(vtkBillboardTextActor3D)运行时字形渲染:注册 FreeType 工厂 InteractionStyle FiltersSources ) @@ -88,5 +89,8 @@ add_subdirectory(tools/gpr_poc) # gpr3dv 冒烟 CLI:走 vendored 原版 API(loadImpulseMultiChannel → buildVolumeData → runPipeline)。 add_subdirectory(tools/gpr3dv_smoke) +# volbench:临时基准,真实反演点云 CSV 实跑 buildVolume 度量耗时/填充。 +add_subdirectory(tools/volbench) + enable_testing() add_subdirectory(tests) diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 4f24d84..90ed455 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -1,6 +1,7 @@ #include "VtkSceneView.hpp" #include +#include #include #include #include @@ -95,9 +96,11 @@ VtkSceneView::~VtkSceneView() { if (renderWindow_) { if (auto* iren = renderWindow_->GetInteractor()) { if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_); + if (gnomonHoverTag_ != 0) iren->RemoveObserver(gnomonHoverTag_); } } gnomonClickTag_ = 0; + gnomonHoverTag_ = 0; // 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。 if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_); gnomonCamTag_ = 0; @@ -510,25 +513,33 @@ void VtkSceneView::ensureGnomon() { auto* iren = renderWindow_->GetInteractor(); if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装 - // 叠加渲染器:图层1 固定右下角(x∈[0.85,1.0]、y∈[0.10,0.30]) —— 避开底部满宽沿线滑块条(仅雷达体 - // 时显示、约占底 46px)。透明背景只显 gizmo 图元;FXAA 抗锯齿使边缘平滑;非交互不响应任何输入。 + // 叠加渲染器:图层1 固定右下角(见下 SetViewport) —— 避开底部满宽沿线滑块条(仅雷达体时显示、 + // 约占底 46px)。透明背景只显 gizmo 图元;FXAA + MSAA 抗锯齿使边缘平滑;非交互不响应任何输入。 renderWindow_->SetNumberOfLayers(2); gnomonRenderer_ = vtkSmartPointer::New(); gnomonRenderer_->SetLayer(1); gnomonRenderer_->InteractiveOff(); - gnomonRenderer_->SetViewport(0.85, 0.10, 1.0, 0.30); + // 右下角、上抬避开底部满宽「沿线位置」滑块条(约占底 46px):y 从 0.10 抬到 0.15;靠右留 ~1% 边距。 + gnomonRenderer_->SetViewport(0.855, 0.15, 0.995, 0.35); gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块 - gnomonRenderer_->SetUseFXAA(true); // 抗锯齿:轴线/球边缘平滑 + gnomonRenderer_->SetUseFXAA(true); // FXAA + 窗口 MSAA 双重:轴线/球/字形边缘平滑 renderWindow_->AddRenderer(gnomonRenderer_); - const double L = 1.0; // 球心到原点距离(= 轴线长度) + // 抗锯齿(spec §7):整窗多重采样一次性开(仅当尚未开,不覆盖既有设置) → 平涂盘/白字边缘平滑。 + if (renderWindow_->GetMultiSamples() == 0) renderWindow_->SetMultiSamples(8); - // 三根过原点的轴线:X=红、Y=绿、Z=蓝(无光照纯色、加粗、不可拾取)。 - struct AxisLine { double to[3]; double col[3]; }; + const double L = 1.0; // 球心到原点距离(= 轴线长度) + // 业界柔和轴色(非纯 RGB):X 红 / Y 绿 / Z 蓝(spec §2)。正向球=本色,负向球=本色×0.42(更暗)。 + const std::array kColX = {0.90, 0.30, 0.36}; + const std::array kColY = {0.55, 0.78, 0.33}; + const std::array kColZ = {0.28, 0.45, 0.90}; + + // 三根过原点的轴线:仅连到【正向】球,X=红 / Y=绿 / Z=蓝(平涂纯色、细、不可拾取;spec §5)。 + struct AxisLine { double to[3]; std::array col; }; const AxisLine lines[3] = { - {{L, 0, 0}, {0.90, 0.26, 0.26}}, // X 红 - {{0, L, 0}, {0.32, 0.78, 0.36}}, // Y 绿 - {{0, 0, L}, {0.36, 0.56, 0.96}}, // Z 蓝 + {{L, 0, 0}, kColX}, // X 红 + {{0, L, 0}, kColY}, // Y 绿 + {{0, 0, L}, kColZ}, // Z 蓝 }; for (const auto& ln : lines) { vtkNew src; @@ -539,62 +550,70 @@ void VtkSceneView::ensureGnomon() { vtkNew a; a->SetMapper(mapper); a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]); - a->GetProperty()->SetLineWidth(2.6f); - a->GetProperty()->SetLighting(false); + a->GetProperty()->SetLineWidth(1.8f); // 细轴线 + a->GetProperty()->LightingOff(); // 平涂纯色,无高光 a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义) gnomonRenderer_->AddViewProp(a); } - // 6 个方向球:正向亮色填充 + XYZ 黑标签、稍大;负向暗色、稍小、无标签(业界导航 gizmo 风格)。 + // 6 个方向球(平涂实心盘,非高光 3D 球):正向亮盘 + 白色 XYZ 字标、稍大;负向同色更暗、更小、无字标。 // 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、−Z=Bottom、+Y=Back、−Y=Front、+X=Right、−X=Left。 struct DirSpec { geopro::controller::ViewDir dir; double pos[3]; - double col[3]; + std::array base; // 该轴柔和本色 bool positive; - const char* label; // 正向标签;负向 nullptr + const char* label; // 正向字标;负向 nullptr }; const DirSpec specs[6] = { - {geopro::controller::ViewDir::Right, {L, 0, 0}, {0.92, 0.27, 0.27}, true, "X"}, // +X - {geopro::controller::ViewDir::Left, {-L, 0, 0}, {0.46, 0.17, 0.17}, false, nullptr}, // −X - {geopro::controller::ViewDir::Back, {0, L, 0}, {0.31, 0.78, 0.35}, true, "Y"}, // +Y - {geopro::controller::ViewDir::Front, {0, -L, 0}, {0.17, 0.41, 0.19}, false, nullptr}, // −Y - {geopro::controller::ViewDir::Top, {0, 0, L}, {0.35, 0.57, 0.96}, true, "Z"}, // +Z - {geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.19, 0.29, 0.52}, false, nullptr}, // −Z + {geopro::controller::ViewDir::Right, {L, 0, 0}, kColX, true, "X"}, // +X + {geopro::controller::ViewDir::Left, {-L, 0, 0}, kColX, false, nullptr}, // −X + {geopro::controller::ViewDir::Back, {0, L, 0}, kColY, true, "Y"}, // +Y + {geopro::controller::ViewDir::Front, {0, -L, 0}, kColY, false, nullptr}, // −Y + {geopro::controller::ViewDir::Top, {0, 0, L}, kColZ, true, "Z"}, // +Z + {geopro::controller::ViewDir::Bottom, {0, 0, -L}, kColZ, false, nullptr}, // −Z }; gnomonDirs_.clear(); + gnomonBaseColor_.clear(); + gnomonLabels_.clear(); for (const auto& s : specs) { + // 正向盘 r=0.32(稍大);负向盘 r=0.20(更小、更暗 → 呈"淡环/凹陷"观感,仍可拾取)。 + const double radius = s.positive ? 0.32 : 0.20; + const std::array col = + s.positive ? s.base + : std::array{s.base[0] * 0.42, s.base[1] * 0.42, s.base[2] * 0.42}; vtkNew sphere; - sphere->SetRadius(s.positive ? 0.30 : 0.22); // 正向大、负向小(hollow-looking) - sphere->SetThetaResolution(32); - sphere->SetPhiResolution(32); + sphere->SetRadius(radius); + sphere->SetThetaResolution(48); // 高分辨率 → 轮廓平滑(平涂下尤重要) + sphere->SetPhiResolution(48); sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]); vtkNew mapper; mapper->SetInputConnection(sphere->GetOutputPort()); auto actor = vtkSmartPointer::New(); actor->SetMapper(mapper); auto* prop = actor->GetProperty(); - prop->SetColor(s.col[0], s.col[1], s.col[2]); - prop->SetAmbient(0.45); // 高环境光 → 颜色读数接近本色、球体仍有微弱立体感 - prop->SetDiffuse(0.60); - prop->SetSpecular(0.20); - prop->SetSpecularPower(20.0); - actor->SetPickable(1); // 仅方向球可拾取 + prop->SetColor(col[0], col[1], col[2]); + prop->LightingOff(); // 关键(spec §1):平涂实心盘,无镜面/渐变 → 干净的 Blender 式 gizmo + actor->SetOrigin(s.pos[0], s.pos[1], s.pos[2]); // 缩放原点=球心 → hover 放大就地不位移 + actor->SetPickable(1); // 6 球均可拾取(负向点击仍 orbit 到对侧) gnomonRenderer_->AddViewProp(actor); - gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向 + gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向 + gnomonBaseColor_[actor.Get()] = col; // 记本色 → hover 提亮后可复原 - if (s.positive && s.label) { // 正向轴标签:始终朝相机的公告板文字,黑色粗体、居中于球心 + if (s.positive && s.label) { // 正向轴字标:始终朝相机的公告板文字,白色粗体、居中于球心 auto lbl = vtkSmartPointer::New(); lbl->SetInput(s.label); lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]); auto* tp = lbl->GetTextProperty(); - tp->SetFontSize(15); + tp->SetFontSize(20); tp->SetBold(true); - tp->SetColor(0.06, 0.06, 0.06); + tp->SetColor(1.0, 1.0, 1.0); // 白字 tp->SetJustificationToCentered(); tp->SetVerticalJustificationToCentered(); lbl->SetPickable(0); gnomonRenderer_->AddViewProp(lbl); + gnomonLabels_.push_back({lbl.Get(), + {s.pos[0], s.pos[1], s.pos[2]}}); // syncGnomonCamera 推到球前避遮挡 } } @@ -608,6 +627,14 @@ void VtkSceneView::ensureGnomon() { }); gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0); + // 鼠标移动高优先级观察者:仅角落内拾取做 hover 高亮,永不 abort → 不阻塞场景旋转/平移/切片交互。 + gnomonHoverCmd_ = vtkSmartPointer::New(); + gnomonHoverCmd_->SetClientData(this); + gnomonHoverCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { + static_cast(client)->handleGnomonHover(); + }); + gnomonHoverTag_ = iren->AddObserver(vtkCommand::MouseMoveEvent, gnomonHoverCmd_, 1.0); + // 相机同步:观察主相机 ModifiedEvent,每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。 gnomonCamCmd_ = vtkSmartPointer::New(); gnomonCamCmd_->SetClientData(this); @@ -630,16 +657,36 @@ void VtkSceneView::syncGnomonCamera() { if (!mainCam || !gcam) return; // 复制主相机投影方向 + view-up:gizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。 double dir[3]; - mainCam->GetDirectionOfProjection(dir); // 已归一化(F−P) + mainCam->GetDirectionOfProjection(dir); // 已归一化(F−P),指向场景内(背离相机) double up[3]; mainCam->GetViewUp(up); const double dist = 10.0; gcam->SetParallelProjection(1); // 正交投影:gizmo 无透视畸变(业界标准) - gcam->SetParallelScale(1.8); // 取景半高:球到 ±(L+r)≈1.3 + 标签留边 + // 按视口像素长宽比自适应取景半高:balls 到 ±(L+r)≈1.32 + 白字留边 → halfExtent=1.5。 + // parallelScale = 视口世界半高;水平可见半宽 = scale×aspect。取 scale=halfExtent/min(1,aspect) + // 保证长/宽两向都容得下所有球 → 任意窗口长宽比不裁切(视口归一化随窗拉伸也不失效)。 + const double halfExtent = 1.5; + double aspect = 1.0; + const int* wsz = renderWindow_->GetSize(); + const double* vp = gnomonRenderer_->GetViewport(); + if (wsz && wsz[0] > 0 && wsz[1] > 0) { + const double vpH = (vp[3] - vp[1]) * wsz[1]; + if (vpH > 0.0) aspect = (vp[2] - vp[0]) * wsz[0] / vpH; + } + gcam->SetParallelScale(halfExtent / std::min(1.0, aspect)); gcam->SetFocalPoint(0.0, 0.0, 0.0); gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist); gcam->SetViewUp(up[0], up[1], up[2]); gnomonRenderer_->ResetCameraClippingRange(); + + // 正向白字标签推到球【前】(朝相机 = −dir 方向、偏移略大于球半径):billboard 恒面相机,位于球前 + // → 不被球面前半遮挡,读数清晰。相机每次转动都随之更新前向偏移。 + const double front = 0.40; // > 正向球半径 0.32 → 字浮于球前表面之外 + for (const auto& lp : gnomonLabels_) { + if (!lp.first) continue; + lp.first->SetPosition(lp.second[0] - dir[0] * front, lp.second[1] - dir[1] * front, + lp.second[2] - dir[2] * front); + } } void VtkSceneView::handleGnomonClick() { @@ -667,6 +714,58 @@ void VtkSceneView::handleGnomonClick() { // 未命中球 → 不 abort:左键继续走正常交互(旋转/平移/缩放/切片/拾取),保证非干扰。 } +void VtkSceneView::handleGnomonHover() { + // 非阻塞 hover 高亮:绝不 SetAbortFlag → 鼠标移动照常传给交互样式(旋转/平移/切片)。 + if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return; + auto* iren = renderWindow_->GetInteractor(); + if (!iren) return; + + // 提亮本色(向白混 0.42);复原用记录的本色。二者共用,避免重复。 + auto applyColor = [](vtkProp* p, const std::array& c, bool highlight, double scale) { + auto* a = vtkActor::SafeDownCast(p); + if (!a) return; + if (highlight) { + a->GetProperty()->SetColor(c[0] * 0.58 + 0.42, c[1] * 0.58 + 0.42, c[2] * 0.58 + 0.42); + a->SetScale(scale); // 就地放大(origin=球心) + } else { + a->GetProperty()->SetColor(c[0], c[1], c[2]); + a->SetScale(1.0); + } + }; + auto restore = [&]() { + if (!gnomonHovered_) return; + auto it = gnomonBaseColor_.find(gnomonHovered_); + if (it != gnomonBaseColor_.end()) applyColor(gnomonHovered_, it->second, false, 1.0); + gnomonHovered_ = nullptr; + }; + + const int ex = iren->GetEventPosition()[0]; + const int ey = iren->GetEventPosition()[1]; + const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax] + const int* sz = renderWindow_->GetSize(); + if (sz[0] <= 0 || sz[1] <= 0) { restore(); return; } + const double fx = static_cast(ex) / sz[0]; + const double fy = static_cast(ey) / sz[1]; + if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) { // 光标离开角落 → 复原并跳过拾取(廉价) + if (gnomonHovered_) { restore(); renderWindow_->Render(); } + return; + } + // 角落内才做硬件拾取:命中方向球 → 高亮该球、复原旧的;未命中 → 复原。 + vtkProp* hit = nullptr; + if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) { + vtkProp* leaf = gnomonPicker_->GetViewProp(); + if (gnomonBaseColor_.find(leaf) != gnomonBaseColor_.end()) hit = leaf; + } + if (hit == gnomonHovered_) return; // 无变化 → 不重绘 + restore(); + if (hit) { + auto it = gnomonBaseColor_.find(hit); + if (it != gnomonBaseColor_.end()) applyColor(hit, it->second, true, 1.18); + gnomonHovered_ = hit; + } + renderWindow_->Render(); // 高亮/复原变更 → 立即刷新 +} + void VtkSceneView::rebuildAxes() { // 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render + // 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。 diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 3c5da36..748a53c 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include #include +#include #include #include @@ -23,6 +25,7 @@ class vtkVolume; class vtkPropPicker; class vtkCallbackCommand; class vtkCamera; +class vtkBillboardTextActor3D; namespace geopro::app { @@ -133,8 +136,12 @@ private: // 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort // (消费事件,阻止相机旋转/场景拾取);否则不 abort,放行正常交互(旋转/平移/缩放/切片/拾取)。 void handleGnomonClick(); + // 鼠标移动(非 abort、不阻塞场景交互)回调:仅当光标落在 gnomon 角落视口内才拾取,命中方向球 → + // 高亮(提亮本色 + 放大 ~1.18×),复原其余;离开角落或未命中 → 复原全部。picking 只在角落内进行(廉价)。 + void handleGnomonHover(); // 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点), // 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。 + // 同时按视口像素长宽比自适应取景半高(球始终不裁切) + 把正向标签推到球前(朝相机)避免被球面遮挡。 void syncGnomonCamera(); public: @@ -214,6 +221,15 @@ private: bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon) // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向)。actor 由叠加渲染器持有保活。 std::map gnomonDirs_; + + // ── hover 高亮(spec §6)───────────────────────────────────────────────────── + vtkSmartPointer gnomonHoverCmd_; // 鼠标移动观察者命令(不 abort,非阻塞) + unsigned long gnomonHoverTag_ = 0; // 移动观察者句柄(析构摘除) + vtkProp* gnomonHovered_ = nullptr; // 当前高亮的方向球(裸指针,renderer 保活) + std::map> gnomonBaseColor_; // 各球本色(hover 复原用) + // 正向标签(白字) + 其球心:每次 syncGnomonCamera 把标签推到球前(朝相机)→ 不被球面遮挡。 + // raw ptr 非拥有,由叠加渲染器持有保活(与 gnomonDirs_ 同生命周期约定)。 + std::vector>> gnomonLabels_; }; } // namespace geopro::app