Bevy 着色器迁移到 WESL:从 naga_oil 预处理到原生着色器模块
2026/9/8 21:04:02 网站建设 项目流程

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 枚举 中的SpirVWgslWesl。也就是说,纯 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::Cimport 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_bindingsbevy_pbr::render::mesh_view_bindings(对应 crates/bevy_pbr/src/render/mesh_view_bindings.wesl);
  • bevy_pbr::prepass_utilsbevy_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是否存在);
  • 附着 importpbr.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 着色器时:

  1. 收集定义闭包(L229-L247):从当前着色器出发沿imports图做 BFS,把依赖链上每个库着色器自带的shader_defs一并纳入closure_defs——这意味着库着色器里写死的constants::取值会随依赖自动传播;
  2. 分流处理(L248-L280):Bool(key, v)写入compiler_options.features.flags作为@if开关;Int/UInt除了启用同名开关外,还会把key=value追加进一张常量表;
  3. 生成虚拟constants模块:常量表被拼成const NAME = value;形式的源码(L277-L280),由 ShaderResolver::resolve_source 在解析到模块名constants时注入,因此constants::MATERIAL_BINDING在文本上就是普通 WGSL 的常量引用;
  4. 编译输出 WGSLwesl::compile_sourcemapimports: 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.weslbevy_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 这类项目内文件)的迁移步骤可以归纳为:

  1. 判断是否需要迁移:文件中不含#import/#ifdef/#{...}等预处理指令时,保留.wgsl原样即可;否则执行后续步骤;
  2. 重命名.wgsl.wesl,同步更新 Rust 侧加载路径(ShaderRef/AssetPath中的扩展名);
  3. 翻译指令#import Ximport X;(移到文件顶部、加分号);#ifdef X ... #endif→ 在声明前加@if(X)(无结束标记);#{NAME}constants::NAME
  4. 修正模块名:引擎内置着色器按“crate 内文件路径”重命名导入(如bevy_pbr::render::forward_io);本地文件用相对模块名(super::util)或资产路径引用,删除所有#define_import_path
  5. 核对 shader defs:确认布尔定义用于@if开关;数值定义(ShaderDefVal::Int/UInt)通过constants::NAME读取;
  6. 验证:着色器源在 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询