ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

在QT中嵌入OSG三维场景:VS2017 64位环境配置与实现

在QT中嵌入OSG三维场景:VS2017 64位环境配置与实现 简介面向在VS2017环境下将OSG三维渲染与Qt界面框架进行集成的开发者这套可直接运行的OSGtree示例工程提供了清晰的参考实现。项目覆盖OSG嵌入Qt所需的常用交互模块包括场景相机控制、树形场景节点管理、模型视图联动等并实测于64位OSG调试环境可顺利运行通常只需调整环境变量即可迁移到本机配置。压缩包共72个文件含17个cpp源码、14个h头文件、5个ui界面文件以及qrc、pro、vcxproj等工程配置另附部分编译中间产物整体仅2.88MB。目录按源码、界面定义和生成文件归类便于对照工程结构快速定位。目前已有2347人学习该资源适合希望理解OSG与Qt集成思路、参考完整工程组织方式的初中级开发者。 如果你正在做桌面级三维可视化项目手里已经有一套QT程序框架想往里嵌入OpenSceneGraphOSG渲染的三维场景那么“VS201764位”这个组合应该不陌生。网上能找到的旧资料大多还在讲怎样把OSG窗口当作一个独立窗口再塞进QT控件里照着做十有八九会遇到黑屏、崩溃、窗口无法交互。我前一阵把OSG 3.6.5和QT 5.15.2在VS2017 64位环境下完整跑通场景绘制、鼠标交互、窗口缩放、打包发布都没有问题效果就是干净的QT界面里内嵌一个三维视口拖拽流畅部署到别的机器也能跑。这里把整个思路和踩过的坑都记录下来给准备做三维可视化应用的朋友一份可以直接上手的参考。1. VS201764位QT版本先把环境的底子打对1.1 版本组合为什么这么讲究很多人一上来就在VS2017里新建QT项目然后下载一个QT最新版结果编译一堆错误。问题多半出在版本匹配上。VS2017对应的编译器工具集是v141QT官方安装包里的编译版本分得非常细有msvc2015、msvc2017、msvc2019、msvc2022这些目录还有MinGW系列。这里必须选带msvc2017_64字样的QT组件不要选MinGW版本。MinGW用的是另一套GCC编译链C ABI和MSVC不兼容头文件能include但链接时各种符号对不上纯属给自己挖坑。OSG这边我选用3.6.5这个版本和QT 5.15.2搭配比较稳。如果你用更新版的QT 6系列OSG官方很多老接口已经不适用重新适配的成本会高很多。VS2017配QT 5.15.2配OSG 3.6.5是我实测后确认比较省心的一套组合。打个比方C库之间的兼容性就像方言VS2017编译出来的库和VS2019、VS2022编译出来的库函数名可能一样但底层调用约定、标准库实现细节都有差别混用轻则链接告警重则运行直接崩溃。所以最省事的做法是QT、OSG、OpenThreads这几个核心库全部统一用VS2017 64位编译。1.2 64位系统下的依赖检索差异这个坑非常隐蔽。程序的DLL搜索顺序32位和64位在Windows上有差异。64位程序会去System32找DLL32位程序在64位系统上会重定向到SysWOW64目录找DLL。同样的开发库如果你机器上同时装过32位和64位版本稍不留神动态加载就会拿错货。我建议从一开始就把目录结构理顺单独建一个D:\libs里面专门放64位的OSG编译产物和QT库目录名区分清楚。在系统环境变量PATH里只保留64位版本的bin目录32位目录一律不加入。改完环境变量之后务必重启VS2017因为VS启动时会把当时的环境变量读进内存不重启就会继续用旧PATH这可能直接导致莫名其妙的DLL加载错误。1.3 开发库的准备清单要把环境搭齐需要准备这些部件组件推荐版本备注操作系统Windows 10/11 64位Win7 64位也能跑但建议打齐系统补丁IDEVS2017 15.9.xv141工具集建议安装“VC 2017 工具集”和“Windows 10 SDK”QTQT 5.15.2安装时勾选msvc2017 64-bit组件OSG3.6.5建议用CMake从源码编译或找对应的预编译包CMake3.16以上用于编译OSGQt VS Tools对应VS2017版本便于在VS里直接创建、编译QT工程OSG库的获取方式有两种一是下载别人编译好的预编译包二是自己下载源码用CMake编译。我建议自己编译一次虽然耗时但你能确定编译器版本、第三方依赖、Debug/Release配置都是统一的。OSG 3.6.5源码编译时原则上只需要依赖OpenThreads和少量系统库流程不算复杂。CMake配置里注意把BUILD_OSG_EXAMPLES设为OFF可以省不少编译时间但要把BUILD_OSG_QT设为ON这样会编译出osgQt库后面可能用得上。2. 为什么我放弃了GraphicsWindowQt和winId嵌入2.1 旧方案到底差在哪里早期做OSG和QT结合最常见的方法有两种一种是用osgViewer::Viewer创建独立窗口然后通过createWindowContainer或者winId()的方式把OSG窗口嵌入QT控件另一种是使用osgQt库里的GraphicsWindowQt类让OSG直接渲染到QT窗口上。这两个方案我都试过体验都不好。第一种方案在窗口嵌入时经常出现尺寸变化、重绘闪烁的问题尤其在高DPI显示器上嵌入区域的坐标会和OSG内部坐标错位鼠标点击位置明显偏移。第二种方案在QT 5.15下需要单独编译osgQt库而且这套老接口的新增维护基本处于停滞状态一旦遇到QT版本升级就很被动。更重要的是这两种方案的事件循环和渲染循环相互纠缠OSG内部有自己的帧循环QT有事件循环两者在窗口拖拽、最小化、弹出菜单时容易互相阻塞表现就是界面卡顿或者直接崩溃。2.2 我现在采用的整体结构最终我放弃所有旧方案转而使用QOpenGLWidget结合osgViewer::GraphicsWindowEmbedded这个组合。原理其实不复杂。QOpenGLWidget本身就提供了一个完整的OpenGL渲染表面而且被QT纳入控件体系管理所有鼠标、键盘、尺寸变化事件都由QT正常传递。我们要做的是让OSG不再创建自己的渲染窗口而是把渲染目标指向QOpenGLWidget当前的这个OpenGL上下文。GraphicsWindowEmbedded就是OSG专门为这类嵌入式场景准备的图形窗口实现它不创建原生窗口只负责在外部提供的OpenGL上下文上完成绘制。每次QT触发paintGL时我们就调用一次viewer-frame()OSG在这个调用内完成场景遍历、裁剪、绘制然后在QOpenGLWidget表面上呈现。这种结构相比旧方案有一个显著优势渲染和界面事件全部跑在QT主线程不需要自己锁互斥量也不存在跨线程更新资源的问题。2.3 线程模型为何必须锁定SingleThreadedOSG的Viewer支持多种线程模型默认可能是CullDrawThreadPerContext之类的多线程模式。但在QOpenGLWidget里渲染必须在QT安排好的OpenGL上下文中执行不能脱离QT的控制去另开线程。所以我强制在初始化时调用了m_viewer-setThreadingModel(osgViewer::ViewerBase::SingleThreaded);这是整个集成里最关键的一行代码。不设置这行OSG会在内部启动自己的渲染线程和QT的主线程抢OpenGL上下文轻则画面撕裂重则程序秒崩。单线程模式下OSG每一帧都在paintGL里同步执行虽然理论上有一定性能损耗但对于大多数三维可视化应用来说完全够用。3. 最小可运行Demo把OSG场景装进QT窗口3.1 工程骨架我用VS2017配合Qt VS Tools打开一个.pro工程文件文件树相当简洁OSGQTExample/ ├── OSGQTOpenGLWidget.h ├── OSGQTOpenGLWidget.cpp ├── main.cpp └── OSGQTExample.pro.pro文件里需要做几处关键声明保证能找到头文件和库QT core gui widgets opengl TEMPLATE app TARGET OSGQTExample CONFIG c11 INCLUDEPATH D:/libs/OSG-3.6.5/include \ D:/Qt/Qt5.15.2/5.15.2/msvc2017_64/include LIBS -LD:/libs/OSG-3.6.5/lib \ -losgViewer -losgDB -losgUtil -losgGA -losg -lOpenThreads LIBS -LD:/Qt/Qt5.15.2/5.15.2/msvc2017_64/lib \ -lQt5Widgets -lQt5Gui -lQt5Core -lQt5OpenGL3.2 核心实现代码自定义的OSGQTOpenGLWidget类继承自QOpenGLWidget。头文件声明如下#ifndef OSGQTOPENGLWIDGET_H #define OSGQTOPENGLWIDGET_H #include QOpenGLWidget #include QOpenGLFunctions #include osgViewer/Viewer #include osgViewer/GraphicsWindowEmbedded class OSGQTOpenGLWidget : public QOpenGLWidget { Q_OBJECT public: explicit OSGQTOpenGLWidget(QWidget* parent nullptr); virtual ~OSGQTOpenGLWidget(); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; private: osg::ref_ptrosgViewer::Viewer m_viewer; osg::ref_ptrosgViewer::GraphicsWindowEmbedded m_graphicsWindow; }; #endif实现在构造函数里不直接创建OSG相关对象等initializeGL里再初始化#include OSGQTOpenGLWidget.h #include osgGA/TrackballManipulator #include osgDB/ReadFile #include osg/Camera #include osg/Material #include QMouseEvent #include QWheelEvent OSGQTOpenGLWidget::OSGQTOpenGLWidget(QWidget* parent) : QOpenGLWidget(parent) { setFocusPolicy(Qt::StrongFocus); } OSGQTOpenGLWidget::~OSGQTOpenGLWidget() { } void OSGQTOpenGLWidget::initializeGL() { m_graphicsWindow new osgViewer::GraphicsWindowEmbedded(0, 0, width(), height()); m_viewer new osgViewer::Viewer; m_viewer-setThreadingModel(osgViewer::ViewerBase::SingleThreaded); osg::Camera* camera m_viewer-getCamera(); camera-setGraphicsContext(m_graphicsWindow.get()); camera-setViewport(new osg::Viewport(0, 0, width(), height())); camera-setProjectionMatrixAsPerspective(45.0, static_castdouble(width()) / static_castdouble(height()), 0.1, 1000.0); osg::ref_ptrosg::Group root new osg::Group; osg::ref_ptrosg::Node model osgDB::readNodeFile(cow.osg); if (model.valid()) root-addChild(model.get()); else root-addChild(createGrid()); m_viewer-setSceneData(root.get()); m_viewer-setCameraManipulator(new osgGA::TrackballManipulator); m_viewer-realize(); }如果当前路径下没有cow.osg程序也不会直接黑屏可以调用一个简单函数生成地面网格加坐标轴作为验证场景osg::Node* OSGQTOpenGLWidget::createGrid() { osg::ref_ptrosg::Geode geode new osg::Geode; osg::ref_ptrosg::Geometry geom new osg::Geometry; const float size 10.0f; osg::ref_ptrosg::Vec3Array vertices new osg::Vec3Array; // 生成网格顶点 for (int i -10; i 10; i) { vertices-push_back(osg::Vec3(-size, i * size / 10.0f, 0.0f)); vertices-push_back(osg::Vec3(size, i * size / 10.0f, 0.0f)); vertices-push_back(osg::Vec3(i * size / 10.0f, -size, 0.0f)); vertices-push_back(osg::Vec3(i * size / 10.0f, size, 0.0f)); } geom-setVertexArray(vertices.get()); geom-addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, vertices-size())); osg::ref_ptrosg::Vec4Array colors new osg::Vec4Array; colors-push_back(osg::Vec4(0.6f, 0.6f, 0.6f, 1.0f)); geom-setColorArray(colors.get(), osg::Array::BIND_OVERALL); geode-addDrawable(geom.get()); return geode.release(); }然后重写三个GL生命周期函数和事件函数void OSGQTOpenGLWidget::paintGL() { makeCurrent(); m_viewer-frame(); } void OSGQTOpenGLWidget::resizeGL(int w, int h) { makeCurrent(); m_graphicsWindow-resized(0, 0, w, h); m_viewer-getCamera()-setViewport(0, 0, w, h); m_viewer-getCamera()-setProjectionMatrixAsPerspective(45.0, static_castdouble(w) / static_castdouble(h), 0.1, 1000.0); update(); }鼠标事件需要把QT事件转成OSG的GUIEventAdapter。绝大部分人容易漏掉的是Y轴翻转QT窗口坐标原点在左上角Y轴向下而OSG窗口坐标原点在左下角Y轴向上所以传递前要做height() - y()转换。没有这个转换旋转视角的鼠标操作会上下颠倒特别别扭。void OSGQTOpenGLWidget::mousePressEvent(QMouseEvent* event) { if (!m_graphicsWindow.valid()) return; unsigned int buttonMask 0; if (event-button() Qt::LeftButton) buttonMask | osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON; if (event-button() Qt::MidButton) buttonMask | osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON; if (event-button() Qt::RightButton) buttonMask | osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON; m_graphicsWindow-getEventQueue()-mouseButtonPress( event-x(), height() - event-y(), buttonMask); } void OSGQTOpenGLWidget::mouseMoveEvent(QMouseEvent* event) { if (!m_graphicsWindow.valid()) return; m_graphicsWindow-getEventQueue()-mouseMotion( event-x(), height() - event-y()); } void OSGQTOpenGLWidget::mouseReleaseEvent(QMouseEvent* event) { if (!m_graphicsWindow.valid()) return; unsigned int buttonMask 0; if (event-button() Qt::LeftButton) buttonMask | osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON; if (event-button() Qt::MidButton) buttonMask | osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON; if (event-button() Qt::RightButton) buttonMask | osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON; m_graphicsWindow-getEventQueue()-mouseButtonRelease( event-x(), height() - event-y(), buttonMask); } void OSGQTOpenGLWidget::wheelEvent(QWheelEvent* event) { if (!m_graphicsWindow.valid()) return; if (event-angleDelta().y() 0) m_graphicsWindow-getEventQueue()-mouseScroll( osgGA::GUIEventAdapter::SCROLL_UP); else m_graphicsWindow-getEventQueue()-mouseScroll( osgGA::GUIEventAdapter::SCROLL_DOWN); }main.cpp就更直白了#include QApplication #include QMainWindow #include OSGQTOpenGLWidget.h int main(int argc, char* argv[]) { QApplication app(argc, argv); QMainWindow mainWindow; mainWindow.setWindowTitle(OSG QT VS2017 x64); mainWindow.resize(1024, 768); OSGQTOpenGLWidget* osgWidget new OSGQTOpenGLWidget(mainWindow); mainWindow.setCentralWidget(osgWidget); mainWindow.show(); return app.exec(); }3.3 交互事件处理的关键细节鼠标事件如果直接抄网上老代码经常见到直接把event-x()传给OSG这在普通窗口下可能问题不大一旦窗口有工具栏、菜单栏或左边有停靠面板坐标偏移就很明显。我建议不依赖任何全局坐标QOpenGLWidget收到的鼠标事件坐标本身就是相对于控件的本地坐标直接把x和height()-y传过去就行。滚轮事件在QT 5.15里QWheelEvent::angleDelta()是推荐的读取方式delta()旧接口已经废弃。另外OSG的mouseScroll只接受方向枚举不接收距离如果想实现高精度缩放需要自己做累积逻辑不过在可视化场景里每滚一格按步进缩放一次体验反而更顺手。resizeGL里调用update()看起来有点多余但实际很有用。QT在窗口尺寸变化时会对QOpenGLWidget做重绘调度但OSG内部相机的视口尺寸需要我们自己更新显式触发不加update()偶发情况会出现拉伸变形区域后才恢复。4. 从编译通过到正常显示工程配置和部署4.1 VS2017工程配置清单很多人在VS2017里建好QT工程后编译报一堆“无法打开包括文件”或“无法解析的外部符号”基本就是配置没填对。下面按VS2017的工程属性逐项说明配置项位置示例值包含目录常规 - VC 目录 - 包含目录D:\libs\OSG-3.6.5\include;D:\Qt\Qt5.15.2\5.15.2\msvc2017_64\include库目录常规 - VC 目录 - 库目录D:\libs\OSG-3.6.5\lib;D:\Qt\Qt5.15.2\5.15.2\msvc2017_64\lib附加依赖项Release链接器 - 输入 - 附加依赖项osgViewer.lib;osgDB.lib;osgUtil.lib;osgGA.lib;osg.lib;OpenThreads.lib;Qt5Widgets.lib;Qt5Gui.lib;Qt5Core.lib;Qt5OpenGL.lib附加依赖项Debug链接器 - 输入 - 附加依赖项在Release基础上所有OSG库名加字母d如osgViewerd.lib;osgDBd.lib运行库Release/DebugC/C - 代码生成 - 运行库Release用/MDDebug用/MDd这里有个细节容易被忽略如果用的OSG库是动态编译Debug和Release的库名后缀规则是固定的osgViewerd.lib对应Debug版不带d的对应Release版。两个必须严格对应不能贪省事在Debug配置里也用不带d的库否则链接能过运行到场景加载或绘制时大概率崩溃。4.2 Debug与Release不一致导致的崩溃我遇到过一种特别隐蔽的情况工程本身是Debug配置链接了始终是我的第三方库的Release版本编译可以过但点击菜单加载大模型时程序直接闪退。根本原因是Debug和Release使用了不同的堆管理器和C运行库跨版本传递STL容器或者osg::ref_ptr会在内部的引用计数操作上产生不可预知的破坏。所以一个基本纪律是QT选Debug版OSG库也选Debug版QT选Release版OSG库也选Release版。Qt5Cored.dll和Qt5Core.dll目录名差异很大一眼能分辨OSG库就得靠文件名上那个d确认。4.3 用windeployqt和批处理脚本完成发布程序在开发机上能跑并不代表拷到别的机器也能跑。QT依赖的插件和运行库比较多需要用windeployqt工具自动部署。在VS2017的QT目录里找到D:\Qt\Qt5.15.2\5.15.2\msvc2017_64\bin\windeployqt.exe在编译产物的Release目录执行set QT_BIND:\Qt\Qt5.15.2\5.15.2\msvc2017_64\bin set OSG_BIND:\libs\OSG-3.6.5\bin set DEPLOY.\release %QT_BIN%\windeployqt.exe %DEPLOY%\OSGQTExample.exe --release --no-compiler-runtime copy %OSG_BIN%\osgViewer.dll %DEPLOY%\ copy %OSG_BIN%\osgDB.dll %DEPLOY%\ copy %OSG_BIN%\osgUtil.dll %DEPLOY%\ copy %OSG_BIN%\osgGA.dll %DEPLOY%\ copy %OSG_BIN%\osg.dll %DEPLOY%\ copy %OSG_BIN%\osgText.dll %DEPLOY%\ copy %OSG_BIN%\OpenThreads.dll %DEPLOY%\windeployqt只能自动识别QT自身的依赖识别不了OSG的DLL所以OSG的十几个动态库需要自己手动复制。如果你的项目还用了osgQt或其他OSG插件库记得一并拷贝。--no-compiler-runtime的意思是让windeployqt不要拷贝VC运行库这些运行库用VS2017自带的“安装Redistributable”方式更稳妥。5. 高频报错的定位思路与修复5.1 平台插件初始化失败运行程序时最常见的一条报错是qt.qpa.plugin: Could not find the Qt platform plugin windows in This application failed to start because no Qt platform plugin could be initialized.这条报错的原因是程序找不到qwindows.dll它必须存在于可执行文件旁边的plugins\platforms目录下。用windeployqt部署后会生成正确结构但如果直接把exe单独拷走这个插件目录就丢了。临时解决时可以在环境变量里增加QT_QPA_PLATFORM_PLUGIN_PATHD:\Qt\Qt5.15.2\5.15.2\msvc2017_64\plugins\platforms正式发布时一定不要依赖环境变量保证plugins目录和exe的相对位置正确。双击exe验证时优先看这个目录是不是存在且包含qwindows.dll。5.2 找不到OSG DLL和0xc000007b如果启动时提示“找不到osgViewer.dll”或“无法定位程序输入点”多半是系统PATH里没有OSG的bin目录或者OSG的DLL版本和QT不一致。这时候不要急着改代码先用dumpbin查一下目标DLL的架构。在VS2017的开发人员命令行里执行dumpbin /headers D:\libs\OSG-3.6.5\bin\osgViewer.dll | findstr machine如果输出显示8664是x64版本如果显示14C是x86版本。32位DLL被64位程序加载时会报0xc000007b错误这个错误码本身没有明确提示很容易让人摸不着头脑。排查思路很简单确认所有涉及的DLL都是64位确认程序是x64配置确认QT的msvc2017_64目录下的DLL被加载而不是其他目录。5.3 窗口有控件但画面黑屏QOpenGLWidget能正常显示OSG也加载了但场景区域一片黑色。优先检查三件事第一场景根节点是否非空osgDB::readNodeFile返回的节点是否为NULL文件路径是否正确第二相机是否设置了GraphicsContext和Viewport第三paintGL里是否真的调用了viewer-frame()。还有一点容易被忽略不要将QOpenGLWidget放到不显示的区域去初始化比如在窗口还没show()时width()和height()可能是默认值100这样一来相机的宽高比就错了。我建议把场景数据、相机参数放在initializeGL中并且保证此时控件已经有一个合理尺寸。5.4 交互或窗口缩放时崩溃如果场景能显示但鼠标一拖动或者缩放窗口就崩90%是线程模型没有设置为SingleThreaded。OSG默认的CullDrawThreadPerContext会启用内部渲染线程和QT主线程的OpenGL上下文发生竞争崩溃是必然结果。检查一下initializeGL里是否有m_viewer-setThreadingModel(osgViewer::ViewerBase::SingleThreaded);另外在resizeGL里调用m_graphicsWindow-resized之后一定要调用m_viewer-getCamera()-setViewport重新设置视口否则相机内部状态和实际窗口尺寸不一致拖到某个临界尺寸时也会崩。5.5 不响应键盘事件键盘事件也是可视化中常用到的交互方式尤其是相机操作器需要用到空格键和R键重置视角。QOpenGLWidget默认可能不接收键盘焦点需要在构造函数里设置setFocusPolicy(Qt::StrongFocus);并在键盘事件里把按键转发给OSG事件队列void OSGQTOpenGLWidget::keyPressEvent(QKeyEvent* event) { if (m_graphicsWindow.valid()) { m_graphicsWindow-getEventQueue()-keyPress(event-key()); } QOpenGLWidget::keyPressEvent(event); }不设焦点策略的话主界面的按键焦点会停留在某个按钮或输入框上OSG相机操作器完全不响应键盘这个问题排查起来也很耗时间。最后聊点个人的实操感受。这套OSGQTVS2017的组合真正的门槛不在代码逻辑而在各库版本之间的一致性。我踩过最大的坑就是混用了不同编译器生成的库表现出的问题千奇百怪但根因永远是版本不匹配。我的习惯是每次配置完环境把包含目录、库目录、附加依赖项、DLL清单全部写成一个配置文件存到工程里下次新项目直接导入使用。跑通Demo之后先用简单场景验证鼠标交互和缩放再逐步加载复杂模型定位问题会快很多。这套嵌入式渲染逻辑也可以平移到QT 5.12、QT 5.9等版本核心代码改动非常小值得长期保留。本文还有配套的精品资源点击获取
返回列表