Qt Quick 3D与glTF资源的简介

Qt Quick 3D - 简介”示例快速介绍了使用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文件外,它还将网格(几何)数据重新打包到优化且快速加载的格式中,并将纹理图图像文件也复制过来。
  • 同样使用Balsam的GUI前端balsamui进行。
  • 如果使用Qt Design Studio,资源导入过程集成到可视设计工具中。可以例如通过将.gltf文件拖放到相应的面板来触发导入。
  • 对于glTF 2.0资源来说,还有一个运行时选项:RuntimeLoader类型。它允许在运行时装载.gltf文件(以及相关的二进制和纹理数据文件),无需通过Balsam之类的工具进行任何预处理。这对于希望打开和加载用户提供的资源的应用程序非常方便。另一方面,这种方法在性能方面的效率明显较低。因此,在这个简介中,我们不会重点介绍这种方法。请参阅Qt Quick 3D - RuntimeLoader Example来了解此方法的示例。

Qt自带了应用程序,假设计算机已安装或构建了Qt Quick 3D,这两个应用程序应出现在与其他类似可执行工具相同的目录中。在很多情况下,从命令行运行balsam的.gltf文件是足够的,无需指定任何额外的参数。不过,值得注意,无论是使用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光照计算,所以没有灯光(如DirectionalLightPointLightSpotLight)或通过图像基础照明启用环境,我们的场景将不显示任何内容。

因此,我们选择添加一个带有默认设置DirectionalLight

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模型在摄像机后面,场景默认为空。缩放也不理想,例如,使用WASDRF键移动不会感觉正确。

为了解决这个问题,我们将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个子网格,映射到一个包含103个元素的单一 Model 对象,及其材质列表,一个资产可以包含任何数量的模型,每个模型具有多个子网格和相关材质。这些模型可以形成父子关系,并且可以与额外的 节点 结合,执行平移、旋转或缩放等变换。因此,将导入的资产视为一个完整的子场景,一个任意的节点树,即使渲染结果在视觉上被感知为一个单一模型,这也是更合适的。在纯文本编辑器中打开生成的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之类的外观工具时这是显而易见的,但开发时没有设计工具可能会觉得能够快速可视化光源和其他节点的位置很有用。

我们可以通过添加一个子节点,一个模型点光源中来实现这一点。子节点的位置相对于父节点,所以默认的(0, 0, 0)实际上是这个场景中点光源的位置。在这个例子中,将光线包含在某个几何形状(内置的是一个立方体)内部对于标准的实时照明计算来说不是问题,因为该系统没有遮挡的概念,这意味着光线不会有穿过“墙壁”的问题。如果我们使用的是预烘焙的光线贴图,其中使用光线追踪来计算照明,那么情况就不同了。在这种情况下,我们需要确保立方体不阻挡光线,也许需要通过将调试立方体稍微移到光线上方来实现。

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
        }
    }
}

我们在这里使用的另一个技巧是关闭用于立方体的材质的照明。它将仅使用默认的基本颜色(白色)显示,不会受到照明的影响。这对于用于调试和可视化的对象非常有用。

结果,注意出现的小白立方体,可视化了点光源的位置

盒子建模和基于图像的照明

另一个显而易见的改进是处理背景。那种绿色半透明的颜色并不完美。为什么不选择一种也有助于照明的环境呢?

因为我们不一定有合适的HDRI全景图像可用,所以让我们使用一个程序生成的动态高动态范围天空图像。在ProceduralSkyTextureDataTexture对非文件支持的动态生成图像数据的支持下,这很简单。我们而不是指定,我们更愿意使用纹理数据属性。

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

注意:示例代码首选定义对象的方式是内联定义。这不是强制性的,场景环境程序化天空纹理数据对象可以定义在对象树的其它地方,并且通过id进行引用。

结果是,我们有了一个既有天盒又有改进的照明。其中天盒是因为背景模式设置为天盒并且光照探头设置为有效的纹理;后者是因为光照探头设置为有效的纹理。)

基本性能研究

为了对场景的资源性能方面有一些基本了解,在开发早期就添加一种显示交互式DebugView元素的方法是很有用的。在这里,我们选择添加一个按钮来切换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
        }
    }
}

此面板显示实时计时,允许检查实时纹理图和网格列表,并且提供了有关需要执行在最终颜色缓冲渲染之前的渲染通道的见解。

由于将点光源制成投射阴影的光源,因此涉及到多个渲染通道

纹理部分,我们可以看到Suzanne和Sponza资产(后者有很多)的纹理图,以及程序生成的天空纹理。

模型页面上并没有惊喜

工具页面上有一些交互式控件可以切换线框模式和各种材质覆盖

在此处,线框模式已启用,并强制渲染只使用材质的基本颜色组件

这结束了我们对使用导入资源的Qt Quick 3D场景基本构建过程的探索。

© 2024 Qt公司。此处包含的文档贡献版权归属各自所有者。所提供的文档根据GNU自由文档许可协议的条款授权,版本为1.3,由自由软件基金会发布。Qt及相应的标志是Qt公司(Finnish and/or其他国家的)的商标。所有其他商标均为各自所有者的财产。