三维编辑功能实现
摘要
本文从一款采用 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)渲染总线 (Qt QObject 子类):把「图层操作」「测量回调」「信号到属性 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 / 操作器的事情) ,而是调节 OpenGL GL_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 与智能指针: OSG ref_ptr、Ref_Ptr 风格降低裸 delete 风险。
设计模式: 策略 (模式切换)、命令 (编辑)、责任链 (Handler 序列)、工厂 (createCommand)、观察者 (Qt 信号槽、ParameterTransferCallBack)。
互操作: Qt 与 OSG 共享 OpenGL 上下文与主线程事件循环 是常见集成方式;本工程通过 在同一主线程同步调用槽 简化模型,代价是长时间计算必须自行切片或使用后台线程,否则阻塞 UI。
结语
点云尺度调节 是无状态显示参数,用 doOperation 表驱动 足够;点云编辑 涉及数据变更与撤销,必须用 命令对象 + undo 栈 ;面积测量 需要 持续的鼠标事件处理 与 阶段性数值回调 解耦,故 OSG Handler + std::function + Qt 信号 各司其职。「轻交互、无副作用」走短路径;「有副作用、要撤销」走命令;「要结构化输出、少耦合」走回调表 + std::function + Qt 信号。