Qt Quick 3D glTF 资源入门
示例“Qt Quick 3D - Introduction”简要介绍了如何使用Qt Quick 3D 创建基于QML的应用程序,但该示例仅使用了球体和圆柱体等内置基本图形。本页面则通过使用glTF 2.0资源进行介绍,其中采用了Khronos glTF示例模型存储库中的部分模型。
我们的骨架应用程序
让我们从以下应用程序开始。该代码片段可直接通过qml 命令行工具运行。运行结果是一个纯绿色的3D视图,其中没有任何其他内容。
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "green"
}
PerspectiveCamera {
id: camera
}
WasdController {
controlledObject: camera
}
}
}
导入资源
我们将使用来自“示例模型”存储库中的两个 glTF 2.0 模型:Sponza 和 Suzanne。
这些模型通常除了 .gltf 文件外,还包含若干纹理贴图,以及存储在单独二进制文件中的网格(几何)数据:

如何将所有这些内容导入到我们的Qt Quick 3D 场景中?
有以下几种方法:
- 生成可在场景中实例化的 QML 组件。 用于执行此转换的命令行工具是Balsam工具。除了生成一个 .qml 文件(实际上是一个子场景)外,它还会将网格(几何)数据重新打包为一种优化且加载速度快的格式,并复制纹理贴图图像文件。
- 使用
balsamui(Balsam 的图形用户界面前端)执行相同操作。 - 如果使用 Qt Design Studio,则资源导入流程已集成到可视化设计工具中。例如,只需将 .gltf 文件拖放至相应面板,即可触发导入。
- 特别是对于 glTF 2.0 资源,还提供了一个运行时选项:RuntimeLoader 类型。这允许在运行时直接加载 .gltf 文件(及其相关的二进制和纹理数据文件),而无需通过Balsam 等工具进行任何预处理。 这在需要打开和加载用户提供的资源的应用程序中非常方便。另一方面,就性能而言,这种方法的效率要低得多。因此,在本介绍中我们将不会重点讨论这种方法。请查看“Qt Quick 3D - RuntimeLoader”示例,了解此方法的具体应用。
balsam 和balsamui 这两个应用程序均随Qt一同提供,假设已安装或编译了Qt Quick 3D ,它们应与其他类似的可执行工具位于同一目录下。在大多数情况下,只需在命令行中对.gltf文件运行balsam即可,无需指定任何额外参数。 不过,值得注意的是,存在许多命令行选项(若使用balsamui 或Qt Design Studio 则为交互式选项)。例如,在使用烘焙光照贴图提供静态全局光照时,通常需要传入--generateLightmapUV 参数,以便在导入资源时生成额外的光照贴图 UV 通道,从而避免在运行时执行这一可能耗时的过程。 同样地,当需要生成简化的网格版本以在场景中启用自动LOD时,--generateMeshLevelsOfDetail 选项至关重要。其他选项可用于生成缺失数据(例如--generateNormals )并执行各种优化操作。
在balsamui 中,这些命令行选项已被映射为交互式元素:

通过 balsam 导入
让我们开始吧!假设已经从https://github.com/KhronosGroup/glTF-Sample-Models检出git 仓库到某个位置,我们只需在示例应用程序目录下运行 balsam,并指定 .gltf 文件的绝对路径:
balsam c:\work\glTF-Sample-Models\2.0\Sponza\glTF\Sponza.gltf
这将生成一个Sponza.qml 文件,在meshes 子目录下生成一个.mesh 文件,并将纹理贴图复制到maps 目录下。
注意:此 qml 文件无法单独运行。它是一个组件,应在与View3D 关联的 3D 场景中进行实例化。
此处的项目结构非常简单,因为资源 QML 文件就位于我们的主 .qml 场景文件旁边。这使我们能够通过标准的 QML 组件系统直接实例化 Sponza 类型。(运行时系统会自动在文件系统中查找 Sponza.qml)
然而,仅添加模型(子场景)是没有意义的,因为默认情况下材质会进行完整的PBR光照计算,因此如果没有DirectionalLight 、PointLight 或SpotLight 等光源,或者未通过the environment 启用基于图像的光照,场景中将不会显示任何内容。
目前,我们选择添加一个DirectionalLight 并采用默认设置。(即颜色为white ,且光源沿Z轴方向发光)
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "green"
}
PerspectiveCamera {
id: camera
}
DirectionalLight {
}
Sponza {
}
WasdController {
controlledObject: camera
}
}
}使用“qml ”工具运行时,场景会加载并运行,但默认情况下场景完全为空,因为 Sponza 模型位于摄像机后方。比例尺也不理想,例如使用 WASD 键和鼠标移动(由WasdController 启用)时,操作手感不佳。
为解决此问题,我们通过100 沿X、Y和Z轴对Sponza模型(子场景)进行缩放。此外,将摄像机的初始Y位置调整为100。
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "green"
}
PerspectiveCamera {
id: camera
y: 100
}
DirectionalLight {
}
Sponza {
scale: Qt.vector3d(100, 100, 100)
}
WasdController {
controlledObject: camera
}
}
}运行后,我们得到:

使用鼠标和 WASDRF 键即可移动:


注: 上文中我们 多次提到subscene ,作为“model”的替代方案。 这是为什么呢?虽然在 Sponza 资源中这一点并不明显——该资源的 glTF 格式是一个包含 103 个子网格的单一模型,映射到一个包含 103 个元素的Model 对象(其materials list 中),但一个资源可以包含任意数量的models ,每个子模型都包含多个子网格和相关材质。 这些模型可以形成父子关系,并可与其他nodes 结合以执行平移、旋转或缩放等变换。因此,即使渲染结果在视觉上被感知为单个模型,将导入的资源视为一个完整的子场景——即由nodes 构成的任意树形结构——更为恰当。 在纯文本编辑器中打开生成的 Sponza.qml 文件,或由此类资源生成的任何其他 QML 文件,以了解其结构(当然,这始终取决于源资源——本例中即 glTF 文件——的设计方式)。
通过 balsamui 导入
对于第二个模型,让我们改用balsam 的图形用户界面。
运行balsamui 即可打开该工具:

现在导入Suzanne模型。这是一个包含两个纹理贴图的较简单模型。

由于无需任何额外配置选项,我们只需点击“转换”即可。结果与运行balsam 相同:在指定的输出目录中生成了 Suzanne.qml 文件以及一些附加文件。

从这一点开始,对生成的资源进行操作与上一节中的操作相同。
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "green"
}
PerspectiveCamera {
id: camera
y: 100
}
DirectionalLight {
}
Sponza {
scale: Qt.vector3d(100, 100, 100)
}
Suzanne {
y: 100
scale: Qt.vector3d(50, 50, 50)
eulerRotation.y: -90
}
WasdController {
controlledObject: camera
}
}
}同样,对实例化的 Suzanne 节点应用了缩放,并稍微调整了 Y 坐标,以免模型最终落在 Sponza 大楼的地板上。

所有属性均可像在Qt Quick 中一样进行修改、绑定和动画处理。例如,让我们对 Suzanne 模型应用一个连续旋转效果:
Suzanne {
y: 100
scale: Qt.vector3d(50, 50, 50)
NumberAnimation on eulerRotation.y {
from: 0
to: 360
duration: 3000
loops: Animation.Infinite
}
}优化外观
增加光源
目前,我们的场景有些昏暗。让我们再添加一盏灯。这次使用PointLight ,并且选择能投射阴影的那种。
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "green"
}
PerspectiveCamera {
id: camera
y: 100
}
DirectionalLight {
}
Sponza {
scale: Qt.vector3d(100, 100, 100)
}
PointLight {
y: 200
color: "#d9c62b"
brightness: 5
castsShadow: true
shadowFactor: 75
}
Suzanne {
y: 100
scale: Qt.vector3d(50, 50, 50)
NumberAnimation on eulerRotation.y {
from: 0
to: 360
duration: 3000
loops: Animation.Infinite
}
}
WasdController {
controlledObject: camera
}
}
}运行该场景并稍微移动摄像机后,可以发现画面确实比之前更美观了:

灯光调试
PointLight 被放置在Suzanne模型的正上方稍高处。当使用Qt Design Studio 等可视化工具设计场景时,这一点显而易见;但在不使用任何设计工具进行开发时,能够快速直观地查看lights 及其他nodes 的位置可能会非常有用。
我们可以通过在PointLight 上添加一个子节点(Model )来实现这一点。子节点的位置是相对于父节点的,因此在此情况下,默认的(0, 0, 0) 实际上就是PointLight 的位置。 将光源封装在某些几何体(此处为内置立方体)内,对于标准的实时光照计算而言并非问题,因为该系统不包含遮挡概念,这意味着光线可以毫无障碍地穿过“墙壁”。 如果我们使用预烘焙光照贴图(其中光照是通过光线追踪计算的),情况就会大不相同。在这种情况下,我们需要确保立方体不会阻挡光线,例如将调试立方体稍微移到光源上方一点。
PointLight {
y: 200
color: "#d9c62b"
brightness: 5
castsShadow: true
shadowFactor: 75
Model {
source: "#Cube"
scale: Qt.vector3d(0.01, 0.01, 0.01)
materials: PrincipledMaterial {
lighting: PrincipledMaterial.NoLighting
}
}
}这里我们还使用了一个技巧,即关闭立方体所用材质的照明效果。这样它将仅显示默认底色(白色),而不受光照影响。这对用于调试和可视化的对象非常有用。
请注意,结果中出现了一个小巧的白色立方体,它用于可视化PointLight 的位置:

天空盒与基于图像的照明
另一个显而易见的改进是处理背景。那种绿色透明色并不太理想。如果添加一些也能为照明提供支持的环境效果,会怎么样呢?
由于我们未必有合适的高动态范围全景图像(HDRI),不妨使用程序生成的动态范围天空图像。借助ProceduralSkyTextureData 和Texture 对非基于文件、动态生成图像数据的支持,这很容易实现。我们不再指定source ,而是使用textureData 属性。
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.SkyBox
lightProbe: Texture {
textureData: ProceduralSkyTextureData {
}
}
}注意: 示例代码 倾向于内联定义对象。这并非强制要求,SceneEnvironment 或ProceduralSkyTextureData 对象也可以定义在对象树的其他位置,然后通过id 进行引用。
因此,我们既拥有了天空盒,又实现了改进的照明效果。(前者是因为将backgroundMode 设置为 SkyBox,并将light probe 设置为有效的Texture ;后者是因为将light probe 设置为有效的Texture )


基本性能排查
为了对场景的资源和性能方面获得一些基本了解,在开发过程的早期阶段添加一种显示交互式DebugView 元素的方法是个好主意。这里我们选择添加一个Button ,用于切换DebugView ,两者均锚定在右上角。
import QtQuick
import QtQuick.Controls
import QtQuick3D
import QtQuick3D.Helpers
Item {
width: 1280
height: 720
View3D {
id: view3D
anchors.fill: parent
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.SkyBox
lightProbe: Texture {
textureData: ProceduralSkyTextureData {
}
}
}
PerspectiveCamera {
id: camera
y: 100
}
DirectionalLight {
}
Sponza {
scale: Qt.vector3d(100, 100, 100)
}
PointLight {
y: 200
color: "#d9c62b"
brightness: 5
castsShadow: true
shadowFactor: 75
Model {
source: "#Cube"
scale: Qt.vector3d(0.01, 0.01, 0.01)
materials: PrincipledMaterial {
lighting: PrincipledMaterial.NoLighting
}
}
}
Suzanne {
y: 100
scale: Qt.vector3d(50, 50, 50)
NumberAnimation on eulerRotation.y {
from: 0
to: 360
duration: 3000
loops: Animation.Infinite
}
}
WasdController {
controlledObject: camera
}
}
Button {
anchors.right: parent.right
text: "Toggle DebugView"
onClicked: debugView.visible = !debugView.visible
DebugView {
id: debugView
source: view3D
visible: false
anchors.top: parent.bottom
anchors.right: parent.right
}
}
}
该面板显示实时计时数据,支持查看纹理贴图和网格的实时列表,并能直观展示在最终颜色缓冲区渲染之前需要执行的渲染通道。
由于将“PointLight ”设为投射阴影的光源,因此涉及多个渲染通道:

在“Textures ”部分,我们可以看到来自Suzanne和Sponza资源的纹理贴图(后者包含大量纹理贴图),以及程序生成的天空纹理。

Models 页面没有意外:

在“Tools ”页面上,有一些交互式控件,用于切换“wireframe mode ”和各种“material overrides ”。
此处启用了线框模式,并强制渲染仅使用材质中的base color 组件:

至此,我们完成了关于构建 Qt Quick 3D 场景的导览。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.