在QT中嵌入OSG三维场景:VS2017 64位环境配置与实现
2026/9/9 17:58:27 网站建设 项目流程

简介:面向在VS2017环境下将OSG三维渲染与Qt界面框架进行集成的开发者,这套可直接运行的OSGtree示例工程提供了清晰的参考实现。项目覆盖OSG嵌入Qt所需的常用交互模块,包括场景相机控制、树形场景节点管理、模型视图联动等,并实测于64位OSG调试环境可顺利运行,通常只需调整环境变量即可迁移到本机配置。压缩包共72个文件,含17个cpp源码、14个h头文件、5个ui界面文件以及qrc、pro、vcxproj等工程配置,另附部分编译中间产物,整体仅2.88MB。目录按源码、界面定义和生成文件归类,便于对照工程结构快速定位。目前已有2347人学习该资源,适合希望理解OSG与Qt集成思路、参考完整工程组织方式的初中级开发者。 如果你正在做桌面级三维可视化项目,手里已经有一套QT程序框架,想往里嵌入OpenSceneGraph(OSG)渲染的三维场景,那么“VS2017+64位”这个组合应该不陌生。网上能找到的旧资料,大多还在讲怎样把OSG窗口当作一个独立窗口再塞进QT控件里,照着做十有八九会遇到黑屏、崩溃、窗口无法交互。我前一阵把OSG 3.6.5和QT 5.15.2在VS2017 64位环境下完整跑通,场景绘制、鼠标交互、窗口缩放、打包发布都没有问题,效果就是干净的QT界面里内嵌一个三维视口,拖拽流畅,部署到别的机器也能跑。这里把整个思路和踩过的坑都记录下来,给准备做三维可视化应用的朋友一份可以直接上手的参考。

1. VS2017+64位+QT版本:先把环境的底子打对

1.1 版本组合为什么这么讲究

很多人一上来就在VS2017里新建QT项目,然后下载一个QT最新版,结果编译一堆错误。问题多半出在版本匹配上。VS2017对应的编译器工具集是v141,QT官方安装包里的编译版本分得非常细,有msvc2015msvc2017msvc2019msvc2022这些目录,还有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找DLL,32位程序在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以上用于编译OSG
Qt 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 线程模型为何必须锁定SingleThreaded

OSG的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 += c++11 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 -lQt5OpenGL

3.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_ptr<osgViewer::Viewer> m_viewer; osg::ref_ptr<osgViewer::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_cast<double>(width()) / static_cast<double>(height()), 0.1, 1000.0); osg::ref_ptr<osg::Group> root = new osg::Group; osg::ref_ptr<osg::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_ptr<osg::Geode> geode = new osg::Geode; osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; const float size = 10.0f; osg::ref_ptr<osg::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_ptr<osg::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_cast<double>(w) / static_cast<double>(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收到的鼠标事件坐标本身就是相对于控件的本地坐标,直接把xheight()-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/Debug)C/C++ -> 代码生成 -> 运行库Release用/MD,Debug用/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.dllQt5Core.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_BIN=D:\Qt\Qt5.15.2\5.15.2\msvc2017_64\bin set OSG_BIN=D:\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_PATH=D:\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,文件路径是否正确;第二,相机是否设置了GraphicsContextViewport;第三,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相机操作器完全不响应键盘,这个问题排查起来也很耗时间。


最后聊点个人的实操感受。这套OSG+QT+VS2017的组合,真正的门槛不在代码逻辑,而在各库版本之间的一致性。我踩过最大的坑就是混用了不同编译器生成的库,表现出的问题千奇百怪,但根因永远是版本不匹配。我的习惯是每次配置完环境,把包含目录、库目录、附加依赖项、DLL清单全部写成一个配置文件存到工程里,下次新项目直接导入使用。跑通Demo之后,先用简单场景验证鼠标交互和缩放,再逐步加载复杂模型,定位问题会快很多。这套嵌入式渲染逻辑也可以平移到QT 5.12、QT 5.9等版本,核心代码改动非常小,值得长期保留。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询