From 181445c56b754af9ec2551c538a73f85ba0339ca Mon Sep 17 00:00:00 2001 From: charlotte Date: Sun, 9 Mar 2025 12:26:55 -0700 Subject: [PATCH] Add support for experimental WESL shader source (#17953) # Objective WESL's pre-MVP `0.1.0` has been [released](https://docs.rs/wesl/latest/wesl/)! Add support for WESL shader source so that we can begin playing and testing WESL, as well as aiding in their development. ## Solution Adds a `ShaderSource::WESL` that can be used to load `.wesl` shaders. Right now, we don't support mixing `naga-oil`. Additionally, WESL shaders currently need to pass through the naga frontend, which the WESL team is aware isn't great for performance (they're working on compiling into naga modules). Also, since our shaders are managed using the asset system, we don't currently support using file based imports like `super` or package scoped imports. Further work will be needed to asses how we want to support this. --- ## Showcase See the `shader_material_wesl` example. Be sure to press space to activate party mode (trigger conditional compilation)! https://github.com/user-attachments/assets/ec6ad19f-b6e4-4e9d-a00f-6f09336b08a4 --- Cargo.toml | 15 +++ assets/shaders/custom_material.wesl | 19 +++ assets/shaders/util.wesl | 29 ++++ crates/bevy_internal/Cargo.toml | 1 + crates/bevy_render/Cargo.toml | 2 + .../src/render_resource/pipeline_cache.rs | 84 ++++++++++++ .../bevy_render/src/render_resource/shader.rs | 52 +++++++- docs/cargo_features.md | 1 + examples/README.md | 1 + examples/shader/shader_material_wesl.rs | 126 ++++++++++++++++++ 10 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 assets/shaders/custom_material.wesl create mode 100644 assets/shaders/util.wesl create mode 100644 examples/shader/shader_material_wesl.rs diff --git a/Cargo.toml b/Cargo.toml index d80a0a85e6..697cf8dada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,6 +442,9 @@ shader_format_glsl = ["bevy_internal/shader_format_glsl"] # Enable support for shaders in SPIR-V shader_format_spirv = ["bevy_internal/shader_format_spirv"] +# Enable support for shaders in WESL +shader_format_wesl = ["bevy_internal/shader_format_wesl"] + # Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"] @@ -2764,6 +2767,18 @@ description = "A shader that uses the GLSL shading language" category = "Shaders" wasm = true +[[example]] +name = "shader_material_wesl" +path = "examples/shader/shader_material_wesl.rs" +doc-scrape-examples = true +required-features = ["shader_format_wesl"] + +[package.metadata.example.shader_material_wesl] +name = "Material - WESL" +description = "A shader that uses WESL" +category = "Shaders" +wasm = true + [[example]] name = "custom_shader_instancing" path = "examples/shader/custom_shader_instancing.rs" diff --git a/assets/shaders/custom_material.wesl b/assets/shaders/custom_material.wesl new file mode 100644 index 0000000000..35340811d9 --- /dev/null +++ b/assets/shaders/custom_material.wesl @@ -0,0 +1,19 @@ +import super::shaders::util::make_polka_dots; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(2) uv: vec2, +} + +struct CustomMaterial { + time: f32, +} + +@group(2) @binding(0) var material: CustomMaterial; + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { + return make_polka_dots(mesh.uv, material.time); +} \ No newline at end of file diff --git a/assets/shaders/util.wesl b/assets/shaders/util.wesl new file mode 100644 index 0000000000..dbb74c3520 --- /dev/null +++ b/assets/shaders/util.wesl @@ -0,0 +1,29 @@ +fn make_polka_dots(pos: vec2, time: f32) -> vec4 { + // Create repeating circles + let scaled_pos = pos * 6.0; + let cell = vec2(fract(scaled_pos.x), fract(scaled_pos.y)); + let dist_from_center = distance(cell, vec2(0.5)); + + // Make dots alternate between pink and purple + let is_even = (floor(scaled_pos.x) + floor(scaled_pos.y)) % 2.0; + + var dot_color = vec3(0.0); + @if(!PARTY_MODE) { + let color1 = vec3(1.0, 0.4, 0.8); // pink + let color2 = vec3(0.6, 0.2, 1.0); // purple + dot_color = mix(color1, color2, is_even); + } + // Animate the colors in party mode + @if(PARTY_MODE) { + let color1 = vec3(1.0, 0.2, 0.2); // red + let color2 = vec3(0.2, 0.2, 1.0); // blue + let oscillation = (sin(time * 10.0) + 1.0) * 0.5; + let animated_color1 = mix(color1, color2, oscillation); + let animated_color2 = mix(color2, color1, oscillation); + dot_color = mix(animated_color1, animated_color2, is_even); + } + + // Draw the dot + let is_dot = step(dist_from_center, 0.3); + return vec4(dot_color * is_dot, is_dot); +} \ No newline at end of file diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index b8e087473a..b0be2f00c8 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -88,6 +88,7 @@ shader_format_glsl = [ "bevy_pbr?/shader_format_glsl", ] shader_format_spirv = ["bevy_render/shader_format_spirv"] +shader_format_wesl = ["bevy_render/shader_format_wesl"] serialize = [ "bevy_a11y?/serialize", diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e91f8cc81f..448c11f204 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -20,6 +20,7 @@ multi_threaded = ["bevy_tasks/multi_threaded"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] +shader_format_wesl = ["wesl"] # Enable SPIR-V shader passthrough spirv_shader_passthrough = ["wgpu/spirv"] @@ -100,6 +101,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } indexmap = { version = "2" } fixedbitset = { version = "0.5" } bitflags = "2" +wesl = { version = "0.1.0", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 37211edb04..4f9fc5ce26 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -127,6 +127,8 @@ struct ShaderData { struct ShaderCache { data: HashMap, ShaderData>, + #[cfg(feature = "shader_format_wesl")] + asset_paths: HashMap>, shaders: HashMap, Shader>, import_path_shaders: HashMap>, waiting_on_import: HashMap>>, @@ -179,6 +181,8 @@ impl ShaderCache { Self { composer, data: Default::default(), + #[cfg(feature = "shader_format_wesl")] + asset_paths: Default::default(), shaders: Default::default(), import_path_shaders: Default::default(), waiting_on_import: Default::default(), @@ -223,6 +227,7 @@ impl ShaderCache { .shaders .get(&id) .ok_or(PipelineCacheError::ShaderNotLoaded(id))?; + let data = self.data.entry(id).or_default(); let n_asset_imports = shader .imports() @@ -267,6 +272,44 @@ impl ShaderCache { let shader_source = match &shader.source { #[cfg(feature = "shader_format_spirv")] Source::SpirV(data) => make_spirv(data), + #[cfg(feature = "shader_format_wesl")] + Source::Wesl(_) => { + if let ShaderImport::AssetPath(path) = shader.import_path() { + let shader_resolver = + ShaderResolver::new(&self.asset_paths, &self.shaders); + let module_path = wesl::syntax::ModulePath::from_path(path); + let mut compiler_options = wesl::CompileOptions { + imports: true, + condcomp: true, + lower: true, + ..default() + }; + + for shader_def in shader_defs { + match shader_def { + ShaderDefVal::Bool(key, value) => { + compiler_options.features.insert(key.clone(), value); + } + _ => debug!( + "ShaderDefVal::Int and ShaderDefVal::UInt are not supported in wesl", + ), + } + } + + let compiled = wesl::compile( + &module_path, + &shader_resolver, + &wesl::EscapeMangler, + &compiler_options, + ) + .unwrap(); + + let naga = naga::front::wgsl::parse_str(&compiled.to_string()).unwrap(); + ShaderSource::Naga(Cow::Owned(naga)) + } else { + panic!("Wesl shaders must be imported from a file"); + } + } #[cfg(not(feature = "shader_format_spirv"))] Source::SpirV(_) => { unimplemented!( @@ -398,6 +441,13 @@ impl ShaderCache { } } + #[cfg(feature = "shader_format_wesl")] + if let Source::Wesl(_) = shader.source { + if let ShaderImport::AssetPath(path) = shader.import_path() { + self.asset_paths + .insert(wesl::syntax::ModulePath::from_path(path), id); + } + } self.shaders.insert(id, shader); pipelines_to_queue } @@ -412,6 +462,40 @@ impl ShaderCache { } } +#[cfg(feature = "shader_format_wesl")] +pub struct ShaderResolver<'a> { + asset_paths: &'a HashMap>, + shaders: &'a HashMap, Shader>, +} + +#[cfg(feature = "shader_format_wesl")] +impl<'a> ShaderResolver<'a> { + pub fn new( + asset_paths: &'a HashMap>, + shaders: &'a HashMap, Shader>, + ) -> Self { + Self { + asset_paths, + shaders, + } + } +} + +#[cfg(feature = "shader_format_wesl")] +impl<'a> wesl::Resolver for ShaderResolver<'a> { + fn resolve_source( + &self, + module_path: &wesl::syntax::ModulePath, + ) -> Result, wesl::ResolveError> { + let asset_id = self.asset_paths.get(module_path).ok_or_else(|| { + wesl::ResolveError::ModuleNotFound(module_path.clone(), "Invalid asset id".to_string()) + })?; + + let shader = self.shaders.get(asset_id).unwrap(); + Ok(Cow::Borrowed(shader.source.as_str())) + } +} + type LayoutCacheKey = (Vec, Vec); #[derive(Default)] struct LayoutCache { diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index e1d00ec0d7..efc33aa7f9 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -155,6 +155,49 @@ impl Shader { } } + #[cfg(feature = "shader_format_wesl")] + pub fn from_wesl(source: impl Into>, path: impl Into) -> Shader { + let source = source.into(); + let path = path.into(); + let (import_path, imports) = Shader::preprocess(&source, &path); + + match import_path { + ShaderImport::AssetPath(asset_path) => { + let asset_path = std::path::PathBuf::from(&asset_path); + // Resolve and normalize the path + let asset_path = asset_path.canonicalize().unwrap_or(asset_path); + // Strip the asset root + let mut base_path = bevy_asset::io::file::FileAssetReader::get_base_path(); + // TODO: integrate better with the asset system rather than hard coding this + base_path.push("assets"); + let asset_path = asset_path + .strip_prefix(&base_path) + .unwrap_or_else(|_| &asset_path); + // Wesl paths are provided as absolute relative to the asset root + let asset_path = std::path::Path::new("/").join(asset_path); + // And with a striped file name + let asset_path = asset_path.with_extension(""); + let asset_path = asset_path.to_str().unwrap_or_else(|| { + panic!("Failed to convert path to string: {:?}", asset_path) + }); + let import_path = ShaderImport::AssetPath(asset_path.to_string()); + Shader { + path, + imports, + import_path, + source: Source::Wesl(source), + additional_imports: Default::default(), + shader_defs: Default::default(), + file_dependencies: Default::default(), + validate_shader: ValidateShader::Disabled, + } + } + ShaderImport::Custom(_) => { + panic!("Wesl shaders must be imported from an asset path"); + } + } + } + pub fn set_import_path>(&mut self, import_path: P) { self.import_path = ShaderImport::Custom(import_path.into()); } @@ -223,6 +266,7 @@ impl<'a> From<&'a Shader> for naga_oil::compose::NagaModuleDescriptor<'a> { #[derive(Debug, Clone)] pub enum Source { Wgsl(Cow<'static, str>), + Wesl(Cow<'static, str>), Glsl(Cow<'static, str>, naga::ShaderStage), SpirV(Cow<'static, [u8]>), // TODO: consider the following @@ -233,7 +277,7 @@ pub enum Source { impl Source { pub fn as_str(&self) -> &str { match self { - Source::Wgsl(s) | Source::Glsl(s, _) => s, + Source::Wgsl(s) | Source::Wesl(s) | Source::Glsl(s, _) => s, Source::SpirV(_) => panic!("spirv not yet implemented"), } } @@ -250,6 +294,7 @@ impl From<&Source> for naga_oil::compose::ShaderLanguage { "GLSL is not supported in this configuration; use the feature `shader_format_glsl`" ), Source::SpirV(_) => panic!("spirv not yet implemented"), + Source::Wesl(_) => panic!("wesl not yet implemented"), } } } @@ -269,6 +314,7 @@ impl From<&Source> for naga_oil::compose::ShaderType { "GLSL is not supported in this configuration; use the feature `shader_format_glsl`" ), Source::SpirV(_) => panic!("spirv not yet implemented"), + Source::Wesl(_) => panic!("wesl not yet implemented"), } } } @@ -312,6 +358,8 @@ impl AssetLoader for ShaderLoader { "comp" => { Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Compute, path) } + #[cfg(feature = "shader_format_wesl")] + "wesl" => Shader::from_wesl(String::from_utf8(bytes)?, path), _ => panic!("unhandled extension: {ext}"), }; @@ -325,7 +373,7 @@ impl AssetLoader for ShaderLoader { } fn extensions(&self) -> &[&str] { - &["spv", "wgsl", "vert", "frag", "comp"] + &["spv", "wgsl", "vert", "frag", "comp", "wesl"] } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 20a1d98600..a96c2697f5 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -102,6 +102,7 @@ The default feature set enables most of the expected features of a game engine, |serialize|Enable serialization support through serde| |shader_format_glsl|Enable support for shaders in GLSL| |shader_format_spirv|Enable support for shaders in SPIR-V| +|shader_format_wesl|Enable support for shaders in WESL| |spirv_shader_passthrough|Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)| |statically-linked-dxc|Statically linked DXC shader compiler for DirectX 12| |symphonia-aac|AAC audio format support (through symphonia)| diff --git a/examples/README.md b/examples/README.md index 088151ea6a..ba1850d231 100644 --- a/examples/README.md +++ b/examples/README.md @@ -447,6 +447,7 @@ Example | Description [Material - Bindless](../examples/shader/shader_material_bindless.rs) | Demonstrates how to make materials that use bindless textures [Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language [Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates +[Material - WESL](../examples/shader/shader_material_wesl.rs) | A shader that uses WESL [Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass [Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) diff --git a/examples/shader/shader_material_wesl.rs b/examples/shader/shader_material_wesl.rs new file mode 100644 index 0000000000..81ee3325b0 --- /dev/null +++ b/examples/shader/shader_material_wesl.rs @@ -0,0 +1,126 @@ +//! A shader that uses the WESL shading language. + +use bevy::{ + asset::{load_internal_asset, weak_handle}, + pbr::{MaterialPipeline, MaterialPipelineKey}, + prelude::*, + reflect::TypePath, + render::{ + mesh::MeshVertexBufferLayoutRef, + render_resource::{ + AsBindGroup, RenderPipelineDescriptor, ShaderDefVal, ShaderRef, + SpecializedMeshPipelineError, + }, + }, +}; + +/// This example uses shader source files from the assets subdirectory +const FRAGMENT_SHADER_ASSET_PATH: &str = "shaders/custom_material.wesl"; +/// An example utility shader that is used by the custom material +pub const UTIL_SHADER_HANDLE: Handle = weak_handle!("748706a1-969e-43d4-be36-74559bd31d23"); + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + MaterialPlugin::::default(), + CustomMaterialPlugin, + )) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +/// A plugin that loads the custom material shader +pub struct CustomMaterialPlugin; + +impl Plugin for CustomMaterialPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + UTIL_SHADER_HANDLE, + "../../assets/shaders/util.wesl", + Shader::from_wesl + ); + } +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(CustomMaterial { + time: 0.0, + party_mode: false, + })), + Transform::from_xyz(0.0, 0.5, 0.0), + )); + + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn update( + time: Res