3D 模型 (GLTF)

可参考的官方例子: load_gltf, update_gltf_scene.


Bevy 使用 GLTF 2.0 文件格式来加载 3D 资源。

(其他格式如 Wavefront OBJ 可通过第三方插件非官方地提供)

快速入门:将 3D 模型生成到你的世界中

最简单的用例是加载一个 "3D 模型" 并将其生成到游戏世界中。

"三维模型"通常很复杂,由多个部分组成。想想一栋房子:窗户、屋顶、门等等,都是独立的部分,可能由多个网格、材质和纹理组成,从技术上来说,Bevy 需要多个 ECS 实体来表示和渲染整栋房子。

这就是为什么你的 GLTF "模型"被 Bevy 表示为一个场景。这样,你可以很容易地生成它,而 Bevy 将创建所有相关的子实体并正确地配置它们。

这样你就可以在世界中把整个东西当作"一个单一的物体"放置,把它挂在一个父实体下面,就可以使用 Transform 来对其进行变换。

fn spawn_gltf(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    // note that we have to include the `Scene0` label
    let my_gltf = ass.load("my.glb#Scene0");

    // to be able to position our 3d model:
    // spawn a parent entity with a Transform and GlobalTransform
    // and spawn our gltf as a scene under it
    commands.spawn_bundle((
        Transform::from_xyz(2.0, 0.0, -5.0),
        GlobalTransform::identity(),
    )).with_children(|parent| {
        parent.spawn_scene(my_gltf);
    });
}

如果你的 GLTF 场景代表了"整个关卡和地图",而不是"一个单独的 3D 模型",并且你不需要移动它,你可以直接生成场景,而不需要为它创建一个父实体。

另外,这个例子假设你有一个简单的 GLTF 文件,只包含一个"默认场景"。GLTF 是一种非常灵活的文件格式,一个文件可以包含许多"模型"或更复杂的"场景"。要想更好地了解 GLTF 和可能的工作流程,请阅读本页面的其他内容。:)

GLTF简介

GLTF 是一种现代开放标准,用于在不同的 3D 软件应用程序之间交换 3D 资源,如游戏引擎和 3D 建模软件。

GLTF 文件格式有两种变体:人类易读的 ascii 文本(*.gltf)和二进制(*.glb)。二进制格式更紧凑,更适合于将资产与你的游戏打包。文本格式对开发有用,因为可以更容易地使用文本编辑器手动查看它。

一个 GLTF 文件可以包含许多对象(子资源):网格、材质、纹理、场景。当加载一个 GLTF 文件时,Bevy 将加载里面包含的所有资源。它们将被映射到适当的 Bevy 内部资源类型

GLTF 子资源

与 Bevy 一起比较时,GLTF 的术语可能令人困惑,因为它有时使用相同的词来指代不同的东西。本节将尝试解释各种 GLTF 术语。

为了帮忙我们理解所有的东西,先来区分这些概念在不同的地方是如何体现的:在你的 3D 建模软件(如 Blender)中、在 GLTF 文件本身,以及在 Bevy 中。

GLTF 场景 是你用来在游戏世界生成内容的原材料。生成后的内容会和你在 3D 软件建模的时候在屏幕上看到的一样。游戏场景整合了游戏引擎所需的所有数据,以创建所有需要的实体来表现你想要的东西。从概念上讲,把一个场景看成是一个 "单元"。根据你的使用情况,这可能是一个"3D模型",甚至是整个地图或游戏关卡。在 Bevy 中,这些被表示为 Bevy 场景和所有的子 ECS 实体。

GLTF 场景是由 GLTF 节点组成的。这些节点描述了游戏场景中的"对象",通常是 GLTF 网格,但也可以是其他东西,如摄相机和灯光。每个 GLTF 节点都有一个用于在场景中定位的变换组件。Bevy 核心功能中没有与 GLTF 节点一一对应的概念。Bevy 只是使用这些数据来创建场景中的 ECS 实体。如果你需要访问这些数据,Bevy 有一个特殊的 GltfNode 资源类型。

GLTF Meshes 代表一个概念性的"3D 对象"。这些对应于你在 3D 建模软件中的"对象"。GLTF 网格有可能会非常复杂,由多个小块组成,称为 GLTF 基元,每个可以使用不同的材质。GLTF 网格没有一个核心的 Bevy 对应物,但有一个特殊的 GltfMesh 资源类型,用来描述基元。

GLTF 基元是单独的 "3D 几何单位",目的是用于渲染。它们包含了实际的几何体和顶点数据,并在绘制时参考需要使用的材质。在 Bevy 中,每个 GLTF 基元都被表示为 Bevy Mesh 资源,并且必须作为一个单独的 ECS 实体被生成以进行渲染。

GLTF 材质描述了你的 3D 模型表面的着色参数,完全支持基于物理的渲染(PBR),还引用了要使用的纹理。在 Bevy 中,GLTF 材质表示为 StandardMaterial 资源,被 Bevy PBR 3D 渲染器所用。

GLTF 纹理(图片)可以嵌入到 GLTF 文件中,也可以存储在外部的独立图像文件中。例如,你可以将你的纹理作为单独的 PNG 或 JPEG 文件,以方便开发,或者将它们全部打包在 GLTF 文件中,以方便分发。在 Bevy 中,GLTF 纹理被作为 Bevy 图片 资源加载。

GLTF 采样器描述了 GPU 应该如何使用一个特定的纹理的设置。Bevy 并没有把这些分开,这些数据被储存在 Bevy 图片资源中(存于 SamplerDescriptor 类型的 sampler 字段)。

GLTF 使用模式

由于 GLTF 非常灵活,如何构建你的资源由你决定。

一个 GLTF 文件可能被用来:

  • 只包含一个带有某模型的 GLTF 场景,表示一个单一的"3D 模型",这样你就可以将其生成到你的游戏中。
  • 作为一个 GLTF 场景,可能还包含摄像机,用来表示整个游戏关卡。这可以让你一次性加载和生成整个关卡和地图。
  • 表示一个关卡或地图的一个部分,比如一个房间。
  • 包含一组不同的 "3D 模型",每个模型都是一个单独的 GLTF 场景。这可以让你一次性加载和管理整个集合,并根据需要单独生成它们。
  • ...其他?

创建 GLTF 资源的工具

如果你使用最新版本的 Blender(2.8以上)进行 3D 建模,GLTF 是开箱即支持的。只要选择 GLTF 作为导出格式。

对于其他工具,你可以试试这些导出插件:

在 Bevy 中使用 GLTF 子资源

包含在 GLTF 文件中的各种子资源可以通过两种方式来处理:

  • 通过索引(整数 id,按照它们在文件中出现的顺序)。
  • 按名称(文本字符串,你在创建资源时在你的 3D 建模软件中设置的名称,可以导出到 GLTF 中)。

为了在 Bevy 中获得相应资源的句柄,你可以使用 Gltf ["master asset"][#gltf-master-asset],或者使用 带标签的资源路径

Gltf 主资源

如果你有一个复杂的 GLTF 文件,这可能是浏览其内容和使用里面不同东西的最灵活和有用的方法。

你必须等待 GLTF 文件加载,然后再使用 Gltf 资源。

use bevy::gltf::Gltf;

/// Helper resource for tracking our asset
struct MyAssetPack(Handle<Gltf>);

fn load_gltf(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    let gltf = ass.load("my_asset_pack.glb");
    commands.insert_resource(MyAssetPack(gltf));
}

fn spawn_gltf_objects(
    mut commands: Commands,
    my: Res<MyAssetPack>,
    assets_gltf: Res<Assets<Gltf>>,
) {
    // if the GLTF has loaded, we can navigate its contents
    if let Some(gltf) = assets_gltf.get(&my.0) {
        // spawn the first scene in the file
        commands.spawn_scene(gltf.scenes[0].clone());

        // spawn the scene named "YellowCar"
        // do it under a parent entity, to position it in the world
        commands.spawn_bundle((
            Transform::from_xyz(1.0, 2.0, 3.0),
            GlobalTransform::identity(),
        )).with_children(|parent| {
            parent.spawn_scene(gltf.named_scenes["YellowCar"].clone());
        });

        // PERF: the `.clone()`s are just for asset handles, don't worry :)
    }
}

对于一个更复杂的例子,比如说,不管出于什么原因我们想直接创建一个 3D PBR 实体(不推荐这样做;你应该直接使用场景):

use bevy::gltf::GltfMesh;

fn gltf_manual_entity(
    mut commands: Commands,
    my: Res<MyAssetPack>,
    assets_gltf: Res<Assets<Gltf>>,
    assets_gltfmesh: Res<Assets<GltfMesh>>,
) {
    if let Some(gltf) = assets_gltf.get(&my.0) {
        // Get the GLTF Mesh named "CarWheel"
        // (unwrap safety: we know the GLTF has loaded already)
        let carwheel = assets_gltfmesh.get(&gltf.named_meshes["CarWheel"]).unwrap();

        // Spawn a PBR entity with the mesh and material of the first GLTF Primitive
        commands.spawn_bundle(PbrBundle {
            mesh: carwheel.primitives[0].mesh.clone(),
            // (unwrap: material is optional, we assume this primitive has one)
            material: carwheel.primitives[0].material.clone().unwrap(),
            ..Default::default()
        });
    }
}

带标签的资源路径

这是另一种访问特定子资源的方式。它不太可靠,但在某些情况下可能更容易使用。

使用 AssetServer 将路径字符串转换为 资源句柄

优点是,你可以立即获得子资源的句柄,即使你的 GLTF 文件还没有加载完成。

缺点是,它更容易出错。如果你指定了一个在文件中不存在的子资源、或者输错了标签、或者用了错误的标签,它只会默默地不工作。另外,目前只支持使用数字索引,不能使用名字来定位子资源。

fn use_gltf_things(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    // spawn the first scene in the file
    let scene0 = ass.load("my_asset_pack.glb#Scene0");
    commands.spawn_scene(scene0);

    // spawn the second scene under a parent entity
    // (to move it)
    let scene1 = ass.load("my_asset_pack.glb#Scene1");
    commands.spawn_bundle((
        Transform::from_xyz(1.0, 2.0, 3.0),
        GlobalTransform::identity(),
    )).with_children(|parent| {
        parent.spawn_scene(scene1);
    });
}

支持以下资产标签({}是数字索引)。

  • Scene{}: 获取 GLTF 场景,作为 Bevy 场景
  • Node{}: 获取 GLTF Node,作为 GltfNode
  • Mesh{}: 获取 GLTF Mesh,作为 GltfMesh
  • Mesh{}/Primitive{}: 获取 GLTF Primitive,作为 Bevy Mesh
  • Texture{}: 获取 GLTF 纹理,作为 Bevy Image
  • Material{}: 获取 GLTF 材质,作为 Bevy StandardMaterial
  • DefaultMaterial: 同上一个,如果 GLTF 文件中包含一个没有索引的默认材质,则直接获取该材质。

GltfNodeGltfMesh 资源类型只对帮助你浏览 GLTF 文件的内容有用。它们不是 Bevy 渲染器的核心类型,也不会被 Bevy 以任何其他方式使用。Bevy 渲染器希望实体具有 MaterialMeshBundle,为此你需要 MeshStandardMaterial

Bevy Limitations

Bevy 并不完全支持 GLTF 格式的所有特性,对数据有一些特殊的要求。不是所有的 GLTF 文件都能在 Bevy 中加载和渲染。不幸的是,在许多这样的情况下,你得不到任何的错误提示或诊断信息。

普遍遇到的限制:

  • 纹理通过 base64 编码后以 ascii 字符形式嵌入的 GLTF 文件不能被加载。需要把你的纹理放在外部文件中,或者使用二进制(*.glb)的格式。
  • 不支持 Mipmaps。你的资源应该仍然可以被加载和渲染,但没有 mipmapping,它看起来可能会很嘈杂、有颗粒、有锯齿。
  • Bevy 的渲染器要求所有的 mesh 和 primitive 都有每个顶点的位置、UV 和法线,请确保所有这些数据都包含在内。
  • 没有纹理的 mesh 和 primitive(如果材质只是一种纯色)仍然必须包含 UV。Bevy 将不会渲染没有 UV 的网格。
  • 当在你的材质中使用法线贴图时,切线也必须包含在网格中。有法线贴图但没有切线的网格是有效的,如果缺少切线,其他软件通常会自动生成切线,但 Bevy 还不支持这个。请确保在导出时勾选包括切线的复选框。
  • Bevy 还没有内置的骨架动画支持,渲染时动画是完全被忽略的。

这份清单并不详尽,可能还有我不知道的或忘了包括在这里的其他不支持的情况。:)