From 6b388633137a4ea58b81509a744a84e96bb3cd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=94=E7=A9=B6=E7=A4=BE=E4=BA=A4?= Date: Thu, 26 Jan 2023 13:18:15 +0000 Subject: [PATCH] Request WGPU Capabilities for Non-uniform Indexing (#6995) # Objective Fixes #6952 ## Solution - Request WGPU capabilities `SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`, `SAMPLER_NON_UNIFORM_INDEXING` and `UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING` when corresponding features are enabled. - Add an example (`shaders/texture_binding_array`) illustrating (and testing) the use of non-uniform indexed textures and samplers. ![image](https://user-images.githubusercontent.com/16053640/209448310-defa4eae-6bcb-460d-9b3d-a3d2fad4316c.png) ## Changelog - Added new capabilities for shader validation. - Added example `shaders/texture_binding_array`. --- Cargo.toml | 10 ++ assets/shaders/texture_binding_array.wgsl | 15 ++ .../bevy_render/src/render_resource/shader.rs | 12 ++ examples/README.md | 1 + examples/shader/texture_binding_array.rs | 165 ++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 assets/shaders/texture_binding_array.wgsl create mode 100644 examples/shader/texture_binding_array.rs diff --git a/Cargo.toml b/Cargo.toml index daa5b45533b2d..ef6baef019441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1301,6 +1301,16 @@ description = "A shader that shows how to reuse the core bevy PBR shading functi category = "Shaders" wasm = true +[[example]] +name = "texture_binding_array" +path = "examples/shader/texture_binding_array.rs" + +[package.metadata.example.texture_binding_array] +name = "Texture Binding Array (Bindless Textures)" +description = "A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures)." +category = "Shaders" +wasm = false + # Stress tests [[package.metadata.category]] name = "Stress Tests" diff --git a/assets/shaders/texture_binding_array.wgsl b/assets/shaders/texture_binding_array.wgsl new file mode 100644 index 0000000000000..00c310ba6aa55 --- /dev/null +++ b/assets/shaders/texture_binding_array.wgsl @@ -0,0 +1,15 @@ +@group(1) @binding(0) +var textures: binding_array>; +@group(1) @binding(1) +var samplers: binding_array; + +@fragment +fn fragment( + #import bevy_pbr::mesh_vertex_output +) -> @location(0) vec4 { + // Select the texture to sample from using non-uniform uv coordinates + let coords = clamp(vec2(uv * 4.0), vec2(0u), vec2(3u)); + let index = coords.y * 4u + coords.x; + let inner_uv = fract(uv * 4.0); + return textureSample(textures[index], samplers[index], inner_uv); +} diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 08f01f64fa81c..63e8b5b64f655 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -141,6 +141,18 @@ impl ProcessedShader { Features::SHADER_PRIMITIVE_INDEX, Capabilities::PRIMITIVE_INDEX, ), + ( + Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + Capabilities::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + ), + ( + Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + Capabilities::SAMPLER_NON_UNIFORM_INDEXING, + ), + ( + Features::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, + Capabilities::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, + ), ]; let mut capabilities = Capabilities::empty(); for (feature, capability) in CAPABILITIES { diff --git a/examples/README.md b/examples/README.md index f2733fcf5b88d..672053b436818 100644 --- a/examples/README.md +++ b/examples/README.md @@ -271,6 +271,7 @@ Example | Description [Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the depth texture generated in a prepass [Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) +[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). ## Stress Tests diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs new file mode 100644 index 0000000000000..def63807c4d3c --- /dev/null +++ b/examples/shader/texture_binding_array.rs @@ -0,0 +1,165 @@ +//! A shader that binds several textures onto one +//! `binding_array>` shader binding slot and sample non-uniformly. + +use bevy::{ + prelude::*, + reflect::TypeUuid, + render::{ + render_asset::RenderAssets, + render_resource::{AsBindGroupError, PreparedBindGroup, *}, + renderer::RenderDevice, + texture::FallbackImage, + }, +}; +use std::num::NonZeroU32; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_plugin(MaterialPlugin::::default()) + .add_startup_system(setup) + .run(); +} + +const MAX_TEXTURE_COUNT: usize = 16; +const TILE_ID: [usize; 16] = [ + 19, 23, 4, 33, 12, 69, 30, 48, 10, 65, 40, 47, 57, 41, 44, 46, +]; + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, + render_device: Res, +) { + // check if the device support the required feature + if !render_device + .features() + .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING) + { + error!( + "Render device doesn't support feature \ + SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \ + which is required for texture binding arrays" + ); + return; + } + + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), + ..Default::default() + }); + + // load 16 textures + let textures: Vec<_> = TILE_ID + .iter() + .map(|id| { + let path = format!("textures/rpg/tiles/generic-rpg-tile{:0>2}.png", id); + asset_server.load(path) + }) + .collect(); + + // a cube with multiple textures + commands.spawn(MaterialMeshBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(BindlessMaterial { textures }), + ..Default::default() + }); +} + +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "8dd2b424-45a2-4a53-ac29-7ce356b2d5fe"] +struct BindlessMaterial { + textures: Vec>, +} + +impl AsBindGroup for BindlessMaterial { + type Data = (); + + fn as_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + image_assets: &RenderAssets, + fallback_image: &FallbackImage, + ) -> Result, AsBindGroupError> { + // retrieve the render resources from handles + let mut images = vec![]; + for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) { + match image_assets.get(handle) { + Some(image) => images.push(image), + None => return Err(AsBindGroupError::RetryNextUpdate), + } + } + + let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT]; + let samplers = vec![&fallback_image.sampler; MAX_TEXTURE_COUNT]; + + // convert bevy's resource types to WGPU's references + let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect(); + let mut samplers: Vec<_> = samplers.into_iter().map(|sampler| &**sampler).collect(); + + // fill in up to the first `MAX_TEXTURE_COUNT` textures and samplers to the arrays + for (id, image) in images.into_iter().enumerate() { + textures[id] = &*image.texture_view; + samplers[id] = &*image.sampler; + } + + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: "bindless_material_bind_group".into(), + layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureViewArray(&textures[..]), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::SamplerArray(&samplers[..]), + }, + ], + }); + + Ok(PreparedBindGroup { + bindings: vec![], + bind_group, + data: (), + }) + } + + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout + where + Self: Sized, + { + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: "bindless_material_layout".into(), + entries: &[ + // @group(1) @binding(0) var textures: binding_array>; + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32), + }, + // @group(1) @binding(1) var samplers: binding_array; + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32), + }, + ], + }) + } +} + +impl Material for BindlessMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/texture_binding_array.wgsl".into() + } +}