首页
/ Bevy 大气散射多相机渲染支持:AtmosphereBuffer 从 Resource 迁移到 Component 的完整指南

Bevy 大气散射多相机渲染支持:AtmosphereBuffer 从 Resource 迁移到 Component 的完整指南

2026-09-05 09:28:20作者:齐冠琰

本文围绕 Bevy 的 AtmosphereBuffer 接口变更展开:解释 Atmosphere(大气散射)模块为何要改为逐相机持有 GPU 缓冲区、init_atmosphere_buffer 系统为何被移除,并结合当前仓库源码给出在渲染世界(Render World)中按组件查询 AtmosphereBuffer 的迁移写法。读完你可以理解 Bevy 大气散射渲染的逐相机数据流(提取 → 纹理/管线 → 绑定 → 渲染),并安全完成从旧版资源访问到新版组件查询的代码迁移。

一、本次变更的三个核心事实

官方迁移文档 atmosphere_multicam.md 声明了三件事,它们构成本次迁移的全部要点:

  1. Atmosphere 现在可以正确支持多个相机(Atmosphere now works correctly with multiple cameras),大多数用户无需做任何改动
  2. init_atmosphere_buffer 系统已被移除
  3. AtmosphereBuffer 的类型从 Resource 变更为挂载在每个相机实体上的 Component。如果你在渲染世界系统中曾把它当资源直接访问(Res<AtmosphereBuffer>),需要改为在相机实体上以组件形式查询(Query<&AtmosphereBuffer>Option<&AtmosphereBuffer>)。

为什么“大多数用户无需改动”?因为绝大多数应用只在自己的游戏逻辑中使用 AtmosphereAtmosphereSettings 等公开组件,从未经由渲染世界的内部资源接口触碰 AtmosphereBuffer。只有直接操作 Bevy 渲染管线内部 API 的第三方渲染扩展才会受影响。

二、为什么必须“一相机一缓冲区”:源码中的多相机架构

要理解这次迁移的必然性,需要看当前源码中大气散射是如何按相机组织的。核心结论是:整条大气散射数据链路上,所有“每相机状态”都已实体化为挂在渲染世界相机实体上的组件AtmosphereBuffer 只是最后对齐的一块拼图。

2.1 缓冲区定义:现在是一个 Component

resources.rs 中可以看到当前定义:

#[derive(Component)]
pub struct AtmosphereBuffer {
    pub(crate) buffer: StorageBuffer<AtmosphereData>,
}

它内部是一个 StorageBuffer<AtmosphereData>,而 AtmosphereDataGpuAtmosphere(行星参数)与 GpuAtmosphereSettings(LUT 参数)两块 shader uniform 数据组成(见 resources.rs#L806-L811)。旧版中这个缓冲区是全局唯一的 Resource,多个相机共享同一份数据;当不同相机需要各自的大气球体(半径、地面反照率、散射介质、世界到大气空间变换)时,单份全局缓冲区就无法表达“这个相机看到的是哪个大气”。

init_atmosphere_buffer 正是旧架构下“一次性初始化全局资源缓冲区”的系统。资源→组件的改造完成后,这个一次性初始化步骤失去了存在意义——缓冲区改为在每帧的准备阶段按相机创建与更新,于是该系统被直接移除。

2.2 每帧的创建与更新:prepare_atmosphere_buffers

替代 init_atmosphere_buffer 的是每帧运行的 prepare_atmosphere_buffersresources.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>,
    ...
)>,

只有当相机同时具备 ExtractedAtmosphereAtmosphereTexturesAtmosphereBuffer 和共享的 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 逐视口执行,绘制一个全屏三角形完成天空合成,绑定组同样取自各相机自己的 AtmosphereBindGroupsnode.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() {
                // 使用本相机专属的绑定
            }
        }
    }
}

迁移时的三个注意事项:

  1. 查询目标从“世界”变为“实体”:不再关心资源是否存在,而是关心哪些相机实体携带该组件;建议用 Option<&AtmosphereBuffer>Has<AtmosphereBuffer> 处理“未启用大气”的相机。
  2. 字段访问路径不变AtmosphereBuffer { buffer: StorageBuffer<AtmosphereData> } 的字段结构未变,原来对 buffer 字段(如 binding()set()write_buffer())的调用可以原样保留。
  3. 生命周期变化:组件随相机实体在渲染世界中的存续而存在。当相机移除 AtmosphereSettings 或场景中不再有 Atmosphere 实体时,extract_atmosphere 会移除该相机上一整套大气状态组件(mod.rs#L217-L229),从结构上杜绝了“副相机残留旧大气”的问题——这正是旧资源模式难以做到的。

3.3 对普通应用开发者意味着什么

如果你的代码只使用公开 API——给场景实体加 AtmosphereGlobalTransform、给 3D 相机加 AtmosphereSettingsmod.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-L436LookupTexture 面向主要位于大气内部的常规场景,性能好但对超远距离/太空视角与锐利体积阴影略欠精确;Raymarched 数值积分更慢但更准,适合轨道视角看行星、电影感镜头等需求。多相机场景下,你可以让主相机用 Raymarched、副相机用 LookupTexture,各取所需。

四、GPU 能力前提与验证

从源码结构看,AtmospherePlugin 在装配前会做三项能力检查,不满足则打印警告并整体跳过大气渲染(mod.rs#L126-L158):

  1. 适配器支持计算着色器(DownlevelFlags::COMPUTE_SHADERS);
  2. TextureFormat::Rgba16Float 支持 STORAGE_BINDING 用途(四个 LUT 均为 Rgba16Float 存储纹理,见 resources.rs#L397-L469);
  3. 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 改为挂在每个相机实体上的 Componentinit_atmosphere_buffer 随旧初始化路径一并移除,取而代之的是每帧 prepare_atmosphere_buffers 的按需创建/更新;
  • 收益:大气散射在任意数量的相机下都能各自绑定正确的大气参数、LUT 与天空管线,多相机渲染从此正确工作;
  • 迁移动作:仅在渲染世界内部直接用过 Res<AtmosphereBuffer> 的代码需要改造——把资源参数改为对相机实体的组件查询(Option<&AtmosphereBuffer>,可按 mesh_view_bindings.rs 的模式书写),字段访问方式保持不变;
  • 不受影响:只使用 Atmosphere / AtmosphereSettings 公开组件的常规应用与示例,无需任何改动。
登录后查看全文
热门项目推荐
相关项目推荐