三维编辑功能实现
摘要
本文从一款采用Qt 作为人机界面框架、OpenSceneGraph(OSG)作为三维场景与事件管线、自研渲染引擎封装 Viewer 与命令管理的桌面软件出发,选取三类典型交互:点云「放大/缩小」(实质为点大小的视觉尺度调节)、点云编辑(拾取与可撤销修改)、面积测量(多边形顶点拾取与度量回调)。分别从为何采用某种调用结构、从界面操作到底层实现的分步说明、以及涉及的 C++ / Qt / STL / 设计模式与惯用法三个维度展开。核心结论可以概括为:
同一套 Viewer 上并列存在多种「输入语义」——无模式浏览操作用「操作表 + OperationBase」短路径;有状态的编辑操作用「命令模式 + CommandManager + 可逆数据修改」;测量类只读结果用「ParameterTransferCallBack + std::function」隔离业务与引擎。三类需求对状态持久性、撤销语义、与 Qt 的耦合程度要求不同,因此分层形态必然不同,并非随意堆砌。
第一章 软件分层与公共基础
1.1 表示层与场景层的边界
本类软件在工程结构上通常分为:
(1)主窗口 / Dock / 工具栏(Qt Widgets):负责菜单、快捷键绑定、属性面板与工程级状态。
(2)三维子窗口(如Window3D):内嵌OSG 的GraphicsWindow或等价封装,鼠标键盘事件首先进入OSG 的GUIEventAdapter管线,而不是先经过 Qt 的某个「坐标发送槽」。
(3)渲染总线(QtQObject子类):把「图层操作」「测量回调」「信号到属性 Dock」黏合在一起。
(4)RenderEngine(Viewer、CommandManager、GUIEventHandler等):三维拾取、射线求交、模式化事件分发。
(5)数据与几何(点云块、瓦片模型、MeasurementTools、Algorithms):真实修改或度量发生的位置。
理解「Qt 信号槽不跑在 OSG 事件最前线」是读懂后面三条链的前提:用户在 3D 视图里的操作,绝大部分是OSG → 引擎 C++ 回调 → 必要时再 emit 到 Qt;而菜单切换模式、工程保存等才是Qt → 调用 Viewer API。
1.2 三条链共用的「齿轮」
几乎所有三维交互都会碰到以下机械结构:
CommandManager::handle(const osgGA::GUIEventAdapter&, …)
继承osgGA::GUIEventHandler,挂到Viewer的Frame上。每当系统投递MOVE / PUSH / RELEASE / SCROLL等事件,OSG 会调用其handle。内部顺序遍历若干GUIEventHandlerBase子类(以及可选的 button 专用数组),将事件交给当前鼠标模式对应的那一个 Handler。这是典型的责任链的变体(短路求值:一旦某 handler 宣告已处理,可停止向后传递,具体实现依项目而定)。Viewer::setMousePickMode(EnPickModeType)
根据枚举查找工厂函数表(如GetAreaMeasurementEventHandler),替换CommandManager里保存的当前唯一主 Handler(setEventHandle会先清理再注册)。从而同一条 OSG 事件流在不同模式下进入不同类,实现策略模式。enableParameterTransferCallBack/getParameterTransferCallBack
在 Viewer 内部维护一张std::map<ParameterTransferCallBackType, CallBackAndState>,用于与鼠标事件并行存在的、按需触发的业务回调——例如测量完成时调用面积回调。这与CommandManager管的「逐事件输入」是正交的第二条附着机制。
这三者在本工程中分工明确:CommandManager解决「事件从哪进、按模式交给谁」;ParameterTransferCallBack解决「异步或阶段结束时如何把结构化结果交给上层」;Qt解决「如何把结构化结果变成界面状态」。
第二章 点云「放大 / 缩小」:从滚轮到点片元尺寸
2.1 产品语义与实现语义的差别
点云放大缩小在实现上往往不是相机拉近(那是 TrackBall / 操作器的事情),而是调节OpenGLGL_POINT的像素大小或着色器里等价的point size,使点看起来更粗或更细。本代码路径中体现为PointCloudRender::mpr_pointCloudSize在1~10范围内递增递减,并调用PointCloudLayer::setPontSize。因此这是显示参数调节。
2.2 调用链(浏览模式 + 快捷键 / 滚轮)
(1)输入:用户在 3D 视图内按住 Shift 并滚动滚轮(浏览模式PMT_None下由BrowseEventHandler处理 SCROLL 事件)。引擎内调用ViewerImp::doOperation(OT_LargerPointCloudSize)或OT_SmallerPointCloudSize。
(2)操作注册表:ViewerImp::addOperation将操作类型枚举映射到OperationBase*,存储在mpr_OperationMap。doOperation根据枚举find到对应项,执行op->second->doWork()。这是命令对象的极简版(GoF Command 的「无 undo」变体):枚举 = 键,Operation = 可调对象。
(3)进入 Qt / 应用层:在Window3D.cpp中,keyLargerPointCloudSizeOperation::doWork与keySmallerPointCloudSizeOperation::doWork显式调用:
getRenderManager()->getPointCloudLayerOperation()->slotLarger()/slotSmaller()- 以及对块点云图层
getBlockPointCloudLayerOperation()的同名槽。
此处出现第一次「引擎事件线程路径 → Qt 侧图层 Operation」的跳转:
OSG Handler 在同一进程、同一线程(主线程)里直接调用 Qt 侧的slot方法(未经过QMetaObject::invokeMethod时即是同步调用)。只要该调用发生在主线程(GUI 线程),在 Qt5/6 中通常是安全的;若未来把 OSG 嵌到独立线程,则需改为QueuedConnection。
(4)图层与渲染:PointCloudLayerOperation::slotLarger调用PointCloudRender::largerPointCloudScale(SPARSE_POINT_CLOUD_NAME)与FILTER_POINT_CLOUD_NAME两遍,因为稀疏层与滤波层在业务上是两个逻辑图层名称;内部对 map 查层、限制大小在 1–10,然后layerTemp->setPontSize,最后viewer->activeRendering()确保按需渲染模式下刷新。BlockPointCloudLayerOperation同理服务块显示。
2.3 为何这条链「长成这样」
- 无持久编辑语义:点大小是视图参数,用户心理预期是「立刻生效、可随时再调」,与「删点了要能撤销」不同,故不需要
CommandManager::createCommand+ undo 栈。 - 滚轮事件已在 BrowseHandler 里消费:浏览模式不能简单把事件扔给测量 Handler,所以用
doOperation统一出口,把「滚轮 + 修饰键」翻译成应用层语义操作,避免在 OSG 里写死业务名。 OperationBase+map<EnOperationType, Operation*>:用多态(virtual void doWork())和表驱动(枚举查表)把「按键/手势」与「业务动作」解耦。新增一种全局快捷键时,只需注册新的 Operation,无需改BrowseEventHandler里一长串switch。
2.4 本条链涉及的「语法与知识点」
- STL 关联容器:
std::map/operator[]插入或替换。 - 枚举作策略键:
EnOperationType与 Handler、与 UI 快捷键配置对应。 - Qt:图层
Operation类往往继承QObject,槽函数可被直接调或emit触发;此处是直接同步调用槽。 - OSG:
GUIEventAdapter::SCROLL、修饰键getModKeyMask()。
第三章 点云编辑:命令模式、辅助数据与可逆修改
3.1 功能
实现框选 / 点选稀疏点云子集并改色、删除点、批量操作等。
(1)操作会改变数据或 GPU 缓冲区的内容;
(2)期望撤销 / 重做;
(3)有时要和其他子系统(要素点、密集点云)共享同一套命令管理器的扩展点。
因此不能沿用第二章的轻量OperationBase路径,而采用CommandBase+CommandManager的 undo/redo 链表。
3.2 调用链概览(从拾取到命令提交)
以稀疏点云为例(具体类名因版本略有差异,逻辑一致):
(1)模式切换:主界面调用setMousePickMode(PMT_PointCloudSelection)(或矩形选点等),CommandManager::setEventHandle安装PointCloudSelectionEventHandler(或RectSelection系列)。
(2)事件:用户在 3D 视图点击,CommandManager::handle把事件交给当前 Handler。Handler 内通过viewer->pickPointCloud、getParameterTransferCallBack(PTCBT_PickPointCloud)等机制,将拾取结果(点 ID 列表、SceneObject*)写入AuxiliaryData或临时结构。
(3)命令创建:当用户触发「应用编辑」(例如确认删除、改色),ViewerImp或业务层调用CommandManager::createCommand(PCT_..., ...)。工厂内部switch (paInType)new出具体CommandBase子类(如SPCCDeletePointsCommand),携带SPCDDeletePointsData等数据结构。
(4)执行与记录:CommandManager::addCommand先execute(),成功则压入mpr_undoList,并清空mpr_redoList。undoComand/redoComand反向或正向调用unexecute()/execute()。
(5)与 Qt 的衔接:编辑结果若需反映到属性面板,往往通过RenderManager的信号、或Dock 主动拉取getModification;这条线与 OSG 事件仍然是分开触发的。
3.3 为何必须是命令模式而不是简单回调
- 可逆性:删除点是不可逆破坏性行为,**必须在内存中记录「删了哪些索引」**才能
unexecute。回调函数若没有数据外壳,无法重做。 - 批处理与合并:未来若有「一次编辑多图层」,命令对象是唯一自然的事务边界。
- 与其它业务统一:
CommandManager还负责密集点云框选、要素点等,getModificationData从 undo 链收集删除索引——这是横切功能。
3.4 本条链涉及的「语法与知识点」
- GoF Command 模式:
CommandBase接口,execute/unexecute。 - 工厂方法:
createCommand巨型工厂(switch+new);可扩展为注册表以削弱编译期依赖。 - 双端链表 /
list<Ref_Ptr<CommandBase>>:undo/redo。 - 辅助数据对象
AuxiliaryData:会话级拾取状态,避免把 OSG 细节泄漏到 Qt。 Ref_Ptr/ 引用计数:引擎内对场景对象与命令的共享所有权风格。- 策略 + 责任链:不同
EnPickModeType对应不同GUIEventHandlerBase。
第四章 面积测量:模式 Handler + 参数传递回调 + Qt 信号
4.1 功能
在模型或点云表面上逐点点击形成多边形,双击闭合,系统计算周长与面积,并在属性页展示。该过程是纯读取几何 + 回调数值,不默认产生可撤销命令。
4.2 调用链(与第二章、第三章对照)
(1)模式与测量使能:
界面调用changeToAreaSelectionMode:
setMousePickMode(PMT_AreaSelection)→AreaMeasurementEventHandler;StartAreaMeasurementEnableFun→enableParameterTransferCallBack(PTCBT_CalculateArea, new CalculateAreaCallBack(mpr_AreaMeasurementFunc))。
(2)初始化时的函数对象:RenderManager构造里SetAreaMeasurementFun(std::bind(&RenderManager::ActiveAreaFun, this, _1)),把成员函数绑定为std::function<void(btVector<double>)>。
(3)交互事件:用户在 3D 视图操作 →CommandManager::handle→AreaMeasurementEventHandler::handle。
释放左键且判定为单击时,CalModelIntersectionPoints(winX, winY, ...)将屏幕坐标射线与模型图层 / 临时模型求交,得到Vec3d,压入mpr_linesVertexs并绘制辅助线;移动鼠标时更新预览。双击后CalPolygonPerimeter/CalPolygonArea,组装AreaMeasurementCallbackParameters::AreaValue(两项:周长、面积)。
(4)参数回调:getParameterTransferCallBack(PTCBT_CalculateArea)取到CalculateAreaCallBack*,调用operator()→ActiveAreaFun。
(5)Qt:ActiveAreaFun填充AreaMeasurementPropertyInfo,emit sigShowMeasurementAttr;主窗口connect(..., m_pAttributeDock, SLOT(slotBaseInfoChanged))更新 UI。
4.3 面积的几何实现(三角剖分 + 海伦公式)
CalPolygonArea:对3D 顶点序列调用doDelaunayTriangulation(来自Geometry/Algorithms),得到三角索引;对每个三角形的三顶点调用CalTriangleArea——用三维欧氏边长 + 海伦公式求面积,再累加。
这不是「经纬度平面投影面积」的严格测绘公式,而是引擎内几何近似,若需工程计量意义上的面积,通常要在GIS 层再做一次投影变换。
4.4 为何这条链同时需要CommandManager与ParameterTransferCallBack
- 鼠标事件必须走
GUIEventHandler,否则无法在每一帧/每次 MOVE 更新预览。 - 测量结果是阶段性产物,若塞进事件
handle的返回值,无法自然表达「结构化参数」;用回调参数对象AreaMeasurementCallbackParameters更干净。 std::function+std::bind把RenderManager::ActiveAreaFun与引擎层AreaMeasurementCallback接口衔接,编译期接口匹配、运行期多态,避免 RenderEngine 直接#include具体业务类。
4.5 本条链涉及的「语法与知识点」
std::function/std::bind/std::placeholders:成员函数作回调。- 类型擦除:引擎只认
AreaMeasurementCallback*,应用层填入子类实例。 std::map型 callback 注册表(Viewer 内部)。- Qt
signals/slots与emit:线程安全与队列连接若后续引入多线程需重温 Qt 文档。 - 计算几何:Delaunay、海伦公式、射线与模型求交(
osgUtil::IntersectionVisitor一类)。
第六章 贯穿全工程的 C++ 与现代惯用法小结
面向对象:大量继承 + 虚函数(GUIEventHandlerBase、OperationBase、CommandBase)。
STL:vector、map、list、function、bind、迭代器与算法。
RAII 与智能指针:OSGref_ptr、Ref_Ptr风格降低裸delete风险。
设计模式:策略(模式切换)、命令(编辑)、责任链(Handler 序列)、工厂(createCommand)、观察者(Qt 信号槽、ParameterTransferCallBack)。
互操作:Qt 与 OSG共享 OpenGL 上下文与主线程事件循环是常见集成方式;本工程通过在同一主线程同步调用槽简化模型,代价是长时间计算必须自行切片或使用后台线程,否则阻塞 UI。
结语
点云尺度调节是无状态显示参数,用doOperation表驱动足够;点云编辑涉及数据变更与撤销,必须用命令对象 + undo 栈;面积测量需要持续的鼠标事件处理与阶段性数值回调解耦,故OSG Handler +std::function+ Qt 信号各司其职。「轻交互、无副作用」走短路径;「有副作用、要撤销」走命令;「要结构化输出、少耦合」走回调表 +std::function+ Qt 信号。