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: `&<Self as RenderAsset>::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<Image>,
}
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<Handle<Shader>> {
        Some(asset_server.load("custom_material.wgsl"))
    }

    fn alpha_mode(render_asset: &<Self as RenderAsset>::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<Image>,
}
```

In WGSL shaders, the binding looks like this:

```wgsl
struct CoolMaterial {
    color: vec4<f32>;
};

[[group(1), binding(0)]]
var<uniform> material: CoolMaterial;
[[group(1), binding(1)]]
var color_texture: texture_2d<f32>;
[[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<Image>` 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<Option<Handle<Image>>>`. In practice, most fields should be a `Handle<Image>` or `Option<Handle<Image>>`. If the value of an `Option<Handle<Image>>` 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<Handle<Image>>` is also supported:
```rust
#[derive(AsBindGroup)]
struct CoolMaterial {
    #[uniform(0)]
    color: Color,
    #[texture(1)]
    #[sampler(2)]
    color_texture: Option<Handle<Image>>,
}
```
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<f32>;
    roughness: f32;
};

[[group(1), binding(0)]]
var<uniform> 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<DataType>` 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<Self>,
        descriptor: &mut RenderPipelineDescriptor,
        layout: &MeshVertexBufferLayout,
        key: MaterialPipelineKey<Self>,
    ) -> 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<Self>,
        descriptor: &mut RenderPipelineDescriptor,
        layout: &MeshVertexBufferLayout,
        key: MaterialPipelineKey<Self>,
    ) -> 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<Handle<Image>>,
    /* 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<Image>,
}

#[derive(Clone)]
pub struct GpuCustomMaterial {
    _buffer: Buffer,
    bind_group: BindGroup,
}

impl RenderAsset for CustomMaterial {
    type ExtractedAsset = CustomMaterial;
    type PreparedAsset = GpuCustomMaterial;
    type Param = (SRes<RenderDevice>, SRes<MaterialPipeline<Self>>);
    fn extract_asset(&self) -> Self::ExtractedAsset {
        self.clone()
    }

    fn prepare_asset(
        extracted_asset: Self::ExtractedAsset,
        (render_device, material_pipeline): &mut SystemParamItem<Self::Param>,
    ) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
        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<Handle<Shader>> {
        Some(asset_server.load("custom_material.wgsl"))
    }

    fn bind_group(render_asset: &<Self as RenderAsset>::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<Image>,
}
```

## 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<Handle<Image>>,
            alpha_mode: AlphaMode,
        }
        ```
    * Or this
        ```rust
        #[derive(AsBindGroup)]
        pub struct CustomMaterial {
            #[binding]
            color: Color,
            #[binding]
            color_texture: Option<Handle<Image>>,
            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<Handle<Image>>,
            #[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!
This commit is contained in:
Carter Anderson 2022-06-30 23:48:46 +00:00
parent d4e4a92982
commit 747b0c69b0
18 changed files with 1367 additions and 1061 deletions

View file

@ -1,10 +1,15 @@
struct CustomMaterial { struct CustomMaterial {
color: vec4<f32>; color: vec4<f32>;
}; };
[[group(1), binding(0)]] [[group(1), binding(0)]]
var<uniform> material: CustomMaterial; var<uniform> material: CustomMaterial;
[[group(1), binding(1)]]
var base_color_texture: texture_2d<f32>;
[[group(1), binding(2)]]
var base_color_sampler: sampler;
[[stage(fragment)]] [[stage(fragment)]]
fn fragment() -> [[location(0)]] vec4<f32> { fn fragment([[location(2)]] uv: vec2<f32>) -> [[location(0)]] vec4<f32> {
return material.color; return material.color * textureSample(base_color_texture, base_color_sampler, uv);
} }

View file

@ -1,34 +1,15 @@
#import bevy_pbr::mesh_types struct CustomMaterial {
#import bevy_pbr::mesh_view_bindings color: vec4<f32>;
};
[[group(1), binding(0)]] [[group(1), binding(0)]]
var<uniform> mesh: Mesh; var<uniform> material: CustomMaterial;
// NOTE: Bindings must come before functions that use them!
#import bevy_pbr::mesh_functions
struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
};
struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
};
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = mesh_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
return out;
}
[[stage(fragment)]] [[stage(fragment)]]
fn fragment() -> [[location(0)]] vec4<f32> { fn fragment() -> [[location(0)]] vec4<f32> {
var color = vec4<f32>(0.0, 0.0, 1.0, 1.0); #ifdef IS_RED
# ifdef IS_RED return vec4<f32>(1.0, 0.0, 0.0, 1.0);
color = vec4<f32>(1.0, 0.0, 0.0, 1.0); #else
# endif return material.color;
return color; #endif
} }

View file

@ -1,4 +1,4 @@
use crate::{DirectionalLight, PointLight, SpecializedMaterial, StandardMaterial}; use crate::{DirectionalLight, Material, PointLight, StandardMaterial};
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent};
use bevy_reflect::Reflect; 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`]. /// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`].
pub type PbrBundle = MaterialMeshBundle<StandardMaterial>; pub type PbrBundle = MaterialMeshBundle<StandardMaterial>;
/// 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)] #[derive(Bundle, Clone)]
pub struct MaterialMeshBundle<M: SpecializedMaterial> { pub struct MaterialMeshBundle<M: Material> {
pub mesh: Handle<Mesh>, pub mesh: Handle<Mesh>,
pub material: Handle<M>, pub material: Handle<M>,
pub transform: Transform, pub transform: Transform,
@ -25,7 +25,7 @@ pub struct MaterialMeshBundle<M: SpecializedMaterial> {
pub computed_visibility: ComputedVisibility, pub computed_visibility: ComputedVisibility,
} }
impl<M: SpecializedMaterial> Default for MaterialMeshBundle<M> { impl<M: Material> Default for MaterialMeshBundle<M> {
fn default() -> Self { fn default() -> Self {
Self { Self {
mesh: Default::default(), mesh: Default::default(),

View file

@ -3,248 +3,228 @@ use crate::{
SetMeshViewBindGroup, SetMeshViewBindGroup,
}; };
use bevy_app::{App, Plugin}; 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_core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d};
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
event::EventReader,
prelude::World, prelude::World,
schedule::ParallelSystemDescriptorCoercion,
system::{ system::{
lifetimeless::{Read, SQuery, SRes}, lifetimeless::{Read, SQuery, SRes},
Query, Res, ResMut, SystemParamItem, Commands, Local, Query, Res, ResMut, SystemParamItem,
}, },
world::FromWorld, world::FromWorld,
}; };
use bevy_reflect::TypeUuid;
use bevy_render::{ use bevy_render::{
extract_component::ExtractComponentPlugin, extract_component::ExtractComponentPlugin,
mesh::{Mesh, MeshVertexBufferLayout}, mesh::{Mesh, MeshVertexBufferLayout},
render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets}, prelude::Image,
render_asset::{PrepareAssetLabel, RenderAssets},
render_phase::{ render_phase::{
AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase,
SetItemPipeline, TrackedRenderPass, SetItemPipeline, TrackedRenderPass,
}, },
render_resource::{ render_resource::{
BindGroup, BindGroupLayout, PipelineCache, RenderPipelineDescriptor, Shader, AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource,
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline,
SpecializedMeshPipelineError, SpecializedMeshPipelines,
}, },
renderer::RenderDevice, renderer::RenderDevice,
texture::FallbackImage,
view::{ExtractedView, Msaa, VisibleEntities}, view::{ExtractedView, Msaa, VisibleEntities},
RenderApp, RenderStage, RenderApp, RenderStage,
}; };
use bevy_utils::tracing::error; use bevy_utils::{tracing::error, HashMap, HashSet};
use std::hash::Hash; use std::hash::Hash;
use std::marker::PhantomData; use std::marker::PhantomData;
/// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`](crate::MaterialMeshBundle) /// 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 /// 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`] /// way to render [`Mesh`] entities with custom shader logic.
/// based on specific material values, see [`SpecializedMaterial`]. [`Material`] automatically implements [`SpecializedMaterial`] ///
/// and can be used anywhere that type is used (such as [`MaterialPlugin`]). /// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders.
pub trait Material: Asset + RenderAsset + Sized { /// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details.
/// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material::bind_group_layout`]. ///
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup; /// Materials must also implement [`TypeUuid`] so they can be treated as an [`Asset`](bevy_asset::Asset).
///
/// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`Material::bind_group`]. /// # Example
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; ///
/// Here is a simple Material implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
/// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. /// check out the [`AsBindGroup`] documentation.
/// Defaults to [`None`]. /// ```
#[allow(unused_variables)] /// # use bevy_pbr::{Material, MaterialMeshBundle};
fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { /// # use bevy_ecs::prelude::*;
None /// # 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<Image>,
/// }
///
/// // 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<Assets<CustomMaterial>>, asset_server: Res<AssetServer>) {
/// 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<f32>;
/// };
///
/// [[group(1), binding(0)]]
/// var<uniform> material: CustomMaterial;
/// [[group(1), binding(1)]]
/// var color_texture: texture_2d<f32>;
/// [[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. /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader
/// Defaults to [`None`]. /// will be used.
#[allow(unused_variables)] #[allow(unused_variables)]
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { fn fragment_shader() -> ShaderRef {
None ShaderRef::Default
} }
/// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`].
#[allow(unused_variables)] #[inline]
fn alpha_mode(material: &<Self as RenderAsset>::PreparedAsset) -> AlphaMode { fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Opaque 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: &<Self as RenderAsset>::PreparedAsset) -> &[u32] {
&[]
}
#[allow(unused_variables)]
#[inline] #[inline]
/// Add a bias to the view depth of the mesh which can be used to force a specific render order /// 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. /// for meshes with equal depth, to avoid z-fighting.
fn depth_bias(material: &<Self as RenderAsset>::PreparedAsset) -> f32 { fn depth_bias(&self) -> f32 {
0.0 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)] #[allow(unused_variables)]
#[inline] #[inline]
fn specialize( fn specialize(
pipeline: &MaterialPipeline<Self>, pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor, descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout, layout: &MeshVertexBufferLayout,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> { ) -> Result<(), SpecializedMeshPipelineError> {
Ok(()) Ok(())
} }
} }
impl<M: Material> SpecializedMaterial for M { /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`]
type Key = (); /// asset type.
pub struct MaterialPlugin<M: Material>(PhantomData<M>);
#[inline] impl<M: Material> Default for MaterialPlugin<M> {
fn key(_material: &<Self as RenderAsset>::PreparedAsset) -> Self::Key {}
#[inline]
fn specialize(
pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_key: Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<(), SpecializedMeshPipelineError> {
<M as Material>::specialize(pipeline, descriptor, layout)
}
#[inline]
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup {
<M as Material>::bind_group(material)
}
#[inline]
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout {
<M as Material>::bind_group_layout(render_device)
}
#[inline]
fn alpha_mode(material: &<Self as RenderAsset>::PreparedAsset) -> AlphaMode {
<M as Material>::alpha_mode(material)
}
#[inline]
fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
<M as Material>::vertex_shader(asset_server)
}
#[inline]
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
<M as Material>::fragment_shader(asset_server)
}
#[inline]
fn dynamic_uniform_indices(material: &<Self as RenderAsset>::PreparedAsset) -> &[u32] {
<M as Material>::dynamic_uniform_indices(material)
}
#[inline]
fn depth_bias(material: &<Self as RenderAsset>::PreparedAsset) -> f32 {
<M as Material>::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: &<Self as RenderAsset>::PreparedAsset) -> Self::Key;
/// Specializes the given `descriptor` according to the given `key`.
fn specialize(
pipeline: &MaterialPipeline<Self>,
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: &<Self as RenderAsset>::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<Handle<Shader>> {
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<Handle<Shader>> {
None
}
/// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`].
#[allow(unused_variables)]
fn alpha_mode(material: &<Self as RenderAsset>::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: &<Self as RenderAsset>::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: &<Self as RenderAsset>::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<M: SpecializedMaterial>(PhantomData<M>);
impl<M: SpecializedMaterial> Default for MaterialPlugin<M> {
fn default() -> Self { fn default() -> Self {
Self(Default::default()) Self(Default::default())
} }
} }
impl<M: SpecializedMaterial> Plugin for MaterialPlugin<M> { impl<M: Material> Plugin for MaterialPlugin<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_asset::<M>() app.add_asset::<M>()
.add_plugin(ExtractComponentPlugin::<Handle<M>>::extract_visible()) .add_plugin(ExtractComponentPlugin::<Handle<M>>::extract_visible());
.add_plugin(RenderAssetPlugin::<M>::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.add_render_command::<Transparent3d, DrawMaterial<M>>() .add_render_command::<Transparent3d, DrawMaterial<M>>()
.add_render_command::<Opaque3d, DrawMaterial<M>>() .add_render_command::<Opaque3d, DrawMaterial<M>>()
.add_render_command::<AlphaMask3d, DrawMaterial<M>>() .add_render_command::<AlphaMask3d, DrawMaterial<M>>()
.init_resource::<MaterialPipeline<M>>() .init_resource::<MaterialPipeline<M>>()
.init_resource::<ExtractedMaterials<M>>()
.init_resource::<RenderMaterials<M>>()
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>() .init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
.add_system_to_stage(RenderStage::Extract, extract_materials::<M>)
.add_system_to_stage(
RenderStage::Prepare,
prepare_materials::<M>.after(PrepareAssetLabel::PreAssetPrepare),
)
.add_system_to_stage(RenderStage::Queue, queue_material_meshes::<M>); .add_system_to_stage(RenderStage::Queue, queue_material_meshes::<M>);
} }
} }
} }
#[derive(Eq, PartialEq, Clone, Hash)] /// A key uniquely identifying a specialized [`MaterialPipeline`].
pub struct MaterialPipelineKey<T> { pub struct MaterialPipelineKey<M: Material> {
pub mesh_key: MeshPipelineKey, pub mesh_key: MeshPipelineKey,
pub material_key: T, pub bind_group_data: M::Data,
} }
pub struct MaterialPipeline<M: SpecializedMaterial> { impl<M: Material> Eq for MaterialPipelineKey<M> where M::Data: PartialEq {}
impl<M: Material> PartialEq for MaterialPipelineKey<M>
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<M: Material> Clone for MaterialPipelineKey<M>
where
M::Data: Clone,
{
fn clone(&self) -> Self {
Self {
mesh_key: self.mesh_key,
bind_group_data: self.bind_group_data.clone(),
}
}
}
impl<M: Material> Hash for MaterialPipelineKey<M>
where
M::Data: Hash,
{
fn hash<H: std::hash::Hasher>(&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<M: Material> {
pub mesh_pipeline: MeshPipeline, pub mesh_pipeline: MeshPipeline,
pub material_layout: BindGroupLayout, pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>, pub vertex_shader: Option<Handle<Shader>>,
@ -252,8 +232,11 @@ pub struct MaterialPipeline<M: SpecializedMaterial> {
marker: PhantomData<M>, marker: PhantomData<M>,
} }
impl<M: SpecializedMaterial> SpecializedMeshPipeline for MaterialPipeline<M> { impl<M: Material> SpecializedMeshPipeline for MaterialPipeline<M>
type Key = MaterialPipelineKey<M::Key>; where
M::Data: PartialEq + Eq + Hash + Clone,
{
type Key = MaterialPipelineKey<M>;
fn specialize( fn specialize(
&self, &self,
@ -274,22 +257,29 @@ impl<M: SpecializedMaterial> SpecializedMeshPipeline for MaterialPipeline<M> {
let descriptor_layout = descriptor.layout.as_mut().unwrap(); let descriptor_layout = descriptor.layout.as_mut().unwrap();
descriptor_layout.insert(1, self.material_layout.clone()); 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) Ok(descriptor)
} }
} }
impl<M: SpecializedMaterial> FromWorld for MaterialPipeline<M> { impl<M: Material> FromWorld for MaterialPipeline<M> {
fn from_world(world: &mut World) -> Self { fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>(); let asset_server = world.resource::<AssetServer>();
let render_device = world.resource::<RenderDevice>(); let render_device = world.resource::<RenderDevice>();
let material_layout = M::bind_group_layout(render_device);
MaterialPipeline { MaterialPipeline {
mesh_pipeline: world.resource::<MeshPipeline>().clone(), mesh_pipeline: world.resource::<MeshPipeline>().clone(),
material_layout, material_layout: M::bind_group_layout(render_device),
vertex_shader: M::vertex_shader(asset_server), vertex_shader: match M::vertex_shader() {
fragment_shader: M::fragment_shader(asset_server), 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, marker: PhantomData,
} }
} }
@ -303,9 +293,10 @@ type DrawMaterial<M> = (
DrawMesh, DrawMesh,
); );
pub struct SetMaterialBindGroup<M: SpecializedMaterial, const I: usize>(PhantomData<M>); /// Sets the bind group for a given [`Material`] at the configured `I` index.
impl<M: SpecializedMaterial, const I: usize> EntityRenderCommand for SetMaterialBindGroup<M, I> { pub struct SetMaterialBindGroup<M: Material, const I: usize>(PhantomData<M>);
type Param = (SRes<RenderAssets<M>>, SQuery<Read<Handle<M>>>); impl<M: Material, const I: usize> EntityRenderCommand for SetMaterialBindGroup<M, I> {
type Param = (SRes<RenderMaterials<M>>, SQuery<Read<Handle<M>>>);
fn render<'w>( fn render<'w>(
_view: Entity, _view: Entity,
item: Entity, item: Entity,
@ -314,17 +305,13 @@ impl<M: SpecializedMaterial, const I: usize> EntityRenderCommand for SetMaterial
) -> RenderCommandResult { ) -> RenderCommandResult {
let material_handle = query.get(item).unwrap(); let material_handle = query.get(item).unwrap();
let material = materials.into_inner().get(material_handle).unwrap(); let material = materials.into_inner().get(material_handle).unwrap();
pass.set_bind_group( pass.set_bind_group(I, &material.bind_group, &[]);
I,
M::bind_group(material),
M::dynamic_uniform_indices(material),
);
RenderCommandResult::Success RenderCommandResult::Success
} }
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn queue_material_meshes<M: SpecializedMaterial>( pub fn queue_material_meshes<M: Material>(
opaque_draw_functions: Res<DrawFunctions<Opaque3d>>, opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
alpha_mask_draw_functions: Res<DrawFunctions<AlphaMask3d>>, alpha_mask_draw_functions: Res<DrawFunctions<AlphaMask3d>>,
transparent_draw_functions: Res<DrawFunctions<Transparent3d>>, transparent_draw_functions: Res<DrawFunctions<Transparent3d>>,
@ -333,7 +320,7 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
mut pipeline_cache: ResMut<PipelineCache>, mut pipeline_cache: ResMut<PipelineCache>,
msaa: Res<Msaa>, msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<Mesh>>, render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderAssets<M>>, render_materials: Res<RenderMaterials<M>>,
material_meshes: Query<(&Handle<M>, &Handle<Mesh>, &MeshUniform)>, material_meshes: Query<(&Handle<M>, &Handle<Mesh>, &MeshUniform)>,
mut views: Query<( mut views: Query<(
&ExtractedView, &ExtractedView,
@ -342,7 +329,9 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
&mut RenderPhase<AlphaMask3d>, &mut RenderPhase<AlphaMask3d>,
&mut RenderPhase<Transparent3d>, &mut RenderPhase<Transparent3d>,
)>, )>,
) { ) where
M::Data: PartialEq + Eq + Hash + Clone,
{
for (view, visible_entities, mut opaque_phase, mut alpha_mask_phase, mut transparent_phase) in for (view, visible_entities, mut opaque_phase, mut alpha_mask_phase, mut transparent_phase) in
views.iter_mut() views.iter_mut()
{ {
@ -372,19 +361,17 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
let mut mesh_key = let mut mesh_key =
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
| msaa_key; | msaa_key;
let alpha_mode = M::alpha_mode(material); let alpha_mode = material.properties.alpha_mode;
if let AlphaMode::Blend = alpha_mode { if let AlphaMode::Blend = alpha_mode {
mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS; mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS;
} }
let material_key = M::key(material);
let pipeline_id = pipelines.specialize( let pipeline_id = pipelines.specialize(
&mut pipeline_cache, &mut pipeline_cache,
&material_pipeline, &material_pipeline,
MaterialPipelineKey { MaterialPipelineKey {
mesh_key, mesh_key,
material_key, bind_group_data: material.key.clone(),
}, },
&mesh.layout, &mesh.layout,
); );
@ -398,8 +385,8 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
// NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix // 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 // 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))
let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3)) + bias; + material.properties.depth_bias;
match alpha_mode { match alpha_mode {
AlphaMode::Opaque => { AlphaMode::Opaque => {
opaque_phase.add(Opaque3d { opaque_phase.add(Opaque3d {
@ -444,3 +431,159 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
} }
} }
} }
/// 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<T: Material> {
pub bindings: Vec<OwnedBindingResource>,
pub bind_group: BindGroup,
pub key: T::Data,
pub properties: MaterialProperties,
}
struct ExtractedMaterials<M: Material> {
extracted: Vec<(Handle<M>, M)>,
removed: Vec<Handle<M>>,
}
impl<M: Material> Default for ExtractedMaterials<M> {
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<T> = HashMap<Handle<T>, PreparedMaterial<T>>;
/// This system extracts all created or modified assets of the corresponding [`Material`] type
/// into the "render world".
fn extract_materials<M: Material>(
mut commands: Commands,
mut events: EventReader<AssetEvent<M>>,
assets: Res<Assets<M>>,
) {
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<M: Material> {
assets: Vec<(Handle<M>, M)>,
}
impl<M: Material> Default for PrepareNextFrameMaterials<M> {
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<M: Material>(
mut prepare_next_frame: Local<PrepareNextFrameMaterials<M>>,
mut extracted_assets: ResMut<ExtractedMaterials<M>>,
mut render_materials: ResMut<RenderMaterials<M>>,
render_device: Res<RenderDevice>,
images: Res<RenderAssets<Image>>,
fallback_image: Res<FallbackImage>,
pipeline: Res<MaterialPipeline<M>>,
) {
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<M: Material>(
material: &M,
render_device: &RenderDevice,
images: &RenderAssets<Image>,
fallback_image: &FallbackImage,
pipeline: &MaterialPipeline<M>,
) -> Result<PreparedMaterial<M>, 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(),
},
})
}

View file

@ -1,15 +1,9 @@
use crate::{AlphaMode, MaterialPipeline, SpecializedMaterial, PBR_SHADER_HANDLE}; use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE};
use bevy_asset::{AssetServer, Handle}; use bevy_asset::Handle;
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
use bevy_math::Vec4; use bevy_math::Vec4;
use bevy_reflect::TypeUuid; use bevy_reflect::TypeUuid;
use bevy_render::{ use bevy_render::{
color::Color, color::Color, mesh::MeshVertexBufferLayout, render_asset::RenderAssets, render_resource::*,
mesh::MeshVertexBufferLayout,
prelude::Shader,
render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
render_resource::*,
renderer::RenderDevice,
texture::Image, texture::Image,
}; };
@ -18,17 +12,23 @@ use bevy_render::{
/// <https://google.github.io/filament/Material%20Properties.pdf>. /// <https://google.github.io/filament/Material%20Properties.pdf>.
/// ///
/// May be created directly from a [`Color`] or an [`Image`]. /// 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"] #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"]
#[bind_group_data(StandardMaterialKey)]
#[uniform(0, StandardMaterialUniform)]
pub struct StandardMaterial { pub struct StandardMaterial {
/// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// 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 /// 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` /// base color as `base_color * base_color_texture_value`
pub base_color: Color, pub base_color: Color,
#[texture(1)]
#[sampler(2)]
pub base_color_texture: Option<Handle<Image>>, pub base_color_texture: Option<Handle<Image>>,
// Use a color for user friendliness even though we technically don't use the alpha channel // 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 // Might be used in the future for exposure correction in HDR
pub emissive: Color, pub emissive: Color,
#[texture(3)]
#[sampler(4)]
pub emissive_texture: Option<Handle<Image>>, pub emissive_texture: Option<Handle<Image>>,
/// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
/// Defaults to minimum of 0.089 /// 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 /// If used together with a roughness/metallic texture, this is factored into the final base
/// color as `metallic * metallic_texture_value` /// color as `metallic * metallic_texture_value`
pub metallic: f32, pub metallic: f32,
#[texture(5)]
#[sampler(6)]
pub metallic_roughness_texture: Option<Handle<Image>>, pub metallic_roughness_texture: Option<Handle<Image>>,
/// Specular intensity for non-metals on a linear scale of [0.0, 1.0] /// 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 /// defaults to 0.5 which is mapped to 4% reflectance in the shader
pub reflectance: f32, pub reflectance: f32,
#[texture(9)]
#[sampler(10)]
pub normal_map_texture: Option<Handle<Image>>, pub normal_map_texture: Option<Handle<Image>>,
/// Normal map textures authored for DirectX have their y-component flipped. Set this to flip /// Normal map textures authored for DirectX have their y-component flipped. Set this to flip
/// it to right-handed conventions. /// it to right-handed conventions.
pub flip_normal_map_y: bool, pub flip_normal_map_y: bool,
#[texture(7)]
#[sampler(8)]
pub occlusion_texture: Option<Handle<Image>>, pub occlusion_texture: Option<Handle<Image>>,
/// Support two-sided lighting by automatically flipping the normals for "back" faces /// Support two-sided lighting by automatically flipping the normals for "back" faces
/// within the PBR lighting shader. /// within the PBR lighting shader.
@ -140,7 +146,7 @@ bitflags::bitflags! {
/// The GPU representation of the uniform data of a [`StandardMaterial`]. /// The GPU representation of the uniform data of a [`StandardMaterial`].
#[derive(Clone, Default, ShaderType)] #[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 /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
/// in between. /// in between.
pub base_color: Vec4, pub base_color: Vec4,
@ -161,105 +167,31 @@ pub struct StandardMaterialUniformData {
pub alpha_cutoff: f32, pub alpha_cutoff: f32,
} }
/// The GPU representation of a [`StandardMaterial`]. impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
#[derive(Debug, Clone)] fn as_bind_group_shader_type(&self, images: &RenderAssets<Image>) -> StandardMaterialUniform {
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<Handle<Image>>,
pub alpha_mode: AlphaMode,
pub depth_bias: f32,
pub cull_mode: Option<Face>,
}
impl RenderAsset for StandardMaterial {
type ExtractedAsset = StandardMaterial;
type PreparedAsset = GpuStandardMaterial;
type Param = (
SRes<RenderDevice>,
SRes<MaterialPipeline<StandardMaterial>>,
SRes<RenderAssets<Image>>,
);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
material: Self::ExtractedAsset,
(render_device, pbr_pipeline, gpu_images): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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));
};
let mut flags = StandardMaterialFlags::NONE; let mut flags = StandardMaterialFlags::NONE;
if material.base_color_texture.is_some() { if self.base_color_texture.is_some() {
flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE; flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE;
} }
if material.emissive_texture.is_some() { if self.emissive_texture.is_some() {
flags |= StandardMaterialFlags::EMISSIVE_TEXTURE; flags |= StandardMaterialFlags::EMISSIVE_TEXTURE;
} }
if material.metallic_roughness_texture.is_some() { if self.metallic_roughness_texture.is_some() {
flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE; flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE;
} }
if material.occlusion_texture.is_some() { if self.occlusion_texture.is_some() {
flags |= StandardMaterialFlags::OCCLUSION_TEXTURE; flags |= StandardMaterialFlags::OCCLUSION_TEXTURE;
} }
if material.double_sided { if self.double_sided {
flags |= StandardMaterialFlags::DOUBLE_SIDED; flags |= StandardMaterialFlags::DOUBLE_SIDED;
} }
if material.unlit { if self.unlit {
flags |= StandardMaterialFlags::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 { if has_normal_map {
match gpu_images match images
.get(material.normal_map_texture.as_ref().unwrap()) .get(self.normal_map_texture.as_ref().unwrap())
.unwrap() .unwrap()
.texture_format .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; flags |= StandardMaterialFlags::FLIP_NORMAL_MAP_Y;
} }
} }
// NOTE: 0.5 is from the glTF default - do we want this? // NOTE: 0.5 is from the glTF default - do we want this?
let mut alpha_cutoff = 0.5; let mut alpha_cutoff = 0.5;
match material.alpha_mode { match self.alpha_mode {
AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE, AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE,
AlphaMode::Mask(c) => { AlphaMode::Mask(c) => {
alpha_cutoff = c; alpha_cutoff = c;
@ -287,86 +219,15 @@ impl RenderAsset for StandardMaterial {
AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND,
}; };
let value = StandardMaterialUniformData { StandardMaterialUniform {
base_color: material.base_color.as_linear_rgba_f32().into(), base_color: self.base_color.as_linear_rgba_f32().into(),
emissive: material.emissive.into(), emissive: self.emissive.into(),
roughness: material.perceptual_roughness, roughness: self.perceptual_roughness,
metallic: material.metallic, metallic: self.metallic,
reflectance: material.reflectance, reflectance: self.reflectance,
flags: flags.bits(), flags: flags.bits(),
alpha_cutoff, 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<Face>, cull_mode: Option<Face>,
} }
impl SpecializedMaterial for StandardMaterial { impl From<&StandardMaterial> for StandardMaterialKey {
type Key = StandardMaterialKey; fn from(material: &StandardMaterial) -> Self {
fn key(render_asset: &<Self as RenderAsset>::PreparedAsset) -> Self::Key {
StandardMaterialKey { StandardMaterialKey {
normal_map: render_asset.has_normal_map, normal_map: material.normal_map_texture.is_some(),
cull_mode: render_asset.cull_mode, cull_mode: material.cull_mode,
} }
} }
}
impl Material for StandardMaterial {
fn specialize( fn specialize(
_pipeline: &MaterialPipeline<Self>, _pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor, descriptor: &mut RenderPipelineDescriptor,
key: Self::Key,
_layout: &MeshVertexBufferLayout, _layout: &MeshVertexBufferLayout,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> { ) -> Result<(), SpecializedMeshPipelineError> {
if key.normal_map { if key.bind_group_data.normal_map {
descriptor descriptor
.fragment .fragment
.as_mut() .as_mut()
@ -400,139 +261,24 @@ impl SpecializedMaterial for StandardMaterial {
.shader_defs .shader_defs
.push(String::from("STANDARDMATERIAL_NORMAL_MAP")); .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 { if let Some(label) = &mut descriptor.label {
*label = format!("pbr_{}", *label).into(); *label = format!("pbr_{}", *label).into();
} }
Ok(()) Ok(())
} }
fn fragment_shader(_asset_server: &AssetServer) -> Option<Handle<Shader>> { fn fragment_shader() -> ShaderRef {
Some(PBR_SHADER_HANDLE.typed()) PBR_SHADER_HANDLE.typed().into()
} }
#[inline] #[inline]
fn bind_group(render_asset: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup { fn alpha_mode(&self) -> AlphaMode {
&render_asset.bind_group self.alpha_mode
}
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"),
})
} }
#[inline] #[inline]
fn alpha_mode(render_asset: &<Self as RenderAsset>::PreparedAsset) -> AlphaMode { fn depth_bias(&self) -> f32 {
render_asset.alpha_mode self.depth_bias
}
#[inline]
fn depth_bias(material: &<Self as RenderAsset>::PreparedAsset) -> f32 {
material.depth_bias
} }
} }

View file

@ -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<BindingState> = 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::<Ident>())
{
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::<LitInt>()
.and_then(|i| i.base10_parse::<u32>())?;
input.parse::<Comma>()?;
let converted_shader_type = input.parse::<Ident>()?;
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::<LitInt>()
.and_then(|i| i.base10_parse::<u32>())
.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<Self>, #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,
})
}
}
})
}

View file

@ -1,3 +1,4 @@
mod as_bind_group;
mod extract_resource; mod extract_resource;
use bevy_macro_utils::BevyManifest; 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 { pub fn derive_extract_resource(input: TokenStream) -> TokenStream {
extract_resource::derive_extract_resource(input) 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)
}

View file

@ -1149,6 +1149,73 @@ impl MulAssign<[f32; 3]> for Color {
} }
} }
impl encase::ShaderType for Color {
type ExtraMetadata = ();
const METADATA: encase::private::Metadata<Self::ExtraMetadata> = {
let size = encase::private::SizeValue::from(<f32 as encase::private::Size>::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<B: encase::private::BufferMut>(&self, writer: &mut encase::private::Writer<B>) {
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<B: encase::private::BufferRef>(
&mut self,
reader: &mut encase::private::Reader<B>,
) {
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<B>(reader: &mut encase::private::Reader<B>) -> 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)] #[derive(Debug, Error)]
pub enum HexColorError { pub enum HexColorError {
#[error("Unexpected length of hex string")] #[error("Unexpected length of hex string")]

View file

@ -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 bevy_reflect::Uuid;
use std::{ops::Deref, sync::Arc}; use std::{ops::Deref, sync::Arc};
use wgpu::BindingResource;
/// A [`BindGroup`] identifier. /// A [`BindGroup`] identifier.
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
@ -42,3 +53,257 @@ impl Deref for BindGroup {
&self.value &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<Image>`] and [`FallbackImage`]. If a type has a [`Handle<Image>`](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<Image>,
/// }
/// ```
///
/// In WGSL shaders, the binding would look like this:
///
/// ```wgsl
/// struct CoolMaterial {
/// color: vec4<f32>;
/// };
///
/// [[group(1), binding(0)]]
/// var<uniform> material: CoolMaterial;
/// [[group(1), binding(1)]]
/// var color_texture: texture_2d<f32>;
/// [[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<Image>`](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<Option<Handle<Image>>>`]. In practice,
/// most fields should be a [`Handle<Image>`](bevy_asset::Handle) or [`Option<Handle<Image>>`]. If the value of an [`Option<Handle<Image>>`] 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<Image>`](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<Option<Handle<Image>>>`]. In practice,
/// most fields should be a [`Handle<Image>`](bevy_asset::Handle) or [`Option<Handle<Image>>`]. If the value of an [`Option<Handle<Image>>`] 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<Handle<Image>>`] 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<Handle<Image>>,
/// }
/// ```
/// 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<f32>;
/// roughness: f32;
/// };
///
/// [[group(1), binding(0)]]
/// var<uniform> 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<ConvertedShaderType>`] trait, which is automatically implemented
/// if `&Self` implements [`Into<ConvertedShaderType>`]. Only use [`AsBindGroupShaderType`] if access to resources like [`RenderAssets<Image>`] is
/// required.
/// * `bind_group_data(DataType)`
/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into<DataType>`] 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<Image>,
fallback_image: &FallbackImage,
) -> Result<PreparedBindGroup<Self>, 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<T: AsBindGroup> {
pub bindings: Vec<OwnedBindingResource>,
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<T: ShaderType> {
/// 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<Image>) -> T;
}
impl<T, U: ShaderType> AsBindGroupShaderType<U> for T
where
for<'a> &'a T: Into<U>,
{
#[inline]
fn as_bind_group_shader_type(&self, _images: &RenderAssets<Image>) -> U {
self.into()
}
}

View file

@ -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_reflect::{TypeUuid, Uuid};
use bevy_utils::{tracing::error, BoxedFuture, HashMap}; use bevy_utils::{tracing::error, BoxedFuture, HashMap};
use naga::back::wgsl::WriterFlags; 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<Shader>`](bevy_asset::Assets) resource
Handle(Handle<Shader>),
/// An asset path leading to a shader
Path(AssetPath<'static>),
}
impl From<Handle<Shader>> for ShaderRef {
fn from(handle: Handle<Shader>) -> Self {
Self::Handle(handle)
}
}
impl From<AssetPath<'static>> 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)] #[cfg(test)]
mod tests { mod tests {
use bevy_asset::{Handle, HandleUntyped}; use bevy_asset::{Handle, HandleUntyped};

View file

@ -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::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
let default_sampler = world.resource::<DefaultImageSampler>();
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,
),
})
}
}

View file

@ -2,6 +2,7 @@
mod basis; mod basis;
#[cfg(feature = "dds")] #[cfg(feature = "dds")]
mod dds; mod dds;
mod fallback_image;
#[cfg(feature = "hdr")] #[cfg(feature = "hdr")]
mod hdr_texture_loader; mod hdr_texture_loader;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
@ -21,6 +22,7 @@ pub use dds::*;
#[cfg(feature = "hdr")] #[cfg(feature = "hdr")]
pub use hdr_texture_loader::*; pub use hdr_texture_loader::*;
pub use fallback_image::*;
pub use image_texture_loader::*; pub use image_texture_loader::*;
pub use texture_cache::*; pub use texture_cache::*;
@ -77,6 +79,7 @@ impl Plugin for ImagePlugin {
render_app render_app
.insert_resource(DefaultImageSampler(default_sampler)) .insert_resource(DefaultImageSampler(default_sampler))
.init_resource::<TextureCache>() .init_resource::<TextureCache>()
.init_resource::<FallbackImage>()
.add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system); .add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system);
} }
} }

View file

@ -1,17 +1,17 @@
use bevy::{ use bevy::{
asset::LoadState, asset::LoadState,
ecs::system::{lifetimeless::SRes, SystemParamItem},
pbr::MaterialPipeline,
prelude::*, prelude::*,
reflect::TypeUuid, reflect::TypeUuid,
render::{ render::{
render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, render_asset::RenderAssets,
render_resource::{ render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension, OwnedBindingResource, PreparedBindGroup, SamplerBindingType, ShaderRef, ShaderStages,
TextureSampleType, TextureViewDimension,
}, },
renderer::RenderDevice, renderer::RenderDevice,
texture::FallbackImage,
}, },
}; };
@ -104,62 +104,48 @@ struct ArrayTextureMaterial {
array_texture: Handle<Image>, array_texture: Handle<Image>,
} }
#[derive(Clone)] impl Material for ArrayTextureMaterial {
pub struct GpuArrayTextureMaterial { fn fragment_shader() -> ShaderRef {
bind_group: BindGroup, "shaders/array_texture.wgsl".into()
}
} }
impl RenderAsset for ArrayTextureMaterial { impl AsBindGroup for ArrayTextureMaterial {
type ExtractedAsset = ArrayTextureMaterial; type Data = ();
type PreparedAsset = GpuArrayTextureMaterial;
type Param = (
SRes<RenderDevice>,
SRes<MaterialPipeline<Self>>,
SRes<RenderAssets<Image>>,
);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset( fn as_bind_group(
extracted_asset: Self::ExtractedAsset, &self,
(render_device, material_pipeline, gpu_images): &mut SystemParamItem<Self::Param>, layout: &BindGroupLayout,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> { render_device: &RenderDevice,
let (array_texture_texture_view, array_texture_sampler) = if let Some(result) = images: &RenderAssets<Image>,
material_pipeline _fallback_image: &FallbackImage,
.mesh_pipeline ) -> Result<PreparedBindGroup<Self>, AsBindGroupError> {
.get_image_texture(gpu_images, &Some(extracted_asset.array_texture.clone())) let image = images
{ .get(&self.array_texture)
result .ok_or(AsBindGroupError::RetryNextUpdate)?;
} else {
return Err(PrepareAssetError::RetryNextUpdate(extracted_asset));
};
let bind_group = render_device.create_bind_group(&BindGroupDescriptor { let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: BindingResource::TextureView(array_texture_texture_view), resource: BindingResource::TextureView(&image.texture_view),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, binding: 1,
resource: BindingResource::Sampler(array_texture_sampler), resource: BindingResource::Sampler(&image.sampler),
}, },
], ],
label: Some("array_texture_material_bind_group"), label: Some("array_texture_material_bind_group"),
layout: &material_pipeline.material_layout, layout,
}); });
Ok(GpuArrayTextureMaterial { bind_group }) Ok(PreparedBindGroup {
} bind_group,
} bindings: vec![
OwnedBindingResource::TextureView(image.texture_view.clone()),
impl Material for ArrayTextureMaterial { OwnedBindingResource::Sampler(image.sampler.clone()),
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { ],
Some(asset_server.load("shaders/array_texture.wgsl")) data: (),
} })
fn bind_group(render_asset: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup {
&render_asset.bind_group
} }
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout {

View file

@ -1,20 +1,15 @@
//! A shader that reads a mesh's custom vertex attribute. //! A shader that reads a mesh's custom vertex attribute.
use bevy::{ use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem}, pbr::{MaterialPipeline, MaterialPipelineKey},
pbr::MaterialPipeline,
prelude::*, prelude::*,
reflect::TypeUuid, reflect::TypeUuid,
render::{ render::{
mesh::{MeshVertexAttribute, MeshVertexBufferLayout}, mesh::{MeshVertexAttribute, MeshVertexBufferLayout},
render_asset::{PrepareAssetError, RenderAsset},
render_resource::{ render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, Buffer, VertexFormat,
BufferBindingType, BufferInitDescriptor, BufferUsages, RenderPipelineDescriptor,
ShaderSize, ShaderStages, ShaderType, SpecializedMeshPipelineError, VertexFormat,
}, },
renderer::RenderDevice,
}, },
}; };
@ -62,90 +57,26 @@ fn setup(
} }
// This is the struct that will be passed to your shader // 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"] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
pub struct CustomMaterial { pub struct CustomMaterial {
#[uniform(0)]
color: Color, 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<RenderDevice>, SRes<MaterialPipeline<Self>>);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
extracted_asset: Self::ExtractedAsset,
(render_device, material_pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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 { impl Material for CustomMaterial {
fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { fn vertex_shader() -> ShaderRef {
Some(asset_server.load("shaders/custom_vertex_attribute.wgsl")) "shaders/custom_vertex_attribute.wgsl".into()
} }
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { fn fragment_shader() -> ShaderRef {
Some(asset_server.load("shaders/custom_vertex_attribute.wgsl")) "shaders/custom_vertex_attribute.wgsl".into()
}
fn bind_group(render_asset: &<Self as RenderAsset>::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 specialize( fn specialize(
_pipeline: &MaterialPipeline<Self>, _pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor, descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout, layout: &MeshVertexBufferLayout,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> { ) -> Result<(), SpecializedMeshPipelineError> {
let vertex_layout = layout.get_layout(&[ let vertex_layout = layout.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0), Mesh::ATTRIBUTE_POSITION.at_shader_location(0),

View file

@ -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::{ use bevy::{
core_pipeline::core_3d::Transparent3d, pbr::{MaterialPipeline, MaterialPipelineKey},
pbr::{
DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup,
SetMeshViewBindGroup,
},
prelude::*, prelude::*,
reflect::TypeUuid,
render::{ render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::MeshVertexBufferLayout, mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
render_resource::{ render_resource::{
PipelineCache, RenderPipelineDescriptor, SpecializedMeshPipeline, AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
SpecializedMeshPipelineError, SpecializedMeshPipelines,
}, },
view::ExtractedView,
RenderApp, RenderStage,
}, },
}; };
pub struct IsRedPlugin;
impl Plugin for IsRedPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ExtractComponentPlugin::<IsRed>::default());
app.sub_app_mut(RenderApp)
.add_render_command::<Transparent3d, DrawIsRed>()
.init_resource::<IsRedPipeline>()
.init_resource::<SpecializedMeshPipelines<IsRedPipeline>>()
.add_system_to_stage(RenderStage::Queue, queue_custom);
}
}
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugin(IsRedPlugin) .add_plugin(MaterialPlugin::<CustomMaterial>::default())
.add_startup_system(setup) .add_startup_system(setup)
.run(); .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::Query>) -> Self {
*item
}
}
/// set up a simple 3D scene /// set up a simple 3D scene
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) { fn setup(
// red cube mut commands: Commands,
commands.spawn().insert_bundle(( mut meshes: ResMut<Assets<Mesh>>,
meshes.add(Mesh::from(shape::Cube { size: 1.0 })), mut materials: ResMut<Assets<CustomMaterial>>,
IsRed(true), ) {
Transform::from_xyz(-1.0, 0.5, 0.0),
GlobalTransform::default(),
Visibility::default(),
ComputedVisibility::default(),
));
// blue cube // blue cube
commands.spawn().insert_bundle(( commands.spawn().insert_bundle(MaterialMeshBundle {
meshes.add(Mesh::from(shape::Cube { size: 1.0 })), mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
IsRed(false), transform: Transform::from_xyz(-1.0, 0.5, 0.0),
Transform::from_xyz(1.0, 0.5, 0.0), material: materials.add(CustomMaterial {
GlobalTransform::default(), color: Color::BLUE,
Visibility::default(), is_red: false,
ComputedVisibility::default(), }),
)); ..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 // camera
commands.spawn_bundle(Camera3dBundle { commands.spawn_bundle(Camera3dBundle {
@ -84,94 +55,48 @@ fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
}); });
} }
struct IsRedPipeline { impl Material for CustomMaterial {
mesh_pipeline: MeshPipeline, fn fragment_shader() -> ShaderRef {
shader: Handle<Shader>, "shaders/shader_defs.wgsl".into()
}
impl FromWorld for IsRedPipeline {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
let mesh_pipeline = world.resource::<MeshPipeline>();
let shader = asset_server.load("shaders/shader_defs.wgsl");
IsRedPipeline {
mesh_pipeline: mesh_pipeline.clone(),
shader,
}
} }
}
impl SpecializedMeshPipeline for IsRedPipeline {
type Key = (IsRed, MeshPipelineKey);
fn specialize( fn specialize(
&self, _pipeline: &MaterialPipeline<Self>,
(is_red, pbr_pipeline_key): Self::Key, descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout, _layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> { key: MaterialPipelineKey<Self>,
let mut shader_defs = Vec::new(); ) -> Result<(), SpecializedMeshPipelineError> {
if is_red.0 { if key.bind_group_data.is_red {
shader_defs.push("IS_RED".to_string()); 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)?; Ok(())
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)
} }
} }
type DrawIsRed = ( // This is the struct that will be passed to your shader
SetItemPipeline, #[derive(AsBindGroup, TypeUuid, Debug, Clone)]
SetMeshViewBindGroup<0>, #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
SetMeshBindGroup<1>, #[bind_group_data(CustomMaterialKey)]
DrawMesh, pub struct CustomMaterial {
); #[uniform(0)]
color: Color,
is_red: bool,
}
#[allow(clippy::too_many_arguments)] // This key is used to identify a specific permutation of this material pipeline.
fn queue_custom( // In this case, we specialize on whether or not to configure the "IS_RED" shader def.
transparent_3d_draw_functions: Res<DrawFunctions<Transparent3d>>, // Specialization keys should be kept as small / cheap to hash as possible,
render_meshes: Res<RenderAssets<Mesh>>, // as they will be used to look up the pipeline for each drawn entity with this material type.
custom_pipeline: Res<IsRedPipeline>, #[derive(Eq, PartialEq, Hash, Clone)]
msaa: Res<Msaa>, pub struct CustomMaterialKey {
mut pipelines: ResMut<SpecializedMeshPipelines<IsRedPipeline>>, is_red: bool,
mut pipeline_cache: ResMut<PipelineCache>, }
material_meshes: Query<(Entity, &Handle<Mesh>, &MeshUniform, &IsRed)>,
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>, impl From<&CustomMaterial> for CustomMaterialKey {
) { fn from(material: &CustomMaterial) -> Self {
let draw_custom = transparent_3d_draw_functions Self {
.read() is_red: material.is_red,
.get_id::<DrawIsRed>()
.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)),
});
}
} }
} }
} }

View file

@ -1,20 +1,9 @@
//! A shader and a material that uses it. //! A shader and a material that uses it.
use bevy::{ use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem},
pbr::MaterialPipeline,
prelude::*, prelude::*,
reflect::TypeUuid, reflect::TypeUuid,
render::{ render::render_resource::{AsBindGroup, ShaderRef},
render_asset::{PrepareAssetError, RenderAsset},
render_resource::{
encase, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, Buffer,
BufferBindingType, BufferInitDescriptor, BufferUsages, ShaderSize, ShaderStages,
ShaderType,
},
renderer::RenderDevice,
},
}; };
fn main() { fn main() {
@ -30,13 +19,16 @@ fn setup(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>, mut materials: ResMut<Assets<CustomMaterial>>,
asset_server: Res<AssetServer>,
) { ) {
// cube // cube
commands.spawn().insert_bundle(MaterialMeshBundle { commands.spawn().insert_bundle(MaterialMeshBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
transform: Transform::from_xyz(0.0, 0.5, 0.0), transform: Transform::from_xyz(0.0, 0.5, 0.0),
material: materials.add(CustomMaterial { material: materials.add(CustomMaterial {
color: Color::GREEN, color: Color::BLUE,
color_texture: Some(asset_server.load("branding/icon.png")),
alpha_mode: AlphaMode::Blend,
}), }),
..default() ..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 // 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"] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
pub struct CustomMaterial { pub struct CustomMaterial {
#[uniform(0)]
color: Color, color: Color,
} #[texture(1)]
#[sampler(2)]
#[derive(Clone)] color_texture: Option<Handle<Image>>,
pub struct GpuCustomMaterial { alpha_mode: AlphaMode,
_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<RenderDevice>, SRes<MaterialPipeline<Self>>);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
extracted_asset: Self::ExtractedAsset,
(render_device, material_pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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
// <https://github.com/bevyengine/bevy/blob/latest/crates/bevy_pbr/src/render/mesh.wgsl>
// For this example we don't need a vertex shader
// fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
// // 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<Handle<Shader>> {
Some(asset_server.load("shaders/custom_material.wgsl"))
}
fn bind_group(render_asset: &<Self as RenderAsset>::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,
})
}
} }

View file

@ -1,15 +1,14 @@
//! A shader that uses the GLSL shading language. //! A shader that uses the GLSL shading language.
use bevy::{ use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem}, pbr::{MaterialPipeline, MaterialPipelineKey},
pbr::{MaterialPipeline, SpecializedMaterial},
prelude::*, prelude::*,
reflect::TypeUuid, reflect::TypeUuid,
render::{ render::{
mesh::MeshVertexBufferLayout, mesh::MeshVertexBufferLayout,
render_asset::{PrepareAssetError, RenderAsset}, render_resource::{
render_resource::*, AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
renderer::RenderDevice, },
}, },
}; };
@ -44,98 +43,33 @@ fn setup(
}); });
} }
#[derive(Debug, Clone, TypeUuid)] #[derive(AsBindGroup, Clone, TypeUuid)]
#[uuid = "4ee9c363-1124-4113-890e-199d81b00281"] #[uuid = "4ee9c363-1124-4113-890e-199d81b00281"]
pub struct CustomMaterial { pub struct CustomMaterial {
#[uniform(0)]
color: Color, color: Color,
} }
#[derive(Clone)] impl Material for CustomMaterial {
pub struct GpuCustomMaterial { fn vertex_shader() -> ShaderRef {
_buffer: Buffer, "shaders/custom_material.vert".into()
bind_group: BindGroup,
}
impl RenderAsset for CustomMaterial {
type ExtractedAsset = CustomMaterial;
type PreparedAsset = GpuCustomMaterial;
type Param = (SRes<RenderDevice>, SRes<MaterialPipeline<Self>>);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
} }
fn prepare_asset( fn fragment_shader() -> ShaderRef {
extracted_asset: Self::ExtractedAsset, "shaders/custom_material.frag".into()
(render_device, material_pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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 SpecializedMaterial for CustomMaterial {
type Key = ();
fn key(_: &<CustomMaterial as RenderAsset>::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( fn specialize(
_pipeline: &MaterialPipeline<Self>, _pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor, descriptor: &mut RenderPipelineDescriptor,
_: Self::Key,
_layout: &MeshVertexBufferLayout, _layout: &MeshVertexBufferLayout,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> { ) -> Result<(), SpecializedMeshPipelineError> {
descriptor.vertex.entry_point = "main".into(); descriptor.vertex.entry_point = "main".into();
descriptor.fragment.as_mut().unwrap().entry_point = "main".into(); descriptor.fragment.as_mut().unwrap().entry_point = "main".into();
Ok(()) Ok(())
} }
fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
Some(asset_server.load("shaders/custom_material.vert"))
}
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
Some(asset_server.load("shaders/custom_material.frag"))
}
fn bind_group(render_asset: &<Self as RenderAsset>::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,
})
}
} }

View file

@ -1,19 +1,9 @@
//! A shader that samples a texture with view-independent UV coordinates. //! A shader that samples a texture with view-independent UV coordinates.
use bevy::{ use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem},
pbr::MaterialPipeline,
prelude::*, prelude::*,
reflect::TypeUuid, reflect::TypeUuid,
render::{ render::render_resource::{AsBindGroup, ShaderRef},
render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension,
},
renderer::RenderDevice,
},
}; };
fn main() { fn main() {
@ -75,88 +65,16 @@ fn rotate_camera(mut camera: Query<&mut Transform, With<MainCamera>>, time: Res<
cam_transform.look_at(Vec3::ZERO, Vec3::Y); cam_transform.look_at(Vec3::ZERO, Vec3::Y);
} }
#[derive(Debug, Clone, TypeUuid)] #[derive(AsBindGroup, Debug, Clone, TypeUuid)]
#[uuid = "b62bb455-a72c-4b56-87bb-81e0554e234f"] #[uuid = "b62bb455-a72c-4b56-87bb-81e0554e234f"]
pub struct CustomMaterial { pub struct CustomMaterial {
#[texture(0)]
#[sampler(1)]
texture: Handle<Image>, texture: Handle<Image>,
} }
#[derive(Clone)]
pub struct GpuCustomMaterial {
bind_group: BindGroup,
}
impl RenderAsset for CustomMaterial {
type ExtractedAsset = CustomMaterial;
type PreparedAsset = GpuCustomMaterial;
type Param = (
SRes<RenderDevice>,
SRes<RenderAssets<Image>>,
SRes<MaterialPipeline<Self>>,
);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
extracted_asset: Self::ExtractedAsset,
(render_device, gpu_images, material_pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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 { impl Material for CustomMaterial {
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { fn fragment_shader() -> ShaderRef {
Some(asset_server.load("shaders/custom_material_screenspace_texture.wgsl")) "shaders/custom_material_screenspace_texture.wgsl".into()
}
fn bind_group(render_asset: &<Self as RenderAsset>::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,
})
} }
} }