@[TOC](OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(5)番外篇:给 CAD 加上"控制台"------让用户能实时"调参数、看性能"))
代码仓库入口:
系列文章规划:
- (OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(1):从开发的视角看下CAD画出那些好看的图形们))
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(2):看似"老派"的 C++ 底层优化,恰恰是这些前沿领域最需要的基础设施)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(3):你的 CAD 终于能画标准零件了,但用户想要"弧面"、"流线型",怎么办?)
OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(4):GstarCAD / AutoCAD 客户端相关产品 ------ 深入骨髓的数据库哲学)
巨人的肩膀:
- deepseek
- gemini
番外篇:给 CAD 加上"控制台"------让用户能实时"调参数、看性能"
你已经有了一个能画 3D 模型、能旋转缩放、能用鼠标拾取螺栓的渲染视口。
但用户打开你的软件,左看右看,问了一句:"我怎么能知道现在每秒多少帧?我想把金属质感调强一点,去哪儿调?"
你愣住了。
你只能在代码里改 roughness 变量,然后重新编译。
用户不是程序员,他们想要一个随时能拖拽的滑块、一个实时跳动的数字、一个能打勾的复选框。
于是你决定:给这个 CAD 加上一个 控制面板。
第一次尝试:用操作系统的原生控件
你想:Windows 不是有按钮、滑块、文本框吗?
你试着在窗口上叠一个 Win32 的 CreateWindow 创建按钮。
但很快你发现,OpenGL 的渲染窗口和这些原生控件是两套绘图机制。
按钮会一直"浮"在 3D 画面上,但当你旋转模型时,按钮并不会跟着相机动------这倒没关系,控制面板本来就应该固定。
问题是:你的鼠标点击事件怎么区分?
用户想旋转模型,却不小心点到了按钮;或者用户想点按钮,鼠标却被 OpenGL 的拾取逻辑抢走了。
你开始理解为什么专业 CAD 软件需要专门的 UI 框架。
第二回合:接入 Dear ImGui,让 UI 和 OpenGL 成为一家人
你找到了一个叫 Dear ImGui 的库。
它的工作方式很特别:每一帧,你告诉它"画一个按钮",它就在屏幕上画出来;然后你去检查"这个按钮被按下了吗?"
它不像传统 UI 那样有"窗口过程回调",而是把 UI 当作渲染的一部分。
你把它集成到你的 render_manager.cpp 中:
-
用
ImGui_ImplGlfw_InitForOpenGL告诉 ImGui:"我的窗口是 GLFW 那个,你帮我接收鼠标键盘事件,但是不要全部吃掉 ,保留一些给 3D 视口。"你特意设置了第二个参数为
false,这让 ImGui 不霸占事件------用户点击 3D 物体时,照样能旋转相机,只有点击 UI 区域时才操作面板。 -
用
ImGui_ImplOpenGL3_Init让 ImGui 把自己的绘图指令转成 OpenGL 的glDrawElements,和你自己画的螺栓、齿轮用同一个渲染管线。
然后你在每一帧的渲染循环里,插入一段"UI 代码":
cpp
// 开始新的一帧
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// 画一个固定的左侧面板
ImGui::SetNextWindowPos(ImVec2(10, 10));
ImGui::SetNextWindowSize(ImVec2(300, 500));
ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
// 性能监控
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::Text("Vertices: %d", g_totalVertices);
ImGui::Text("Triangles: %d", g_totalTriangles);
// 材质调节
ImGui::SliderFloat("Metallic", &material.metallic, 0.0f, 1.0f);
ImGui::SliderFloat("Roughness", &material.roughness, 0.0f, 1.0f);
// 调试开关
ImGui::Checkbox("Show BVH Bounds", &showBVH);
ImGui::End();
// 渲染 UI
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
神奇的事情发生了:
- 你拖动"Roughness"滑块,下一帧 Fragment Shader 就拿到了新值,金属质感实时变化。
- 你勾选"Show BVH Bounds",原本隐藏的包围盒线框瞬间出现在 3D 视口中。
- 鼠标点击面板上的按钮时,3D 旋转完全不受影响。
你的 CAD 第一次有了"工业化"的样子------用户不再对着黑乎乎的代码,而是对着一个左侧整齐排列着参数和数据的驾驶舱。
这和 AutoCAD 的"特性面板"有什么关系?
AutoCAD 右侧那个可以显示"选中线的长度、颜色、图层"的面板,叫做 Properties Palette 。
它背后的 UI 框架不是 ImGui,而是 MFC (老版本)或 Qt (新版本)。
这些是 保留模式 GUI:你创建了一个按钮,它就一直存在,直到你销毁它;按钮被点击时会发出信号,你绑定一个槽函数去响应。
而 ImGui 是 即时模式 GUI :每一帧你都重新"画"一遍界面,然后轮询状态。
两种方式各有优劣:
- 即时模式 :代码简单、和渲染循环天然同步,特别适合性能监控、材质调试、快速原型。
- 保留模式 :内存占用低、事件驱动省电、原生支持复杂的浮动面板、停靠、多标签,是大规模工业软件的首选。
你现在的项目用 ImGui 完全正确------因为你还在验证渲染核心。
但如果你要去浩辰 CAD 或中望 CAD 做客户端,你大概率会用 Qt 搭建主窗口,然后用一个 QOpenGLWidget 把你的渲染引擎嵌进去。
这样,业务逻辑(图层树、属性表)用 Qt 写,图形管线(画螺栓、拾取)用你的 C++ 核心,各司其职。
专业深度扩展 ------ UI 架构与控制面板
1. 即时模式 GUI vs 保留模式 GUI
即时模式 :每帧重新提交 UI 描述(如
ImGui::Button),库内部生成顶点数据并渲染。无事件回调,状态由开发者轮询。优点:简单、与渲染循环紧耦合、适合动态界面。缺点:每帧全部重绘,CPU/GPU 开销略高。保留模式 :UI 控件是持久对象(如
QPushButton),由操作系统或框架管理事件循环。优点:内存高效、事件驱动(省电)、原生支持复杂窗口管理。缺点:学习曲线陡峭,与 OpenGL 渲染集成需要额外处理(如QOpenGLWidget)。工业 CAD 选型:AutoCAD 早期用 MFC(保留模式),新版转向 Qt;SolidWorks 用 Qt;中望 CAD 也使用 Qt。ImGui 常用于性能调试工具、游戏引擎编辑器、快速原型。
2. 数据驱动 UI 与渲染视口分离核心原则:UI 面板不直接操作渲染器内部资源,而是修改"模型层"的数据(如材质参数、可见性标志),然后由渲染线程在下一帧消费。
你的项目实践 :
SliderFloat绑定了material.metallic,没有直接调用 OpenGL 函数。下一帧Shader::setUniform读取该变量。这是标准的 MVC 变体。对比 AutoCAD :特性面板修改实体的
color属性时,实际上是在修改数据库中的AcDbEntity的color字段,然后通过反应器(Reactor)触发重绘,而不是直接调用显示函数。
3. 依赖库的角色GLFW:跨平台窗口创建、OpenGL 上下文管理、原始鼠标/键盘事件分发。它不参与 UI 绘制,只提供"画布"和"输入源"。
GLAD :OpenGL 函数加载器,让你能调用现代 OpenGL API(如
glDrawElements)。ImGui 生成的顶点数据最终通过 GLAD 加载的函数提交给 GPU。ImGui:UI 布局、样式、事件处理(如按钮 hover、滑块拖动),最终输出一组顶点和索引数据。不依赖任何特定渲染 API。
集成关键 :必须保证 ImGui 和你的场景渲染共享同一个 OpenGL 上下文,并且正确处理视口大小变化 (
glViewport同时影响 3D 和 UI 绘制区域)。
4. 对应工业产品特性面板的能力特性面板 (Properties Palette):显示当前选中实体的所有属性(几何参数、层、颜色、线型、材质等),并允许就地修改。修改后立即生效,支持 Undo/Redo。
实现要点 :需要实现一个选择集 (Selection Set) 跟踪当前选中的实体,然后通过反射 (Reflection) 或属性系统动态生成 UI 控件。例如,选中一条线,显示"起点 X/Y/Z"三个浮点数输入框;选中一个螺栓,显示"直径"、"长度"等自定义字段。
你项目的进阶方向 :在你的
Pick功能基础上,将拾取到的实体 ID 传递给 UI,UI 根据实体类型动态显示对应的参数控件,修改后调用updateEntity并重新构建 BVH。
5. MFC / Qt 在 CAD 中的典型应用MFC(Microsoft Foundation Classes):AutoCAD 2009 之前的主 UI 框架,基于 Win32 API 的 C++ 封装。缺点:跨平台困难、代码冗长、现代特性支持差。
Qt:当前主流工业软件 UI 框架(包括 AutoCAD 2010 之后、中望 CAD、浩辰 CAD、SolidWorks 等)。提供信号槽、样式表、图形视图框架、OpenGL 集成等。
浮动面板与停靠 :CAD 中的"图层管理器"、"属性面板"、"工具选项板"都可以拖拽、停靠在主窗口边缘。Qt 的
QDockWidget原生支持,ImGui 需要第三方 Docking 分支(如imgui_docking)才能勉强模拟,但性能和稳定性不如 Qt。对话框 :模态对话框(如"新建文件")用
QDialog::exec();非模态对话框(如"查找替换")用QDialog::show(),并需要处理文档上下文切换。
6. UI 与渲染线程的同步问题单线程(你的项目):ImGui 和 OpenGL 绘制都在主线程,UI 修改的变量在下一帧被渲染使用,无同步问题。
多线程(工业级) :UI 线程(Qt 主线程)和渲染线程(OpenGL 线程)分离。UI 修改属性时,需要将命令通过命令队列 或信号槽跨线程传递给渲染线程,并保证 OpenGL 上下文不被同时访问。
常用技术 :
QOpenGLWidget内部的makeCurrent()/doneCurrent()配合互斥锁;或者使用QMetaObject::invokeMethod将渲染操作投递到 GUI 线程执行。
7. 性能考量:UI 开销不应影响 3D 帧率ImGui 的开销:每帧都要遍历所有控件、生成顶点数据,对于几十个控件的面板开销极小(<0.1ms),可忽略。
Qt 的开销:保留模式的事件驱动,空闲时 CPU 占用几乎为 0。但当面板上有大量实时刷新数据(如 FPS、鼠标坐标)时,需要控制刷新频率(例如 20Hz 更新,而不是每帧都更新)。
你的实践 :FPS 通过
ImGui::GetIO().Framerate显示,这本身就是每帧计算的,开销可接受。如果你要显示实时坐标,建议用独立线程计算,UI 定时读取原子变量。
8. 未来演进路线:从 ImGui 到 Qt 的平滑过渡阶段一(当前):ImGui + GLFW 独立窗口,专注于渲染核心和算法验证。
阶段二 :保持 ImGui 调试面板,但同时集成 Qt 作为主窗口,用
QOpenGLWidget嵌入你的渲染器。ImGui 可作为 Qt 的 Overlay(通过QOpenGLWidget的paintGL中调用 ImGui 渲染)。阶段三 :完全迁移到 Qt 原生控件,移除 ImGui,用 Qt 的属性表格、滑块、复选框重新实现控制面板,并实现选择集联动(点击 3D 物体,Qt 面板自动刷新)。
架构精髓 :无论 UI 框架如何变,你的图形数据库 (Entity/BVH/材质)和渲染管线(OpenGL 绘制)应该完全独立,UI 只是它们的一个"遥控器"。这,才是工业级 CAD 客户端的基石。
现在,你再看你的 Huhb3D-Viewer 左侧那个面板:它不只是几行 ImGui 代码,而是一个经过深思熟虑的架构决策 。
你知道它为什么适合现在的阶段,也知道未来换成 Qt 时该怎么保留你的核心资产。
这份通透,才是大厂最看重的"三维 C++ 开发工程师"的思维深度。
9. 如果还没看困,可以再瞅瞅
项目中涉及的 UI 架构与控制面板细节
在项目中,UI 架构的本质是**"即时模式 GUI (Immediate Mode GUI) 与图形渲染管线的解耦"**。
具体涉及以下几个核心部分:
1. 核心依赖与初始化绑定
- 依赖库 :你使用了
Dear ImGui作为 UI 框架,GLFW作为窗口和事件分发器,GLAD作为 OpenGL 函数加载器。 - 代码位置 :render_manager.cpp 中的
initImGui()函数。 - 实现细节 :
- 通过
ImGui_ImplGlfw_InitForOpenGL(window, false)将 ImGui 绑定到当前的 GLFW 窗口。这里的false是一个非常高级的工程处理:你没有让 ImGui 霸占所有的鼠标/键盘回调,而是保留了 CAD 渲染视口对事件的处理权,实现了 UI 操作和三维空间漫游的共存。 - 你通过
ImGui_ImplOpenGL3_Init("#version 330")将 ImGui 的底层渲染指令转译为 OpenGL 3.3 Core Profile 的着色器代码。
- 通过
2. 常驻控制面板 (Control Panel) 的构建
- 对应 AutoCAD 功能:AutoCAD 右侧的"特性面板 (Properties Palette)" 或左侧的"项目管理器"。
- 代码位置 :render_manager.cpp 中的
updateImGui()函数。 - 实现细节 :你通过
ImGui::SetNextWindowPos和ImGui::Begin创建了一个固定在屏幕左侧的面板,并且使用了ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove锁死了它的位置,这正是标准工业软件(而非游戏)界面的典型特征。
3. 面板内的四大核心业务模块
在你的控制面板中,你巧妙地将 CAD 的业务需求分为了四个区域:
- A. 文件与数据接入 (File Operations) :
- 实现 :一个醒目的
Open STL File按钮。 - 亮点 :点击后,你调用了
openFileDialog()。这个函数使用了 Windows 原生的OPENFILENAMEA结构体(底层 Win32 API)来弹出原生文件选择框,这让你的软件看起来更像一个专业的 Windows 桌面应用,而不是一个玩具 Demo。
- 实现 :一个醒目的
- B. 性能与网格监控 (Model & Performance) :
- 实现 :使用
ImGui::Text实时打印 FPS、顶点数 (Vertices)、三角面片数 (Triangles) 和内存占用 (Memory Usage)。 - 亮点 :你还实时计算并打印了模型的包围盒大小 (
rootBounds) 和 BVH 树的深度。这证明了你的系统具备几何拓扑信息的提取能力,这是三维软件工程师的硬指标。
- 实现 :使用
- C. 材质参数实时驱动 (PBR Material) :
- 实现 :使用了
ImGui::SliderFloat来调节Metallic(金属度) 和Roughness(粗糙度)。 - 亮点 :这两个滑块直接绑定了你在类中定义的
float变量,当滑块拖动时,下一帧的 OpenGL Fragment Shader 就会拿到最新的参数,实现了数据驱动渲染的"所见即所得"。
- 实现 :使用了
- D. 空间调试与射线拾取 (Debug & Pick Information) :
- 实现 :一个勾选框
Show BVH Bounds用于可视化底层加速结构;以及一个面板实时显示鼠标射线命中的三角形顶点坐标和法线向量。 - 亮点 :这是你最核心的竞争力 。你不仅把三角形画出来了,还通过 UI 面板证明了你能与三维空间中的特定实体发生精确到微秒级的物理交互。
- 实现 :一个勾选框
对比工业界(AutoCAD / 浩辰 CAD)的 UI 架构差异
深度思考:
"在
Huhb3D-ViewerV1.0 中,为了追求极致的轻量化和渲染性能验证,我选择了 Dear ImGui(Immediate Mode)。它的好处是与渲染循环(Render Loop)同频,数据同步极快,非常适合做性能监控和材质调试。但是,如果对标浩辰 CAD 或 AutoCAD 客户端,它们底层使用的是 MFC 或 Qt(Retained Mode GUI)。
差异点在于:
- 事件驱动 vs 帧驱动:Qt 是事件驱动的,只有鼠标点击或数据变化时才重绘 UI,极度省电;而 ImGui 哪怕你不动鼠标,每秒也要重绘 170 次 UI。
- 窗口系统:CAD 软件需要复杂的浮动面板(Docking)、多标签页(Tab)、甚至多屏幕显示。虽然 ImGui 也有 Docking 分支,但 Qt 提供的原生 OS 窗口管理能力是不可替代的。
- 未来的演进路线 :在这个项目的后续版本(或在我未来的工作中),合理的架构应该是:用 Qt 搭建主框架(菜单、特性面板、图层树),用一个
QOpenGLWidget承载我现在的 C++ 核心渲染引擎,从而实现"业务逻辑"与"图形管线"的彻底解耦。"
-
如果想了解一些成像系统、图像、人眼、颜色等等的小知识,快去看看视频吧 :
- 抖音:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 快手:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- B站:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 认准一个头像,保你不迷路:

- 认准一个头像,保你不迷路:
-
您要是也想站在文章开头的巨人的肩膀啦,可以动动您发财的小指头,然后把您的想要展现的名称和公开信息发我,这些信息会跟随每篇文章,屹立在文章的顶部哦
