简介:这是一份面向Qt与OpenSceneGraph(OSG)三维渲染开发者的整合示例工程,基于Github上口碑较好的OSGtree项目移植而来,已在VS2017搭配64位OSG环境下完成调试,可直接运行。工程几乎覆盖了OSG与Qt结合所需的各类功能点,适合正在做三维可视化、场景树管理或Qt界面嵌入OSG视口的开发者参考,只需按说明调整环境变量即可上手。压缩包共72个文件,约2.88MB,以cpp与h源码为主体,配合ui界面文件、qrc资源文件、vcxproj工程文件及pro工程文件,另含obj、tlog、pdb等编译中间产物,便于直接对照构建。内容涵盖相机控制、三维视图、场景树视图、模型项管理及主窗口等模块,结构完整,可作为二次开发的基础框架。目前已有2354人学习下载,适合希望快速搭建OSG+Qt开发环境、理解两者交互机制的初中级开发者借鉴。
1. OSG 嵌进 Qt 窗口:VS2017 64 位下这套组合到底能不能跑
如果你正在 Windows 上做三维可视化,大概率绕不开两个东西:OSG 负责渲染,Qt 负责界面。问题是这俩凑一块儿,网上教程要么是 32 位的老黄历,要么是 CMake 配一半报错,要么是编译过了运行黑屏。我手里这份资源就是冲着这个痛点来的——VS2017 加 64 位环境,OSG 和 Qt 的集成示例,目标是编译通过、窗口正常嵌入、鼠标交互能用。它适合谁?适合已经会写 Qt 界面、但第一次把 OSG 塞进 QWidget 的开发者,也适合之前用 32 位跑通、现在要迁到 64 位踩了一路坑的人。下面我按实际拆包和复现的顺序,把选型理由、编译步骤、参数配置和常见翻车点一次讲清楚。
2. 环境选型与依赖版本:为什么是 VS2017 + 64 位 + 这套 OSG/Qt
2.1 编译器、OSG 和 Qt 的版本匹配逻辑
OSG 在 Windows 上的二进制兼容性非常挑剔。VS2017 对应的是 MSVC 14.1 工具集,OSG 官方预编译包如果用的是 VS2015(MSVC 14.0)或者 VS2019(MSVC 14.2),链接阶段就会报LNK2019或LNK2038,提示_MSC_VER不匹配。所以第一件事不是急着编译,而是确认你手头的 OSG 库是不是用 VS2017 编的。常见做法是看 OSG 安装目录下bin文件夹里的osg161-osg.dll这类文件,用dumpbin /headers查一下运行时版本,或者直接看include/osg/Version里的宏定义。
Qt 这边,VS2017 64 位对应的是msvc2017_64套件。如果你装的是 Qt 5.12 或 5.15,安装器里要勾选这个套件。Qt 6 对 OSG 的兼容性目前还不算稳,很多 OSG 版本还在用QGLWidget,而 Qt 6 已经把它移到了兼容模块里,所以这份资源里用的是 Qt 5 系列。版本组合我建议锁死:VS2017 15.9、Qt 5.12.12 或 5.15.2、OSG 3.6.5 或 3.4.1,这三个版本之间的 ABI 冲突最少。
2.2 目录结构与环境变量配置
资源包解压后,我一般会整理成这样的目录结构,避免路径里出现中文和空格:
D:\Dev\ ├── OSG\ │ ├── include\ │ ├── lib\ │ └── bin\ ├── Qt\ │ └── 5.12.12\ │ └── msvc2017_64\ └── Project\ └── OsgQtDemo\环境变量里必须加两个:OSG_ROOT指向D:\Dev\OSG,QT_DIR指向D:\Dev\Qt\5.12.12\msvc2017_64。然后把%OSG_ROOT%\bin和%QT_DIR%\bin都塞进PATH。这一步不做,运行时会出现找不到 osg161-osg.dll或者Qt5Core.dll缺失。注意,PATH里如果有多个 Qt 版本,顺序很重要,64 位的要排在 32 位前面,否则 VS 调试时会加载错位的 DLL,现象是程序启动就崩,没有任何提示。
2.3 在 VS2017 里配置包含目录和库目录
打开项目属性页,配置选Debug|x64和Release|x64,不要选 Win32。在VC++ 目录里设置:
| 配置项 | 值 |
|---|---|
| 包含目录 | $(OSG_ROOT)\include;$(QT_DIR)\include;$(QT_DIR)\include\QtCore;$(QT_DIR)\include\QtGui;$(QT_DIR)\include\QtWidgets;$(QT_DIR)\include\QtOpenGL |
| 库目录 | $(OSG_ROOT)\lib;$(QT_DIR)\lib |
| 可执行文件目录 | $(OSG_ROOT)\bin;$(QT_DIR)\bin |
链接器输入里,Debug 配置加osgDBd.lib、osgViewerd.lib、osgQOpenGLd.lib(如果用的是 osgQOpenGL 方案),Release 配置去掉d后缀。Qt 的库用Qt5Cored.lib、Qt5Guid.lib、Qt5Widgetsd.lib、Qt5OpenGLd.lib。这里有个细节:OSG 的osgQOpenGL库不是官方核心库,是社区维护的,如果你资源包里没有这个库,那就得用QGLWidget的老方案,链接osgViewer.lib和osgGA.lib就行。
3. 把 OSG 渲染窗口嵌入 Qt:两种集成路径与代码实现
3.1 QGLWidget 继承方案:最稳但偏老
这是最经典的集成方式,原理是继承QGLWidget,在initializeGL、paintGL、resizeGL三个虚函数里桥接 OSG 的Viewer。代码结构大致如下:
// OsgWidget.h #include <QGLWidget> #include <osgViewer/Viewer> #include <osgDB/ReadFile> class OsgWidget : public QGLWidget { Q_OBJECT public: explicit OsgWidget(QWidget *parent = nullptr); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; private: osg::ref_ptr<osgViewer::Viewer> m_viewer; osg::ref_ptr<osg::Group> m_root; };// OsgWidget.cpp #include "OsgWidget.h" #include <osgGA/TrackballManipulator> OsgWidget::OsgWidget(QWidget *parent) : QGLWidget(parent) { // 设置 OpenGL 格式,必须带深度缓冲,否则模型会闪烁 QGLFormat fmt; fmt.setDepthBufferSize(24); fmt.setDoubleBuffer(true); setFormat(fmt); m_viewer = new osgViewer::Viewer; m_root = new osg::Group; // 加载一个模型,路径按实际资源改 osg::Node* model = osgDB::readNodeFile("cow.osg"); if (model) m_root->addChild(model); m_viewer->setSceneData(m_root); m_viewer->setCameraManipulator(new osgGA::TrackballManipulator); } void OsgWidget::initializeGL() { // 把 Qt 创建的 OpenGL 上下文交给 OSG 管理 m_viewer->setUpViewerAsEmbeddedInWindow(0, 0, width(), height()); m_viewer->realize(); } void OsgWidget::paintGL() { m_viewer->frame(); } void OsgWidget::resizeGL(int w, int h) { // 通知 OSG 视口变化,参数是像素单位 m_viewer->getCamera()->setViewport(0, 0, w, h); }这段代码的逻辑说明:setUpViewerAsEmbeddedInWindow是关键,它告诉 OSG 不要自己创建窗口,而是复用 Qt 已经建好的 OpenGL 上下文。frame()每帧调用一次,驱动渲染循环。参数方面,setDepthBufferSize(24)必须设,不设的话深度测试失效,模型面片会乱序。resizeGL里的setViewport参数是(x, y, width, height),单位是像素,别传成逻辑坐标。
3.2 osgQOpenGL 方案:更现代但依赖额外库
如果你资源包里带了osgQOpenGL库,那可以用osgQOpenGLWidget,它内部已经处理好了上下文共享和事件转发。用法比继承QGLWidget简单:
#include <osgQOpenGL/osgQOpenGLWidget> #include <osgDB/ReadFile> osgQOpenGLWidget* osgWidget = new osgQOpenGLWidget(this); osgWidget->setCameraManipulator(new osgGA::TrackballManipulator); osgWidget->setSceneData(osgDB::readNodeFile("cow.osg")); // 直接放进布局 ui->verticalLayout->addWidget(osgWidget);这个方案的优点是鼠标事件、键盘事件自动转发给 OSG 的CameraManipulator,不用自己写mousePressEvent转发。缺点是osgQOpenGL库需要单独编译,而且版本要和 OSG 主库对齐。我一般会先试这个方案,如果链接报错找不到osgQOpenGL符号,再退回QGLWidget方案。
3.3 主窗口组装与事件转发
不管用哪种方案,主窗口里要把 OSG 窗口和 Qt 控件拼在一起。下面是一个最小可运行的主窗口:
// MainWindow.cpp #include "MainWindow.h" #include "ui_MainWindow.h" #include "OsgWidget.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); OsgWidget* osgWidget = new OsgWidget(this); // 把 OSG 窗口设为中央控件,铺满整个客户区 setCentralWidget(osgWidget); // 如果要在工具栏加按钮,可以在这里 connect connect(ui->actionReset, &QAction::triggered, [=](){ // 重置视角的常见做法 osgWidget->resetView(); }); }事件转发这块,QGLWidget方案下,鼠标滚轮缩放默认是不生效的,因为QGLWidget不会自动把wheelEvent传给 OSG。我一般会重写wheelEvent:
void OsgWidget::wheelEvent(QWheelEvent* event) { // 把 Qt 滚轮事件转成 OSG 的鼠标事件 osgGA::GUIEventAdapter* ea = m_viewer->getEventQueue()->getCurrentEventState(); ea->setScrollingDelta(event->angleDelta().y()); m_viewer->getEventQueue()->mouseScroll( event->angleDelta().y() > 0 ? osgGA::GUIEventAdapter::SCROLL_UP : osgGA::GUIEventAdapter::SCROLL_DOWN); event->accept(); }参数说明:angleDelta().y()是滚轮增量,正数向上、负数向下。mouseScroll接受的是枚举值,不能直接传整数。这段不写,用户滚轮就没反应,体验上直接扣分。
4. 编译链接与运行时排查:那些让你怀疑人生的坑
4.1 链接报错 LNK2019 和 LNK2038 的定位方法
LNK2019是找不到符号,九成原因是库目录没配对,或者 Debug 配置链接了 Release 库。检查方法:在 VS 输出窗口里看它到底在找哪个.lib,然后去$(OSG_ROOT)\lib下确认文件名是否带d后缀。LNK2038是运行时库不匹配,报错信息里会写_MSC_VER或_ITERATOR_DEBUG_LEVEL不一致。解决办法是把项目属性里C/C++ -> 代码生成 -> 运行时库改成多线程调试 DLL (/MDd)对应 Debug,多线程 DLL (/MD)对应 Release。OSG 预编译包一般用的是/MD系列,如果你项目设成/MT,必炸。
4.2 运行时黑屏、闪退和 DLL 缺失
黑屏最常见的原因是 OpenGL 上下文没建好。现象是窗口出来了,但里面全黑,鼠标拖动也没反应。排查步骤:第一,确认initializeGL里调用了realize();第二,确认paintGL里调用了frame();第三,用osg::notify(osg::INFO)打开 OSG 日志,看有没有Warning: detected OpenGL error之类的输出。闪退的话,大概率是 DLL 加载顺序问题。用Dependencies工具查一下 exe 依赖的 DLL,看有没有 32 位的混进来。我遇到过Qt5Core.dll加载了 32 位版本,程序启动直接0xc000007b,把PATH里 32 位 Qt 路径删掉就好了。
4.3 鼠标交互失效与视口错位
鼠标交互失效分两种情况:一种是完全没反应,一种是拖拽方向反了。完全没反应通常是事件没转发,检查有没有重写mousePressEvent、mouseMoveEvent、mouseReleaseEvent并调用m_viewer->getEventQueue()->mouseButtonPress等方法。方向反了是坐标系问题,OSG 默认 Y 轴向上,Qt 的鼠标坐标 Y 轴向下,需要在转发时做height() - event->y()的转换。视口错位表现为模型渲染在窗口左上角一小块区域,原因是resizeGL里没更新setViewport,或者setUpViewerAsEmbeddedInWindow的宽高传的是初始值而不是实时值。
4.4 64 位下指针截断与类型转换警告
64 位迁移最容易忽略的是int和指针之间的转换。OSG 里有些老代码用int存指针,32 位下没事,64 位下高位被截断,运行时直接访问非法地址。编译时如果看到C4312或C4244警告,不要忽略,逐个检查。常见的是osg::Node*被强转成int再转回来,这种代码在 64 位下必崩。解决办法是用intptr_t或size_t替代int做指针运算。另外,Qt 的connect信号槽如果参数类型不匹配,64 位下也可能静默失败,建议用QOverload明确指定重载版本。
5. 进阶技巧:让嵌入窗口更稳、更好用的几个习惯
5.1 用 QTimer 驱动渲染循环而不是死循环
QGLWidget方案下,如果你在paintGL里写while(true) frame(),界面会卡死。正确做法是用QTimer定时触发update():
QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [=](){ update(); // 触发 paintGL }); timer->start(16); // 约 60 FPS参数16是毫秒,对应 60 帧。如果你的场景复杂,可以降到33(30 帧)。这样 Qt 的事件循环不会被阻塞,窗口拖动、按钮点击都正常。
5.2 多视图共享 OSG 场景数据
有时候一个窗口里要放两个 OSG 视图,比如一个主视图一个俯视图。这时候不要建两个Viewer,而是用一个Viewer加两个Camera:
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer; viewer->setSceneData(root); // 主相机 osg::Camera* mainCam = viewer->getCamera(); mainCam->setViewport(0, 0, w/2, h); // 第二个相机 osg::ref_ptr<osg::Camera> secondCam = new osg::Camera; secondCam->setGraphicsContext(mainCam->getGraphicsContext()); secondCam->setViewport(w/2, 0, w/2, h); secondCam->setProjectionMatrixAsPerspective(30.0, 1.0, 1.0, 1000.0); viewer->addSlave(secondCam);这样两个视图共享同一个场景图,内存不翻倍,而且模型数据只加载一次。注意setGraphicsContext要指向同一个上下文,否则第二个视图会黑屏。
5.3 资源释放与退出时的清理顺序
程序退出时,如果先析构Viewer再析构QGLWidget,会报 OpenGL 上下文无效的警告。我一般会在MainWindow的closeEvent里手动停掉定时器,然后调用m_viewer->setDone(true),等frame()返回后再让 Qt 销毁窗口。顺序错了的话,退出时可能弹一个0xC0000005访问违例。从那以后我每次做 OSG 嵌入 Qt 的项目,都会在closeEvent里强制走一遍「停定时器 → setDone → 等一帧 → 再关窗口」的流程,这个习惯帮我省了很多半夜查崩溃的功夫。希望帮到你。
本文还有配套的精品资源,点击获取