From 747b0c69b01bb40cf8050af1af4c60f9149889ea Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 30 Jun 2022 23:48:46 +0000 Subject: [PATCH] Better Materials: AsBindGroup trait and derive, simpler Material trait (#5053) # Objective This PR reworks Bevy's Material system, making the user experience of defining Materials _much_ nicer. Bevy's previous material system leaves a lot to be desired: * Materials require manually implementing the `RenderAsset` trait, which involves manually generating the bind group, handling gpu buffer data transfer, looking up image textures, etc. Even the simplest single-texture material involves writing ~80 unnecessary lines of code. This was never the long term plan. * There are two material traits, which is confusing, hard to document, and often redundant: `Material` and `SpecializedMaterial`. `Material` implicitly implements `SpecializedMaterial`, and `SpecializedMaterial` is used in most high level apis to support both use cases. Most users shouldn't need to think about specialization at all (I consider it a "power-user tool"), so the fact that `SpecializedMaterial` is front-and-center in our apis is a miss. * Implementing either material trait involves a lot of "type soup". The "prepared asset" parameter is particularly heinous: `&::PreparedAsset`. Defining vertex and fragment shaders is also more verbose than it needs to be. ## Solution Say hello to the new `Material` system: ```rust #[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CoolMaterial { #[uniform(0)] color: Color, #[texture(1)] #[sampler(2)] color_texture: Handle, } impl Material for CoolMaterial { fn fragment_shader() -> ShaderRef { "cool_material.wgsl".into() } } ``` Thats it! This same material would have required [~80 lines of complicated "type heavy" code](https://github.com/bevyengine/bevy/blob/v0.7.0/examples/shader/shader_material.rs) in the old Material system. Now it is just 14 lines of simple, readable code. This is thanks to a new consolidated `Material` trait and the new `AsBindGroup` trait / derive. ### The new `Material` trait The old "split" `Material` and `SpecializedMaterial` traits have been removed in favor of a new consolidated `Material` trait. All of the functions on the trait are optional. The difficulty of implementing `Material` has been reduced by simplifying dataflow and removing type complexity: ```rust // Old impl Material for CustomMaterial { fn fragment_shader(asset_server: &AssetServer) -> Option> { Some(asset_server.load("custom_material.wgsl")) } fn alpha_mode(render_asset: &::PreparedAsset) -> AlphaMode { render_asset.alpha_mode } } // New impl Material for CustomMaterial { fn fragment_shader() -> ShaderRef { "custom_material.wgsl".into() } fn alpha_mode(&self) -> AlphaMode { self.alpha_mode } } ``` Specialization is still supported, but it is hidden by default under the `specialize()` function (more on this later). ### The `AsBindGroup` trait / derive The `Material` trait now requires the `AsBindGroup` derive. This can be implemented manually relatively easily, but deriving it will almost always be preferable. Field attributes like `uniform` and `texture` are used to define which fields should be bindings, what their binding type is, and what index they should be bound at: ```rust #[derive(AsBindGroup)] struct CoolMaterial { #[uniform(0)] color: Color, #[texture(1)] #[sampler(2)] color_texture: Handle, } ``` In WGSL shaders, the binding looks like this: ```wgsl struct CoolMaterial { color: vec4; }; [[group(1), binding(0)]] var material: CoolMaterial; [[group(1), binding(1)]] var color_texture: texture_2d; [[group(1), binding(2)]] var color_sampler: sampler; ``` Note that the "group" index is determined by the usage context. It is not defined in `AsBindGroup`. Bevy material bind groups are bound to group 1. The following field-level attributes are supported: * `uniform(BINDING_INDEX)` * The field will be converted to a shader-compatible type using the `ShaderType` trait, written to a `Buffer`, and bound as a uniform. It can also be derived for custom structs. * `texture(BINDING_INDEX)` * This field's `Handle` will be used to look up the matching `Texture` gpu resource, which will be bound as a texture in shaders. The field will be assumed to implement `Into>>`. In practice, most fields should be a `Handle` or `Option>`. If the value of an `Option>` is `None`, the new `FallbackImage` resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute (with a different binding index). * `sampler(BINDING_INDEX)` * Behaves exactly like the `texture` attribute, but sets the Image's sampler binding instead of the texture. Note that fields without field-level binding attributes will be ignored. ```rust #[derive(AsBindGroup)] struct CoolMaterial { #[uniform(0)] color: Color, this_field_is_ignored: String, } ``` As mentioned above, `Option>` is also supported: ```rust #[derive(AsBindGroup)] struct CoolMaterial { #[uniform(0)] color: Color, #[texture(1)] #[sampler(2)] color_texture: Option>, } ``` This is useful if you want a texture to be optional. When the value is `None`, the `FallbackImage` will be used for the binding instead, which defaults to "pure white". Field uniforms with the same binding index will be combined into a single binding: ```rust #[derive(AsBindGroup)] struct CoolMaterial { #[uniform(0)] color: Color, #[uniform(0)] roughness: f32, } ``` In WGSL shaders, the binding would look like this: ```wgsl struct CoolMaterial { color: vec4; roughness: f32; }; [[group(1), binding(0)]] var material: CoolMaterial; ``` Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes: * `uniform(BINDING_INDEX, ConvertedShaderType)` * Similar to the field-level `uniform` attribute, but instead the entire `AsBindGroup` value is converted to `ConvertedShaderType`, which must implement `ShaderType`. This is useful if more complicated conversion logic is required. * `bind_group_data(DataType)` * The `AsBindGroup` type will be converted to some `DataType` using `Into` and stored as `AsBindGroup::Data` as part of the `AsBindGroup::as_bind_group` call. This is useful if data needs to be stored alongside the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute is "shader pipeline specialization". The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can also be equivalently represented with a single struct-level uniform attribute: ```rust #[derive(AsBindGroup)] #[uniform(0, CoolMaterialUniform)] struct CoolMaterial { color: Color, roughness: f32, } #[derive(ShaderType)] struct CoolMaterialUniform { color: Color, roughness: f32, } impl From<&CoolMaterial> for CoolMaterialUniform { fn from(material: &CoolMaterial) -> CoolMaterialUniform { CoolMaterialUniform { color: material.color, roughness: material.roughness, } } } ``` ### Material Specialization Material shader specialization is now _much_ simpler: ```rust #[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] #[bind_group_data(CoolMaterialKey)] struct CoolMaterial { #[uniform(0)] color: Color, is_red: bool, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] struct CoolMaterialKey { is_red: bool, } impl From<&CoolMaterial> for CoolMaterialKey { fn from(material: &CoolMaterial) -> CoolMaterialKey { CoolMaterialKey { is_red: material.is_red, } } } impl Material for CoolMaterial { fn fragment_shader() -> ShaderRef { "cool_material.wgsl".into() } fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { if key.bind_group_data.is_red { let fragment = descriptor.fragment.as_mut().unwrap(); fragment.shader_defs.push("IS_RED".to_string()); } Ok(()) } } ``` Setting `bind_group_data` is not required for specialization (it defaults to `()`). Scenarios like "custom vertex attributes" also benefit from this system: ```rust impl Material for CustomMaterial { fn vertex_shader() -> ShaderRef { "custom_material.wgsl".into() } fn fragment_shader() -> ShaderRef { "custom_material.wgsl".into() } fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { let vertex_layout = layout.get_layout(&[ Mesh::ATTRIBUTE_POSITION.at_shader_location(0), ATTRIBUTE_BLEND_COLOR.at_shader_location(1), ])?; descriptor.vertex.buffers = vec![vertex_layout]; Ok(()) } } ``` ### Ported `StandardMaterial` to the new `Material` system Bevy's built-in PBR material uses the new Material system (including the AsBindGroup derive): ```rust #[derive(AsBindGroup, Debug, Clone, TypeUuid)] #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"] #[bind_group_data(StandardMaterialKey)] #[uniform(0, StandardMaterialUniform)] pub struct StandardMaterial { pub base_color: Color, #[texture(1)] #[sampler(2)] pub base_color_texture: Option>, /* other fields omitted for brevity */ ``` ### Ported Bevy examples to the new `Material` system The overall complexity of Bevy's "custom shader examples" has gone down significantly. Take a look at the diffs if you want a dopamine spike. Please note that while this PR has a net increase in "lines of code", most of those extra lines come from added documentation. There is a significant reduction in the overall complexity of the code (even accounting for the new derive logic). --- ## Changelog ### Added * `AsBindGroup` trait and derive, which make it much easier to transfer data to the gpu and generate bind groups for a given type. ### Changed * The old `Material` and `SpecializedMaterial` traits have been replaced by a consolidated (much simpler) `Material` trait. Materials no longer implement `RenderAsset`. * `StandardMaterial` was ported to the new material system. There are no user-facing api changes to the `StandardMaterial` struct api, but it now implements `AsBindGroup` and `Material` instead of `RenderAsset` and `SpecializedMaterial`. ## Migration Guide The Material system has been reworked to be much simpler. We've removed a lot of boilerplate with the new `AsBindGroup` derive and the `Material` trait is simpler as well! ### Bevy 0.7 (old) ```rust #[derive(Debug, Clone, TypeUuid)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CustomMaterial { color: Color, color_texture: Handle, } #[derive(Clone)] pub struct GpuCustomMaterial { _buffer: Buffer, bind_group: BindGroup, } impl RenderAsset for CustomMaterial { type ExtractedAsset = CustomMaterial; type PreparedAsset = GpuCustomMaterial; type Param = (SRes, SRes>); fn extract_asset(&self) -> Self::ExtractedAsset { self.clone() } fn prepare_asset( extracted_asset: Self::ExtractedAsset, (render_device, material_pipeline): &mut SystemParamItem, ) -> Result> { let color = Vec4::from_slice(&extracted_asset.color.as_linear_rgba_f32()); let byte_buffer = [0u8; Vec4::SIZE.get() as usize]; let mut buffer = encase::UniformBuffer::new(byte_buffer); buffer.write(&color).unwrap(); let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { contents: buffer.as_ref(), label: None, usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, }); let (texture_view, texture_sampler) = if let Some(result) = material_pipeline .mesh_pipeline .get_image_texture(gpu_images, &Some(extracted_asset.color_texture.clone())) { result } else { return Err(PrepareAssetError::RetryNextUpdate(extracted_asset)); }; let bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ BindGroupEntry { binding: 0, resource: buffer.as_entire_binding(), }, BindGroupEntry { binding: 0, resource: BindingResource::TextureView(texture_view), }, BindGroupEntry { binding: 1, resource: BindingResource::Sampler(texture_sampler), }, ], label: None, layout: &material_pipeline.material_layout, }); Ok(GpuCustomMaterial { _buffer: buffer, bind_group, }) } } impl Material for CustomMaterial { fn fragment_shader(asset_server: &AssetServer) -> Option> { Some(asset_server.load("custom_material.wgsl")) } fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { &render_asset.bind_group } fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ BindGroupLayoutEntry { binding: 0, visibility: ShaderStages::FRAGMENT, ty: BindingType::Buffer { ty: BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: Some(Vec4::min_size()), }, count: None, }, BindGroupLayoutEntry { binding: 1, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { multisampled: false, sample_type: TextureSampleType::Float { filterable: true }, view_dimension: TextureViewDimension::D2Array, }, count: None, }, BindGroupLayoutEntry { binding: 2, visibility: ShaderStages::FRAGMENT, ty: BindingType::Sampler(SamplerBindingType::Filtering), count: None, }, ], label: None, }) } } ``` ### Bevy 0.8 (new) ```rust impl Material for CustomMaterial { fn fragment_shader() -> ShaderRef { "custom_material.wgsl".into() } } #[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CustomMaterial { #[uniform(0)] color: Color, #[texture(1)] #[sampler(2)] color_texture: Handle, } ``` ## Future Work * Add support for more binding types (cubemaps, buffers, etc). This PR intentionally includes a bare minimum number of binding types to keep "reviewability" in check. * Consider optionally eliding binding indices using binding names. `AsBindGroup` could pass in (optional?) reflection info as a "hint". * This would make it possible for the derive to do this: ```rust #[derive(AsBindGroup)] pub struct CustomMaterial { #[uniform] color: Color, #[texture] #[sampler] color_texture: Option>, alpha_mode: AlphaMode, } ``` * Or this ```rust #[derive(AsBindGroup)] pub struct CustomMaterial { #[binding] color: Color, #[binding] color_texture: Option>, alpha_mode: AlphaMode, } ``` * Or even this (if we flip to "include bindings by default") ```rust #[derive(AsBindGroup)] pub struct CustomMaterial { color: Color, color_texture: Option>, #[binding(ignore)] alpha_mode: AlphaMode, } ``` * If we add the option to define custom draw functions for materials (which could be done in a type-erased way), I think that would be enough to support extra non-material bindings. Worth considering! --- assets/shaders/custom_material.wgsl | 9 +- assets/shaders/shader_defs.wgsl | 37 +- crates/bevy_pbr/src/bundle.rs | 8 +- crates/bevy_pbr/src/material.rs | 525 +++++++++++------- crates/bevy_pbr/src/pbr_material.rs | 358 ++---------- .../bevy_render/macros/src/as_bind_group.rs | 389 +++++++++++++ crates/bevy_render/macros/src/lib.rs | 6 + crates/bevy_render/src/color/mod.rs | 67 +++ .../src/render_resource/bind_group.rs | 265 +++++++++ .../bevy_render/src/render_resource/shader.rs | 30 +- .../bevy_render/src/texture/fallback_image.rs | 52 ++ crates/bevy_render/src/texture/mod.rs | 3 + examples/shader/array_texture.rs | 78 ++- examples/shader/custom_vertex_attribute.rs | 89 +-- examples/shader/shader_defs.rs | 203 +++---- examples/shader/shader_material.rs | 119 +--- examples/shader/shader_material_glsl.rs | 96 +--- .../shader_material_screenspace_texture.rs | 94 +--- 18 files changed, 1367 insertions(+), 1061 deletions(-) create mode 100644 crates/bevy_render/macros/src/as_bind_group.rs create mode 100644 crates/bevy_render/src/texture/fallback_image.rs diff --git a/assets/shaders/custom_material.wgsl b/assets/shaders/custom_material.wgsl index 4f40d3cff6..33d706f9d8 100644 --- a/assets/shaders/custom_material.wgsl +++ b/assets/shaders/custom_material.wgsl @@ -1,10 +1,15 @@ struct CustomMaterial { color: vec4; }; + [[group(1), binding(0)]] var material: CustomMaterial; +[[group(1), binding(1)]] +var base_color_texture: texture_2d; +[[group(1), binding(2)]] +var base_color_sampler: sampler; [[stage(fragment)]] -fn fragment() -> [[location(0)]] vec4 { - return material.color; +fn fragment([[location(2)]] uv: vec2) -> [[location(0)]] vec4 { + return material.color * textureSample(base_color_texture, base_color_sampler, uv); } diff --git a/assets/shaders/shader_defs.wgsl b/assets/shaders/shader_defs.wgsl index a26e988ed9..5518a73b02 100644 --- a/assets/shaders/shader_defs.wgsl +++ b/assets/shaders/shader_defs.wgsl @@ -1,34 +1,15 @@ -#import bevy_pbr::mesh_types -#import bevy_pbr::mesh_view_bindings +struct CustomMaterial { + color: vec4; +}; [[group(1), binding(0)]] -var mesh: Mesh; - -// NOTE: Bindings must come before functions that use them! -#import bevy_pbr::mesh_functions - -struct Vertex { - [[location(0)]] position: vec3; - [[location(1)]] normal: vec3; - [[location(2)]] uv: vec2; -}; - -struct VertexOutput { - [[builtin(position)]] clip_position: vec4; -}; - -[[stage(vertex)]] -fn vertex(vertex: Vertex) -> VertexOutput { - var out: VertexOutput; - out.clip_position = mesh_position_local_to_clip(mesh.model, vec4(vertex.position, 1.0)); - return out; -} +var material: CustomMaterial; [[stage(fragment)]] fn fragment() -> [[location(0)]] vec4 { - var color = vec4(0.0, 0.0, 1.0, 1.0); -# ifdef IS_RED - color = vec4(1.0, 0.0, 0.0, 1.0); -# endif - return color; +#ifdef IS_RED + return vec4(1.0, 0.0, 0.0, 1.0); +#else + return material.color; +#endif } diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index c42ce981b9..7052459dea 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -1,4 +1,4 @@ -use crate::{DirectionalLight, PointLight, SpecializedMaterial, StandardMaterial}; +use crate::{DirectionalLight, Material, PointLight, StandardMaterial}; use bevy_asset::Handle; use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; use bevy_reflect::Reflect; @@ -12,9 +12,9 @@ use bevy_transform::components::{GlobalTransform, Transform}; /// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`]. pub type PbrBundle = MaterialMeshBundle; -/// A component bundle for entities with a [`Mesh`] and a [`SpecializedMaterial`]. +/// A component bundle for entities with a [`Mesh`] and a [`Material`]. #[derive(Bundle, Clone)] -pub struct MaterialMeshBundle { +pub struct MaterialMeshBundle { pub mesh: Handle, pub material: Handle, pub transform: Transform, @@ -25,7 +25,7 @@ pub struct MaterialMeshBundle { pub computed_visibility: ComputedVisibility, } -impl Default for MaterialMeshBundle { +impl Default for MaterialMeshBundle { fn default() -> Self { Self { mesh: Default::default(), diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 28620aaec1..a21aed617b 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -3,248 +3,228 @@ use crate::{ SetMeshViewBindGroup, }; use bevy_app::{App, Plugin}; -use bevy_asset::{AddAsset, Asset, AssetServer, Handle}; +use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; use bevy_core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy_ecs::{ entity::Entity, + event::EventReader, prelude::World, + schedule::ParallelSystemDescriptorCoercion, system::{ lifetimeless::{Read, SQuery, SRes}, - Query, Res, ResMut, SystemParamItem, + Commands, Local, Query, Res, ResMut, SystemParamItem, }, world::FromWorld, }; +use bevy_reflect::TypeUuid; use bevy_render::{ extract_component::ExtractComponentPlugin, mesh::{Mesh, MeshVertexBufferLayout}, - render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets}, + prelude::Image, + render_asset::{PrepareAssetLabel, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{ - BindGroup, BindGroupLayout, PipelineCache, RenderPipelineDescriptor, Shader, - SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource, + PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, + SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, + texture::FallbackImage, view::{ExtractedView, Msaa, VisibleEntities}, RenderApp, RenderStage, }; -use bevy_utils::tracing::error; +use bevy_utils::{tracing::error, HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; /// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`](crate::MaterialMeshBundle) /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level -/// way to render [`Mesh`] entities with custom shader logic. For materials that can specialize their [`RenderPipelineDescriptor`] -/// based on specific material values, see [`SpecializedMaterial`]. [`Material`] automatically implements [`SpecializedMaterial`] -/// and can be used anywhere that type is used (such as [`MaterialPlugin`]). -pub trait Material: Asset + RenderAsset + Sized { - /// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material::bind_group_layout`]. - fn bind_group(material: &::PreparedAsset) -> &BindGroup; - - /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`Material::bind_group`]. - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; - - /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - None +/// way to render [`Mesh`] entities with custom shader logic. +/// +/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// Materials must also implement [`TypeUuid`] so they can be treated as an [`Asset`](bevy_asset::Asset). +/// +/// # Example +/// +/// Here is a simple Material implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// ``` +/// # use bevy_pbr::{Material, MaterialMeshBundle}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_reflect::TypeUuid; +/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color}; +/// # use bevy_asset::{Handle, AssetServer, Assets}; +/// +/// #[derive(AsBindGroup, TypeUuid, Debug, Clone)] +/// #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: Color, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `Material` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl Material for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity using `CustomMaterial`. +/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { +/// commands.spawn_bundle(MaterialMeshBundle { +/// material: materials.add(CustomMaterial { +/// color: Color::RED, +/// color_texture: asset_server.load("some_image.png"), +/// }), +/// ..Default::default() +/// }); +/// } +/// ``` +/// In WGSL shaders, the material's binding would look like this: +/// +/// ```wgsl +/// struct CustomMaterial { +/// color: vec4; +/// }; +/// +/// [[group(1), binding(0)]] +/// var material: CustomMaterial; +/// [[group(1), binding(1)]] +/// var color_texture: texture_2d; +/// [[group(1), binding(2)]] +/// var color_sampler: sampler; +/// ``` +pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'static { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default } - /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. - /// Defaults to [`None`]. + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader + /// will be used. #[allow(unused_variables)] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - None + fn fragment_shader() -> ShaderRef { + ShaderRef::Default } /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. - #[allow(unused_variables)] - fn alpha_mode(material: &::PreparedAsset) -> AlphaMode { + #[inline] + fn alpha_mode(&self) -> AlphaMode { AlphaMode::Opaque } - /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. - /// Defaults to an empty array / no dynamic uniform indices. - #[allow(unused_variables)] - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - &[] - } - - #[allow(unused_variables)] #[inline] /// Add a bias to the view depth of the mesh which can be used to force a specific render order /// for meshes with equal depth, to avoid z-fighting. - fn depth_bias(material: &::PreparedAsset) -> f32 { + fn depth_bias(&self) -> f32 { 0.0 } - /// Customizes the default [`RenderPipelineDescriptor`]. + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's + /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input. #[allow(unused_variables)] #[inline] fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, + key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } -impl SpecializedMaterial for M { - type Key = (); +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] +/// asset type. +pub struct MaterialPlugin(PhantomData); - #[inline] - fn key(_material: &::PreparedAsset) -> Self::Key {} - - #[inline] - fn specialize( - pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - _key: Self::Key, - layout: &MeshVertexBufferLayout, - ) -> Result<(), SpecializedMeshPipelineError> { - ::specialize(pipeline, descriptor, layout) - } - - #[inline] - fn bind_group(material: &::PreparedAsset) -> &BindGroup { - ::bind_group(material) - } - - #[inline] - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - ::bind_group_layout(render_device) - } - - #[inline] - fn alpha_mode(material: &::PreparedAsset) -> AlphaMode { - ::alpha_mode(material) - } - - #[inline] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - ::vertex_shader(asset_server) - } - - #[inline] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - ::fragment_shader(asset_server) - } - - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - ::dynamic_uniform_indices(material) - } - - #[inline] - fn depth_bias(material: &::PreparedAsset) -> f32 { - ::depth_bias(material) - } -} - -/// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`](crate::MaterialMeshBundle) -/// to spawn entities that are rendered with a specific [`SpecializedMaterial`] type. They serve as an easy to use high level -/// way to render [`Mesh`] entities with custom shader logic. [`SpecializedMaterials`](SpecializedMaterial) use their [`SpecializedMaterial::Key`] -/// to customize their [`RenderPipelineDescriptor`] based on specific material values. The slightly simpler [`Material`] trait -/// should be used for materials that do not need specialization. [`Material`] types automatically implement [`SpecializedMaterial`]. -pub trait SpecializedMaterial: Asset + RenderAsset + Sized { - /// The key used to specialize this material's [`RenderPipelineDescriptor`]. - type Key: PartialEq + Eq + Hash + Clone + Send + Sync; - - /// Extract the [`SpecializedMaterial::Key`] for the "prepared" version of this material. This key will be - /// passed in to the [`SpecializedMaterial::specialize`] function when compiling the [`RenderPipeline`](bevy_render::render_resource::RenderPipeline) - /// for a given entity's material. - fn key(material: &::PreparedAsset) -> Self::Key; - - /// Specializes the given `descriptor` according to the given `key`. - fn specialize( - pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - key: Self::Key, - layout: &MeshVertexBufferLayout, - ) -> Result<(), SpecializedMeshPipelineError>; - - /// Returns this material's [`BindGroup`]. This should match the layout returned by [`SpecializedMaterial::bind_group_layout`]. - fn bind_group(material: &::PreparedAsset) -> &BindGroup; - - /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`SpecializedMaterial::bind_group`]. - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; - - /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - None - } - - /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - None - } - - /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. - #[allow(unused_variables)] - fn alpha_mode(material: &::PreparedAsset) -> AlphaMode { - AlphaMode::Opaque - } - - /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. - /// Defaults to an empty array / no dynamic uniform indices. - #[allow(unused_variables)] - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - &[] - } - - #[allow(unused_variables)] - #[inline] - /// Add a bias to the view depth of the mesh which can be used to force a specific render order - /// for meshes with equal depth, to avoid z-fighting. - fn depth_bias(material: &::PreparedAsset) -> f32 { - 0.0 - } -} - -/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`SpecializedMaterial`] -/// asset type (which includes [`Material`] types). -pub struct MaterialPlugin(PhantomData); - -impl Default for MaterialPlugin { +impl Default for MaterialPlugin { fn default() -> Self { Self(Default::default()) } } -impl Plugin for MaterialPlugin { +impl Plugin for MaterialPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ fn build(&self, app: &mut App) { app.add_asset::() - .add_plugin(ExtractComponentPlugin::>::extract_visible()) - .add_plugin(RenderAssetPlugin::::default()); + .add_plugin(ExtractComponentPlugin::>::extract_visible()); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>() + .init_resource::>() + .init_resource::>() .init_resource::>>() + .add_system_to_stage(RenderStage::Extract, extract_materials::) + .add_system_to_stage( + RenderStage::Prepare, + prepare_materials::.after(PrepareAssetLabel::PreAssetPrepare), + ) .add_system_to_stage(RenderStage::Queue, queue_material_meshes::); } } } -#[derive(Eq, PartialEq, Clone, Hash)] -pub struct MaterialPipelineKey { +/// A key uniquely identifying a specialized [`MaterialPipeline`]. +pub struct MaterialPipelineKey { pub mesh_key: MeshPipelineKey, - pub material_key: T, + pub bind_group_data: M::Data, } -pub struct MaterialPipeline { +impl Eq for MaterialPipelineKey where M::Data: PartialEq {} + +impl PartialEq for MaterialPipelineKey +where + M::Data: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data + } +} + +impl Clone for MaterialPipelineKey +where + M::Data: Clone, +{ + fn clone(&self) -> Self { + Self { + mesh_key: self.mesh_key, + bind_group_data: self.bind_group_data.clone(), + } + } +} + +impl Hash for MaterialPipelineKey +where + M::Data: Hash, +{ + fn hash(&self, state: &mut H) { + self.mesh_key.hash(state); + self.bind_group_data.hash(state); + } +} + +/// Render pipeline data for a given [`Material`]. +pub struct MaterialPipeline { pub mesh_pipeline: MeshPipeline, pub material_layout: BindGroupLayout, pub vertex_shader: Option>, @@ -252,8 +232,11 @@ pub struct MaterialPipeline { marker: PhantomData, } -impl SpecializedMeshPipeline for MaterialPipeline { - type Key = MaterialPipelineKey; +impl SpecializedMeshPipeline for MaterialPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = MaterialPipelineKey; fn specialize( &self, @@ -274,22 +257,29 @@ impl SpecializedMeshPipeline for MaterialPipeline { let descriptor_layout = descriptor.layout.as_mut().unwrap(); descriptor_layout.insert(1, self.material_layout.clone()); - M::specialize(self, &mut descriptor, key.material_key, layout)?; + M::specialize(self, &mut descriptor, layout, key)?; Ok(descriptor) } } -impl FromWorld for MaterialPipeline { +impl FromWorld for MaterialPipeline { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); let render_device = world.resource::(); - let material_layout = M::bind_group_layout(render_device); MaterialPipeline { mesh_pipeline: world.resource::().clone(), - material_layout, - vertex_shader: M::vertex_shader(asset_server), - fragment_shader: M::fragment_shader(asset_server), + material_layout: M::bind_group_layout(render_device), + vertex_shader: match M::vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + fragment_shader: match M::fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, marker: PhantomData, } } @@ -303,9 +293,10 @@ type DrawMaterial = ( DrawMesh, ); -pub struct SetMaterialBindGroup(PhantomData); -impl EntityRenderCommand for SetMaterialBindGroup { - type Param = (SRes>, SQuery>>); +/// Sets the bind group for a given [`Material`] at the configured `I` index. +pub struct SetMaterialBindGroup(PhantomData); +impl EntityRenderCommand for SetMaterialBindGroup { + type Param = (SRes>, SQuery>>); fn render<'w>( _view: Entity, item: Entity, @@ -314,17 +305,13 @@ impl EntityRenderCommand for SetMaterial ) -> RenderCommandResult { let material_handle = query.get(item).unwrap(); let material = materials.into_inner().get(material_handle).unwrap(); - pass.set_bind_group( - I, - M::bind_group(material), - M::dynamic_uniform_indices(material), - ); + pass.set_bind_group(I, &material.bind_group, &[]); RenderCommandResult::Success } } #[allow(clippy::too_many_arguments)] -pub fn queue_material_meshes( +pub fn queue_material_meshes( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, transparent_draw_functions: Res>, @@ -333,7 +320,7 @@ pub fn queue_material_meshes( mut pipeline_cache: ResMut, msaa: Res, render_meshes: Res>, - render_materials: Res>, + render_materials: Res>, material_meshes: Query<(&Handle, &Handle, &MeshUniform)>, mut views: Query<( &ExtractedView, @@ -342,7 +329,9 @@ pub fn queue_material_meshes( &mut RenderPhase, &mut RenderPhase, )>, -) { +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ for (view, visible_entities, mut opaque_phase, mut alpha_mask_phase, mut transparent_phase) in views.iter_mut() { @@ -372,19 +361,17 @@ pub fn queue_material_meshes( let mut mesh_key = MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | msaa_key; - let alpha_mode = M::alpha_mode(material); + let alpha_mode = material.properties.alpha_mode; if let AlphaMode::Blend = alpha_mode { mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS; } - let material_key = M::key(material); - let pipeline_id = pipelines.specialize( &mut pipeline_cache, &material_pipeline, MaterialPipelineKey { mesh_key, - material_key, + bind_group_data: material.key.clone(), }, &mesh.layout, ); @@ -398,8 +385,8 @@ pub fn queue_material_meshes( // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix // gives the z component of translation of the mesh in view space - let bias = M::depth_bias(material); - let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3)) + bias; + let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3)) + + material.properties.depth_bias; match alpha_mode { AlphaMode::Opaque => { opaque_phase.add(Opaque3d { @@ -444,3 +431,159 @@ pub fn queue_material_meshes( } } } + +/// Common [`Material`] properties, calculated for a specific material instance. +pub struct MaterialProperties { + /// The [`AlphaMode`] of this material. + pub alpha_mode: AlphaMode, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + pub depth_bias: f32, +} + +/// Data prepared for a [`Material`] instance. +pub struct PreparedMaterial { + pub bindings: Vec, + pub bind_group: BindGroup, + pub key: T::Data, + pub properties: MaterialProperties, +} + +struct ExtractedMaterials { + extracted: Vec<(Handle, M)>, + removed: Vec>, +} + +impl Default for ExtractedMaterials { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + } + } +} + +/// Stores all prepared representations of [`Material`] assets for as long as they exist. +pub type RenderMaterials = HashMap, PreparedMaterial>; + +/// This system extracts all created or modified assets of the corresponding [`Material`] type +/// into the "render world". +fn extract_materials( + mut commands: Commands, + mut events: EventReader>, + assets: Res>, +) { + let mut changed_assets = HashSet::default(); + let mut removed = Vec::new(); + for event in events.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + changed_assets.insert(handle); + } + AssetEvent::Removed { handle } => { + changed_assets.remove(handle); + removed.push(handle.clone_weak()); + } + } + } + + let mut extracted_assets = Vec::new(); + for handle in changed_assets.drain() { + if let Some(asset) = assets.get(handle) { + extracted_assets.push((handle.clone_weak(), asset.clone())); + } + } + + commands.insert_resource(ExtractedMaterials { + extracted: extracted_assets, + removed, + }); +} + +/// All [`Material`] values of a given type that should be prepared next frame. +pub struct PrepareNextFrameMaterials { + assets: Vec<(Handle, M)>, +} + +impl Default for PrepareNextFrameMaterials { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`Material`] type +/// which where extracted this frame for the GPU. +fn prepare_materials( + mut prepare_next_frame: Local>, + mut extracted_assets: ResMut>, + mut render_materials: ResMut>, + render_device: Res, + images: Res>, + fallback_image: Res, + pipeline: Res>, +) { + let mut queued_assets = std::mem::take(&mut prepare_next_frame.assets); + for (handle, material) in queued_assets.drain(..) { + match prepare_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, material)); + } + } + } + + for removed in std::mem::take(&mut extracted_assets.removed) { + render_materials.remove(&removed); + } + + for (handle, material) in std::mem::take(&mut extracted_assets.extracted) { + match prepare_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, material)); + } + } + } +} + +fn prepare_material( + material: &M, + render_device: &RenderDevice, + images: &RenderAssets, + fallback_image: &FallbackImage, + pipeline: &MaterialPipeline, +) -> Result, AsBindGroupError> { + let prepared = material.as_bind_group( + &pipeline.material_layout, + render_device, + images, + fallback_image, + )?; + Ok(PreparedMaterial { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + properties: MaterialProperties { + alpha_mode: material.alpha_mode(), + depth_bias: material.depth_bias(), + }, + }) +} diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 3c91e7f9ad..3da72ab12a 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,15 +1,9 @@ -use crate::{AlphaMode, MaterialPipeline, SpecializedMaterial, PBR_SHADER_HANDLE}; -use bevy_asset::{AssetServer, Handle}; -use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE}; +use bevy_asset::Handle; use bevy_math::Vec4; use bevy_reflect::TypeUuid; use bevy_render::{ - color::Color, - mesh::MeshVertexBufferLayout, - prelude::Shader, - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, - render_resource::*, - renderer::RenderDevice, + color::Color, mesh::MeshVertexBufferLayout, render_asset::RenderAssets, render_resource::*, texture::Image, }; @@ -18,17 +12,23 @@ use bevy_render::{ /// . /// /// May be created directly from a [`Color`] or an [`Image`]. -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, Debug, Clone, TypeUuid)] #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"] +#[bind_group_data(StandardMaterialKey)] +#[uniform(0, StandardMaterialUniform)] pub struct StandardMaterial { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. If used together with a base_color_texture, this is factored into the final /// base color as `base_color * base_color_texture_value` pub base_color: Color, + #[texture(1)] + #[sampler(2)] pub base_color_texture: Option>, // Use a color for user friendliness even though we technically don't use the alpha channel // Might be used in the future for exposure correction in HDR pub emissive: Color, + #[texture(3)] + #[sampler(4)] pub emissive_texture: Option>, /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Defaults to minimum of 0.089 @@ -39,14 +39,20 @@ pub struct StandardMaterial { /// If used together with a roughness/metallic texture, this is factored into the final base /// color as `metallic * metallic_texture_value` pub metallic: f32, + #[texture(5)] + #[sampler(6)] pub metallic_roughness_texture: Option>, /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] /// defaults to 0.5 which is mapped to 4% reflectance in the shader pub reflectance: f32, + #[texture(9)] + #[sampler(10)] pub normal_map_texture: Option>, /// Normal map textures authored for DirectX have their y-component flipped. Set this to flip /// it to right-handed conventions. pub flip_normal_map_y: bool, + #[texture(7)] + #[sampler(8)] pub occlusion_texture: Option>, /// Support two-sided lighting by automatically flipping the normals for "back" faces /// within the PBR lighting shader. @@ -140,7 +146,7 @@ bitflags::bitflags! { /// The GPU representation of the uniform data of a [`StandardMaterial`]. #[derive(Clone, Default, ShaderType)] -pub struct StandardMaterialUniformData { +pub struct StandardMaterialUniform { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. pub base_color: Vec4, @@ -161,105 +167,31 @@ pub struct StandardMaterialUniformData { pub alpha_cutoff: f32, } -/// The GPU representation of a [`StandardMaterial`]. -#[derive(Debug, Clone)] -pub struct GpuStandardMaterial { - /// A buffer containing the [`StandardMaterialUniformData`] of the material. - pub buffer: Buffer, - /// The bind group specifying how the [`StandardMaterialUniformData`] and - /// all the textures of the material are bound. - pub bind_group: BindGroup, - pub has_normal_map: bool, - pub flags: StandardMaterialFlags, - pub base_color_texture: Option>, - pub alpha_mode: AlphaMode, - pub depth_bias: f32, - pub cull_mode: Option, -} - -impl RenderAsset for StandardMaterial { - type ExtractedAsset = StandardMaterial; - type PreparedAsset = GpuStandardMaterial; - type Param = ( - SRes, - SRes>, - SRes>, - ); - - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } - - fn prepare_asset( - material: Self::ExtractedAsset, - (render_device, pbr_pipeline, gpu_images): &mut SystemParamItem, - ) -> Result> { - let (base_color_texture_view, base_color_sampler) = if let Some(result) = pbr_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &material.base_color_texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; - - let (emissive_texture_view, emissive_sampler) = if let Some(result) = pbr_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &material.emissive_texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; - - let (metallic_roughness_texture_view, metallic_roughness_sampler) = if let Some(result) = - pbr_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &material.metallic_roughness_texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; - let (normal_map_texture_view, normal_map_sampler) = if let Some(result) = pbr_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &material.normal_map_texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; - let (occlusion_texture_view, occlusion_sampler) = if let Some(result) = pbr_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &material.occlusion_texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; +impl AsBindGroupShaderType for StandardMaterial { + fn as_bind_group_shader_type(&self, images: &RenderAssets) -> StandardMaterialUniform { let mut flags = StandardMaterialFlags::NONE; - if material.base_color_texture.is_some() { + if self.base_color_texture.is_some() { flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE; } - if material.emissive_texture.is_some() { + if self.emissive_texture.is_some() { flags |= StandardMaterialFlags::EMISSIVE_TEXTURE; } - if material.metallic_roughness_texture.is_some() { + if self.metallic_roughness_texture.is_some() { flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE; } - if material.occlusion_texture.is_some() { + if self.occlusion_texture.is_some() { flags |= StandardMaterialFlags::OCCLUSION_TEXTURE; } - if material.double_sided { + if self.double_sided { flags |= StandardMaterialFlags::DOUBLE_SIDED; } - if material.unlit { + if self.unlit { flags |= StandardMaterialFlags::UNLIT; } - let has_normal_map = material.normal_map_texture.is_some(); + let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { - match gpu_images - .get(material.normal_map_texture.as_ref().unwrap()) + match images + .get(self.normal_map_texture.as_ref().unwrap()) .unwrap() .texture_format { @@ -272,13 +204,13 @@ impl RenderAsset for StandardMaterial { } _ => {} } - if material.flip_normal_map_y { + if self.flip_normal_map_y { flags |= StandardMaterialFlags::FLIP_NORMAL_MAP_Y; } } // NOTE: 0.5 is from the glTF default - do we want this? let mut alpha_cutoff = 0.5; - match material.alpha_mode { + match self.alpha_mode { AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE, AlphaMode::Mask(c) => { alpha_cutoff = c; @@ -287,86 +219,15 @@ impl RenderAsset for StandardMaterial { AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, }; - let value = StandardMaterialUniformData { - base_color: material.base_color.as_linear_rgba_f32().into(), - emissive: material.emissive.into(), - roughness: material.perceptual_roughness, - metallic: material.metallic, - reflectance: material.reflectance, + StandardMaterialUniform { + base_color: self.base_color.as_linear_rgba_f32().into(), + emissive: self.emissive.into(), + roughness: self.perceptual_roughness, + metallic: self.metallic, + reflectance: self.reflectance, flags: flags.bits(), alpha_cutoff, - }; - - let byte_buffer = [0u8; StandardMaterialUniformData::SIZE.get() as usize]; - let mut buffer = encase::UniformBuffer::new(byte_buffer); - buffer.write(&value).unwrap(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - label: Some("pbr_standard_material_uniform_buffer"), - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - contents: buffer.as_ref(), - }); - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::TextureView(base_color_texture_view), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::Sampler(base_color_sampler), - }, - BindGroupEntry { - binding: 3, - resource: BindingResource::TextureView(emissive_texture_view), - }, - BindGroupEntry { - binding: 4, - resource: BindingResource::Sampler(emissive_sampler), - }, - BindGroupEntry { - binding: 5, - resource: BindingResource::TextureView(metallic_roughness_texture_view), - }, - BindGroupEntry { - binding: 6, - resource: BindingResource::Sampler(metallic_roughness_sampler), - }, - BindGroupEntry { - binding: 7, - resource: BindingResource::TextureView(occlusion_texture_view), - }, - BindGroupEntry { - binding: 8, - resource: BindingResource::Sampler(occlusion_sampler), - }, - BindGroupEntry { - binding: 9, - resource: BindingResource::TextureView(normal_map_texture_view), - }, - BindGroupEntry { - binding: 10, - resource: BindingResource::Sampler(normal_map_sampler), - }, - ], - label: Some("pbr_standard_material_bind_group"), - layout: &pbr_pipeline.material_layout, - }); - - Ok(GpuStandardMaterial { - buffer, - bind_group, - flags, - has_normal_map, - base_color_texture: material.base_color_texture, - alpha_mode: material.alpha_mode, - depth_bias: material.depth_bias, - cull_mode: material.cull_mode, - }) + } } } @@ -376,23 +237,23 @@ pub struct StandardMaterialKey { cull_mode: Option, } -impl SpecializedMaterial for StandardMaterial { - type Key = StandardMaterialKey; - - fn key(render_asset: &::PreparedAsset) -> Self::Key { +impl From<&StandardMaterial> for StandardMaterialKey { + fn from(material: &StandardMaterial) -> Self { StandardMaterialKey { - normal_map: render_asset.has_normal_map, - cull_mode: render_asset.cull_mode, + normal_map: material.normal_map_texture.is_some(), + cull_mode: material.cull_mode, } } +} +impl Material for StandardMaterial { fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, - key: Self::Key, _layout: &MeshVertexBufferLayout, + key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { - if key.normal_map { + if key.bind_group_data.normal_map { descriptor .fragment .as_mut() @@ -400,139 +261,24 @@ impl SpecializedMaterial for StandardMaterial { .shader_defs .push(String::from("STANDARDMATERIAL_NORMAL_MAP")); } - descriptor.primitive.cull_mode = key.cull_mode; + descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; if let Some(label) = &mut descriptor.label { *label = format!("pbr_{}", *label).into(); } Ok(()) } - fn fragment_shader(_asset_server: &AssetServer) -> Option> { - Some(PBR_SHADER_HANDLE.typed()) + fn fragment_shader() -> ShaderRef { + PBR_SHADER_HANDLE.typed().into() } #[inline] - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout( - render_device: &RenderDevice, - ) -> bevy_render::render_resource::BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(StandardMaterialUniformData::min_size()), - }, - count: None, - }, - // Base Color Texture - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Base Color Texture Sampler - BindGroupLayoutEntry { - binding: 2, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - // Emissive Texture - BindGroupLayoutEntry { - binding: 3, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Emissive Texture Sampler - BindGroupLayoutEntry { - binding: 4, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - // Metallic Roughness Texture - BindGroupLayoutEntry { - binding: 5, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Metallic Roughness Texture Sampler - BindGroupLayoutEntry { - binding: 6, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - // Occlusion Texture - BindGroupLayoutEntry { - binding: 7, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Occlusion Texture Sampler - BindGroupLayoutEntry { - binding: 8, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - // Normal Map Texture - BindGroupLayoutEntry { - binding: 9, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Normal Map Texture Sampler - BindGroupLayoutEntry { - binding: 10, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("pbr_material_layout"), - }) + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode } #[inline] - fn alpha_mode(render_asset: &::PreparedAsset) -> AlphaMode { - render_asset.alpha_mode - } - - #[inline] - fn depth_bias(material: &::PreparedAsset) -> f32 { - material.depth_bias + fn depth_bias(&self) -> f32 { + self.depth_bias } } diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs new file mode 100644 index 0000000000..f0c196dfd3 --- /dev/null +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -0,0 +1,389 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::quote; +use syn::{ + parse::ParseStream, parse_macro_input, token::Comma, Data, DataStruct, DeriveInput, Field, + Fields, LitInt, +}; + +const BINDING_ATTRIBUTE_NAME: &str = "binding"; +const UNIFORM_ATTRIBUTE_NAME: &str = "uniform"; +const TEXTURE_ATTRIBUTE_NAME: &str = "texture"; +const SAMPLER_ATTRIBUTE_NAME: &str = "sampler"; +const BIND_GROUP_DATA_ATTRIBUTE_NAME: &str = "bind_group_data"; + +#[derive(Copy, Clone, Debug)] +enum BindingType { + Uniform, + Texture, + Sampler, +} + +#[derive(Clone)] +enum BindingState<'a> { + Free, + Occupied { + binding_type: BindingType, + ident: &'a Ident, + }, + OccupiedConvertedUniform, + OccupiedMergableUniform { + uniform_fields: Vec<&'a Field>, + }, +} + +pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + let manifest = BevyManifest::default(); + let render_path = manifest.get_path("bevy_render"); + let asset_path = manifest.get_path("bevy_asset"); + + let mut binding_states: Vec = Vec::new(); + let mut binding_impls = Vec::new(); + let mut bind_group_entries = Vec::new(); + let mut binding_layouts = Vec::new(); + let mut attr_prepared_data_ident = None; + + // Read struct-level attributes + for attr in &ast.attrs { + if let Some(attr_ident) = attr.path.get_ident() { + if attr_ident == BIND_GROUP_DATA_ATTRIBUTE_NAME { + if let Ok(prepared_data_ident) = + attr.parse_args_with(|input: ParseStream| input.parse::()) + { + attr_prepared_data_ident = Some(prepared_data_ident); + } + } else if attr_ident == UNIFORM_ATTRIBUTE_NAME { + let (binding_index, converted_shader_type) = attr + .parse_args_with(|input: ParseStream| { + let binding_index = input + .parse::() + .and_then(|i| i.base10_parse::())?; + input.parse::()?; + let converted_shader_type = input.parse::()?; + Ok((binding_index, converted_shader_type)) + }) + .unwrap_or_else(|_| { + panic!("struct-level uniform bindings must be in the format: uniform(BINDING_INDEX, ConvertedShaderType)"); + }); + + binding_impls.push(quote! {{ + use #render_path::render_resource::AsBindGroupShaderType; + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + let converted: #converted_shader_type = self.as_bind_group_shader_type(images); + buffer.write(&converted).unwrap(); + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM, + contents: buffer.as_ref(), + }, + )) + }}); + + binding_layouts.push(quote!{ + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + }); + + let binding_vec_index = bind_group_entries.len(); + bind_group_entries.push(quote! { + #render_path::render_resource::BindGroupEntry { + binding: #binding_index, + resource: bindings[#binding_vec_index].get_binding(), + } + }); + + let required_len = binding_index as usize + 1; + if required_len > binding_states.len() { + binding_states.resize(required_len, BindingState::Free); + } + binding_states[binding_index as usize] = BindingState::OccupiedConvertedUniform; + } + } + } + + let fields = match &ast.data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => &fields.named, + _ => panic!("Expected a struct with named fields"), + }; + + // Read field-level attributes + for field in fields.iter() { + for attr in &field.attrs { + let attr_ident = if let Some(ident) = attr.path.get_ident() { + ident + } else { + continue; + }; + + let binding_type = if attr_ident == UNIFORM_ATTRIBUTE_NAME { + BindingType::Uniform + } else if attr_ident == TEXTURE_ATTRIBUTE_NAME { + BindingType::Texture + } else if attr_ident == SAMPLER_ATTRIBUTE_NAME { + BindingType::Sampler + } else { + continue; + }; + + let binding_index = attr + .parse_args_with(|input: ParseStream| { + let binding_index = input + .parse::() + .and_then(|i| i.base10_parse::()) + .expect("binding index was not a valid u32"); + Ok(binding_index) + }) + .unwrap_or_else(|_| { + panic!("Invalid `{}` attribute format", BINDING_ATTRIBUTE_NAME) + }); + + let field_name = field.ident.as_ref().unwrap(); + let required_len = binding_index as usize + 1; + if required_len > binding_states.len() { + binding_states.resize(required_len, BindingState::Free); + } + + match &mut binding_states[binding_index as usize] { + value @ BindingState::Free => { + *value = match binding_type { + BindingType::Uniform => BindingState::OccupiedMergableUniform { + uniform_fields: vec![field], + }, + _ => { + // only populate bind group entries for non-uniforms + // uniform entries are deferred until the end + let binding_vec_index = bind_group_entries.len(); + bind_group_entries.push(quote! { + #render_path::render_resource::BindGroupEntry { + binding: #binding_index, + resource: bindings[#binding_vec_index].get_binding(), + } + }); + BindingState::Occupied { + binding_type, + ident: field_name, + }}, + } + }, + BindingState::Occupied { binding_type, ident: occupied_ident} => panic!( + "The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by the field '{occupied_ident}' of type {binding_type:?}." + ), + BindingState::OccupiedConvertedUniform => panic!( + "The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by a struct-level uniform binding at the same index." + ), + BindingState::OccupiedMergableUniform { uniform_fields } => { + match binding_type { + BindingType::Uniform => { + uniform_fields.push(field); + }, + _ => {panic!("The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by a {:?}.", BindingType::Uniform)}, + } + }, + } + + match binding_type { + BindingType::Uniform => { /* uniform codegen is deferred to account for combined uniform bindings */ + } + BindingType::Texture => { + binding_impls.push(quote! { + #render_path::render_resource::OwnedBindingResource::TextureView({ + let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + fallback_image.texture_view.clone() + } + }) + }); + + binding_layouts.push(quote!{ + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Texture { + multisampled: false, + sample_type: #render_path::render_resource::TextureSampleType::Float { filterable: true }, + view_dimension: #render_path::render_resource::TextureViewDimension::D2, + }, + count: None, + } + }); + } + BindingType::Sampler => { + binding_impls.push(quote! { + #render_path::render_resource::OwnedBindingResource::Sampler({ + let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone() + } else { + fallback_image.sampler.clone() + } + }) + }); + + binding_layouts.push(quote!{ + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::SamplerBindingType::Filtering), + count: None, + } + }); + } + } + } + } + + // Produce impls for fields with uniform bindings + let struct_name = &ast.ident; + let mut field_struct_impls = Vec::new(); + for (binding_index, binding_state) in binding_states.iter().enumerate() { + let binding_index = binding_index as u32; + if let BindingState::OccupiedMergableUniform { uniform_fields } = binding_state { + let binding_vec_index = bind_group_entries.len(); + bind_group_entries.push(quote! { + #render_path::render_resource::BindGroupEntry { + binding: #binding_index, + resource: bindings[#binding_vec_index].get_binding(), + } + }); + // single field uniform bindings for a given index can use a straightforward binding + if uniform_fields.len() == 1 { + let field = &uniform_fields[0]; + let field_name = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + binding_impls.push(quote! {{ + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + buffer.write(&self.#field_name).unwrap(); + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM, + contents: buffer.as_ref(), + }, + )) + }}); + + binding_layouts.push(quote!{ + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + }); + // multi-field uniform bindings for a given index require an intermediate struct to derive ShaderType + } else { + let uniform_struct_name = Ident::new( + &format!("_{struct_name}AsBindGroupUniformStructBindGroup{binding_index}"), + Span::call_site(), + ); + let field_name = uniform_fields.iter().map(|f| f.ident.as_ref().unwrap()); + let field_type = uniform_fields.iter().map(|f| &f.ty); + field_struct_impls.push(quote! { + #[derive(#render_path::render_resource::ShaderType)] + struct #uniform_struct_name<'a> { + #(#field_name: &'a #field_type,)* + } + }); + + let field_name = uniform_fields.iter().map(|f| f.ident.as_ref().unwrap()); + binding_impls.push(quote! {{ + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + buffer.write(&#uniform_struct_name { + #(#field_name: &self.#field_name,)* + }).unwrap(); + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM, + contents: buffer.as_ref(), + }, + )) + }}); + + binding_layouts.push(quote!{ + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(<#uniform_struct_name as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + }); + } + } + } + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let (prepared_data, get_prepared_data) = if let Some(prepared) = attr_prepared_data_ident { + let get_prepared_data = quote! { self.into() }; + (quote! {#prepared}, get_prepared_data) + } else { + let prepared_data = quote! { () }; + (prepared_data.clone(), prepared_data) + }; + + TokenStream::from(quote! { + #(#field_struct_impls)* + + impl #impl_generics #render_path::render_resource::AsBindGroup for #struct_name #ty_generics #where_clause { + type Data = #prepared_data; + fn as_bind_group( + &self, + layout: &#render_path::render_resource::BindGroupLayout, + render_device: &#render_path::renderer::RenderDevice, + images: &#render_path::render_asset::RenderAssets<#render_path::texture::Image>, + fallback_image: &#render_path::texture::FallbackImage, + ) -> Result<#render_path::render_resource::PreparedBindGroup, #render_path::render_resource::AsBindGroupError> { + let bindings = vec![#(#binding_impls,)*]; + + let bind_group = { + let descriptor = #render_path::render_resource::BindGroupDescriptor { + entries: &[#(#bind_group_entries,)*], + label: None, + layout: &layout, + }; + render_device.create_bind_group(&descriptor) + }; + + Ok(#render_path::render_resource::PreparedBindGroup { + bindings, + bind_group, + data: #get_prepared_data, + }) + } + + fn bind_group_layout(render_device: &#render_path::renderer::RenderDevice) -> #render_path::render_resource::BindGroupLayout { + render_device.create_bind_group_layout(&#render_path::render_resource::BindGroupLayoutDescriptor { + entries: &[#(#binding_layouts,)*], + label: None, + }) + } + } + }) +} diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 0079780b8e..5e0851a69a 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -1,3 +1,4 @@ +mod as_bind_group; mod extract_resource; use bevy_macro_utils::BevyManifest; @@ -14,3 +15,8 @@ pub(crate) fn bevy_render_path() -> syn::Path { pub fn derive_extract_resource(input: TokenStream) -> TokenStream { extract_resource::derive_extract_resource(input) } + +#[proc_macro_derive(AsBindGroup, attributes(uniform, texture, sampler, bind_group_data))] +pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { + as_bind_group::derive_as_bind_group(input) +} diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index b249cb1887..77bf75bb81 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -1149,6 +1149,73 @@ impl MulAssign<[f32; 3]> for Color { } } +impl encase::ShaderType for Color { + type ExtraMetadata = (); + + const METADATA: encase::private::Metadata = { + let size = encase::private::SizeValue::from(::SIZE).mul(4); + let alignment = encase::private::AlignmentValue::from_next_power_of_two_size(size); + + encase::private::Metadata { + alignment, + has_uniform_min_alignment: false, + min_size: size, + extra: (), + } + }; + + const UNIFORM_COMPAT_ASSERT: fn() = || {}; +} + +impl encase::private::WriteInto for Color { + fn write_into(&self, writer: &mut encase::private::Writer) { + let linear = self.as_linear_rgba_f32(); + for el in &linear { + encase::private::WriteInto::write_into(el, writer); + } + } +} + +impl encase::private::ReadFrom for Color { + fn read_from( + &mut self, + reader: &mut encase::private::Reader, + ) { + let mut buffer = [0.0f32; 4]; + for el in &mut buffer { + encase::private::ReadFrom::read_from(el, reader); + } + + *self = Color::RgbaLinear { + red: buffer[0], + green: buffer[1], + blue: buffer[2], + alpha: buffer[3], + } + } +} +impl encase::private::CreateFrom for Color { + fn create_from(reader: &mut encase::private::Reader) -> Self + where + B: encase::private::BufferRef, + { + // These are intentionally not inlined in the constructor to make this + // resilient to internal Color refactors / implicit type changes. + let red: f32 = encase::private::CreateFrom::create_from(reader); + let green: f32 = encase::private::CreateFrom::create_from(reader); + let blue: f32 = encase::private::CreateFrom::create_from(reader); + let alpha: f32 = encase::private::CreateFrom::create_from(reader); + Color::RgbaLinear { + red, + green, + blue, + alpha, + } + } +} + +impl encase::Size for Color {} + #[derive(Debug, Error)] pub enum HexColorError { #[error("Unexpected length of hex string")] diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 6ee9633c2b..4356a085d7 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -1,5 +1,16 @@ +pub use bevy_render_macros::AsBindGroup; +use encase::ShaderType; + +use crate::{ + prelude::Image, + render_asset::RenderAssets, + render_resource::{BindGroupLayout, Buffer, Sampler, TextureView}, + renderer::RenderDevice, + texture::FallbackImage, +}; use bevy_reflect::Uuid; use std::{ops::Deref, sync::Arc}; +use wgpu::BindingResource; /// A [`BindGroup`] identifier. #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] @@ -42,3 +53,257 @@ impl Deref for BindGroup { &self.value } } + +/// Converts a value to a [`BindGroup`] with a given [`BindGroupLayout`], which can then be used in Bevy shaders. +/// This trait can be derived (and generally should be). Read on for details and examples. +/// +/// This is an opinionated trait that is intended to make it easy to generically +/// convert a type into a [`BindGroup`]. It provides access to specific render resources, +/// such as [`RenderAssets`] and [`FallbackImage`]. If a type has a [`Handle`](bevy_asset::Handle), +/// these can be used to retrieve the corresponding [`Texture`](crate::render_resource::Texture) resource. +/// +/// [`AsBindGroup::as_bind_group`] is intended to be called once, then the result cached somewhere. It is generally +/// ok to do "expensive" work here, such as creating a [`Buffer`] for a uniform. +/// +/// If for some reason a [`BindGroup`] cannot be created yet (for example, the [`Texture`](crate::render_resource::Texture) +/// for an [`Image`] hasn't loaded yet), just return [`AsBindGroupError::RetryNextUpdate`], which signals that the caller +/// should retry again later. +/// +/// # Deriving +/// +/// This trait can be derived. Field attributes like `uniform` and `texture` are used to define which fields should be bindings, +/// what their binding type is, and what index they should be bound at: +/// +/// ``` +/// # use bevy_render::{color::Color, render_resource::*, texture::Image}; +/// # use bevy_asset::Handle; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: Color, +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// ``` +/// +/// In WGSL shaders, the binding would look like this: +/// +/// ```wgsl +/// struct CoolMaterial { +/// color: vec4; +/// }; +/// +/// [[group(1), binding(0)]] +/// var material: CoolMaterial; +/// [[group(1), binding(1)]] +/// var color_texture: texture_2d; +/// [[group(1), binding(2)]] +/// var color_sampler: sampler; +/// ``` +/// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups +/// are generally bound to group 1. +/// +/// The following field-level attributes are supported: +/// * `uniform(BINDING_INDEX)` +/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a uniform. +/// [`ShaderType`] is implemented for most math types already, such as [`f32`], [`Vec4`](bevy_math::Vec4), and +/// [`Color`](crate::color::Color). It can also be derived for custom structs. +/// * `texture(BINDING_INDEX)` +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) +/// GPU resource, which will be bound as a texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute +/// (with a different binding index) if a binding of the sampler for the [`Image`] is also required. +/// * `sampler(BINDING_INDEX)` +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Sampler`](crate::render_resource::Sampler) GPU +/// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute +/// (with a different binding index) if a binding of the texture for the [`Image`] is also required. +/// +/// Note that fields without field-level binding attributes will be ignored. +/// ``` +/// # use bevy_render::{color::Color, render_resource::AsBindGroup}; +/// # use bevy_asset::Handle; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: Color, +/// this_field_is_ignored: String, +/// } +/// ``` +/// +/// As mentioned above, [`Option>`] is also supported: +/// ``` +/// # use bevy_render::{color::Color, render_resource::AsBindGroup, texture::Image}; +/// # use bevy_asset::Handle; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: Color, +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Option>, +/// } +/// ``` +/// This is useful if you want a texture to be optional. When the value is [`None`], the [`FallbackImage`] will be used for the binding instead, which defaults +/// to "pure white". +/// +/// Field uniforms with the same index will be combined into a single binding: +/// ``` +/// # use bevy_render::{color::Color, render_resource::AsBindGroup}; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: Color, +/// #[uniform(0)] +/// roughness: f32, +/// } +/// ``` +/// +/// In WGSL shaders, the binding would look like this: +/// ```wgsl +/// struct CoolMaterial { +/// color: vec4; +/// roughness: f32; +/// }; +/// +/// [[group(1), binding(0)]] +/// var material: CoolMaterial; +/// ``` +/// +/// Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes: +/// * `uniform(BINDING_INDEX, ConvertedShaderType)` +/// * This also creates a [`Buffer`] using [`ShaderType`] and binds it as a uniform, much +/// much like the field-level `uniform` attribute. The difference is that the entire [`AsBindGroup`] value is converted to `ConvertedShaderType`, +/// which must implement [`ShaderType`], instead of a specific field implementing [`ShaderType`]. This is useful if more complicated conversion +/// logic is required. The conversion is done using the [`AsBindGroupShaderType`] trait, which is automatically implemented +/// if `&Self` implements [`Into`]. Only use [`AsBindGroupShaderType`] if access to resources like [`RenderAssets`] is +/// required. +/// * `bind_group_data(DataType)` +/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into`] and stored +/// as [`AsBindGroup::Data`] as part of the [`AsBindGroup::as_bind_group`] call. This is useful if data needs to be stored alongside +/// the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute +/// is "shader pipeline specialization". See [`SpecializedRenderPipeline`](crate::render_resource::SpecializedRenderPipeline). +/// +/// The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can +/// also be equivalently represented with a single struct-level uniform attribute: +/// ``` +/// # use bevy_render::{color::Color, render_resource::{AsBindGroup, ShaderType}}; +/// #[derive(AsBindGroup)] +/// #[uniform(0, CoolMaterialUniform)] +/// struct CoolMaterial { +/// color: Color, +/// roughness: f32, +/// } +/// +/// #[derive(ShaderType)] +/// struct CoolMaterialUniform { +/// color: Color, +/// roughness: f32, +/// } +/// +/// impl From<&CoolMaterial> for CoolMaterialUniform { +/// fn from(material: &CoolMaterial) -> CoolMaterialUniform { +/// CoolMaterialUniform { +/// color: material.color, +/// roughness: material.roughness, +/// } +/// } +/// } +/// ``` +/// +/// Setting `bind_group_data` looks like this: +/// ``` +/// # use bevy_render::{color::Color, render_resource::AsBindGroup}; +/// #[derive(AsBindGroup)] +/// #[bind_group_data(CoolMaterialKey)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: Color, +/// is_shaded: bool, +/// } +/// +/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] +/// struct CoolMaterialKey { +/// is_shaded: bool, +/// } +/// +/// impl From<&CoolMaterial> for CoolMaterialKey { +/// fn from(material: &CoolMaterial) -> CoolMaterialKey { +/// CoolMaterialKey { +/// is_shaded: material.is_shaded, +/// } +/// } +/// } +/// ``` +pub trait AsBindGroup: Sized { + /// Data that will be stored alongside the "prepared" bind group. + type Data: Send + Sync; + + /// Creates a bind group for `self` matching the layout defined in [`AsBindGroup::bind_group_layout`]. + fn as_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + images: &RenderAssets, + fallback_image: &FallbackImage, + ) -> Result, AsBindGroupError>; + + /// Creates the bind group layout matching all bind groups returned by [`AsBindGroup::as_bind_group`] + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; +} + +/// An error that occurs during [`AsBindGroup::as_bind_group`] calls. +pub enum AsBindGroupError { + /// The bind group could not be generated. Try again next frame. + RetryNextUpdate, +} + +/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. +pub struct PreparedBindGroup { + pub bindings: Vec, + pub bind_group: BindGroup, + pub data: T::Data, +} + +/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc). +/// This is used by types like [`PreparedBindGroup`] to hold a single list of all +/// render resources used by bindings. +pub enum OwnedBindingResource { + Buffer(Buffer), + TextureView(TextureView), + Sampler(Sampler), +} + +impl OwnedBindingResource { + pub fn get_binding(&self) -> BindingResource { + match self { + OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(), + OwnedBindingResource::TextureView(view) => BindingResource::TextureView(view), + OwnedBindingResource::Sampler(sampler) => BindingResource::Sampler(sampler), + } + } +} + +/// Converts a value to a [`ShaderType`] for use in a bind group. +/// This is automatically implemented for references that implement [`Into`]. +/// Generally normal [`Into`] / [`From`] impls should be preferred, but +/// sometimes additional runtime metadata is required. +/// This exists largely to make some [`AsBindGroup`] use cases easier. +pub trait AsBindGroupShaderType { + /// Return the `T` [`ShaderType`] for `self`. When used in [`AsBindGroup`] + /// derives, it is safe to assume that all images in `self` exist. + fn as_bind_group_shader_type(&self, images: &RenderAssets) -> T; +} + +impl AsBindGroupShaderType for T +where + for<'a> &'a T: Into, +{ + #[inline] + fn as_bind_group_shader_type(&self, _images: &RenderAssets) -> U { + self.into() + } +} diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 475654e5a1..c952e60077 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -1,4 +1,4 @@ -use bevy_asset::{AssetLoader, Handle, LoadContext, LoadedAsset}; +use bevy_asset::{AssetLoader, AssetPath, Handle, LoadContext, LoadedAsset}; use bevy_reflect::{TypeUuid, Uuid}; use bevy_utils::{tracing::error, BoxedFuture, HashMap}; use naga::back::wgsl::WriterFlags; @@ -518,6 +518,34 @@ impl ShaderProcessor { } } +/// A reference to a shader asset. +pub enum ShaderRef { + /// Use the "default" shader for the current context. + Default, + /// A handle to a shader stored in the [`Assets`](bevy_asset::Assets) resource + Handle(Handle), + /// An asset path leading to a shader + Path(AssetPath<'static>), +} + +impl From> for ShaderRef { + fn from(handle: Handle) -> Self { + Self::Handle(handle) + } +} + +impl From> for ShaderRef { + fn from(path: AssetPath<'static>) -> Self { + Self::Path(path) + } +} + +impl From<&'static str> for ShaderRef { + fn from(path: &'static str) -> Self { + Self::Path(AssetPath::from(path)) + } +} + #[cfg(test)] mod tests { use bevy_asset::{Handle, HandleUntyped}; diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs new file mode 100644 index 0000000000..97ede813ea --- /dev/null +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -0,0 +1,52 @@ +use crate::{render_resource::*, texture::DefaultImageSampler}; +use bevy_derive::Deref; +use bevy_ecs::prelude::FromWorld; +use bevy_math::Vec2; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +use crate::{ + prelude::Image, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, ImageSampler}, +}; + +/// A [`RenderApp`](crate::RenderApp) resource that contains the default "fallback image", +/// which can be used in situations where an image was not explicitly defined. The most common +/// use case is [`AsBindGroup`] implementations (such as materials) that support optional textures. +/// [`FallbackImage`] defaults to a 1x1 fully white texture, making blending colors with it a no-op. +#[derive(Deref)] +pub struct FallbackImage(GpuImage); + +impl FromWorld for FallbackImage { + fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let default_sampler = world.resource::(); + let image = Image::new_fill( + Extent3d::default(), + TextureDimension::D2, + &[255u8; 4], + TextureFormat::bevy_default(), + ); + let texture = render_device.create_texture_with_data( + render_queue, + &image.texture_descriptor, + &image.data, + ); + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + let sampler = match image.sampler_descriptor { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor), + }; + Self(GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), + }) + } +} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 5b34210df6..cebb5d34cf 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -2,6 +2,7 @@ mod basis; #[cfg(feature = "dds")] mod dds; +mod fallback_image; #[cfg(feature = "hdr")] mod hdr_texture_loader; #[allow(clippy::module_inception)] @@ -21,6 +22,7 @@ pub use dds::*; #[cfg(feature = "hdr")] pub use hdr_texture_loader::*; +pub use fallback_image::*; pub use image_texture_loader::*; pub use texture_cache::*; @@ -77,6 +79,7 @@ impl Plugin for ImagePlugin { render_app .insert_resource(DefaultImageSampler(default_sampler)) .init_resource::() + .init_resource::() .add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system); } } diff --git a/examples/shader/array_texture.rs b/examples/shader/array_texture.rs index 6cbcbc2e5c..5987372476 100644 --- a/examples/shader/array_texture.rs +++ b/examples/shader/array_texture.rs @@ -1,17 +1,17 @@ use bevy::{ asset::LoadState, - ecs::system::{lifetimeless::SRes, SystemParamItem}, - pbr::MaterialPipeline, prelude::*, reflect::TypeUuid, render::{ - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, + render_asset::RenderAssets, render_resource::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, + AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, - SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension, + OwnedBindingResource, PreparedBindGroup, SamplerBindingType, ShaderRef, ShaderStages, + TextureSampleType, TextureViewDimension, }, renderer::RenderDevice, + texture::FallbackImage, }, }; @@ -104,62 +104,48 @@ struct ArrayTextureMaterial { array_texture: Handle, } -#[derive(Clone)] -pub struct GpuArrayTextureMaterial { - bind_group: BindGroup, +impl Material for ArrayTextureMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/array_texture.wgsl".into() + } } -impl RenderAsset for ArrayTextureMaterial { - type ExtractedAsset = ArrayTextureMaterial; - type PreparedAsset = GpuArrayTextureMaterial; - type Param = ( - SRes, - SRes>, - SRes>, - ); - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } +impl AsBindGroup for ArrayTextureMaterial { + type Data = (); - fn prepare_asset( - extracted_asset: Self::ExtractedAsset, - (render_device, material_pipeline, gpu_images): &mut SystemParamItem, - ) -> Result> { - let (array_texture_texture_view, array_texture_sampler) = if let Some(result) = - material_pipeline - .mesh_pipeline - .get_image_texture(gpu_images, &Some(extracted_asset.array_texture.clone())) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(extracted_asset)); - }; + fn as_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + images: &RenderAssets, + _fallback_image: &FallbackImage, + ) -> Result, AsBindGroupError> { + let image = images + .get(&self.array_texture) + .ok_or(AsBindGroupError::RetryNextUpdate)?; let bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ BindGroupEntry { binding: 0, - resource: BindingResource::TextureView(array_texture_texture_view), + resource: BindingResource::TextureView(&image.texture_view), }, BindGroupEntry { binding: 1, - resource: BindingResource::Sampler(array_texture_sampler), + resource: BindingResource::Sampler(&image.sampler), }, ], label: Some("array_texture_material_bind_group"), - layout: &material_pipeline.material_layout, + layout, }); - Ok(GpuArrayTextureMaterial { bind_group }) - } -} - -impl Material for ArrayTextureMaterial { - fn fragment_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/array_texture.wgsl")) - } - - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group + Ok(PreparedBindGroup { + bind_group, + bindings: vec![ + OwnedBindingResource::TextureView(image.texture_view.clone()), + OwnedBindingResource::Sampler(image.sampler.clone()), + ], + data: (), + }) } fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { diff --git a/examples/shader/custom_vertex_attribute.rs b/examples/shader/custom_vertex_attribute.rs index 8ed4a28947..100c60dbaa 100644 --- a/examples/shader/custom_vertex_attribute.rs +++ b/examples/shader/custom_vertex_attribute.rs @@ -1,20 +1,15 @@ //! A shader that reads a mesh's custom vertex attribute. use bevy::{ - ecs::system::{lifetimeless::SRes, SystemParamItem}, - pbr::MaterialPipeline, + pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, reflect::TypeUuid, render::{ mesh::{MeshVertexAttribute, MeshVertexBufferLayout}, - render_asset::{PrepareAssetError, RenderAsset}, render_resource::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, Buffer, - BufferBindingType, BufferInitDescriptor, BufferUsages, RenderPipelineDescriptor, - ShaderSize, ShaderStages, ShaderType, SpecializedMeshPipelineError, VertexFormat, + AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError, + VertexFormat, }, - renderer::RenderDevice, }, }; @@ -62,90 +57,26 @@ fn setup( } // This is the struct that will be passed to your shader -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, Debug, Clone, TypeUuid)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CustomMaterial { + #[uniform(0)] color: Color, } -#[derive(Clone)] -pub struct GpuCustomMaterial { - _buffer: Buffer, - bind_group: BindGroup, -} - -// The implementation of [`Material`] needs this impl to work properly. -impl RenderAsset for CustomMaterial { - type ExtractedAsset = CustomMaterial; - type PreparedAsset = GpuCustomMaterial; - type Param = (SRes, SRes>); - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } - - fn prepare_asset( - extracted_asset: Self::ExtractedAsset, - (render_device, material_pipeline): &mut SystemParamItem, - ) -> Result> { - let color = Vec4::from_slice(&extracted_asset.color.as_linear_rgba_f32()); - - let byte_buffer = [0u8; Vec4::SIZE.get() as usize]; - let mut buffer = bevy::render::render_resource::encase::UniformBuffer::new(byte_buffer); - buffer.write(&color).unwrap(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - contents: buffer.as_ref(), - label: None, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - }); - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - label: None, - layout: &material_pipeline.material_layout, - }); - - Ok(GpuCustomMaterial { - _buffer: buffer, - bind_group, - }) - } -} - impl Material for CustomMaterial { - fn vertex_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_vertex_attribute.wgsl")) + fn vertex_shader() -> ShaderRef { + "shaders/custom_vertex_attribute.wgsl".into() } - fn fragment_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_vertex_attribute.wgsl")) - } - - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(Vec4::min_size()), - }, - count: None, - }], - label: None, - }) + fn fragment_shader() -> ShaderRef { + "shaders/custom_vertex_attribute.wgsl".into() } fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, + _key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { let vertex_layout = layout.get_layout(&[ Mesh::ATTRIBUTE_POSITION.at_shader_location(0), diff --git a/examples/shader/shader_defs.rs b/examples/shader/shader_defs.rs index 3164da5c01..6babb53243 100644 --- a/examples/shader/shader_defs.rs +++ b/examples/shader/shader_defs.rs @@ -1,81 +1,52 @@ -//! A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) +//! A shader that uses "shaders defs", which selectively toggle parts of a shader. use bevy::{ - core_pipeline::core_3d::Transparent3d, - pbr::{ - DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup, - SetMeshViewBindGroup, - }, + pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, + reflect::TypeUuid, render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::MeshVertexBufferLayout, - render_asset::RenderAssets, - render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, render_resource::{ - PipelineCache, RenderPipelineDescriptor, SpecializedMeshPipeline, - SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError, }, - view::ExtractedView, - RenderApp, RenderStage, }, }; -pub struct IsRedPlugin; - -impl Plugin for IsRedPlugin { - fn build(&self, app: &mut App) { - app.add_plugin(ExtractComponentPlugin::::default()); - app.sub_app_mut(RenderApp) - .add_render_command::() - .init_resource::() - .init_resource::>() - .add_system_to_stage(RenderStage::Queue, queue_custom); - } -} - fn main() { App::new() .add_plugins(DefaultPlugins) - .add_plugin(IsRedPlugin) + .add_plugin(MaterialPlugin::::default()) .add_startup_system(setup) .run(); } -#[derive(Component, Hash, PartialEq, Eq, Copy, Clone)] -struct IsRed(bool); - -impl ExtractComponent for IsRed { - type Query = &'static IsRed; - - type Filter = (); - - fn extract_component(item: bevy::ecs::query::QueryItem) -> Self { - *item - } -} - /// set up a simple 3D scene -fn setup(mut commands: Commands, mut meshes: ResMut>) { - // red cube - commands.spawn().insert_bundle(( - meshes.add(Mesh::from(shape::Cube { size: 1.0 })), - IsRed(true), - Transform::from_xyz(-1.0, 0.5, 0.0), - GlobalTransform::default(), - Visibility::default(), - ComputedVisibility::default(), - )); - +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { // blue cube - commands.spawn().insert_bundle(( - meshes.add(Mesh::from(shape::Cube { size: 1.0 })), - IsRed(false), - Transform::from_xyz(1.0, 0.5, 0.0), - GlobalTransform::default(), - Visibility::default(), - ComputedVisibility::default(), - )); + commands.spawn().insert_bundle(MaterialMeshBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + transform: Transform::from_xyz(-1.0, 0.5, 0.0), + material: materials.add(CustomMaterial { + color: Color::BLUE, + is_red: false, + }), + ..default() + }); + + // red cube (with green color overridden by the IS_RED "shader def") + commands.spawn().insert_bundle(MaterialMeshBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + transform: Transform::from_xyz(1.0, 0.5, 0.0), + material: materials.add(CustomMaterial { + color: Color::GREEN, + is_red: true, + }), + ..default() + }); // camera commands.spawn_bundle(Camera3dBundle { @@ -84,94 +55,48 @@ fn setup(mut commands: Commands, mut meshes: ResMut>) { }); } -struct IsRedPipeline { - mesh_pipeline: MeshPipeline, - shader: Handle, -} - -impl FromWorld for IsRedPipeline { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let mesh_pipeline = world.resource::(); - let shader = asset_server.load("shaders/shader_defs.wgsl"); - IsRedPipeline { - mesh_pipeline: mesh_pipeline.clone(), - shader, - } +impl Material for CustomMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/shader_defs.wgsl".into() } -} - -impl SpecializedMeshPipeline for IsRedPipeline { - type Key = (IsRed, MeshPipelineKey); fn specialize( - &self, - (is_red, pbr_pipeline_key): Self::Key, - layout: &MeshVertexBufferLayout, - ) -> Result { - let mut shader_defs = Vec::new(); - if is_red.0 { - shader_defs.push("IS_RED".to_string()); + _pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayout, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + if key.bind_group_data.is_red { + let fragment = descriptor.fragment.as_mut().unwrap(); + fragment.shader_defs.push("IS_RED".to_string()); } - let mut descriptor = self.mesh_pipeline.specialize(pbr_pipeline_key, layout)?; - descriptor.vertex.shader = self.shader.clone(); - descriptor.vertex.shader_defs = shader_defs.clone(); - let fragment = descriptor.fragment.as_mut().unwrap(); - fragment.shader = self.shader.clone(); - fragment.shader_defs = shader_defs; - descriptor.layout = Some(vec![ - self.mesh_pipeline.view_layout.clone(), - self.mesh_pipeline.mesh_layout.clone(), - ]); - Ok(descriptor) + Ok(()) } } -type DrawIsRed = ( - SetItemPipeline, - SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, - DrawMesh, -); +// This is the struct that will be passed to your shader +#[derive(AsBindGroup, TypeUuid, Debug, Clone)] +#[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] +#[bind_group_data(CustomMaterialKey)] +pub struct CustomMaterial { + #[uniform(0)] + color: Color, + is_red: bool, +} -#[allow(clippy::too_many_arguments)] -fn queue_custom( - transparent_3d_draw_functions: Res>, - render_meshes: Res>, - custom_pipeline: Res, - msaa: Res, - mut pipelines: ResMut>, - mut pipeline_cache: ResMut, - material_meshes: Query<(Entity, &Handle, &MeshUniform, &IsRed)>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, -) { - let draw_custom = transparent_3d_draw_functions - .read() - .get_id::() - .unwrap(); - let msaa_key = MeshPipelineKey::from_msaa_samples(msaa.samples); - for (view, mut transparent_phase) in views.iter_mut() { - let view_matrix = view.transform.compute_matrix(); - let view_row_2 = view_matrix.row(2); - for (entity, mesh_handle, mesh_uniform, is_red) in material_meshes.iter() { - if let Some(mesh) = render_meshes.get(mesh_handle) { - let key = - msaa_key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); - let pipeline = pipelines - .specialize( - &mut pipeline_cache, - &custom_pipeline, - (*is_red, key), - &mesh.layout, - ) - .unwrap(); - transparent_phase.add(Transparent3d { - entity, - pipeline, - draw_function: draw_custom, - distance: view_row_2.dot(mesh_uniform.transform.col(3)), - }); - } +// This key is used to identify a specific permutation of this material pipeline. +// In this case, we specialize on whether or not to configure the "IS_RED" shader def. +// Specialization keys should be kept as small / cheap to hash as possible, +// as they will be used to look up the pipeline for each drawn entity with this material type. +#[derive(Eq, PartialEq, Hash, Clone)] +pub struct CustomMaterialKey { + is_red: bool, +} + +impl From<&CustomMaterial> for CustomMaterialKey { + fn from(material: &CustomMaterial) -> Self { + Self { + is_red: material.is_red, } } } diff --git a/examples/shader/shader_material.rs b/examples/shader/shader_material.rs index bb9d571c56..17e02de8d5 100644 --- a/examples/shader/shader_material.rs +++ b/examples/shader/shader_material.rs @@ -1,20 +1,9 @@ //! A shader and a material that uses it. use bevy::{ - ecs::system::{lifetimeless::SRes, SystemParamItem}, - pbr::MaterialPipeline, prelude::*, reflect::TypeUuid, - render::{ - render_asset::{PrepareAssetError, RenderAsset}, - render_resource::{ - encase, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, Buffer, - BufferBindingType, BufferInitDescriptor, BufferUsages, ShaderSize, ShaderStages, - ShaderType, - }, - renderer::RenderDevice, - }, + render::render_resource::{AsBindGroup, ShaderRef}, }; fn main() { @@ -30,13 +19,16 @@ fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, + asset_server: Res, ) { // cube commands.spawn().insert_bundle(MaterialMeshBundle { mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), transform: Transform::from_xyz(0.0, 0.5, 0.0), material: materials.add(CustomMaterial { - color: Color::GREEN, + color: Color::BLUE, + color_texture: Some(asset_server.load("branding/icon.png")), + alpha_mode: AlphaMode::Blend, }), ..default() }); @@ -48,91 +40,26 @@ fn setup( }); } +/// The Material trait is very configurable, but comes with sensible defaults for all methods. +/// You only need to implement functions for features that need non-default behavior. See the Material api docs for details! +impl Material for CustomMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/custom_material.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } +} + // This is the struct that will be passed to your shader -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CustomMaterial { + #[uniform(0)] color: Color, -} - -#[derive(Clone)] -pub struct GpuCustomMaterial { - _buffer: Buffer, - bind_group: BindGroup, -} - -// The implementation of [`Material`] needs this impl to work properly. -impl RenderAsset for CustomMaterial { - type ExtractedAsset = CustomMaterial; - type PreparedAsset = GpuCustomMaterial; - type Param = (SRes, SRes>); - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } - - fn prepare_asset( - extracted_asset: Self::ExtractedAsset, - (render_device, material_pipeline): &mut SystemParamItem, - ) -> Result> { - let color = Vec4::from_slice(&extracted_asset.color.as_linear_rgba_f32()); - - let byte_buffer = [0u8; Vec4::SIZE.get() as usize]; - let mut buffer = encase::UniformBuffer::new(byte_buffer); - buffer.write(&color).unwrap(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - contents: buffer.as_ref(), - label: None, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - }); - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - label: None, - layout: &material_pipeline.material_layout, - }); - - Ok(GpuCustomMaterial { - _buffer: buffer, - bind_group, - }) - } -} - -impl Material for CustomMaterial { - // When creating a custom material, you need to define either a vertex shader, a fragment shader or both. - // If you don't define one of them it will use the default mesh shader which can be found at - // - - // For this example we don't need a vertex shader - // fn vertex_shader(asset_server: &AssetServer) -> Option> { - // // Use the same path as the fragment shader since wgsl let's you define both shader in the same file - // Some(asset_server.load("shaders/custom_material.wgsl")) - // } - - fn fragment_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_material.wgsl")) - } - - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(Vec4::min_size()), - }, - count: None, - }], - label: None, - }) - } + #[texture(1)] + #[sampler(2)] + color_texture: Option>, + alpha_mode: AlphaMode, } diff --git a/examples/shader/shader_material_glsl.rs b/examples/shader/shader_material_glsl.rs index 181661b804..54526ea59f 100644 --- a/examples/shader/shader_material_glsl.rs +++ b/examples/shader/shader_material_glsl.rs @@ -1,15 +1,14 @@ //! A shader that uses the GLSL shading language. use bevy::{ - ecs::system::{lifetimeless::SRes, SystemParamItem}, - pbr::{MaterialPipeline, SpecializedMaterial}, + pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, reflect::TypeUuid, render::{ mesh::MeshVertexBufferLayout, - render_asset::{PrepareAssetError, RenderAsset}, - render_resource::*, - renderer::RenderDevice, + render_resource::{ + AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError, + }, }, }; @@ -44,98 +43,33 @@ fn setup( }); } -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, Clone, TypeUuid)] #[uuid = "4ee9c363-1124-4113-890e-199d81b00281"] pub struct CustomMaterial { + #[uniform(0)] color: Color, } -#[derive(Clone)] -pub struct GpuCustomMaterial { - _buffer: Buffer, - bind_group: BindGroup, -} - -impl RenderAsset for CustomMaterial { - type ExtractedAsset = CustomMaterial; - type PreparedAsset = GpuCustomMaterial; - type Param = (SRes, SRes>); - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() +impl Material for CustomMaterial { + fn vertex_shader() -> ShaderRef { + "shaders/custom_material.vert".into() } - fn prepare_asset( - extracted_asset: Self::ExtractedAsset, - (render_device, material_pipeline): &mut SystemParamItem, - ) -> Result> { - let color = Vec4::from_slice(&extracted_asset.color.as_linear_rgba_f32()); - - let byte_buffer = [0u8; Vec4::SIZE.get() as usize]; - let mut buffer = encase::UniformBuffer::new(byte_buffer); - buffer.write(&color).unwrap(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - contents: buffer.as_ref(), - label: None, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - }); - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - label: None, - layout: &material_pipeline.material_layout, - }); - - Ok(GpuCustomMaterial { - _buffer: buffer, - bind_group, - }) + fn fragment_shader() -> ShaderRef { + "shaders/custom_material.frag".into() } -} - -impl SpecializedMaterial for CustomMaterial { - type Key = (); - - fn key(_: &::PreparedAsset) -> Self::Key {} + // Bevy assumes by default that vertex shaders use the "vertex" entry point + // and fragment shaders use the "fragment" entry point (for WGSL shaders). + // GLSL uses "main" as the entry point, so we must override the defaults here fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, - _: Self::Key, _layout: &MeshVertexBufferLayout, + _key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { descriptor.vertex.entry_point = "main".into(); descriptor.fragment.as_mut().unwrap().entry_point = "main".into(); Ok(()) } - - fn vertex_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_material.vert")) - } - - fn fragment_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_material.frag")) - } - - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(Vec4::min_size()), - }, - count: None, - }], - label: None, - }) - } } diff --git a/examples/shader/shader_material_screenspace_texture.rs b/examples/shader/shader_material_screenspace_texture.rs index a196700328..5f92fd060c 100644 --- a/examples/shader/shader_material_screenspace_texture.rs +++ b/examples/shader/shader_material_screenspace_texture.rs @@ -1,19 +1,9 @@ //! A shader that samples a texture with view-independent UV coordinates. use bevy::{ - ecs::system::{lifetimeless::SRes, SystemParamItem}, - pbr::MaterialPipeline, prelude::*, reflect::TypeUuid, - render::{ - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, - render_resource::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, - SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension, - }, - renderer::RenderDevice, - }, + render::render_resource::{AsBindGroup, ShaderRef}, }; fn main() { @@ -75,88 +65,16 @@ fn rotate_camera(mut camera: Query<&mut Transform, With>, time: Res< cam_transform.look_at(Vec3::ZERO, Vec3::Y); } -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, Debug, Clone, TypeUuid)] #[uuid = "b62bb455-a72c-4b56-87bb-81e0554e234f"] pub struct CustomMaterial { + #[texture(0)] + #[sampler(1)] texture: Handle, } -#[derive(Clone)] -pub struct GpuCustomMaterial { - bind_group: BindGroup, -} - -impl RenderAsset for CustomMaterial { - type ExtractedAsset = CustomMaterial; - type PreparedAsset = GpuCustomMaterial; - type Param = ( - SRes, - SRes>, - SRes>, - ); - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } - - fn prepare_asset( - extracted_asset: Self::ExtractedAsset, - (render_device, gpu_images, material_pipeline): &mut SystemParamItem, - ) -> Result> { - let gpu_image = match gpu_images.get(&extracted_asset.texture) { - Some(gpu_image) => gpu_image, - // if the image isn't loaded yet, try next frame - None => return Err(PrepareAssetError::RetryNextUpdate(extracted_asset)), - }; - - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&gpu_image.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: None, - layout: &material_pipeline.material_layout, - }); - - Ok(GpuCustomMaterial { bind_group }) - } -} - impl Material for CustomMaterial { - fn fragment_shader(asset_server: &AssetServer) -> Option> { - Some(asset_server.load("shaders/custom_material_screenspace_texture.wgsl")) - } - - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - label: None, - }) + fn fragment_shader() -> ShaderRef { + "shaders/custom_material_screenspace_texture.wgsl".into() } }