Skip to content

Commit e6ba9a6

Browse files
Type erased materials (#19667)
# Objective Closes #18075 In order to enable a number of patterns for dynamic materials in the engine, it's necessary to decouple the renderer from the `Material` trait. This opens the possibility for: - Materials that aren't coupled to `AsBindGroup`. - 2d using the underlying 3d bindless infrastructure. - Dynamic materials that can change their layout at runtime. - Materials that aren't even backed by a Rust struct at all. ## Solution In short, remove all trait bounds from render world material systems and resources. This means moving a bunch of stuff onto `MaterialProperties` and engaging in some hacks to make specialization work. Rather than storing the bind group data in `MaterialBindGroupAllocator`, right now we're storing it in a closure on `MaterialProperties`. TBD if this has bad performance characteristics. ## Benchmarks - `many_cubes`: `cargo run --example many_cubes --release --features=bevy/trace_tracy -- --vary-material-data-per-instance`: ![Screenshot 2025-06-26 235426](https://github.com/user-attachments/assets/10a0ee29-9932-4f91-ab43-33518b117ac5) - @DGriffin91's Caldera `cargo run --release --features=bevy/trace_tracy -- --random-materials` ![image](https://github.com/user-attachments/assets/ef91ba6a-8e88-4922-a73f-acb0af5b0dbc) - @DGriffin91's Caldera with 20 unique material types (i.e. `MaterialPlugin<M>`) and random materials per mesh `cargo run --release --features=bevy/trace_tracy -- --random-materials` ![Screenshot 2025-06-27 000425](https://github.com/user-attachments/assets/9561388b-881d-46cf-8c3d-b15b3e9aedc7) ### TODO - We almost certainly lost some parallelization from removing the type params that could be gained back from smarter iteration. - Test all the things that could have broken. - ~Fix meshlets~ ## Showcase See [the example](https://github.com/bevyengine/bevy/pull/19667/files#diff-9d768cfe1c3aa81eff365d250d3cbe5a63e8df63e81dd85f64c3c3cd993f6d94) for a custom material implemented without the use of the `Material` trait and thus `AsBindGroup`. ![image](https://github.com/user-attachments/assets/e3fcca7c-e04e-4a4e-9d89-39d697a9e3b8) --------- Co-authored-by: IceSentry <IceSentry@users.noreply.github.com> Co-authored-by: IceSentry <c.giguere42@gmail.com>
1 parent 73a3132 commit e6ba9a6

33 files changed

+1672
-945
lines changed

Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ ron = "0.10"
579579
flate2 = "1.0"
580580
serde = { version = "1", features = ["derive"] }
581581
serde_json = "1.0.140"
582-
bytemuck = "1.7"
582+
bytemuck = "1"
583583
bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false }
584584
# The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself.
585585
bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false }
@@ -1051,6 +1051,17 @@ description = "Showcases different blend modes"
10511051
category = "3D Rendering"
10521052
wasm = true
10531053

1054+
[[example]]
1055+
name = "manual_material"
1056+
path = "examples/3d/manual_material.rs"
1057+
doc-scrape-examples = true
1058+
1059+
[package.metadata.example.manual_material]
1060+
name = "Manual Material Implementation"
1061+
description = "Demonstrates how to implement a material manually using the mid-level render APIs"
1062+
category = "3D Rendering"
1063+
wasm = true
1064+
10541065
[[example]]
10551066
name = "edit_material_on_gltf"
10561067
path = "examples/3d/edit_material_on_gltf.rs"

assets/shaders/manual_material.wgsl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import bevy_pbr::forward_io::VertexOutput
2+
3+
@group(3) @binding(0) var material_color_texture: texture_2d<f32>;
4+
@group(3) @binding(1) var material_color_sampler: sampler;
5+
6+
@fragment
7+
fn fragment(
8+
mesh: VertexOutput,
9+
) -> @location(0) vec4<f32> {
10+
return textureSample(material_color_texture, material_color_sampler, mesh.uv);
11+
}

crates/bevy_gizmos/src/pipeline_3d.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ impl Plugin for LineGizmo3dPlugin {
5050
.init_resource::<SpecializedRenderPipelines<LineJointGizmoPipeline>>()
5151
.configure_sets(
5252
Render,
53-
GizmoRenderSystems::QueueLineGizmos3d
54-
.in_set(RenderSystems::Queue)
55-
.ambiguous_with(bevy_pbr::queue_material_meshes::<bevy_pbr::StandardMaterial>),
53+
GizmoRenderSystems::QueueLineGizmos3d.in_set(RenderSystems::Queue),
5654
)
5755
.add_systems(
5856
Render,

crates/bevy_pbr/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea
5151
] }
5252

5353
# other
54-
bitflags = "2.3"
54+
bitflags = { version = "2.3", features = ["bytemuck"] }
5555
fixedbitset = "0.5"
5656
thiserror = { version = "2", default-features = false }
5757
derive_more = { version = "2", default-features = false, features = ["from"] }

crates/bevy_pbr/src/extended_material.rs

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use alloc::borrow::Cow;
22

3-
use bevy_asset::{Asset, Handle};
3+
use bevy_asset::Asset;
44
use bevy_ecs::system::SystemParamItem;
55
use bevy_platform::{collections::HashSet, hash::FixedHasher};
66
use bevy_reflect::{impl_type_path, Reflect};
@@ -9,8 +9,8 @@ use bevy_render::{
99
mesh::MeshVertexBufferLayoutRef,
1010
render_resource::{
1111
AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor,
12-
BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader,
13-
ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup,
12+
BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, ShaderRef,
13+
SpecializedMeshPipelineError, UnpreparedBindGroup,
1414
},
1515
renderer::RenderDevice,
1616
};
@@ -19,10 +19,6 @@ use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshP
1919

2020
pub struct MaterialExtensionPipeline {
2121
pub mesh_pipeline: MeshPipeline,
22-
pub material_layout: BindGroupLayout,
23-
pub vertex_shader: Option<Handle<Shader>>,
24-
pub fragment_shader: Option<Handle<Shader>>,
25-
pub bindless: bool,
2622
}
2723

2824
pub struct MaterialExtensionKey<E: MaterialExtension> {
@@ -150,12 +146,19 @@ where
150146
}
151147
}
152148

149+
#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Hash)]
150+
#[repr(C, packed)]
151+
pub struct MaterialExtensionBindGroupData<B, E> {
152+
pub base: B,
153+
pub extension: E,
154+
}
155+
153156
// We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]`
154157
// causes the `TypePath` derive to not generate an implementation.
155158
impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial<B: Material, E: MaterialExtension>);
156159

157160
impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
158-
type Data = (<B as AsBindGroup>::Data, <E as AsBindGroup>::Data);
161+
type Data = MaterialExtensionBindGroupData<B::Data, E::Data>;
159162
type Param = (<B as AsBindGroup>::Param, <E as AsBindGroup>::Param);
160163

161164
fn bindless_slot_count() -> Option<BindlessSlabResourceLimit> {
@@ -179,20 +182,24 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
179182
}
180183
}
181184

185+
fn bind_group_data(&self) -> Self::Data {
186+
MaterialExtensionBindGroupData {
187+
base: self.base.bind_group_data(),
188+
extension: self.extension.bind_group_data(),
189+
}
190+
}
191+
182192
fn unprepared_bind_group(
183193
&self,
184194
layout: &BindGroupLayout,
185195
render_device: &RenderDevice,
186196
(base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>,
187197
mut force_non_bindless: bool,
188-
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
198+
) -> Result<UnpreparedBindGroup, AsBindGroupError> {
189199
force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none();
190200

191201
// add together the bindings of the base material and the user material
192-
let UnpreparedBindGroup {
193-
mut bindings,
194-
data: base_data,
195-
} = B::unprepared_bind_group(
202+
let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group(
196203
&self.base,
197204
layout,
198205
render_device,
@@ -209,10 +216,7 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
209216

210217
bindings.extend(extended_bindgroup.bindings.0);
211218

212-
Ok(UnpreparedBindGroup {
213-
bindings,
214-
data: (base_data, extended_bindgroup.data),
215-
})
219+
Ok(UnpreparedBindGroup { bindings })
216220
}
217221

218222
fn bind_group_layout_entries(
@@ -373,57 +377,28 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
373377
}
374378

375379
fn specialize(
376-
pipeline: &MaterialPipeline<Self>,
380+
pipeline: &MaterialPipeline,
377381
descriptor: &mut RenderPipelineDescriptor,
378382
layout: &MeshVertexBufferLayoutRef,
379383
key: MaterialPipelineKey<Self>,
380384
) -> Result<(), SpecializedMeshPipelineError> {
381385
// Call the base material's specialize function
382-
let MaterialPipeline::<Self> {
383-
mesh_pipeline,
384-
material_layout,
385-
vertex_shader,
386-
fragment_shader,
387-
bindless,
388-
..
389-
} = pipeline.clone();
390-
let base_pipeline = MaterialPipeline::<B> {
391-
mesh_pipeline,
392-
material_layout,
393-
vertex_shader,
394-
fragment_shader,
395-
bindless,
396-
marker: Default::default(),
397-
};
398386
let base_key = MaterialPipelineKey::<B> {
399387
mesh_key: key.mesh_key,
400-
bind_group_data: key.bind_group_data.0,
388+
bind_group_data: key.bind_group_data.base,
401389
};
402-
B::specialize(&base_pipeline, descriptor, layout, base_key)?;
390+
B::specialize(pipeline, descriptor, layout, base_key)?;
403391

404392
// Call the extended material's specialize function afterwards
405-
let MaterialPipeline::<Self> {
406-
mesh_pipeline,
407-
material_layout,
408-
vertex_shader,
409-
fragment_shader,
410-
bindless,
411-
..
412-
} = pipeline.clone();
413-
414393
E::specialize(
415394
&MaterialExtensionPipeline {
416-
mesh_pipeline,
417-
material_layout,
418-
vertex_shader,
419-
fragment_shader,
420-
bindless,
395+
mesh_pipeline: pipeline.mesh_pipeline.clone(),
421396
},
422397
descriptor,
423398
layout,
424399
MaterialExtensionKey {
425400
mesh_key: key.mesh_key,
426-
bind_group_data: key.bind_group_data.1,
401+
bind_group_data: key.bind_group_data.extension,
427402
},
428403
)
429404
}

crates/bevy_pbr/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ impl Plugin for PbrPlugin {
239239
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
240240
debug_flags: self.debug_flags,
241241
},
242+
MaterialsPlugin {
243+
debug_flags: self.debug_flags,
244+
},
242245
MaterialPlugin::<StandardMaterial> {
243246
prepass_enabled: self.prepass_enabled,
244247
debug_flags: self.debug_flags,

0 commit comments

Comments
 (0)