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
This commit is contained in:
parent
c14733d177
commit
181445c56b
15
Cargo.toml
15
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"
|
||||
|
19
assets/shaders/custom_material.wesl
Normal file
19
assets/shaders/custom_material.wesl
Normal file
@ -0,0 +1,19 @@
|
||||
import super::shaders::util::make_polka_dots;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct CustomMaterial {
|
||||
time: f32,
|
||||
}
|
||||
|
||||
@group(2) @binding(0) var<uniform> material: CustomMaterial;
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
mesh: VertexOutput,
|
||||
) -> @location(0) vec4<f32> {
|
||||
return make_polka_dots(mesh.uv, material.time);
|
||||
}
|
29
assets/shaders/util.wesl
Normal file
29
assets/shaders/util.wesl
Normal file
@ -0,0 +1,29 @@
|
||||
fn make_polka_dots(pos: vec2<f32>, time: f32) -> vec4<f32> {
|
||||
// Create repeating circles
|
||||
let scaled_pos = pos * 6.0;
|
||||
let cell = vec2<f32>(fract(scaled_pos.x), fract(scaled_pos.y));
|
||||
let dist_from_center = distance(cell, vec2<f32>(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<f32>(0.0);
|
||||
@if(!PARTY_MODE) {
|
||||
let color1 = vec3<f32>(1.0, 0.4, 0.8); // pink
|
||||
let color2 = vec3<f32>(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<f32>(1.0, 0.2, 0.2); // red
|
||||
let color2 = vec3<f32>(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<f32>(dot_color * is_dot, is_dot);
|
||||
}
|
@ -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",
|
||||
|
@ -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.
|
||||
|
@ -127,6 +127,8 @@ struct ShaderData {
|
||||
|
||||
struct ShaderCache {
|
||||
data: HashMap<AssetId<Shader>, ShaderData>,
|
||||
#[cfg(feature = "shader_format_wesl")]
|
||||
asset_paths: HashMap<wesl::syntax::ModulePath, AssetId<Shader>>,
|
||||
shaders: HashMap<AssetId<Shader>, Shader>,
|
||||
import_path_shaders: HashMap<ShaderImport, AssetId<Shader>>,
|
||||
waiting_on_import: HashMap<ShaderImport, Vec<AssetId<Shader>>>,
|
||||
@ -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<wesl::syntax::ModulePath, AssetId<Shader>>,
|
||||
shaders: &'a HashMap<AssetId<Shader>, Shader>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "shader_format_wesl")]
|
||||
impl<'a> ShaderResolver<'a> {
|
||||
pub fn new(
|
||||
asset_paths: &'a HashMap<wesl::syntax::ModulePath, AssetId<Shader>>,
|
||||
shaders: &'a HashMap<AssetId<Shader>, 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<Cow<str>, 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<BindGroupLayoutId>, Vec<PushConstantRange>);
|
||||
#[derive(Default)]
|
||||
struct LayoutCache {
|
||||
|
@ -155,6 +155,49 @@ impl Shader {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "shader_format_wesl")]
|
||||
pub fn from_wesl(source: impl Into<Cow<'static, str>>, path: impl Into<String>) -> 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<P: Into<String>>(&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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)|
|
||||
|
@ -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)
|
||||
|
126
examples/shader/shader_material_wesl.rs
Normal file
126
examples/shader/shader_material_wesl.rs
Normal file
@ -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<Shader> = weak_handle!("748706a1-969e-43d4-be36-74559bd31d23");
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((
|
||||
DefaultPlugins,
|
||||
MaterialPlugin::<CustomMaterial>::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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<CustomMaterial>>,
|
||||
) {
|
||||
// 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<Time>,
|
||||
mut query: Query<&MeshMaterial3d<CustomMaterial>>,
|
||||
mut materials: ResMut<Assets<CustomMaterial>>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
for material in query.iter_mut() {
|
||||
let material = materials.get_mut(material).unwrap();
|
||||
material.time = time.elapsed_secs();
|
||||
if keys.just_pressed(KeyCode::Space) {
|
||||
material.party_mode = !material.party_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the struct that will be passed to your shader
|
||||
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
||||
#[bind_group_data(CustomMaterialKey)]
|
||||
struct CustomMaterial {
|
||||
#[uniform(0)]
|
||||
time: f32,
|
||||
party_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
struct CustomMaterialKey {
|
||||
party_mode: bool,
|
||||
}
|
||||
|
||||
impl From<&CustomMaterial> for CustomMaterialKey {
|
||||
fn from(material: &CustomMaterial) -> Self {
|
||||
Self {
|
||||
party_mode: material.party_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Material for CustomMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
FRAGMENT_SHADER_ASSET_PATH.into()
|
||||
}
|
||||
|
||||
fn specialize(
|
||||
_pipeline: &MaterialPipeline<Self>,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
_layout: &MeshVertexBufferLayoutRef,
|
||||
key: MaterialPipelineKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
let fragment = descriptor.fragment.as_mut().unwrap();
|
||||
fragment.shader_defs.push(ShaderDefVal::Bool(
|
||||
"PARTY_MODE".to_string(),
|
||||
key.bind_group_data.party_mode,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user