Qt Quick 3D 使用 glTF 资产介绍

Qt Quick 3D - Introduction示例快速介绍了如何使用Qt Quick 3D 创建基于 QML 的应用程序,但它仅使用了球体和圆柱体等内置基元。本页将介绍如何使用glTF 2.0资产,并使用Khronos glTF Sample Models 资源库中的一些模型。

我们的骨架应用程序

让我们从下面的应用程序开始。该代码片段可使用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 文件(实际上是一个子场景)外,它还会将网格(几何体)数据重新打包为优化的、可快速加载的格式,并复制纹理贴图图像文件。
  • 使用balsamuiBalsam 的图形用户界面前端)执行同样的操作。
  • 如果使用 Qt Design Studio,资产导入过程将集成到可视化设计工具中。例如,将 .gltf 文件拖放到相应的面板上即可触发导入。
  • 特别是对于 glTF 2.0 资产,还有一个运行时选项:RuntimeLoader 类型。这允许在运行时加载 .gltf 文件(以及相关的二进制文件和纹理数据文件),而无需通过Balsam 等工具进行任何预处理。这在希望打开和加载用户提供的资产的应用程序中非常方便。另一方面,这种方法在性能方面的效率要低得多。因此,我们在本介绍中将不再重点介绍这种方法。请查看Qt Quick 3D - RuntimeLoader 示例,了解这种方法的示例。

balsambalsamui 应用程序都随 Qt XML 一起发布,假定Qt Quick 3D 已安装或已构建,它们应该与其他类似的可执行工具一起出现在目录中。在许多情况下,通过命令行在 .gltf 文件上运行balsam即可,无需指定任何额外参数。不过,值得注意的是命令行选项(如果使用balsamuiQt Design Studio ,则为交互式选项)很多。例如,在使用烘焙光照贴图提供静态全局光照时,很可能需要通过--generateLightmapUV 在资产导入时生成额外的光照贴图 UV 通道,而不是在运行时执行这一潜在的消耗性过程。同样,当需要生成简化版网格以便在场景中启用自动 LOD时,--generateMeshLevelsOfDetail 也是必不可少的。其他选项允许生成缺失数据(如--generateNormals )和执行各种优化。

balsamui 中,命令行选项被映射为交互式元素:

通过 balsam 导入

让我们开始吧!假定https://github.com/KhronosGroup/glTF-Sample-Models git 资源库已在某处签出,我们只需指定 .gltf 文件的绝对路径,即可在示例应用程序目录下运行 balsam:

balsam c:\work\glTF-Sample-Models\2.0\Sponza\glTF\Sponza.gltf

这样我们就有了Sponza.qmlmeshes 子目录下的.mesh 文件以及复制到maps 下的纹理贴图。

注意: 该 qml 文件本身不可运行。它是一个组件,应在与View3D 相关联的 3D 场景中实例化。

在这里,我们的项目结构非常简单,因为资产 qml 文件就在主 .qml 场景旁边。这样我们就可以使用标准 QML 组件系统简单地实例化 Sponza 类型。(运行时将在文件系统中查找 Sponza.qml)。

然而,仅仅添加模型(子场景)是没有意义的,因为默认情况下,材质具有完整的PBR 照明计算功能,所以如果没有灯光(如DirectionalLightPointLightSpotLight ),或没有通过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 启用)移动感觉不对。

为了解决这个问题,我们将 Sponza 模型(子场景)沿 X、Y 和 Z 轴缩放至100 。此外,摄像机的起始 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 可以替代 "模型"。这是为什么呢?Sponza 资产在 glTF 形式下是一个包含 103 个子主题的单一模型,映射到Model 对象,其materials list 中包含 103 个元素,虽然这一点在 Sponza 资产中并不明显,但一个资产可以包含任意数量的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 放置在苏珊娜模型的稍上方。在使用可视化工具(如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 全景图像,那就使用程序生成的高动态范围天空图像吧。有了ProceduralSkyTextureDataTexture 对非基于文件的动态生成图像数据的支持,我们就可以轻松做到这一点。与其指定source ,不如使用textureData 属性。

environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.SkyBox
    lightProbe: Texture {
        textureData: ProceduralSkyTextureData {
        }
    }
}

注意: 示例代码倾向于使用内联方式定义对象。这并不是强制性的,SceneEnvironmentProceduralSkyTextureData 对象也可以在对象树的其他地方定义,然后由id 引用。

因此,我们同时拥有了天空盒和改进的照明。(前者是因为backgroundMode 被设置为 SkyBox,light probe 被设置为有效的Texture ;后者是因为light probe 被设置为有效的Texture)

基本性能调查

为了对场景的资源和性能方面有一些基本的了解,最好在开发过程的早期就添加一种方法来显示交互式DebugView 项目。在这里,我们选择添加一个可切换DebugViewButton ,两者都锚定在右上角。

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场景的基础知识。

© 2025 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.