Bevy 大气散射多相机渲染支持:AtmosphereBuffer 从 Resource 迁移到 Component 的完整指南
本文围绕 Bevy 的 AtmosphereBuffer 接口变更展开:解释 Atmosphere(大气散射)模块为何要改为逐相机持有 GPU 缓冲区、init_atmosphere_buffer 系统为何被移除,并结合当前仓库源码给出在渲染世界(Render World)中按组件查询 AtmosphereBuffer 的迁移写法。读完你可以理解 Bevy 大气散射渲染的逐相机数据流(提取 → 纹理/管线 → 绑定 → 渲染),并安全完成从旧版资源访问到新版组件查询的代码迁移。
一、本次变更的三个核心事实
官方迁移文档 atmosphere_multicam.md 声明了三件事,它们构成本次迁移的全部要点:
- Atmosphere 现在可以正确支持多个相机(Atmosphere now works correctly with multiple cameras),大多数用户无需做任何改动;
init_atmosphere_buffer系统已被移除;AtmosphereBuffer的类型从Resource变更为挂载在每个相机实体上的Component。如果你在渲染世界系统中曾把它当资源直接访问(Res<AtmosphereBuffer>),需要改为在相机实体上以组件形式查询(Query<&AtmosphereBuffer>或Option<&AtmosphereBuffer>)。
为什么“大多数用户无需改动”?因为绝大多数应用只在自己的游戏逻辑中使用 Atmosphere、AtmosphereSettings 等公开组件,从未经由渲染世界的内部资源接口触碰 AtmosphereBuffer。只有直接操作 Bevy 渲染管线内部 API 的第三方渲染扩展才会受影响。
二、为什么必须“一相机一缓冲区”:源码中的多相机架构
要理解这次迁移的必然性,需要看当前源码中大气散射是如何按相机组织的。核心结论是:整条大气散射数据链路上,所有“每相机状态”都已实体化为挂在渲染世界相机实体上的组件,AtmosphereBuffer 只是最后对齐的一块拼图。
2.1 缓冲区定义:现在是一个 Component
在 resources.rs 中可以看到当前定义:
#[derive(Component)]
pub struct AtmosphereBuffer {
pub(crate) buffer: StorageBuffer<AtmosphereData>,
}
它内部是一个 StorageBuffer<AtmosphereData>,而 AtmosphereData 由 GpuAtmosphere(行星参数)与 GpuAtmosphereSettings(LUT 参数)两块 shader uniform 数据组成(见 resources.rs#L806-L811)。旧版中这个缓冲区是全局唯一的 Resource,多个相机共享同一份数据;当不同相机需要各自的大气球体(半径、地面反照率、散射介质、世界到大气空间变换)时,单份全局缓冲区就无法表达“这个相机看到的是哪个大气”。
init_atmosphere_buffer 正是旧架构下“一次性初始化全局资源缓冲区”的系统。资源→组件的改造完成后,这个一次性初始化步骤失去了存在意义——缓冲区改为在每帧的准备阶段按相机创建与更新,于是该系统被直接移除。
2.2 每帧的创建与更新:prepare_atmosphere_buffers
替代 init_atmosphere_buffer 的是每帧运行的 prepare_atmosphere_buffers(resources.rs#L818-L846)。它的逻辑值得逐行看,因为它体现了“组件化”之后的标准写法:
pub(crate) fn prepare_atmosphere_buffers(
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
mut views: Query<
(
Entity,
&GpuAtmosphere,
&GpuAtmosphereSettings,
Option<&mut AtmosphereBuffer>, // 关键点:按实体查询旧缓冲区
),
With<ExtractedAtmosphere>, // 只处理带大气设置的相机
>,
mut commands: Commands,
) {
for (entity, atmosphere, settings, existing_buffer) in &mut views {
let data = AtmosphereData {
atmosphere: atmosphere.clone(),
settings: settings.clone(),
};
if let Some(mut atmosphere_buffer) = existing_buffer {
// 缓冲区已存在:原地更新内容
atmosphere_buffer.buffer.set(data);
atmosphere_buffer.buffer.write_buffer(&device, &queue);
} else {
// 首次出现:新建 StorageBuffer 并作为组件挂到该相机实体上
let mut buffer = StorageBuffer::from(data);
buffer.write_buffer(&device, &queue);
commands.entity(entity).insert(AtmosphereBuffer { buffer });
}
}
}
三个设计要点:
- 查询以
Entity开头并配合Option<&mut AtmosphereBuffer>,实现了“存在则更新、不存在则创建”的幂等模式,避免了旧式if let Ok(_) = world.try_resource::<AtmosphereBuffer>()判断; With<ExtractedAtmosphere>过滤条件保证只有真正启用了大气散射的相机才会分配这块缓冲区;- 该每帧系统注册在
RenderSystems::PrepareResources集合中,见插件装配代码 mod.rs#L186。
2.3 消费端:主材质渲染如何拿到“本相机”的大气缓冲区
缓冲区的真正价值在于供不透明网格 shader 采样大气数据。在 mesh_view_bindings.rs 中,构建每个视口绑定组时的相机查询包含:
views: Query<(
Entity,
Option<&ExtractedCamera>,
...
Option<&AtmosphereTextures>,
Option<&AtmosphereBuffer>, // L657:按组件从相机实体上取
...
Has<ExtractedAtmosphere>,
...
)>,
只有当相机同时具备 ExtractedAtmosphere、AtmosphereTextures、AtmosphereBuffer 和共享的 AtmosphereSampler 时,才会把三者打进动态绑定组的索引 31/32/33(mesh_view_bindings.rs#L828-L840):
if has_atmosphere
&& let Some(atmosphere_textures) = atmosphere_textures
&& let Some(atmosphere_buffer) = atmosphere_buffer
&& let Some(atmosphere_sampler) = atmosphere_sampler.as_ref()
&& let Some(atmosphere_buffer_binding) = atmosphere_buffer.buffer.binding()
{
layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE;
entries = entries.extend_with_indices((
(31, &atmosphere_textures.transmittance_lut.default_view),
(32, &***atmosphere_sampler),
(33, atmosphere_buffer_binding),
));
}
这段代码是“多相机正确性”的直接证据:每个相机的视口绑定组绑定的是自己实体上的 AtmosphereBuffer,而不是某个全局资源。如果仍用资源方式,多相机场景下所有视口会读到同一份大气参数,导致副相机渲染出错误的天空与散射光照。
2.4 配套的逐相机管线与渲染通道
同样的“按相机特化”思路贯穿其他环节,可以作为理解整体架构的参照:
- 管线特化:
queue_render_sky_pipelines按每个相机的Msaa采样数与设备是否支持DUAL_SOURCE_BLENDING分别特化render_sky管线,并把结果作为RenderSkyPipelineId组件挂回相机实体(resources.rs#L366-L387)。 - 渲染通道:
render_sky节点通过ViewQuery逐视口执行,绘制一个全屏三角形完成天空合成,绑定组同样取自各相机自己的AtmosphereBindGroups(node.rs#L147-L222);atmosphere_luts计算通道则逐相机派发四个 LUT(透射率、多重散射、天空视角、空中视角)(node.rs#L23-L145)。 - 大气选择:
extract_atmosphere在提取阶段为每个带AtmosphereSettings的 3D 相机挑选世界空间中距离其原点最近的Atmosphere实体,把参数拷贝为该相机实体的ExtractedAtmosphere组件(mod.rs#L206-L262)。因此一个场景中可以放置多套大气(例如地球与火星两套预设),每个相机自动获得离自己最近的那一套。
三、迁移操作指南:资源访问改为组件查询
3.1 受影响代码的识别特征
旧版代码中,受影响写法通常长这样(渲染世界系统中的资源参数):
// 旧写法(迁移前):把 AtmosphereBuffer 当作全局资源
fn some_render_system(
atmosphere_buffer: Res<AtmosphereBuffer>,
...
) {
let binding = atmosphere_buffer.buffer.binding().unwrap();
// ...
}
这类写法在当前仓库中已无对应物:init_atmosphere_buffer 不存在,AtmosphereBuffer 也不再派生 Resource。
3.2 迁移后的标准写法
按迁移文档的指引,“query for it as a component on camera entities instead”,即改为对相机实体做组件查询。当前仓库自身的使用方式给出了参考模板(mesh_view_bindings.rs#L647-L674):
// 新写法(迁移后):在相机实体上按组件查询
fn some_render_system(
views: Query<(Entity, Option<&AtmosphereBuffer>), With<Camera3d>>,
...
) {
for (entity, atmosphere_buffer) in &views {
if let Some(atmosphere_buffer) = atmosphere_buffer {
if let Some(binding) = atmosphere_buffer.buffer.binding() {
// 使用本相机专属的绑定
}
}
}
}
迁移时的三个注意事项:
- 查询目标从“世界”变为“实体”:不再关心资源是否存在,而是关心哪些相机实体携带该组件;建议用
Option<&AtmosphereBuffer>或Has<AtmosphereBuffer>处理“未启用大气”的相机。 - 字段访问路径不变:
AtmosphereBuffer { buffer: StorageBuffer<AtmosphereData> }的字段结构未变,原来对buffer字段(如binding()、set()、write_buffer())的调用可以原样保留。 - 生命周期变化:组件随相机实体在渲染世界中的存续而存在。当相机移除
AtmosphereSettings或场景中不再有Atmosphere实体时,extract_atmosphere会移除该相机上一整套大气状态组件(mod.rs#L217-L229),从结构上杜绝了“副相机残留旧大气”的问题——这正是旧资源模式难以做到的。
3.3 对普通应用开发者意味着什么
如果你的代码只使用公开 API——给场景实体加 Atmosphere 与 GlobalTransform、给 3D 相机加 AtmosphereSettings(mod.rs 的模块文档明确了这一用法)——那么本次迁移对你完全无感,多相机场景(分屏、画中画、多目标渲染)现在可以直接工作。
AtmosphereSettings 本身也是组件,因此每台相机可以拥有独立的大气参数。其默认值定义在 mod.rs#L345-L362,可作为调参基线:
| 参数 | 默认值 | 含义 |
|---|---|---|
transmittance_lut_size |
(256, 128) |
透射率 LUT 尺寸 |
transmittance_lut_samples |
40 |
计算透射率 LUT 时每条光线的采样点数 |
multiscattering_lut_size |
(32, 32) |
多重散射 LUT 尺寸 |
multiscattering_lut_dirs / samples |
64 / 20 |
多重散射每像素的光线数与沿线采样数 |
sky_view_lut_size / sky_view_lut_samples |
(400, 200) / 16 |
天空视角 LUT 尺寸与采样数 |
aerial_view_lut_size |
(32, 32, 32) |
空中视角 3D LUT 尺寸 |
aerial_view_lut_max_distance |
3.2e4(米) |
空中视角 LUT 的最大评估距离 |
sky_max_samples |
16 |
光线行进渲染天空时每片元的采样点数 |
rendering_method |
AtmosphereMode::LookupTexture |
渲染方法(默认查表,可选 Raymarched) |
rendering_method 二选一的取舍见 mod.rs#L418-L436:LookupTexture 面向主要位于大气内部的常规场景,性能好但对超远距离/太空视角与锐利体积阴影略欠精确;Raymarched 数值积分更慢但更准,适合轨道视角看行星、电影感镜头等需求。多相机场景下,你可以让主相机用 Raymarched、副相机用 LookupTexture,各取所需。
四、GPU 能力前提与验证
从源码结构看,AtmospherePlugin 在装配前会做三项能力检查,不满足则打印警告并整体跳过大气渲染(mod.rs#L126-L158):
- 适配器支持计算着色器(
DownlevelFlags::COMPUTE_SHADERS); TextureFormat::Rgba16Float支持STORAGE_BINDING用途(四个 LUT 均为Rgba16Float存储纹理,见 resources.rs#L397-L469);max_storage_textures_per_shader_stage > 0。
因此验证多相机行为时,请确保目标 GPU/后端满足上述条件(桌面端 DX12/Vulkan 通常没有问题)。仓库提供的 atmosphere 示例 演示了 Atmosphere 场景实体、AtmosphereSettings 相机组件(示例中以 Query<&mut AtmosphereSettings, With<Camera3d>> 遍历所有相机调整参数,atmosphere.rs#L71-L79)以及 LookupTexture/Raymarched 两种渲染模式的切换,可用 cargo run --example atmosphere 运行;在其基础上增加第二台带 AtmosphereSettings 的相机,即可直观验证每台相机各自独立的大气 LUT 与缓冲区行为。
五、小结
- 变更本质:
AtmosphereBuffer由全局Resource改为挂在每个相机实体上的Component,init_atmosphere_buffer随旧初始化路径一并移除,取而代之的是每帧prepare_atmosphere_buffers的按需创建/更新; - 收益:大气散射在任意数量的相机下都能各自绑定正确的大气参数、LUT 与天空管线,多相机渲染从此正确工作;
- 迁移动作:仅在渲染世界内部直接用过
Res<AtmosphereBuffer>的代码需要改造——把资源参数改为对相机实体的组件查询(Option<&AtmosphereBuffer>,可按 mesh_view_bindings.rs 的模式书写),字段访问方式保持不变; - 不受影响:只使用
Atmosphere/AtmosphereSettings公开组件的常规应用与示例,无需任何改动。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00