Bevy 0.11

发布于 2023 年 7 月 9 日 由 Bevy 贡献者

An image representing the article

感谢 166 位贡献者、522 次拉取请求、社区审阅者以及我们 慷慨的赞助商,我们很高兴在 crates.io 上宣布发布 Bevy 0.11

对于那些不知道的人,Bevy 是一款用 Rust 构建的令人耳目一新的简单数据驱动游戏引擎。您可以查看我们的 快速入门指南 以立即尝试它。它是免费的,并且永远开源!您可以在 GitHub 上获取完整的 源代码。查看 Bevy 资产 以获取社区开发的插件、游戏和学习资源的集合。

要将现有的 Bevy 应用或插件更新到 Bevy 0.11,请查看我们的 0.10 到 0.11 迁移指南.

自从我们几个月前的上一次发布以来,我们添加了 大量 新功能、错误修复和使用体验改进,但以下是其中的一些亮点

  • 屏幕空间环境光遮蔽 (SSAO):通过模拟“间接”漫反射光的遮挡来提高场景渲染质量
  • 时间抗锯齿 (TAA):一种流行的抗锯齿技术,它使用运动向量将当前帧与过去帧混合在一起以平滑掉伪影
  • 变形目标:在预定义状态之间对网格上的顶点位置进行动画处理。非常适合角色定制!
  • 鲁棒对比度自适应锐化 (RCAS):智能地锐化渲染,与 TAA 搭配使用效果很好
  • WebGPU 支持:Bevy 现在可以使用现代 WebGPU Web API 在网络上更快、更具功能地渲染
  • 改进的着色器导入:Bevy 着色器现在支持细粒度的导入和其他新功能
  • 视差贴图:材质现在支持可选的深度图,通过对材质纹理进行视差处理,使平面表面具有深度感
  • Schedule-First ECS API:更简单、更符合人体工程学的 ECS 系统调度 API
  • 立即模式 Gizmo 渲染:轻松高效地渲染 2D 和 3D 形状,用于调试和编辑器场景
  • ECS 音频 API:一种更直观、更惯用的音频回放方式
  • UI 边框:UI 节点现在可以具有可配置的边框!
  • 网格 UI 布局:Bevy UI 现在支持 CSS 风格的网格布局
  • UI 性能改进:UI 批处理算法已更改,带来了显著的性能提升

屏幕空间环境光遮蔽 #

作者:@JMS55、@danchia、@superdump

拖动此图像进行比较

The Sponza scene without SSAO, it contains a lot of persian-style velvet curtains, they look awkward.The Sponza scene with SSAO, the curtains look much more realistic and sculptued. SSAO darkens the ridges between the folds, making the curtains much more interesting to look at

仅 SSAO ssao_only

Bevy 现在支持屏幕空间环境光遮蔽 (SSAO)。虽然 Bevy 已经支持来自直接光源的阴影(DirectionalLightPointLightSpotLight)通过阴影贴图,Bevy 现在支持来自 间接 漫反射光的阴影,如 AmbientLightEnvironmentMapLight.

这些阴影通过估计通过屏幕空间深度和法线预处理周围几何体对入射光的阻挡程度,使场景更加“接地”。您可以在新的 SSAO 示例 中试用它。

请注意,将 SSAO 与新添加的时间抗锯齿一起使用会导致质量和降噪的 大幅 提升。

平台支持目前有限 - 目前仅支持 Vulkan、DirectX12 和 Metal。WebGPU 支持将在稍后的日期提供。WebGL 可能不会被支持,因为它没有计算着色器。

特别感谢英特尔及其开源 XeGTAO 项目,该项目在开发此功能方面提供了巨大的帮助。

时间抗锯齿 #

作者:@JMS55、@DGriffin91

拖动此图像进行比较

The Helmet model with MSAA, anti-aliasing. The edge between meshes are well aliased, but crenellation is noticeable on sharp shadows and specular highlightsWith TAA, little crenellation is visible, but it feels a bit 'smudgy'

除了 MSAA 和 FXAA 之外,Bevy 现在支持时间抗锯齿 (TAA) 作为一种抗锯齿选项。

TAA 通过将新渲染的帧与过去帧混合在一起以平滑图像中的锯齿伪影来工作。TAA 在业界越来越受欢迎,因为它能够掩盖许多渲染伪影:它可以平滑阴影(全局照明和“投射”阴影)、网格边缘、纹理,并减少反射表面上光的镜面锯齿。但是,由于“平滑”效果非常明显,因此有些人更喜欢其他方法。

以下是对 Bevy 支持的每种抗锯齿方法的优点和缺点的快速概览

  • 多重采样抗锯齿 (MSAA)
    • 在平滑网格边缘(抗几何锯齿)方面做得很好。对镜面锯齿没有帮助。性能成本随三角形数量而变化,在具有许多三角形的场景中表现非常差
  • 快速近似抗锯齿 (FXAA)
    • 在处理几何和镜面锯齿方面做得相当好。在所有场景中性能成本都很低。结果有点模糊且质量低
  • 时间抗锯齿 (TAA)
    • 在处理几何和镜面锯齿方面做得非常好。在处理时间锯齿方面做得很好,其中高频细节随着时间的推移或随着您移动相机或事物动画而闪烁。性能成本适中,仅随屏幕分辨率而变化。可能会出现“重影”,其中网格或灯光效果可能会留下随着时间的推移而逐渐消失的轨迹。虽然 TAA 有助于减少时间锯齿,但它也可能会引入额外的时间锯齿,尤其是在远处渲染的薄几何体或纹理细节上。需要 2 视图的额外 GPU 内存,以及启用运动向量和深度预处理。需要准确的运动向量和深度预处理,这会使自定义材质变得复杂

TAA 实现是一系列权衡,并且依赖于易于出错的启发式方法。在 Bevy 0.11 中,TAA 被标记为一项实验性功能,原因如下

  • TAA 目前不适用于以下 Bevy 功能:蒙皮、变形目标和视差贴图
  • TAA 目前往往会稍微柔化图像,可以通过后期处理锐化来解决
  • 我们的 TAA 启发式方法目前不可用户配置(这些启发式方法可能会发生变化和发展)

我们将在未来的版本中继续改进质量、兼容性和性能。如果您遇到任何错误,请报告!

您可以在 Bevy 改进的 抗锯齿示例 中比较我们所有的抗锯齿方法。

鲁棒对比度自适应锐化 #

作者:@Elabajaba

TAA 和 FXAA 等效果会导致最终渲染变得模糊。锐化后期处理效果可以帮助抵消这种情况。在 Bevy 0.11 中,我们添加了 AMD 的鲁棒对比度自适应锐化 (RCAS) 的端口。

拖动此图像进行比较

TAATAA+RCAS

请注意,头盔皮革部分的纹理要清晰得多!

变形目标 #

作者:@nicopap、@cart

Bevy 自 0.7 版本以来支持 3D 动画。

但它只支持 骨骼 动画。在人行道上留下了一种常见的动画类型,称为 变形目标(又名混合形状、关键形状,以及一大堆其他名称)。这是所有 3D 角色动画的鼻祖!Crash Bandicoot 的跑步循环使用了变形目标。

角色模型由 Samuel Rosario (© 保留所有权利)提供,经许可使用。由 nicopap 修改,使用 Blender Studios 的 Snow 角色纹理,由 Demeter Dzadik (🅯 CC-BY) 提供。

如今,动画师通常会使用骨骼绑定来进行大幅度移动,并使用变形目标来完善细节动作。

然而,在游戏资产方面,艺术家用于面部和手的复杂骨骼绑定过于沉重。通常,姿势会被“烘焙”到变形姿势中,并且面部表情转换会在引擎中通过变形目标进行处理。

变形目标是一种非常简单的动画方法。获取模型,获得基础顶点位置,移动顶点以创建多个姿势

默认

A wireframe rendering of a character's face with a neutral expression

皱眉

Wireframe rendering of a frowning character

狡黠一笑

Wireframe rendering of a smirking character

将这些姿势存储为基础网格与变体姿势之间的差异,然后在运行时,混合 每个姿势。现在,有了与基础网格的差异,我们可以通过简单地将基础顶点位置添加起来获得变体姿势。

就是这样,变形目标着色器看起来像这样

fn morph_vertex(vertex: Vertex) {
    for (var i: u32 = 0u; i < pose_count(); i++) {
        let weight = weight_for_pose(i);
        vertex.position += weight * get_difference(vertex.index, position_offset, i);
        vertex.normal += weight * get_difference(vertex.index, normal_offset, i);
    }
}

在 Bevy 中,我们将每个姿势的权重存储在 MorphWeights 组件中。

fn set_weights_system(mut morph_weights: Query<&mut MorphWeights>) {
    for mut entity_weights in &mut morph_weights {
        let weights = entity_weights.weights_mut();

        weights[0] = 0.5;
        weights[1] = 0.25;
    }
}

现在假设我们有两个变形目标,(1) 皱眉姿势,(2) 狡黠一笑姿势

[0.0, 0.0]

默认姿势

Neutral face expression

[1.0, 0.0]

仅皱眉

Frowning

[0.0, 1.0]

仅狡黠一笑

Smirking

[0.5, 0.0]

半皱眉

Slightly frowning

[1.0, 1.0]

两者都达到最大值

Making faces

[0.5, 0.25]

两者都有点

Slightly frowning/smirking

虽然概念很简单,但它需要向 GPU 传输大量数据。成千上万个顶点,每个 288 位,多个模型变体,有时甚至多达一百个。

我们将顶点数据存储为 3D 纹理中的像素。这使得变形目标不仅可以在 WebGPU 上运行,还可以使用 WebGL2 wgpu 后端运行。

可以通过多种方式改进,但对于初始实现已经足够了。

视差贴图 #

作者:@nicopap

Bevy 现在支持视差贴图和深度图。视差贴图在赋予材质“深度感”方面让法线贴图相形见绌。这段视频的上半部分使用视差贴图加上法线贴图,而下半部分只使用法线贴图。

地球视图、海拔和夜间视图由 NASA 提供(公有领域)

注意,不仅仅是像素的阴影在改变,它们在屏幕上的实际位置也在改变。山顶隐藏了它们背后的山脊。高山比沿海地区移动得更快。

视差贴图根据几何体表面上的透视和深度移动像素。为平面添加真正的 3D 深度。

所有这一切,都没有向几何体添加任何顶点。整个地球只有 648 个顶点。与更原始的着色器(如置换贴图)不同,视差贴图只需要一个额外的灰度图像,称为 depth_map

游戏经常使用视差贴图来模拟鹅卵石或砖墙,所以让我们在 Bevy 中创建一个砖墙!首先,我们生成一个网格。

commands.spawn(PbrBundle {
    mesh: meshes.add(shape::Box::new(30.0, 10.0, 1.0).into()),
    material: materials.add(StandardMaterial {
        base_color: Color::WHITE,
        ..default()
    }),
    ..default()
});

A 3D desert scene with two flat white walls and a pebble path winding between them

当然,它只是一个扁平的白色盒子,我们没有添加任何纹理。所以让我们添加一个法线贴图。

normal_map_texture: Some(assets.load("normal_map.png")),

The same scene with normal maps

这要好得多。阴影也会根据光线方向而改变!但是,角落的镜面高光过于强烈,几乎很吵。

让我们看看深度图如何帮助我们。

depth_map: Some(assets.load("depth_map.png")),

The same scene with a depth texture

我们消除了噪音!还有一种令人想起 90 年代游戏预渲染过场动画的甜蜜的 3D 感觉。

那么,到底发生了什么,为什么视差贴图消除了墙上的难看的镜面光呢?

这是因为视差贴图将砖块之间的脊线内缩,这样它们就会被砖块本身遮挡。

Illustration of the previous paragraph

由于法线贴图不会“移动”阴影区域,只是用不同的方式对它们进行阴影处理,所以我们得到了那些奇怪的镜面高光。使用视差贴图,它们消失了。

拖动此图像进行比较

Normals OnlyParallax & Normal Mapping

Bevy 中的视差贴图仍然非常有限。最痛苦的一点是它不是标准的 glTF 特性,这意味着如果材质来自 GLTF 文件,则需要以编程方式将深度纹理添加到材质中。

此外,视差贴图与时间抗锯齿着色器不兼容,在曲面上效果不佳,并且不会影响对象的轮廓。

然而,这些并不是视差贴图的根本限制,将来可能会得到解决。

天空盒 #

作者:@JMS55,@superdump

skybox

Bevy 现在内置支持将 HDRI 环境显示为场景背景。

只需将新的 Skybox 组件附加到您的 Camera。它与现有的 EnvironmentMapLight 配合良好,后者将使用环境贴图来照亮场景。

我们还计划在将来添加对内置程序化天空盒的支持!

WebGPU 支持 #

作者:@mockersf,Bevy 开发过程中的许多其他人员

webgpu

Bevy 现在支持在 Web 上进行 WebGPU 渲染(除了 WebGL 2)。WebGPU 支持仍在不断推出,但是如果您拥有 支持的 Web 浏览器,您可以探索我们新的 实时 WebGPU 示例 页面。

什么是 WebGPU? #

WebGPU 是一个 令人兴奋的新 Web 标准,用于进行现代 GPU 图形和计算。它从 Vulkan、Direct3D 12 和 Metal 中汲取灵感。事实上,它通常在这些 API 之上实现。WebGPU 使我们能够访问比 WebGL2 更多的 GPU 功能(例如计算着色器),并且也有可能更快。这意味着 Bevy 的更多原生渲染器功能现在也适用于 Web。它还使用新的 WGSL 着色器语言。我们对 WGSL 随着时间的推移而发展感到非常满意,并且 Bevy 在内部使用它作为我们的着色器。我们还添加了诸如导入之类的可用性功能!但是使用 Bevy,您仍然可以选择使用 GLSL(如果您更喜欢)。

工作原理 #

Bevy 建立在 wgpu 库的基础之上,wgpu 是一种现代的低级 GPU API,可以针对几乎所有流行的 API:Vulkan、Direct3D 12、Metal、OpenGL、WebGL2 和 WebGPU。会为给定平台选择最佳的后台 API。它是一种“原生”渲染 API,但它通常遵循 WebGPU 的术语和 API 设计。与 WebGPU 不同,它可以提供对原生 API 的直接访问,这意味着 Bevy 处于“集所有优点于一身”的境地

WebGPU 示例 #

点击下面的图像之一查看我们的实时 WebGPU 示例(如果您的 浏览器支持)。

webgpu examples

改进的着色器导入 #

作者:@robtfm

Bevy 的渲染引擎有很多很棒的选项和功能。例如,PBR StandardMaterial 管道支持桌面/webgpu 和 webgl,6 个可选的网格属性,4 个可选的纹理,以及大量可选的功能,例如雾、蒙皮和 Alpha 混合模式,并且每个版本都会添加更多功能。

许多功能组合需要专门的着色器变体,并且总共超过 3000 行着色器代码分布在 50 个文件中,基于文本替换的着色器处理器开始不堪重负。

在这个版本中,我们已切换到使用 naga_oil,它为我们提供了一个基于模块的着色器框架。它将每个文件单独编译为 naga 的 IR,然后根据需要将它们组合成最终的着色器。这还没有太大可见影响,但它确实带来了一些直接的好处。

  • 引擎的着色器代码更易于导航,也更不神奇。以前只有一个全局作用域,因此即使是间接导入的项目也可以引用。这有时会使查找引用背后的实际代码变得困难。现在必须显式导入项目,因此您始终可以通过查看当前文件来了解变量或函数的来源。
    imported items
  • 着色器现在具有代码段报告功能,错误会将您指向着色器文件和行号,从而避免了在复杂的着色器代码库中进行大量查找。
    codespan
  • naga_oil 的预处理器支持更多条件指令,您可以使用 #else if#else ifndef 以及 #else ifdef(以前支持)。
  • 函数、变量和结构体都有适当的作用域,因此着色器文件不需要使用全局唯一名称来避免冲突。
  • 着色器定义可以直接添加到模块中。例如,任何导入 bevy_pbr::mesh_view_types 的着色器现在都会自动定义 MAX_DIRECTIONAL_LIGHTS,不再需要记住为每个使用该模块的新管道添加它。

未来的可能性更加令人兴奋。使用 naga IR 打开了一系列我们希望在未来版本中引入的优秀功能的大门。

  • 自动绑定槽分配将允许插件扩展核心视图绑定组,这意味着用于照明和阴影方法、常见材质属性等功能的独立插件变得可行。这将使我们能够模块化核心管道,使代码库的增长(同时保持对多个目标的支持)更加可持续。
  • “虚拟”着色器函数将允许用户修改核心函数(例如照明),并可能导致模板风格的材质系统,用户可以在其中提供将在管道中正确点调用的“钩子”。
  • 语言互操作性:混合使用 glsl 和 wgsl,以便从您的 glsl 材质着色器访问 Bevy 的 pbr 管道功能,或者为 glsl 编写的实用程序可以在 wgsl 代码中使用。我们希望这也能扩展到 spirv(和 rust-gpu)。
  • 我们还没有想到的其他更酷的事情。能够在运行时检查和修改着色器非常强大,并使许多事情成为可能!

UI 节点边框 #

作者:@ickshonpe

UI 节点现在会绘制边框,其颜色可以使用新的 BorderColor 组件进行配置。

borders

commands.spawn(ButtonBundle {
    style: Style {
        border: UiRect::all(Val::Px(5.0)),
        ..default()
    },
    border_color: BorderColor(Color::rgb(0.9, 0.9, 0.9)),
    ..default()
})

边框的每一侧都可以配置。

border sides

网格 UI 布局 #

作者:@nicoburns

在 Bevy UI 中,我们连接了我们使用的布局库(Taffy)中的新 grid 功能。这使 CSS 风格的网格布局成为可能。

grid

这可以在 Style 组件上进行配置。

Style {
    /// Use grid layout for this node
    display: Display::Grid,
    /// Make the grid have a 1:1 aspect ratio
    /// This means the width will adjust to match the height
    aspect_ratio: Some(1.0),
    // Add 24px of padding around the grid
    padding: UiRect::all(Val::Px(24.0)),
    /// Set the grid to have 4 columns all with sizes minmax(0, 1fr)
    /// This creates 4 exactly evenly sized columns
    grid_template_columns: RepeatedGridTrack::flex(4, 1.0),
    /// Set the grid to have 4 rows all with sizes minmax(0, 1fr)
    /// This creates 4 exactly evenly sized rows
    grid_template_rows: RepeatedGridTrack::flex(4, 1.0),
    /// Set a 12px gap/gutter between rows and columns
    row_gap: Val::Px(12.0),
    column_gap: Val::Px(12.0),
    ..default()
},

以调度为中心的 ECS API #

作者:@cart

Bevy 0.10 中,我们引入了 ECS 调度 V3,它极大地改进了 Bevy ECS 系统调度的功能:调度程序 API 人体工程学、系统链接、在调度中的任何点运行独占系统和应用延迟系统操作的能力、单一统一调度、可配置的系统集、运行条件以及更好的状态系统。

但是很快就很明显,新系统仍然有一些需要改进的地方。

消除复杂性 #

如果您在试图理解这一点时开始感到眼花缭乱,或者诸如“隐式添加到 CoreSet::Update 基本集”之类的短语让您感到恐惧... 别担心。经过 深思熟虑,我们已经消除了复杂性,构建了清晰简单的东西。

Bevy 0.11中,“调度模型”变得更加简单,得益于先调度 ECS API

app
    .add_systems(Startup, (a, b))
    .add_systems(Update, (c, d, e))
    .add_systems(FixedUpdate, (f, g))
    .add_systems(PostUpdate, h)
    .add_systems(OnEnter(AppState::Menu), enter_menu)
    .add_systems(OnExit(AppState::Menu), exit_menu)
  • 调度系统有一种方式
    • 调用add_systems,指定调度名称,并指定一个或多个系统
  • 为了方便,基集已被完全移除,取而代之的是调度,它们拥有友好简洁的名称
    • 例如:CoreSet::Update基集已变成Update
  • 不再存在隐式或默认的配置
    • 默认调度和默认基集不存在
  • 语法直观易懂,使用方便
    • 调度位于首位,因此在格式化时它们会“排成一行”
为了比较,请展开此内容查看旧版本!
app
    // Startup system variant 1.
    // Has an implied default StartupSet::Startup base set
    // Has an implied CoreSchedule::Startup schedule
    .add_startup_systems((a, b))
    // Startup system variant 2.
    // Has an implied default StartupSet::Startup base set
    // Has an implied CoreSchedule::Startup schedule
    .add_systems((a, b).on_startup())
    // Startup system variant 3.
    // Has an implied default StartupSet::Startup base set
    .add_systems((a, b).in_schedule(CoreSchedule::Startup))
    // Update system variant 1.
    // `CoreSet::Update` base set and `CoreSchedule::Main` are implied
    .add_system(c)
    // Update system variant 2 (note the add_system vs add_systems difference)
    // `CoreSet::Update` base set and `CoreSchedule::Main` are implied
    .add_systems((d, e))
    // No implied default base set because CoreSchedule::FixedUpdate doesn't have one
    .add_systems((f, g).in_schedule(CoreSchedule::FixedUpdate))
    // `CoreSchedule::Main` is implied, in_base_set overrides the default CoreSet::Update set
    .add_system(h.in_base_set(CoreSet::PostUpdate))
    // This has no implied default base set
    .add_systems(enter_menu.in_schedule(OnEnter(AppState::Menu)))
    // This has no implied default base set
    .add_systems(exit_menu.in_schedule(OnExit(AppState::Menu)))

请注意,普通“系统集”仍然存在!您仍然可以使用集来组织和排序您的系统

app.add_systems(Update, (
    (walk, jump).in_set(Movement),
    collide.after(Movement),
))

configure_set API 也已调整,以实现一致性

// Bevy 0.10
app.configure_set(Foo.after(Bar).in_schedule(PostUpdate))
// Bevy 0.11
app.configure_set(PostUpdate, Foo.after(Bar))

嵌套系统元组和链式调用 #

作者:@cart

现在可以无限嵌套 .add_systems 调用中的系统元组!

app.add_systems(Update, (
    (a, (b, c, d, e), f),
    (g, h),
    i
))

乍一看,这似乎没什么用处。但与每个元组的配置结合使用,它可以让您轻松简洁地表达调度

app.add_systems(Update, (
    (attack, defend).in_set(Combat).before(check_health),
    check_health,
    (handle_death, respawn).after(check_health)
))

.chain() 也已适应,以支持任意嵌套!上面的示例中的排序可以这样重新描述

app.add_systems(Update,
    (
        (attack, defend).in_set(Combat),
        check_health,
        (handle_death, respawn)
    ).chain()
)

这将首先(并行地)运行 attackdefend,然后运行 check_health,最后(并行地)运行 handle_deathrespawn

这允许进行强大且表达力强的“图形化”排序表达

app.add_systems(Update,
    (
        (a, (b, c, d).chain()),
        (e, f),
    ).chain()
)

这将并行运行 ab->c->d,然后在它们完成运行后,并行运行 ef

Gizmos #

作者:@devil-ira、@mtsr、@aevyrie、@jannik4、@lassade、@The5-1、@Toqozz、@nicopap

能够在 2D 和 3D 中绘制简单的形状和线条对于编辑器控件和调试视图非常有用。游戏开发是一项非常“空间”的工作,能够快速绘制形状就如同“打印调试信息”的视觉等效。它有助于回答诸如“这根射线是否在正确的方向上发射?”和“这个碰撞体是否足够大?”等问题。

Bevy 0.11中,我们添加了一个“立即模式”Gizmos 绘制 API,使这些操作变得简单而高效。在 2D 和 3D 中,您可以绘制线条、矩形、圆形、圆弧、球体、立方体、线段以及更多!

2D Gizmos 2d gizmos 3D Gizmos 3d gizmos

您可以从任何系统中生成形状(用于 2D 和 3D)

fn system(mut gizmos: Gizmos) {
    // 2D
    gizmos.line_2d(Vec2::new(0., 0.), Vec2::new(0., 10.), Color::RED);
    gizmos.circle_2d(Vec2::new(0., 0.), 40., Color::BLUE);
    // 3D
    gizmos.circle(Vec3::ZERO, Vec3::Y, 3., Color::BLACK);
    gizmos.ray(Vec3::new(0., 0., 0.), Vec3::new(5., 5., 5.), Color::BLUE);
    gizmos.sphere(Vec3::ZERO, Quat::IDENTITY, 3.2, Color::BLACK)
}

由于 API 是“立即模式”,因此 gizmos 仅在“排队”的帧上绘制,这意味着您无需担心清理 gizmo 状态!

Gizmos 以批次绘制,这意味着它们非常便宜。您可以拥有数十万个 gizmos!

ECS 音频 API #

作者:@inodentry

Bevy 的音频播放 API 已重新设计,以更好地与 Bevy 的 ECS 集成。

在之前的 Bevy 版本中,您会这样播放音频

#[derive(Resource)]
struct MyMusic {
    sink: Handle<AudioSink>,
}

fn play_music(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    audio: Res<Audio>,
    audio_sinks: Res<Assets<AudioSink>>
) {
    let weak_handle = audio.play(asset_server.load("my_music.ogg"));
    let strong_handle = audio_sinks.get_handle(weak_handle);
    commands.insert_resource(MyMusic {
        sink: strong_handle,
    });
}

这对于仅仅播放一个声音来说,实在是太多样板代码了!然后为了调整播放,您会像这样访问 AudioSink


fn pause_music(my_music: Res<MyMusic>, audio_sinks: Res<Assets<AudioSink>>) {
    if let Some(sink) = audio_sinks.get(&my_music.sink) {
        sink.pause();
    }
}

将音频播放视为资源导致了许多问题,尤其是在 Bevy 场景等方面表现不佳。在Bevy 0.11中,音频播放被表示为一个 Entity,它具有 AudioBundle 组件

#[derive(Component)]
struct MyMusic;

fn play_music(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        AudioBundle {
            source: asset_server.load("my_music.ogg"),
            ..default()
        },
        MyMusic,
    ));
}

mode 字段位于 PlaybackSettings 结构体中,提供了一种直接管理这些音频实体生命周期的方式。

通过传递一个 PlaybackMode,您可以选择是播放一次还是重复播放,分别使用 OnceLoop。如果您预计音频可能会再次播放,则可以通过使用 Despawn 暂时卸载它来节省资源,或者如果它是单次效果则通过使用 Remove 立即释放其内存。

AudioBundle {
    source: asset_server.load("hit_sound.ogg"),
    settings: PlaybackSettings {
        mode: PlaybackMode::Despawn,
        ..default()
    }
}

简单多了!为了调整播放,您可以查询 AudioSink 组件

fn pause_music(query_music: Query<&AudioSink, With<MyMusic>>) {
    if let Ok(sink) = query.get_single() {
        sink.pause();
    }
}

全局音频音量 #

作者:@mrchantey

Bevy 现在有一个全局音量级别,可以通过 [GlobalVolume] 资源进行配置

app.insert_resource(GlobalVolume::new(0.2));

场景中的资源支持 #

作者:@Carbonhell、@Davier

Bevy 的场景格式是将游戏状态序列化和反序列化到场景文件中的非常有用的工具。

以前,捕获的状态仅限于实体及其组件。在Bevy 0.11中,场景现在也支持序列化资源。

这在场景格式中添加了一个新的 resources 字段

(
    resources: {
        "my_game::stats::TotalScore": (
            score: 9001,
        ),
    },
    entities: {
        // Entity scene data...
    },
)

场景过滤 #

作者:@MrGVSV

将数据序列化到场景时,所有组件和 资源 默认情况下都会被序列化。在之前的版本中,您必须使用给定的 TypeRegistry 作为过滤器,排除您不想包含的类型。

在 0.11 中,现在有一个专门的 SceneFilter 类型,使过滤变得更简单、更干净、更直观。它可以与 DynamicSceneBuilder 一起使用,以对实际序列化的内容进行细粒度控制。

我们可以 allow 一部分类型

let mut builder = DynamicSceneBuilder::from_world(&world);
let scene = builder
    .allow::<ComponentA>()
    .allow::<ComponentB>()
    .extract_entity(entity)
    .build();

或者 deny 它们

let mut builder = DynamicSceneBuilder::from_world(&world);
let scene = builder
    .deny::<ComponentA>()
    .deny::<ComponentB>()
    .extract_entity(entity)
    .build();

默认字体 #

作者:@mockersf

Bevy 现在支持可配置的默认字体,并嵌入了一个微小的默认字体(Fira Mono 的最小版本)。如果您在整个项目中使用一种常用字体,这将非常有用。而且,它还可以更轻松地使用“占位符字体”来原型化新的更改,而无需担心在每个节点上设置它。

default font

UI 纹理图集支持 #

作者:@mwbryant

以前,UI ImageBundle 节点只能使用完整图像的句柄,而没有使用 UI 中 TextureAtlases 的便捷方法。在此版本中,我们添加了对 AtlasImageBundle UI 节点的支持,它将现有的 TextureAtlas 支持引入 UI。

这是通过合并现有的机制来实现的,该机制允许文本渲染选择要使用的字形以及允许使用 TextureAtlasSprite 的机制。

手柄震动 API #

作者:@johanhelsing、@nicopap

现在您可以使用 EventWriter<GamepadRumbleRequest> 系统参数来触发控制器的力反馈电机。

gilrs 是 Bevy 用于手柄支持的 crate,它允许控制力反馈电机。遗憾的是,在 Bevy 中没有简单的方法可以访问力反馈 API,而无需进行繁琐的簿记工作。

现在 Bevy 有了 GamepadRumbleRequest 事件来实现这一点。

fn rumble_system(
    gamepads: Res<Gamepads>,
    mut rumble_requests: EventWriter<GamepadRumbleRequest>,
) {
    for gamepad in gamepads.iter() {
        rumble_requests.send(GamepadRumbleRequest::Add {
            gamepad,
            duration: Duration::from_secs(5),
            intensity: GamepadRumbleIntensity::MAX,
        });
    }
}

GamepadRumbleRequest::Add 事件会触发力反馈电机,控制震动持续时间、要激活的电机以及震动强度。GamepadRumbleRequest::Stop 会立即停止所有电机。

新的默认色调映射方法 #

作者:@JMS55

Bevy 0.10中,我们使色调映射可配置,并添加了许多新的色调映射选项。在Bevy 0.11中,我们将默认的色调映射方法从“Reinhard 亮度”色调映射切换到“TonyMcMapface”

拖动此图像进行比较

Reinhard-luminanceTonyMcMapface

TonyMcMapface(由 Tomasz Stachowiak 创建)是一种更加中性的显示变换,它试图尽可能地接近输入“光线”。这有助于保留场景中的艺术选择。值得注意的是,亮度在整个光谱中都会发生去饱和(与 Reinhard 亮度不同)。与 Reinhard 亮度相比,它在与泛光结合使用时效果也更好。

EntityRef 查询 #

作者:@james7132

EntityRef 现在实现了 WorldQuery,这使得在 ECS 系统中更容易查询任意组件

fn system(query: Query<EntityRef>) {
    for entity in &query {
        if let Some(mesh) = entity.get::<Handle<Mesh>>() {
            let transform = entity.get::<Transform>().unwrap();
        }
    }
}

请注意,EntityRef 查询默认情况下会访问整个 World 中的每个实体和每个组件。这意味着它们将与任何“可变”查询冲突

/// These queries will conflict, making this system invalid
fn system(query: Query<EntityRef>, mut enemies: Query<&mut Enemy>) { }

为了解决冲突(或减少访问的实体数量),您可以添加过滤器

/// These queries will not conflict
fn system(
    players: Query<EntityRef, With<Player>>,
    mut enemies: Query<&mut Enemy, Without<Player>>
) {
    // only iterates players
    for entity in &players {
        if let Some(mesh) = entity.get::<Handle<Mesh>>() {
            let transform = entity.get::<Transform>().unwrap();
        }
    }
}

请注意,直接查询所需的组件通常仍然更符合人体工程学(而且更有效率)

fn system(players: Query<(&Transform, &Handle<Mesh>), With<Player>>) {
    for (transform, mesh) in &players {
    }
}

截图 API #

作者:@TheRawMeatball

Bevy 现在有一个简单的截图 API,可以将给定窗口的截图保存到磁盘

fn take_screenshot(
    mut screenshot_manager: ResMut<ScreenshotManager>,
    input: Res<Input<KeyCode>>,
    primary_window: Query<Entity, With<PrimaryWindow>>,
) {
    if input.just_pressed(KeyCode::Space) {
        screenshot_manager
            .save_screenshot_to_disk(primary_window.single(), "screenshot.png")
            .unwrap();
    }
}

RenderTarget::TextureView #

作者:@mrchantey

现在,可以将 Camera RenderTarget 设置为 wgpu TextureView。这允许第三方 Bevy 插件管理 Camera 的纹理。这种方法启用的一种特别有趣的用例是 XR/VR 支持。一些社区成员已经 证明了这一点!

改进的文本换行 #

作者:@ickshonpe

之前的 Bevy 版本没有正确换行文本,因为它在计算布局之前计算了实际文本。Bevy 0.11 添加了一个“文本测量步骤”,它在布局之前计算文本大小,然后在布局之后计算实际文本。

text wrap

BreakLineOn 设置中还有一个新的 NoWrap 变体,当需要时可以完全禁用文本换行。

更快的 UI 渲染批处理 #

作者:@ickshonpe

我们通过在纹理发生改变但下一个节点没有纹理的情况下避免拆分 UI 批次,获得了巨大的 UI 性能提升。

这是我们“许多按钮”压力测试的性能分析。红色表示优化之前,黄色表示优化之后

ui profile

更好的反射代理 #

作者:@MrGVSV

Bevy 的反射 API 有几个结构体,它们统称为“动态”类型。其中包括 DynamicStructDynamicTuple 等等,它们用于在运行时动态构造任何形状或形式的类型。这些类型也用于创建通常称为“代理”的类型,这些类型是用于表示实际具体类型的动态类型。

这些代理是 Reflect::clone_value 方法的强大之处,该方法在幕后生成这些代理,以便构造数据的运行时克隆。

不幸的是,这会导致一些难以察觉的陷阱,可能会让用户感到意外,例如代理的哈希值与它们所代表的具体类型的哈希值不同,代理不被视为等效于它们的具体对应物,等等。

虽然此版本不一定修复了这些问题,但它确实为将来修复这些问题奠定了坚实的基础。它是通过改变代理的定义方式来实现的。

在 0.11 之前,代理仅仅通过克隆具体类型的Reflect::type_name 字符串并将其作为自身的 Reflect::type_name 返回来定义。

现在在 0.11 中,代理通过复制对具体类型静态TypeInfo 的引用来定义。这将允许我们动态地访问更多具体类型的类型信息,而无需 TypeRegistry。在未来的版本中,我们将利用这一点,将哈希和比较策略直接存储在 TypeInfo 中,以缓解上面提到的代理问题。

FromReflect 人体工程学 #

作者:@MrGVSV

Bevy 的反射 API 通常使用类型擦除的 dyn Reflect 特性对象传递数据。这通常可以通过 <dyn Reflect>::downcast_ref::<T> 将其向下转换为其具体类型;但是,如果底层数据已转换为“动态”表示(例如,结构体类型的 DynamicStruct,列表类型的 DynamicList 等等),则此方法无效。

let data: Vec<i32> = vec![1, 2, 3];

let reflect: &dyn Reflect = &data;
let cloned: Box<dyn Reflect> = reflect.clone_value();

// `reflect` really is a `Vec<i32>`
assert!(reflect.is::<Vec<i32>>());
assert!(reflect.represents::<Vec<i32>>());

// `cloned` is a `DynamicList`, but represents a `Vec<i32>`
assert!(cloned.is::<DynamicList>());
assert!(cloned.represents::<Vec<i32>>());

// `cloned` is equivalent to the original `reflect`, despite not being a `Vec<i32>`
assert!(cloned.reflect_partial_eq(reflect).unwrap_or_default());

为了解决这个问题,可以使用FromReflect 特性将任何 dyn Reflect 特性对象转换回其具体类型——无论它实际上是否是该类型,还是它的动态表示。它甚至可以使用ReflectFromReflect 类型数据动态调用。

在 0.11 之前,用户必须手动为需要它的每个类型派生 FromReflect,并手动注册 ReflectFromReflect 类型数据。这使得它使用起来很麻烦,也意味着它经常被遗忘,导致下游用户在进行反射转换时遇到困难。

现在在 0.11 中,FromReflect 会自动派生,并且 ReflectFromReflect 会自动为所有派生 Reflect 的类型注册。这意味着大多数类型默认情况下将具有 FromReflect 能力,从而减少了样板代码,并增强了围绕 FromReflect 的逻辑。

用户仍然可以通过在类型上添加#[reflect(from_reflect = false)] 属性来选择退出此行为。

#[derive(Reflect)]
struct Foo;

#[derive(Reflect)]
#[reflect(from_reflect = false)]
struct Bar;

fn test<T: FromReflect>(value: T) {}

test(Foo); // <-- OK!
test(Bar); // <-- ERROR! `Bar` does not implement trait `FromReflect`

Deref 派生属性 #

作者:@MrGVSV

Bevy 代码倾向于大量使用newtype 模式,这就是我们为DerefDerefMut 提供专门派生的原因。

这以前只适用于只有一个字段的结构体。

#[derive(Resource, Deref, DerefMut)]
struct Score(i32);

对于 0.11,我们通过添加 #[deref] 属性改进了这些派生,这允许它们用于具有多个字段的结构体。这使得使用泛型 newtype 变得更容易。

#[derive(Component, Deref, DerefMut)]
struct Health<T: Character> {
    #[deref] // <- use the `health` field as the `Deref` and `DerefMut` target
    health: u16,
    _character_type: PhantomData<T>,
}

更简单的 RenderGraph 构建 #

作者:@IceSentry,@cart

Node 添加到 RenderGraph 需要大量的样板代码。在这个版本中,我们尝试为大多数常见操作减少样板代码。没有移除现有的 API,这些只是帮助简化使用 RenderGraph 的辅助函数。

我们在 App 中添加了 RenderGraphApp 特性。此特性包含各种辅助函数,以减少向图形添加节点和边的样板代码。

RenderGraph Node 的另一个痛点是通过每个节点传递视图实体,并手动更新该视图上的查询。为了解决这个问题,我们添加了 ViewNode 特性和 ViewNodeRunner,它们将自动处理在视图实体上运行 Query。我们还将视图实体作为 RenderGraph 的一等公民概念。因此,您现在可以在图形的任何地方访问正在运行的视图实体,而无需在每个 Node 之间传递它。

所有这些新 API 都假设您的 Node 实现了 FromWorldDefault

以下是 BloomNode 的实际应用示例。

// Adding the node to the 3d graph
render_app
    // To run a ViewNode you need to create a ViewNodeRunner
    .add_render_graph_node::<ViewNodeRunner<BloomNode>>(
        CORE_3D,
        core_3d::graph::node::BLOOM,
    );

// Defining the node
#[derive(Default)]
struct BloomNode;
// This can replace your `impl Node` block of any existing `Node` that operated on a view
impl ViewNode for BloomNode {
    // You need to define your view query as an associated type
    type ViewQuery = (
        &'static ExtractedCamera,
        &'static ViewTarget,
        &'static BloomSettings,
    );
    // You don't need Node::input() or Node::update() anymore. If you still need these they are still available but they have an empty default implementation.
    fn run(
        &self,
        graph: &mut RenderGraphContext,
        render_context: &mut RenderContext,
        // This is the result of your query. If it is empty the run function will not be called
        (camera, view_target, bloom_settings): QueryItem<Self::ViewQuery>,
        world: &World,
    ) -> Result<(), NodeRunError> {
        // When using the ViewNode you probably won't need the view entity but here's how to get it if you do
        let view_entity = graph.view_entity();

        // Run the node
    }
}

#[reflect(default)] 用于枚举变体字段 #

作者:@MrGVSV

使用 FromReflect 特性时,如果标记为 #[reflect(default)] 的字段在反射对象中不存在,则它们将被设置为其 Default 值。

以前,这仅支持结构体字段。现在,它也支持所有枚举变体字段。

#[derive(Reflect)]
enum MyEnum {
    Data {
        #[reflect(default)]
        a: u32,
        b: u32,
    },
}

let mut data = DynamicStruct::default ();
data.insert("b", 1);

let dynamic_enum = DynamicEnum::new("Data", data);

let my_enum = MyEnum::from_reflect( & dynamic_enum).unwrap();
assert_eq!(u32::default(), my_enum.a);

延迟的资产热重载 #

作者:@JMS55

Bevy 现在会在“文件系统上资产更改”事件之后等待 50 毫秒,然后再重新加载资产。在没有延迟的情况下重新加载会导致某些系统上读取无效的资产内容。等待时间是可配置的。

自定义 glTF 顶点属性 #

作者:@komadori

现在可以从 glTF 文件加载具有自定义顶点属性的网格。自定义属性可以映射到 Bevy 的MeshVertexAttribute 格式,该格式由Mesh 类型在GltfPlugin 设置中使用。这些属性然后可以在 Bevy 着色器中使用。有关示例,请查看我们的新示例

custom vertex attribute

稳定 TypePath #

作者:@soqb,@tguichaoua

Bevy 历来使用std::any::type_name 在许多地方使用友好的名称来标识 Rust 类型:Bevy Reflect、Bevy Scenes、Bevy Assets、Bevy ECS 等等。不幸的是,Rust 并没有对type_name 的稳定性或格式做出任何保证,这使得在理论上构建它很危险(尽管在实践中它迄今为止一直很稳定)。

也没有内置的方法来检索类型名称的“部分”。如果您想要简短名称,没有内部类型的泛型类型的名称,模块名称或包名称,则必须对type_name 进行字符串操作(这可能容易出错/非平凡)。

此外,type_name 无法自定义。在某些情况下,作者可能会选择使用除完整模块路径以外的其他内容来标识类型(例如,如果他们更喜欢更短的路径,或者想要抽象出私有/内部模块)。

出于这些原因,我们开发了一个新的稳定TypePath,它会自动为所有派生Reflect 的类型实现。此外,它可以在没有派生Reflect 的情况下手动派生。

mod my_mod {
    #[derive(Reflect)]
    struct MyType;
}

/// prints: "my_crate::my_mod::MyType"
println!("{}", MyType::type_path());
/// prints: "MyType"
println!("{}", MyType::short_type_path());
/// prints: "my_crate"
println!("{}", MyType::crate_name().unwrap());
/// prints: "my_crate::my_mod"
println!("{}", MyType::module_path().unwrap());

这对泛型也有效,这可能很方便。

// prints: "Option<MyType>"
println!("{}", Option::<MyType>::short_type_path());
// prints: "Option"
println!("{}", Option::<MyType>::type_ident().unwrap());

TypePath 可以由类型作者自定义。

#[derive(TypePath)]
#[type_path = "some_crate::some_module"]
struct MyType;

我们正在将 Bevy 的内部type_name 使用移植到TypePath 上,这将在Bevy 0.12中实现。

系统元组的 run_if #

作者:@geieredgar

现在可以将“运行条件” 添加到系统元组。

app.add_systems(Update, (run, jump).run_if(in_state(GameState::Playing)))

这将精确地评估一次“运行条件”,并将结果用于元组中的每个系统。

这使我们能够移除状态的 OnUpdate 系统集(以前用于在处于给定状态时运行系统组)。

Has 查询 #

作者:@wainwrightmark

您现在可以在查询中使用 Has<Component>,如果该组件存在则返回 true,如果不存在则返回 false。

fn system(query: Query<Has<Player>>) {
    for has_player in &query {
        if has_player {
            // do something
        }
    }
}

派生 Event #

作者:@CatThingy

Bevy 的Event 特性现在是派生的,而不是为所有内容自动实现的。

#[derive(Event)]
struct Collision {
    a: Entity,
    b: Entity,
}

这可以防止某些类型的错误,使Event 类型更具自述性,并与其他 Bevy ECS 特性(如组件和资源)保持一致。它还为配置Event 存储类型打开了大门,我们计划在将来的版本中进行此操作。

三次贝塞尔曲线示例 #

作者:@Kjolnyr

一个展示如何绘制 3D 曲线以及如何沿路径移动对象的示例。

cubic_curve

尺寸约束示例 #

作者:@ickshonpe

一个交互式示例,展示了各种Style 尺寸约束如何影响 UI 节点。

size constraints

显示和可见性示例 #

作者:@ickshonpe

一个示例,展示了显示和可见性设置如何影响 UI 节点。

display and visibility

不再有 Bors! #

作者:@cart,@mockersf

Bevy 历来使用 Bors 合并系统来确保我们永远不会合并 GitHub 上破坏了 CI 验证的拉取请求。这是确保我们能够安全有效地协作的关键基础设施。幸运的是,GitHub 终于推出了合并队列,它解决了与 Bors 相同的问题,但好处是与 GitHub 集成得更加紧密。

在这个发布周期中,我们迁移到了合并队列,我们对体验非常满意!

新的 CI 作业 #

作者:@mockersf

我们添加了许多新的 CI 作业,以改善 Bevy 的开发体验。

  • 每天都会在真实的 Android 和 iOS 设备上运行 Bevy 的移动示例的作业!这有助于防止编译器可能无法捕获的回归。
  • 添加了在 CI 中截取屏幕截图的功能,这可以用来验证 Bevy 示例运行的结果。
  • 一项在缺少功能或示例文档更新的 PR 上留下 GitHub 评论的作业。

接下来是什么? #

我们有很多工作已经接近完成,因此很有可能在Bevy 0.12中发布。

  • Bevy Asset V2:一个全新的资产系统,它添加了“资产预处理”、可选的资产 .meta 文件、递归资产依赖关系跟踪和事件、异步资产 IO、更强大的资产句柄、更高效的资产存储以及各种可用性改进!这项工作已经接近完成。它几乎进入了 Bevy 0.11,但还需要更多时间来完善。
  • PBR 材质光透射:透射/屏幕空间折射允许模拟玻璃、塑料、液体和凝胶、宝石、蜡等材料。这一个也即将完成
  • TAA 改进:我们正在为 TAA 进行一些更改,这将提高其质量、速度和在引擎中的支持。
  • GPU 拾取:通过使用颜色 ID 在渲染中识别网格,在 GPU 上高效且准确地选择实体
  • 用于方向光和聚光灯阴影的 PCF减少阴影边缘的锯齿
  • UI 节点边框圆角和阴影:在您的 UI 节点上添加曲率和“投影阴影”
  • 延迟渲染:Bevy 已经通过为深度和法线提供可选的单独通道来实现“混合模式”正向渲染。我们目前正在尝试支持“完全延迟”渲染,这将为新效果和不同的性能折衷方案打开大门。

从高层次来看,我们计划在下个周期重点关注资产系统、UI、渲染功能和场景。

查看 Bevy 0.12 里程碑,了解目前正在考虑用于 Bevy 0.12 的最新工作列表。

支持 Bevy #

赞助有助于使我们对 Bevy 的工作可持续发展。如果您相信 Bevy 的使命,请考虑 赞助我们……每一份帮助都至关重要!

捐赠 爱心图标

贡献者 #

Bevy 是由 一大群人 制作的。非常感谢 166 位贡献者,他们使此次发布(以及相关的文档)成为可能!按随机顺序排列

  • @TheBlek
  • @hank
  • @TimJentzsch
  • @Suficio
  • @SmartManLudo
  • @BlondeBurrito
  • @lewiszlw
  • @paul-hansen
  • @boringsan
  • @superdump
  • @JonahPlusPlus
  • @airingursb
  • @Sheepyhead
  • @nakedible
  • @Testare
  • @andresovela
  • @SkiFire13
  • @doup
  • @BlackPhlox
  • @nicoburns
  • @wpederzoli
  • @adtennant
  • @LoopyAshy
  • @KernelFreeze
  • @ickshonpe
  • @jim-ec
  • @mrchantey
  • @frengor
  • @Joakker
  • @arendjr
  • @MJohnson459
  • @TheTacBanana
  • @IceSentry
  • @ItsDoot
  • @Anti-Alias
  • @mwbryant
  • @inodentry
  • @LiamGallagher737
  • @robtfm
  • @mockersf
  • @ndarilek
  • @samtenna
  • @Estus-Dev
  • @InnocentusLime
  • @p-hueber
  • @B-Reif
  • @Adamkob12
  • @payload
  • @JohnTheCoolingFan
  • @djeedai
  • @SludgePhD
  • @s-lambert
  • @kjolnyr
  • @Skovrup1
  • @Ababwa
  • @Illiux
  • @Carter0
  • @luca-della-vedova
  • @Neo-Zhixing
  • @coreh
  • @helvieq499
  • @Carbonhell
  • @BrandonDyer64
  • @hymm
  • @JMS55
  • @iiYese
  • @mtsr
  • @jannik4
  • @natasria
  • @Trouv
  • @minchopaskal
  • @chrisjuchem
  • @marlyx
  • @valaphee
  • @hankjordan
  • @rparrett
  • @Selene-Amanita
  • @opstic
  • @loganbenjamin
  • @MrGunflame
  • @pyrotechnick
  • @mjhostet
  • @VitalyAnkh
  • @CatThingy
  • @maniwani
  • @Themayu
  • @SET001
  • @jakobhellermann
  • @MrGVSV
  • @nicopap
  • @Wcubed
  • @aevyrie
  • @NiklasEi
  • @bonsairobo
  • @cart
  • @TotalKrill
  • @raffaeleragni
  • @Aceeri
  • @Shatur
  • @orzogc
  • @UncleScientist
  • @Elabajaba
  • @vyb
  • @komadori
  • @jnhyatt
  • @harudagondi
  • @konsti219
  • @james7132
  • @mvlabat
  • @neithanmo
  • @dgunay
  • @Shfty
  • @hate
  • @B-head
  • @MinerSebas
  • @chescock
  • @BorMor
  • @lupan
  • @CrazyRoka
  • @bzm3r
  • @Sixmorphugus
  • @JoJoJet
  • @eltociear
  • @gakit
  • @geieredgar
  • @tjamaan
  • @alice-i-cecile
  • @NoahShomette
  • @james-j-obrien
  • @tinrab
  • @Olle-Lukowski
  • @TheRawMeatball
  • @sarkahn
  • @RobWalt
  • @johanhelsing
  • @SneakyBerry
  • @beeryt
  • @Vrixyz
  • @wainwrightmark
  • @EliasPrescott
  • @konsolas
  • @ameknite
  • @Connor-McMillin
  • @Weibye
  • @SpecificProtagonist
  • @danchia
  • @vallrand
  • @atornity
  • @soqb
  • @devil-ira
  • @AnthonyKalaitzis
  • @yyogo
  • @NiseVoid
  • @gajop
  • @Gingeh
  • @zendril
  • @ezekg
  • @ickk
  • @Leonss23
  • @kellencataldo
  • @akappel
  • @hazelsparrow
  • @mattfbacon
  • @gianzellweger
  • @lakrsv
  • @laundmo

完整变更日志 #

渲染 #

音频 #

诊断 #

场景 #

变换 + 层级结构 #

Gizmo #

反射 #