Bevy 着色器迁移到 WESL:从 naga_oil 预处理到原生着色器模块
【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy
Bevy 的着色器已从 naga_oil 预处理器方言全面切换到 WESL,完整覆盖新旧语法对照、模块命名规则、着色器定义(shader defs)的映射方式,并结合 bevy_shader 源码 与 着色器缓存实现 讲解 WESL 在 Bevy 中的编译、解析与缓存机制,帮助你安全完成自定义着色器的迁移。
核心变化:WESL 取代 naga_oil
Bevy 引擎内置的所有着色器现在都直接用 WESL 编写,naga_oil预处理器被彻底移除。迁移指南给出的结论是:
- 使用 naga_oil 方言的自定义着色器必须翻译成 WESL,并将扩展名从
.wgsl改为.wesl; - 不含任何预处理指令的纯 WGSL 文件可以继续原样使用,无需改动。
这一判断的依据可以在加载器源码中得到印证:ShaderLoader 按扩展名分派到三种Source变体——"spv"走Shader::from_spirv、"wgsl"走Shader::from_wgsl、"wesl"走Shader::from_wesl,三者分别对应 Source 枚举 中的SpirV、Wgsl、Wesl。也就是说,纯 WGSL 文件进入缓存后会被原样交给渲染器编译,完全绕开 WESL 管线。
同时有两条加载规则值得注意(shader.rs L288-L293):
- 只有
.wesl文件支持 shader defs。如果给.wgsl/.spv着色器附加了shader_defs,Bevy 会打印警告 “Tried to load a non-wesl shader with shader defs, this isn't supported” 并忽略这些定义; - 不支持的扩展名会直接 panic(
"unhandled extension"),加载器声明的合法扩展名为["spv", "wgsl", "wesl"]。
语法对照:BEFORE / AFTER 逐行解析
迁移指南给出的新旧对照示例是本次迁移的核心参照,这里完整保留并逐条解释:
// BEFORE(naga_oil 方言) #import bevy_pbr::forward_io::VertexOutput #import "shaders/util.wgsl"::hsv_to_rgb #ifdef VERTEX_COLORS var<private> tint: vec4<f32>; #endif @group(2) @binding(#{MATERIAL_BINDING}) var<uniform> color: vec4<f32>; // AFTER(WESL) import bevy_pbr::render::forward_io::VertexOutput; import super::util::hsv_to_rgb; @if(VERTEX_COLORS) var<private> tint: vec4<f32>; @group(2) @binding(constants::MATERIAL_BINDING) var<uniform> color: vec4<f32>;映射关系可归纳为四类:
| naga_oil 写法 | WESL 写法 | 说明 |
|---|---|---|
#import a::b::C | import a::b::C; | import 以分号结尾,且必须置于文件最前 |
#ifdef X…#endif | @if(X)(无结束标记) | 条件编译直接附着在声明上 |
#{NAME} | constants::NAME | 数值定义变成可读常量 |
#define_import_path | (移除) | 导入路径由文件位置自动推导 |
1. import 语句:位置与分号规则
WESL 的import必须以分号结尾,并且必须位于文件开头,出现在任何声明或enable指令之前。这与 Rust 的use风格一致,也和仓库中引擎自带着色器的实际写法吻合,例如 bevy_pbr 的 pbr.wesl 开篇就是一组import:
import package::{ render::{ pbr_types, pbr_functions::alpha_discard, pbr_fragment::pbr_input_from_standard_material, }, decal::clustered::apply_decals, };此外 WESL 支持跨 crate 的具名导入,如import bevy_core_pipeline::oit::draw::oit_draw;(见 pbr.wesl)。
2. 模块名与着色器在 crate 中的路径一致
指南指出:模块名现在必须与着色器文件在其 crate 中的路径匹配。由于 Bevy 引擎的着色器被重组进了子目录,一批旧路径发生了平移,例如:
bevy_pbr::mesh_view_bindings→bevy_pbr::render::mesh_view_bindings(对应 crates/bevy_pbr/src/render/mesh_view_bindings.wesl);bevy_pbr::prepass_utils→bevy_pbr::prepass::utils。
这条规则的底层逻辑在 Shader::from_wesl 中非常清晰:对于embedded://前缀的路径,Bevy 去掉协议前缀与文件扩展名后,把路径按/拆分成::分隔的模块名——embedded://bevy_foo/bar.wesl因此成为可导入的bevy_foo::bar;非内嵌着色器则生成以/开头的资产路径(ShaderImport::AssetPath),即指南所说的 “anything else at its asset path”(其余着色器按资产路径导入)。
对于项目内自定义着色器,指南示例中的import super::util::hsv_to_rgb;展示了相对引用写法:shaders/util.wesl这样的本地文件可以用super::util相对模块名导入,替代过去#import "shaders/util.wgsl"::hsv_to_rgb的字符串路径。
3. 条件编译:@if 附着在语言元素上
@if/@elif/@else直接附着在完整的声明、结构体成员、函数参数、import 乃至单条语句上,不再需要#ifdef/#endif包裹块。引擎着色器中的真实用例覆盖了指南提到的所有附着位置:
- 附着结构体成员:forward_io.wesl 中
UncompressedVertex的每个顶点属性都按能力开关裁掉:
struct UncompressedVertex { @builtin(instance_index) instance_index: u32, @if(VERTEX_POSITIONS) @location(0) position: vec3<f32>, @if(VERTEX_NORMALS) @location(1) normal: vec3<f32>, ... @if(VERTEX_COLORS) @location(5) color: vec4<f32>, };- 附着函数参数:
pbr.wesl的 fragment 入口按管线阶段裁剪入参(@if(MESHLET_MESH_MATERIAL_PASS)控制frag_coord是否存在); - 附着 import:
pbr.wesl中 prepass 与 forward 两条管线引用不同的输入输出类型:
@if(PREPASS_PIPELINE) import package::{ prepass::io::{VertexOutput, FragmentOutput}, deferred::functions::deferred_output, }; @else import package::render::{ forward_io::{VertexOutput, FragmentOutput}, ... };- 附着语句:
pbr.wesl的 fragment 体内用@if(...)包裹逐语句逻辑。
4. shader defs:布尔变开关、数值变常量
指南的关键一条:布尔型着色器定义(Bool)变成条件编译开关,Int/UInt定义则同时可读作constants::NAME常量并启用一个同名开关。例如@binding(#{MATERIAL_BINDING})改为@binding(constants::MATERIAL_BINDING),值在编译期由 Bevy 注入。
这一机制在 ShaderCache::get 中有完整实现。编译一个 WESL 着色器时:
- 收集定义闭包(L229-L247):从当前着色器出发沿
imports图做 BFS,把依赖链上每个库着色器自带的shader_defs一并纳入closure_defs——这意味着库着色器里写死的constants::取值会随依赖自动传播; - 分流处理(L248-L280):
Bool(key, v)写入compiler_options.features.flags作为@if开关;Int/UInt除了启用同名开关外,还会把key=value追加进一张常量表; - 生成虚拟
constants模块:常量表被拼成const NAME = value;形式的源码(L277-L280),由 ShaderResolver::resolve_source 在解析到模块名constants时注入,因此constants::MATERIAL_BINDING在文本上就是普通 WGSL 的常量引用; - 编译输出 WGSL:
wesl::compile_sourcemap以imports: true, condcomp: true的 CompileOptions 把模块及其依赖拼成最终 WGSL 源,再交给渲染器编译。
Int/UInt定义的常量用法在单元测试 constants_module 中得到验证——@group(constants::MATERIAL_BIND_GROUP)与array<vec4<f32>, constants::BATCH_SIZE>在给定ShaderDefVal::UInt后,编译产物中确实出现了对应的= 2;与= 4;。
5.#define_import_path移除:路径即身份
过去 naga_oil 需要#define_import_path手动声明着色器的导入名;现在该指令不复存在,导入路径完全由文件来源决定:
- 从
embedded://加载的着色器按其crate 名 + 文件路径可导入(embedded://bevy_foo/bar.wesl即bevy_foo::bar); - 其他着色器按其资产路径可导入。
对应的代码证据是 scan_wesl_imports:它解析 WESL 源码中的每条import,把Package来源的模块路径拼回crate::module::item形式的ShaderImport::Custom,把Absolute来源拼回/a/b/c形式的ShaderImport::AssetPath,从而让资产系统能自动跟踪导入依赖(加载器中的依赖收集逻辑 会为每个AssetPath导入load_context.load一份强引用,防止被引用文件提前释放)。
特性开关与兼容边界
指南末尾给出了三条框架级变化,逐条对应源码事实:
shader_format_weslcargo feature 已移除,WESL 支持始终启用。WESL 是 bevy_shader 的无条件依赖(wesl = { version = "0.4.2", features = ["naga-ext"] }),不再有任何开关;- GLSL 支持一并移除,着色器源只保留
Source::{Wgsl, Wesl, SpirV}三种形态; - SPIR-V 直通(passthrough)保持不变,
.spv文件依旧按字节直传(ShaderLoader 分派),但如前所述,SPIR-V 着色器不支持 shader defs,且 ValidateShader 文档中说明对不受信任的 SPIR-V 启用运行期校验会 panic。
从源码结构看:缓存、重试与失效
理解 WESL 管线的一个隐藏重点是导入解析的时序。着色器资产按加载先后入缓存,若某个import的目标尚未加载完成,get() 会返回ShaderImportNotYetAvailable,下一帧重试;import_retry 测试 验证了这一“先失败、后成功”的流程。其他几个单测也很有参考价值:
- library_def_scoping:两个库着色器各自定义不同
BATCH_SIZE(3 与 7),根着色器分别引用后,各自的编译产物只含自己的常量——说明常量闭包是按依赖图精确收集的,不会串值; - cyclic_import_invalidation:循环导入的模块被替换后,依赖它的管线会全部被标记重编译,缓存失效传播是闭环的;
- import_resolution:库着色器热替换后,引用它的根着色器自动进入重编译队列。
迁移操作清单
结合指南与源码,一个自定义着色器(例如仓库 assets/shaders/custom_material.wesl 这类项目内文件)的迁移步骤可以归纳为:
- 判断是否需要迁移:文件中不含
#import/#ifdef/#{...}等预处理指令时,保留.wgsl原样即可;否则执行后续步骤; - 重命名
.wgsl→.wesl,同步更新 Rust 侧加载路径(ShaderRef/AssetPath中的扩展名); - 翻译指令:
#import X→import X;(移到文件顶部、加分号);#ifdef X ... #endif→ 在声明前加@if(X)(无结束标记);#{NAME}→constants::NAME; - 修正模块名:引擎内置着色器按“crate 内文件路径”重命名导入(如
bevy_pbr::render::forward_io);本地文件用相对模块名(super::util)或资产路径引用,删除所有#define_import_path; - 核对 shader defs:确认布尔定义用于
@if开关;数值定义(ShaderDefVal::Int/UInt)通过constants::NAME读取; - 验证:着色器源在 ShaderCache::get 中经
wesl::compile_sourcemap产出 WGSL,若导入缺失会输出 “Shader...has an unresolved import” 警告(每个着色器只告警一次,见 L297-L305),语法问题则以ProcessShaderError报错并附带可读的诊断位置信息。
小结
这次迁移的本质是:Bevy 把着色器的模块系统、条件编译和常量注入从外部预处理器(naga_oil)内化到了 WESL 语言本身,import走资产系统、@if走 WESL 条件编译、constants::走虚拟常量模块。纯 WGSL 与 SPIR-V 两条通路保持不变,但只有 WESL 能享受 defs 与导入追踪能力。对引擎作者而言,bevy_pbr等模块的.wesl文件是现成的、带注释的迁移范本;对使用者而言,照上文对照表逐条替换并核对模块路径,即可完成迁移。
【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考