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