简介:本资源面向三维图形开发初学者与工具开发者,提供一套基于ImGui与OpenSceneGraph(OSG)集成的模型编辑器完整工程,解决在OpenGL环境下对三维模型进行实时交互操控的问题。项目将ImGui绘制命令嵌入OSG渲染循环,无需依赖其他第三方界面库,即可实现三轴方向的实时平移、旋转与缩放,并支持调整模型透明度与颜色,便于查看内部结构或个性化着色。压缩包共24个文件,约3.98MB,以9个h头文件、8个cpp源文件为核心,辅以vcxproj与sln工程文件、hpp封装、ini配置、gif效果演示及exe可执行程序,结构清晰,可直接编译运行。目前已有605人学习下载。通过研读源码,读者可掌握ImGui与OSG的集成方式、渲染循环插入技巧及模型材质属性控制思路,为构建更复杂的3D编辑工具打下基础。
1. imgui+OSG 模型编辑器:三轴实时变换到底难在哪
如果你做过三维工具,大概率遇到过这种场景:模型加载进场景后,想让它绕 X 轴转 30 度、再沿 Z 轴平移 2 个单位,结果拖完滑块模型飞到了屏幕外,或者旋转中心跑到了世界原点而不是模型自身中心。这不是你代码写错了,而是 imgui 的 UI 坐标系、OSG 的场景图节点变换、以及三轴操作的参考系没有对齐。imgui+OSG 制作模型编辑器,核心要解决的就是让三轴方向的实时平移、旋转和缩放变化既跟手又不漂移。这套方案适合做三维可视化工具、数字孪生场景搭建、机器人仿真调试的工程师,也适合刚接触 OSG 想找一个能跑通的最小闭环的开发者。下面从场景图变换原理讲到可复现的代码,再到参数调优和踩坑记录,全部按我实际做过的路径来。
2. OSG 场景图与 imgui 事件循环怎么接上
2.1 为什么不能直接在 OSG 回调里改矩阵
OSG 的每一帧渲染由 Viewer 驱动,场景图节点的变换通过 MatrixTransform 或 PositionAttitudeTransform 管理。如果你在 imgui 的按钮回调里直接调用 setMatrix,而 imgui 的渲染又发生在 OSG 的 draw 阶段之后,就会出现一帧延迟甚至矩阵被覆盖的情况。常见做法是把 imgui 的输入值先存到一个共享状态结构体里,然后在 OSG 的 update 遍历或一个自定义的 osgGA::GUIEventHandler 中统一应用变换。这样做的另一个好处是:平移、旋转、缩放的数值可以同时被多个节点复用,不会因为 UI 刷新频率和渲染帧率不一致导致抖动。
具体来说,我会定义一个 TransformState 结构体,里面放 position(osg::Vec3)、rotation(osg::Quat 或欧拉角)、scale(osg::Vec3),以及一个 dirty 标志。imgui 的每一帧只负责读写这个结构体,OSG 的 update 回调检查 dirty 后一次性构建矩阵并赋给 MatrixTransform。这样就把 UI 线程和渲染线程的耦合降到了最低。
2.2 最小可运行骨架:imgui 窗口嵌入 OSG Viewer
下面这段代码展示的是把 imgui 的窗口绘制到 OSG 的 Viewer 上,并创建一个带 MatrixTransform 的模型节点。假设你已经编译好了 osg、osgViewer、imgui 以及 imgui 的 OSG 后端(常见的是 osgImgui 或自己写的 ImGui_ImplOpenGL 适配层)。
// main.cpp #include <osgViewer/Viewer> #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <imgui.h> #include <imgui_impl_opengl3.h> #include <imgui_impl_osg.h> // 假设已有 OSG 后端适配 struct TransformState { osg::Vec3 position{0.f, 0.f, 0.f}; osg::Vec3 rotation{0.f, 0.f, 0.f}; // 欧拉角,单位度 osg::Vec3 scale{1.f, 1.f, 1.f}; bool dirty = true; }; int main() { osgViewer::Viewer viewer; osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cow.osg"); if (!model) return -1; mt->addChild(model); viewer.setSceneData(mt); TransformState state; // 初始化 imgui IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); ImGui_ImplOpenGL3_Init("#version 130"); ImGui_ImplOSG_Init(&viewer); // 绑定到 viewer while (!viewer.done()) { viewer.frame(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplOSG_NewFrame(); ImGui::NewFrame(); ImGui::Begin("Transform Panel"); bool changed = false; changed |= ImGui::DragFloat3("Position", &state.position[0], 0.01f); changed |= ImGui::DragFloat3("Rotation", &state.rotation[0], 0.5f); changed |= ImGui::DragFloat3("Scale", &state.scale[0], 0.01f, 0.01f, 100.f); if (changed) state.dirty = true; ImGui::End(); ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); if (state.dirty) { osg::Matrix mat; mat.makeIdentity(); // 先缩放,再旋转,最后平移(顺序很重要,后面会讲) mat.preMultScale(state.scale); mat.preMultRotate(osg::Quat( osg::DegreesToRadians(state.rotation.x()), osg::X_AXIS, osg::DegreesToRadians(state.rotation.y()), osg::Y_AXIS, osg::DegreesToRadians(state.rotation.z()), osg::Z_AXIS)); mat.preMultTranslate(state.position); mt->setMatrix(mat); state.dirty = false; } } ImGui_ImplOpenGL3_Shutdown(); ImGui::DestroyContext(); return 0; }逻辑说明:imgui 的 DragFloat3 返回 true 表示数值被用户修改,此时把 dirty 置位。在 OSG 的帧循环末尾统一构建矩阵。矩阵构建顺序采用 preMultScale → preMultRotate → preMultTranslate,等价于先缩放、再旋转、最后平移。如果你反过来写,模型会绕着世界原点转而不是自身中心转,这是最常见的翻车点。
参数说明:DragFloat3 的第三个参数是拖拽速度,平移用 0.01 比较跟手,旋转用 0.5 度每像素,缩放用 0.01 并限制最小 0.01 防止矩阵退化。旋转的欧拉角顺序在 osg::Quat 构造函数里是 Z-Y-X 还是 X-Y-Z 取决于你传入的顺序,上面代码是按 X、Y、Z 依次传入,实际旋转顺序是 Z 先转、Y 再转、X 最后转,如果你需要固定顺序,建议用 osg::Quat 的 set 方法显式指定。
2.3 三轴变换的参考系选择:局部轴还是世界轴
在模型编辑器里,用户拖拽 X 轴滑块时,期望的是模型沿自身局部坐标系的 X 轴移动,还是沿世界坐标系的 X 轴移动?这两种行为在实现上完全不同。局部轴平移需要把平移量先乘以当前旋转矩阵的逆,再叠加到位置上;世界轴平移则直接加。我一般会在 UI 上放一个切换按钮,默认用局部轴,因为做模型摆放时局部轴更符合直觉。旋转同理:局部轴旋转用四元数右乘,世界轴旋转用左乘。缩放一般只做局部轴,因为非均匀缩放叠加旋转会产生切变,OSG 的 MatrixTransform 虽然支持,但后续拾取和包围盒计算会变得很麻烦。
3. 用 imgui 滑块驱动三轴实时变换的完整实现
3.1 平移:从滑块数值到 osg::Vec3 的映射
平移是最简单的一环,但有两个细节容易忽略。第一,滑块范围要设得足够大,比如 -1000 到 1000,但步长要小,否则拖一下模型就飞出视锥体。第二,如果场景里有多个模型,平移量应该相对于当前选中节点的父节点坐标系,而不是世界坐标系。下面是一个带选中节点管理的平移实现片段。
// 假设 selectedMt 是当前选中的 MatrixTransform osg::Vec3 currentPos = selectedMt->getMatrix().getTrans(); float pos[3] = {currentPos.x(), currentPos.y(), currentPos.z()}; if (ImGui::DragFloat3("Translate", pos, 0.01f, -1000.f, 1000.f)) { osg::Matrix mat = selectedMt->getMatrix(); mat.setTrans(osg::Vec3(pos[0], pos[1], pos[2])); selectedMt->setMatrix(mat); }逻辑说明:getTrans 取出当前矩阵的平移分量,修改后 setTrans 写回。注意 setTrans 会保留旋转和缩放分量,不会破坏已有变换。参数说明:DragFloat3 的第四个和第五个参数是软范围,超出后滑块变红但依然可拖,适合做粗调。
3.2 旋转:欧拉角、四元数与万向锁的取舍
旋转是三轴变换里最容易出玄学问题的地方。imgui 的 DragFloat3 给出的是三个欧拉角,但 OSG 内部用四元数存储旋转。如果你每帧都把欧拉角转成四元数再写回矩阵,当某个轴接近 90 度时会出现万向锁,表现为拖 Y 轴滑块时模型绕 Z 轴乱转。解决办法有两种:一是用四元数增量累加,每次拖拽只施加一个小的旋转增量,而不是从欧拉角重建;二是限制欧拉角的范围,比如 Y 轴限制在 -89 到 89 度。我一般用第一种,因为更通用。
// 增量式旋转:每次拖拽施加一个小角度 float delta[3] = {0.f, 0.f, 0.f}; if (ImGui::DragFloat3("Rotate", delta, 0.5f)) { osg::Quat q = selectedMt->getMatrix().getRotate(); osg::Quat dq; dq.makeRotate(osg::DegreesToRadians(delta[0]), osg::X_AXIS, osg::DegreesToRadians(delta[1]), osg::Y_AXIS, osg::DegreesToRadians(delta[2]), osg::Z_AXIS); q = q * dq; // 局部轴旋转用右乘 osg::Matrix mat = selectedMt->getMatrix(); mat.setRotate(q); selectedMt->setMatrix(mat); // 重置 delta,避免累加 delta[0] = delta[1] = delta[2] = 0.f; }逻辑说明:每次拖拽只取增量角度,构造一个小的四元数 dq,然后右乘到当前四元数上。右乘表示绕局部轴旋转,左乘表示绕世界轴旋转。参数说明:DragFloat3 的返回值在拖拽过程中持续为 true,所以每次都会施加一个增量,松手后 delta 归零。注意这里没有把 delta 写回 UI,因为 imgui 的 DragFloat3 是即时模式,下一帧会重新从 delta 变量读取,所以手动归零是必要的。
3.3 缩放:均匀与非均匀的边界处理
缩放滑块通常给三个轴分别控制,但非均匀缩放和旋转叠加后会产生切变,导致模型看起来是斜的。如果你只需要均匀缩放,用一个滑块控制所有轴即可。如果需要非均匀缩放,建议在 UI 上明确提示用户先缩放再旋转,或者在矩阵构建时固定顺序为缩放→旋转→平移。下面是一个带均匀缩放开关的实现。
static bool uniformScale = true; float scale[3] = {1.f, 1.f, 1.f}; // 从当前矩阵提取缩放分量(假设没有切变) osg::Vec3 curScale = selectedMt->getMatrix().getScale(); scale[0] = curScale.x(); scale[1] = curScale.y(); scale[2] = curScale.z(); if (ImGui::Checkbox("Uniform Scale", &uniformScale)) {} if (uniformScale) { float s = scale[0]; if (ImGui::DragFloat("Scale", &s, 0.01f, 0.01f, 100.f)) { scale[0] = scale[1] = scale[2] = s; osg::Matrix mat = selectedMt->getMatrix(); mat.setScale(osg::Vec3(s, s, s)); selectedMt->setMatrix(mat); } } else { if (ImGui::DragFloat3("Scale XYZ", scale, 0.01f, 0.01f, 100.f)) { osg::Matrix mat = selectedMt->getMatrix(); mat.setScale(osg::Vec3(scale[0], scale[1], scale[2])); selectedMt->setMatrix(mat); } }逻辑说明:getScale 从矩阵中提取缩放分量,setScale 写回。注意 setScale 会保留旋转和平移,但如果你之前有非均匀缩放叠加旋转,getScale 提取出来的值可能不准确,因为矩阵已经包含了切变。参数说明:最小缩放限制为 0.01,防止矩阵行列式为零导致模型消失或渲染异常。
3.4 实时性保障:帧率与 UI 刷新解耦
imgui 是即时模式 UI,每一帧都重新构建界面。如果你的场景有几十万个顶点,OSG 的渲染帧率可能只有 30 帧,而 imgui 的滑块拖拽事件可能以 60 次每秒的频率触发。如果每次触发都直接修改场景图并触发重绘,会出现 UI 卡顿。我的做法是在 TransformState 里加一个时间戳,只有距离上次应用超过 16 毫秒才真正写入矩阵,否则只更新 UI 显示值。这样既保证了视觉上的实时性,又避免了不必要的矩阵重建。
4. 避坑与排查:三轴变换常见的 5 个翻车现场
4.1 模型绕世界原点旋转而不是自身中心
现象:拖旋转滑块时,模型像地球绕太阳一样公转,而不是自转。原因:矩阵构建顺序错误,先平移后旋转,导致旋转中心在世界原点。解决:确保矩阵乘法顺序为缩放→旋转→平移,用 preMult 系列函数时注意调用顺序,或者直接用 osg::Matrix::scale、rotate、translate 组合。
4.2 滑块拖拽时模型抖动或跳变
现象:缓慢拖动滑块,模型位置或角度突然跳一下。原因:imgui 的 DragFloat3 在拖拽过程中返回的数值是累积的,如果你每帧都从矩阵读取当前值再叠加,会形成正反馈。解决:用增量方式处理旋转,平移和缩放直接从 UI 读取绝对值并 set,不要累加。
4.3 非均匀缩放后模型法线错误
现象:模型被压扁后光照变暗或出现黑面。原因:非均匀缩放改变了法线方向,OSG 默认不自动重新归一化法线。解决:在场景中添加 osg::StateSet 并设置 RESCALE_NORMAL 为 ON,或者避免非均匀缩放叠加旋转。
4.4 多节点选中时变换只作用于最后一个
现象:场景里有多个模型,选中多个后拖滑块只有一个动。原因:变换状态没有和选中节点列表绑定,只操作了当前指针。解决:维护一个 std::vector<osg::ref_ptr osg::MatrixTransform > 选中列表,每次变换遍历所有节点,但注意每个节点的局部坐标系不同,需要把世界空间增量转换到各自局部空间。
4.5 旋转角度超过 360 度后数值溢出
现象:连续拖旋转滑块,角度累加到 720 度后模型突然反向转。原因:欧拉角没有做归一化,浮点数精度丢失。解决:每次旋转后对角度取模 360,或者直接用四元数存储旋转,四元数天然没有这个问题。
5. 进阶技巧:用 gizmo 拖拽替代滑块与变换验证
滑块做久了你会发现,真正好用的模型编辑器需要三轴 gizmo——在模型中心画出红绿蓝三个箭头,鼠标直接拖箭头实现平移和旋转。imgui 本身不提供 3D gizmo,但你可以用 OSG 的 osgUtil::LineSegmentIntersector 做拾取,再结合 imgui 的鼠标位置判断是否按下了 gizmo 的某个轴。我一般会先实现一个简化版:在模型中心画三条线段,每条线段用不同颜色,鼠标按下时发射一条射线,检测与哪条线段最近,然后根据鼠标移动量计算平移或旋转增量。
验证变换是否正确有一个笨但有效的方法:在场景里加一个 osg::Vec3Array 画出的坐标轴,长度固定为 1,跟随模型变换。如果坐标轴的长度变了,说明缩放矩阵有问题;如果坐标轴不垂直了,说明出现了切变。这个习惯帮我省了很多后悔药。
另一个技巧是给每个变换操作加撤销栈。imgui 的按钮触发时,把当前矩阵压入一个 std::stack osg::Matrix ,按 Ctrl+Z 时弹出并恢复。注意栈的深度不要超过 50,否则内存占用会很明显。
最后说一个我踩过的坑:OSG 的 MatrixTransform 在每帧 setMatrix 时会触发场景图的脏标记,如果场景里有大量节点,频繁 setMatrix 会导致遍历开销飙升。我的做法是只在 dirty 标志为 true 时 setMatrix,并且把多个变换合并成一个矩阵再写入。希望帮到你。
本文还有配套的精品资源,点击获取