☰
基于QOpenGLWidget的Qt 3D模型加载与PBR渲染框架实现
2026/10/20 19:59:11 网站建设 项目流程

简介:这是一份演示Qt结合OpenGL加载3D模型原理的工程代码,适合有一定C++基础、希望了解3D模型解析与渲染流程的Qt开发者。代码未提供exe程序,核心逻辑集中在model.h与model.cpp两个文件中,重点展示如何读取模型数据并使用OpenGL绘制,可帮助读者快速把握基本框架。压缩包共11个文件,包含3个cpp与3个h源码文件、1个3ds格式示例模型、1个qt3dtest.ui界面文件及工程配置等,整体约62KB,轻量便于对照阅读。已有3406人学习过。需要留意的是,源码中的解析算法并非完整覆盖所有3DS格式字符,仅处理了v、vn、f、#等常见项,若你的模型包含其他关键字,还需自行扩展解析逻辑。总体而言,这套代码适合用作Qt+OpenGL加载3D模型的入门参考,借此理解基本实现思路后,再针对自身模型补充完善。

1. Qt加载3D模型:从QOpenGLWidget到PBR材质,一个能直接落地的渲染框架

很多人在Qt里加载3D模型,第一反应是去翻QML里的Scene3D或Qt3D模块的教程,结果照着抄完,发现模型是转起来了,但一换到自家美术出的带法线贴图和金属度的glTF文件,就变成一片死黑或者惨白。这个项目解决的正是这个问题:它没有绕开OpenGL,而是直接用QOpenGLWidget搭了一条从文件解析到PBR渲染的完整管线,支持OBJ、glTF两种格式,自带相机漫游、光照调节、模型树形结构查看。适合谁?适合那些需要在Qt桌面应用里嵌入3D预览、但又不想被Qt3D的高层封装牵着走的开发者;也适合想弄明白模型文件里的顶点、法线、UV、材质参数到底是怎么一步步变成屏幕上像素的人。整个工程拆得很干净,渲染核心和UI解耦,换到别的Qt版本或自家业务里,改改就能用。

2. 项目架构与目录结构:模块拆分背后的渲染思路

2.1 顶层目录:三块各管各的,不互相污染

这个项目的目录结构是典型的小型渲染引擎布局,顶层三个文件夹直接把职责分开:

Qt3DModelLoader/ ├── core/ # 与Qt无关的纯C++渲染核心 ├── ui/ # Qt界面层,负责窗口、交互、信号槽 ├── resources/ # 着色器、模型文件、纹理贴图 └── CMakeLists.txt # 顶层构建脚本

core目录里是模型解析器、OpenGL资源封装、着色器管理、相机和光照系统;ui目录里是QOpenGLWidget子类、主窗口、属性面板。这样拆有个很实际的好处:core层不依赖Qt,将来你想把渲染核心抽出来给别的框架用,或者写单元测试,都不需要拉起整个Qt事件循环。我拆这个项目时第一感觉就是,它不像很多Demo那样把所有OpenGL调用都堆在QOpenGLWidget的paintGL里,而是把渲染数据结构和GL调用分开,这对做二次开发来说太重要了。

2.2 core层的核心类:谁负责数据,谁负责绘制

看core目录里的类,能大概猜出渲染管线的分工:

core/ ├── ModelLoader/ # OBJ/glTF解析,输出统一Mesh结构 ├── Mesh.h/.cpp # 顶点、索引、材质、VAO/VBO管理 ├── Shader.h/.cpp # 着色器编译链接封装 ├── Camera.h/.cpp # 透视/正交相机,轨道控制 ├── Light.h/.cpp # 方向光、点光参数 └── Renderer.cpp # 遍历Mesh,绑定Shader,调用glDrawElements

ModelLoader解析完文件后,会把所有几何数据塞进Mesh结构体:位置、法线、切线、UV、索引数组、材质索引。Mesh类负责把这些数据上传到GPU,生成VAO/VBO/EBO。Shader封装了编译、链接、uniform设置。Renderer则不管文件格式,只认Mesh和Shader,逐个绘制。整个数据流非常清晰:文件 -> ModelLoader -> Mesh -> Renderer -> 屏幕。你在研究代码的时候,顺着这条线读,基本不会迷路。

2.3 ui层是标准的QOpenGLWidget用法

ui目录下的GLWidget继承自QOpenGLWidget,重写了initializeGL、paintGL、resizeGL三个虚函数。initializeGL里初始化GL函数指针、创建Shader和Mesh;paintGL里调用Renderer绘制;resizeGL里设置视口和投影矩阵。鼠标事件处理拖拽旋转、滚轮缩放。主窗口MainWindow里放了一个QSplitter,左边是GLWidget,右边是属性面板,下方是日志输出。这套布局在QtCreator里直接就能跑起来,交互逻辑集中在GLWidget里,没有搞复杂的状态机,蛮适合新手读的。

3. 模型加载与Mesh构建:从文件到GPU缓冲区

3.1 OBJ解析器:逐个关键字处理,不搞正则硬匹配

OBJ格式看着简单,但真要解析起来,坑不少。这个项目用的解析方式很务实:逐行读文件,按行首关键字分发。见到v开头就解析顶点坐标,vn解析法线,vt解析UV,f解析面索引。关键是索引处理,OBJ的f行的格式是v/vt/vn三重索引,同一个顶点在不同面里可能用了不同的 UV 或法线,所以不能直接拿索引去填VBO,得做顶点去重。

bool ObjLoader::load(const QString& path, Mesh& mesh) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return false; QTextStream in(&file); QVector<QVector3D> positions, normals; QVector<QVector2D> texcoords; QVector<FaceVertex> faceVertices; while (!in.atEnd()) { QString line = in.readLine().trimmed(); if (line.isEmpty() || line.startsWith("#")) continue; QStringList tokens = line.split(QRegularExpression("\\s+")); if (tokens[0] == "v") { positions.append(QVector3D(tokens[1].toFloat(), tokens[2].toFloat(), tokens[3].toFloat())); } else if (tokens[0] == "vn") { normals.append(QVector3D(tokens[1].toFloat(), tokens[2].toFloat(), tokens[3].toFloat())); } else if (tokens[0] == "vt") { texcoords.append(QVector2D(tokens[1].toFloat(), tokens[2].toFloat())); } else if (tokens[0] == "f") { for (int i = 1; i < tokens.size(); ++i) { FaceVertex fv; QStringList idx = tokens[i].split('/'); fv.posIdx = idx[0].toInt() - 1; if (idx.size() > 1 && !idx[1].isEmpty()) fv.uvIdx = idx[1].toInt() - 1; if (idx.size() > 2 && !idx[2].isEmpty()) fv.normalIdx = idx[2].toInt() - 1; faceVertices.append(fv); } } } buildMesh(positions, normals, texcoords, faceVertices, mesh); return true; }

buildMesh这一步是关键。因为OBJ的索引是分开的,我们需要为每个FaceVertex生成一个独立的顶点。这里用一个QHash来做映射,键是posIdx/uvIdx/normalIdx的组合,值是新顶点数组里的下标。如果遇到相同的组合,就直接复用索引,这样能大幅减少顶点数,尤其是那些四边形面拆成三角形后,共用顶点的场景能省不少显存。

void ObjLoader::buildMesh(const QVector<QVector3D>& positions, const QVector<QVector3D>& normals, const QVector<QVector2D>& texcoords, const QVector<FaceVertex>& faceVertices, Mesh& mesh) { QHash<QString, quint32> vertexMap; QVector<float> vertexData; QVector<quint32> indices; for (const FaceVertex& fv : faceVertices) { QString key = QString("%1_%2_%3").arg(fv.posIdx).arg(fv.uvIdx).arg(fv.normalIdx); if (vertexMap.contains(key)) { indices.append(vertexMap[key]); continue; } quint32 newIndex = vertexData.size() / 8; vertexData.append(positions[fv.posIdx].x()); vertexData.append(positions[fv.posIdx].y()); vertexData.append(positions[fv.posIdx].z()); if (fv.normalIdx >= 0) { vertexData.append(normals[fv.normalIdx].x()); vertexData.append(normals[fv.normalIdx].y()); vertexData.append(normals[fv.normalIdx].z()); } else { vertexData.append(0.0f); vertexData.append(0.0f); vertexData.append(1.0f); } if (fv.uvIdx >= 0) { vertexData.append(texcoords[fv.uvIdx].x()); vertexData.append(texcoords[fv.uvIdx].y()); } else { vertexData.append(0.0f); vertexData.append(0.0f); } vertexMap.insert(key, newIndex); indices.append(newIndex); } mesh.setVertexData(vertexData, indices); }

这里每个顶点的布局是8个float:position(3)+ normal(3)+ uv(2),顺序和后面着色器里的attribute location对应。如果原始模型没有法线,代码不会崩,而是给一个朝Z轴的单位法线,画面虽然光照不对,但至少能显示出来。这种做法很贴心,调试模型文件的时候能看出问题在哪。

3.2 glTF解析:JSON + BIN,材质从这里开始

glTF是现在3D资产的主流格式,尤其适合带PBR材质的模型。这个项目对glTF的解析分两步:先用QJsonDocument读glTF的JSON部分,取出顶点位置、法线、UV这些accessor和bufferView的索引,再去bin文件里按字节偏移读取实际的float数组。核心逻辑是读取bufferView的byteOffset和byteLength,然后按componentType(通常是5126,也就是GL_FLOAT)和count来切分数据。

bool GltfLoader::load(const QString& path, Mesh& mesh) { QFileInfo info(path); QString binPath = info.path() + "/" + info.baseName() + ".bin"; QFile binFile(binPath); if (!binFile.open(QIODevice::ReadOnly)) return false; QByteArray binData = binFile.readAll(); QFile jsonFile(path); jsonFile.open(QIODevice::ReadOnly); QJsonDocument doc = QJsonDocument::fromJson(jsonFile.readAll()); QJsonObject root = doc.object(); // 解析 mesh.primitives 里的 attributes, indices QJsonArray meshes = root["meshes"].toArray(); QJsonArray buffers = root["buffers"].toArray(); // ... 具体解析略,逻辑和 OBJ 类似,但多了 byteOffset 定位 }

glTF的麻烦在于它引入了bufferView和accessor两层间接寻址,但好处是它支持稀疏访问、支持矩阵变换、支持多mesh嵌套。这个项目解析glTF只取了最常用的属性,不算完整实现,但应付大多数模型预览场景足够了。材质方面会读baseColorFactor、metallicFactor、roughnessFactor,以及对应的纹理索引,传给Shader做PBR计算。

3.3 Mesh类:VAO/VBO/EBO的生命周期管理

Mesh类里封装了OpenGL对象的创建和销毁。构造函数里生成VAO、VBO、EBO;setVertexData里把顶点数据和索引数据上传,同时通过glVertexAttribPointer设置属性指针。这里有个细节:它用的顶点属性location是硬编码的0、1、2,对应position、normal、uv。如果你自己扩展顶点属性比如加tangent,记得修改这里的枚举和Shader那边的location,两边对不上渲染就黑屏。

void Mesh::setupVAO() { glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glGenBuffers(1, &ebo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.constData(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexData.size() * sizeof(quint32), indexData.constData(), GL_STATIC_DRAW); // position glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // normal glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // uv glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); glBindVertexArray(0); }

这段代码几乎是现代OpenGL的固定套路,但新手最容易踩的坑是忘记在glBindVertexArray(0)之前把所有Enable都做完,或者把stride算错。这里的stride是8 * sizeof(float),因为每个顶点有8个float,一个都不能少。还有,EBO的绑定需要放在VAO绑定时完成,数组顶点缓冲区的绑定顺序不能乱,不然glDrawElements会报0x0501错误。

4. 渲染管线与Shader:从顶点到PBR的完整链路

4.1 顶点着色器:矩阵变换和法线修正

着色器是用GLSL写的,整个渲染跑的是OpenGL 3.3 Core Profile。顶点着色器做的事情很常规:把模型坐标变换到裁剪空间,同时把法线变换到世界空间以便光照计算。但这里有个容易被忽略的细节:法线不能直接用model矩阵变换,如果model矩阵里包含非均匀缩放,法线会失真,必须用model矩阵的逆转置矩阵。

#version 330 core layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNormal; layout(location = 2) in vec2 aUV; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProj; uniform mat3 uNormalMatrix; out vec3 vWorldPos; out vec3 vNormal; out vec2 vUV; void main() { vec4 world = uModel * vec4(aPos, 1.0); vWorldPos = world.xyz; vNormal = uNormalMatrix * aNormal; vUV = aUV; gl_Position = uProj * uView * world; }

uNormalMatrix在C++端通过glm::mat3(glm::transpose(glm::inverse(model)))计算。我拆这个项目时发现它把uNormalMatrix单独传,而不是直接用uModel去切,说明写代码的人对非均匀缩放这个坑是有警觉的。如果你在渲染场景里加了骨骼动画或者物体缩放过,这个矩阵就必须单独算。

4.2 片元着色器:PBR光照模型实现

片元着色器是这个项目里最有价值的部分。它没有用传统的Blinn-Phong,而是实现了一个简化版的Cook-Torrance BRDF,支持金属度和粗糙度两个PBR参数。核心计算分为三部分:法线分布函数GGX、几何遮蔽函数Smith-Schlick、菲涅尔近似Schlick。

#version 330 core in vec3 vWorldPos; in vec3 vNormal; in vec2 vUV; out vec4 FragColor; uniform vec3 uCameraPos; uniform vec3 uLightDir; uniform vec3 uLightColor; uniform float uLightIntensity; uniform sampler2D uAlbedoMap; uniform sampler2D uMetallicMap; uniform sampler2D uRoughnessMap; uniform vec3 uAlbedoColor; uniform float uMetallic; uniform float uRoughness; const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness * roughness; float a2 = a * a; float NdotH = max(dot(N, H), 0.0); float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); return (NdotV / (NdotV * (1.0 - k) + k)) * (NdotL / (NdotL * (1.0 - k) + k)); } vec3 FresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } void main() { vec3 albedo = texture(uAlbedoMap, vUV).rgb * uAlbedoColor; float metallic = texture(uMetallicMap, vUV).r * uMetallic; float roughness = texture(uRoughnessMap, vUV).r * uRoughness; vec3 N = normalize(vNormal); vec3 V = normalize(uCameraPos - vWorldPos); vec3 L = normalize(-uLightDir); vec3 H = normalize(V + L); float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); vec3 F0 = mix(vec3(0.04), albedo, metallic); float D = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; vec3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 0.001); vec3 diffuse = kD * albedo / PI; vec3 color = (diffuse + specular) * uLightColor * uLightIntensity * NdotL; // 加一个简单的环境光避免死黑 color += albedo * 0.03; FragColor = vec4(color, 1.0); }

这里有几个参数值得注意:uLightDir是方向光的方向向量,代码里用的是物体指向光源的方向的负值,也就是从光源指向物体的向量被正向了,这个符号搞反会出现背光面亮、受光面暗的诡异效果。金属度为0时,F0是0.04,这是非金属材质的常用基准反射率;金属度越高,F0越接近 albedo 本身,这就是金属和非金属反射行为的主要区别。粗糙度直接影响GGX的a值,粗糙度越大,高光越散。

4.3 Renderer:Draw Call的组织方式

Renderer类的paint方法很简单:遍历模型列表,对每个Mesh调用glDrawElements。但里面有个优化细节:它会把相同Shader的Mesh分在一组,避免频繁切换着色器。虽然这个项目场景不大,但这个习惯很好。另外,它维护了一个uniform参数缓存,只在参数改变时才去调用glUniform函数,减少CPU到GPU的通信开销。

void Renderer::render() { glEnable(GL_DEPTH_TEST); glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shader->use(); shader->setMat4("uView", camera->viewMatrix()); shader->setMat4("uProj", camera->projMatrix()); shader->setMat3("uNormalMatrix", glm::mat3(glm::transpose(glm::inverse(modelMatrix)))); shader->setVec3("uCameraPos", camera->position()); shader->setVec3("uLightDir", light->direction()); shader->setVec3("uLightColor", light->color()); shader->setFloat("uLightIntensity", light->intensity()); for (Mesh* mesh : meshes) { shader->setMat4("uModel", mesh->modelMatrix()); mesh->draw(); } }

注意,深度测试是在每个render开头就enable的。很多新手在Qt的QOpenGLWidget里忘记开深度测试,导致模型自身的前后面互相穿透,看起来像透明的一样。这个项目把glEnable(GL_DEPTH_TEST)放在paintGL外面,但效果一样的。

5. 交互与运行时避坑:相机、多线程与Qt的坑

5.1 相机交互:轨道相机与鼠标事件

这个项目的相机不是FPS相机,而是轨道相机,绕着模型转。鼠标左键拖拽改变方位角和俯仰角,滚轮改变距离,中键平移。实现方式很标准:在GLWidget的mouseMoveEvent里记录鼠标偏移量,更新相机的yaw和pitch,然后调用update触发重绘。

void GLWidget::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { lastMousePos = event->pos(); } } void GLWidget::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { QPoint delta = event->pos() - lastMousePos; lastMousePos = event->pos(); camera.rotate(delta.x() * 0.2f, delta.y() * 0.2f); update(); } } void GLWidget::wheelEvent(QWheelEvent* event) { camera.zoom(event->angleDelta().y() * 0.001f); update(); }

camera.rotate内置了一套基于四元数的旋转,避免万向锁。我用这个项目跑了一个复杂的角色模型,转了半小时没有出现翻转失控,说明四元数这步没偷懒。

5.2 Qt + OpenGL的常见疑难:上下文、线程和Shading语言版本

拆这个项目时,我在Qt版本兼容性上踩了几个坑,记下来给后来人参考。第一个坑是QOpenGLWidget和QGLWidget混用。Qt5以后鼓励用QOpenGLWidget,老代码里的QGLWidget在新版本里虽然还能编译,但跟QOpenGLContext是两套体系,混用会闪退。这个项目用的是QOpenGLWidget,没问题。

第二个坑是CoreProfile和QSurfaceFormat的设置顺序。必须在创建QApplication之前设置QSurfaceFormat::setDefaultFormat,否则OpenGL上下文可能还是兼容模式,导致GLSL 330的着色器编译报错。

int main(int argc, char *argv[]) { QApplication app(argc, argv); QSurfaceFormat format; format.setVersion(3, 3); format.setProfile(QSurfaceFormat::CoreProfile); format.setDepthBufferSize(24); QSurfaceFormat::setDefaultFormat(format); MainWindow w; w.show(); return app.exec(); }

第三个坑是纹理坐标的V方向。Qt在加载QImage时,图片原点在左上角,而OpenGL纹理坐标原点在左下角,如果不做flip,贴图会上下颠倒。项目里在loadTexture函数里先对QImage做mirrored()再上传GL纹理,这个细节新手容易漏。

5.3 避坑清单:渲染黑屏、模型变形、加载白屏的排查路径

这里直接给一份排查清单,每条都是我实际调这个项目时碰到过的。

现象一:模型能加载但整个屏幕是黑的。原因通常是Shader编译失败或uniform绑定错误。检查方法是在initializeGL里打印glGetShaderInfoLog和glGetProgramInfoLog,如果确认无误,再检查uniform名字是否拼错,GLSL编译器对未使用的uniform会直接优化掉,glGetUniformLocation返回-1,setUniform静默失败。解法:用glGetUniformLocation的返回值做断言,Debug模式下直接输出的方式定位。

现象二:模型加载后畸形,三角形乱飞。原因多半是顶点属性步长不对,或者索引数据没解析对。OBJ的f行如果是1/2/3格式,用split('/')没问题,但遇到1//3这种省略UV的情况,idx[1]是空字符串,直接toInt()会得到0,导致UV索引错位,顶点映射错乱。解法:在buildMesh里对空字符串做判断,上面贴的代码已经处理了。

现象三:切换到glTF后,模型位置偏得离谱。原因通常是glTF的node里带了translate或scale变换,而这个项目只读取了mesh的position数据,没应用node的层次变换。解法:要么在加载时遍历node层级,累加矩阵传给Mesh的modelMatrix,要么暂时忽略,但你要知道调模型时为什么对不上。

现象四:多线程加载模型时界面卡死或崩溃。我用一个后台线程加载大体积OBJ文件,直接调用了Mesh::setupVAO,结果OpenGL上下文不在当前线程,程序直接abort。正确做法是把文件解析放在工作线程,生成顶点索引数据后通过信号槽传回主线程,再在主线程创建GL资源。这个项目没有做异步加载,但拆的时候你要注意,别自己加上线程后踩这个坑。

现象五:材质贴图完全不生效,模型是白的。查一下资源路径。这个项目用QFileInfo拼接纹理路径,如果模型文件放在resources/models下,纹理文件在resources/textures下,而glTF里写的是相对路径,加载时没修正基准路径,就会找不到纹理。解法:在loadTexture之前用QDir清理路径,或者把图片也拷贝到模型同目录。

6. 二次开发改造:把渲染核心变成你自己的3D预览组件

到了这一步,你已经有了一个能加载OBJ/glTF、能渲染PBR材质、能交互旋转的Qt程序。接下来要做的不是加功能炫技,而是把它接进自己的业务里。我拆完这个项目后的第一个改造是将它变成一个可复用的3D预览Widget,供项目里的模型管理模块调用。具体做法是给GLWidget增加一个公共接口:

class ModelPreviewWidget : public QOpenGLWidget { Q_OBJECT public: explicit ModelPreviewWidget(QWidget* parent = nullptr); bool loadModel(const QString& filePath, QString* errMsg); void setAlbedoPath(const QString& path); void setMetallicPath(const QString& path); void setRoughnessPath(const QString& path); void setLightDirection(const QVector3D& dir); void setMetalness(float value); void setRoughnessValue(float value); void resetCamera(); signals: void modelLoaded(bool success); void loadError(const QString& message); };

这样外部业务只需要调用loadModel和setMetalness等方法,不需要关心内部Renderer是怎么工作的。另一个改造点是把Shader的硬编码uniform名抽到一个配置表里,或者考虑用Uniform Buffer Object统一管理光照参数,这样如果从单方向光扩展到多光源,就不需要改Shader签名了。我在改造时还把Renderer的mesh列表改成了QSharedPointer管理,关闭窗口时自动释放GL资源,避免了QOpenGLWidget析构时上下文已经被销毁导致的程序崩溃。

将来你想做模型动画、拖拽导入、碰撞拾取,这个项目的代码结构也能兜得住。拾取的话需要在Renderer的render里加一个id framebuffer,或者用鼠标射线和Mesh的AABB做相交测试。这个项目没有实现,但Mesh类里可以轻松加一个boundingBox方法。我一直觉得,看一个开源项目不是看它能跑多少Demo,而是看它能不能在半个工作日里被拉进你的业务代码里。这个Qt3DModelLoader做到了。我后来在自己的产品里集成这个组件时,几乎没动core目录的代码,只改了些ui层的参数绑定和信号槽。从那以后,我每次拿到一个渲染Demo,都会强制走一遍“先拆目录、再理数据流、最后看交互”的流程,确定它的核心和界面耦合程度再动手。希望帮到你。

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

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

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

立即咨询