
# Cold Specialization ## Objective An ongoing part of our quest to retain everything in the render world, cold-specialization aims to cache pipeline specialization so that pipeline IDs can be recomputed only when necessary, rather than every frame. This approach reduces redundant work in stable scenes, while still accommodating scenarios in which materials, views, or visibility might change, as well as unlocking future optimization work like retaining render bins. ## Solution Queue systems are split into a specialization system and queue system, the former of which only runs when necessary to compute a new pipeline id. Pipelines are invalidated using a combination of change detection and ECS ticks. ### The difficulty with change detection Detecting “what changed” can be tricky because pipeline specialization depends not only on the entity’s components (e.g., mesh, material, etc.) but also on which view (camera) it is rendering in. In other words, the cache key for a given pipeline id is a view entity/render entity pair. As such, it's not sufficient simply to react to change detection in order to specialize -- an entity could currently be out of view or could be rendered in the future in camera that is currently disabled or hasn't spawned yet. ### Why ticks? Ticks allow us to ensure correctness by allowing us to compare the last time a view or entity was updated compared to the cached pipeline id. This ensures that even if an entity was out of view or has never been seen in a given camera before we can still correctly determine whether it needs to be re-specialized or not. ## Testing TODO: Tested a bunch of different examples, need to test more. ## Migration Guide TODO - `AssetEvents` has been moved into the `PostUpdate` schedule. --------- Co-authored-by: Patrick Walton <pcwalton@mimiga.net>
1367 lines
54 KiB
Rust
1367 lines
54 KiB
Rust
use crate::material_bind_groups::{MaterialBindGroupAllocator, MaterialBindingId};
|
|
#[cfg(feature = "meshlet")]
|
|
use crate::meshlet::{
|
|
prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes,
|
|
InstanceManager,
|
|
};
|
|
use crate::*;
|
|
use bevy_asset::prelude::AssetChanged;
|
|
use bevy_asset::{Asset, AssetEvents, AssetId, AssetServer, UntypedAssetId};
|
|
use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred};
|
|
use bevy_core_pipeline::prepass::{AlphaMask3dPrepass, Opaque3dPrepass};
|
|
use bevy_core_pipeline::{
|
|
core_3d::{
|
|
AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, ScreenSpaceTransmissionQuality,
|
|
Transmissive3d, Transparent3d,
|
|
},
|
|
prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey},
|
|
tonemapping::Tonemapping,
|
|
};
|
|
use bevy_derive::{Deref, DerefMut};
|
|
use bevy_ecs::component::Tick;
|
|
use bevy_ecs::entity::EntityHash;
|
|
use bevy_ecs::system::SystemChangeTick;
|
|
use bevy_ecs::{
|
|
prelude::*,
|
|
system::{
|
|
lifetimeless::{SRes, SResMut},
|
|
SystemParamItem,
|
|
},
|
|
};
|
|
use bevy_platform_support::collections::HashMap;
|
|
use bevy_reflect::std_traits::ReflectDefault;
|
|
use bevy_reflect::Reflect;
|
|
use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed;
|
|
use bevy_render::{
|
|
batching::gpu_preprocessing::GpuPreprocessingSupport,
|
|
extract_resource::ExtractResource,
|
|
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
|
|
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
|
|
render_phase::*,
|
|
render_resource::*,
|
|
renderer::RenderDevice,
|
|
sync_world::MainEntity,
|
|
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility},
|
|
Extract,
|
|
};
|
|
use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap};
|
|
use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities};
|
|
use core::{hash::Hash, marker::PhantomData};
|
|
use tracing::error;
|
|
|
|
/// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`]
|
|
/// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level
|
|
/// way to render [`Mesh3d`] entities with custom shader logic.
|
|
///
|
|
/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders.
|
|
/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
|
|
/// check out the [`AsBindGroup`] documentation.
|
|
///
|
|
/// ```
|
|
/// # use bevy_pbr::{Material, MeshMaterial3d};
|
|
/// # use bevy_ecs::prelude::*;
|
|
/// # use bevy_image::Image;
|
|
/// # use bevy_reflect::TypePath;
|
|
/// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}};
|
|
/// # use bevy_color::LinearRgba;
|
|
/// # use bevy_color::palettes::basic::RED;
|
|
/// # use bevy_asset::{Handle, AssetServer, Assets, Asset};
|
|
/// # use bevy_math::primitives::Capsule3d;
|
|
/// #
|
|
/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)]
|
|
/// 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: LinearRgba,
|
|
/// // 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 with a mesh using `CustomMaterial`.
|
|
/// fn setup(
|
|
/// mut commands: Commands,
|
|
/// mut meshes: ResMut<Assets<Mesh>>,
|
|
/// mut materials: ResMut<Assets<CustomMaterial>>,
|
|
/// asset_server: Res<AssetServer>
|
|
/// ) {
|
|
/// commands.spawn((
|
|
/// Mesh3d(meshes.add(Capsule3d::default())),
|
|
/// MeshMaterial3d(materials.add(CustomMaterial {
|
|
/// color: RED.into(),
|
|
/// color_texture: asset_server.load("some_image.png"),
|
|
/// })),
|
|
/// ));
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// In WGSL shaders, the material's binding would look like this:
|
|
///
|
|
/// ```wgsl
|
|
/// @group(2) @binding(0) var<uniform> color: vec4<f32>;
|
|
/// @group(2) @binding(1) var color_texture: texture_2d<f32>;
|
|
/// @group(2) @binding(2) var color_sampler: sampler;
|
|
/// ```
|
|
pub trait Material: Asset + AsBindGroup + Clone + Sized {
|
|
/// 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 [`ShaderRef::Default`] is returned, the default mesh fragment shader
|
|
/// will be used.
|
|
fn fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`].
|
|
#[inline]
|
|
fn alpha_mode(&self) -> AlphaMode {
|
|
AlphaMode::Opaque
|
|
}
|
|
|
|
/// Returns if this material should be rendered by the deferred or forward renderer.
|
|
/// for `AlphaMode::Opaque` or `AlphaMode::Mask` materials.
|
|
/// If `OpaqueRendererMethod::Auto`, it will default to what is selected in the `DefaultOpaqueRendererMethod` resource.
|
|
#[inline]
|
|
fn opaque_render_method(&self) -> OpaqueRendererMethod {
|
|
OpaqueRendererMethod::Forward
|
|
}
|
|
|
|
#[inline]
|
|
/// Add a bias to the view depth of the mesh which can be used to force a specific render order.
|
|
/// for meshes with similar depth, to avoid z-fighting.
|
|
/// The bias is in depth-texture units so large values may be needed to overcome small depth differences.
|
|
fn depth_bias(&self) -> f32 {
|
|
0.0
|
|
}
|
|
|
|
#[inline]
|
|
/// Returns whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture).
|
|
///
|
|
/// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires
|
|
/// rendering to take place in a separate [`Transmissive3d`] pass.
|
|
fn reads_view_transmission_texture(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader
|
|
/// will be used.
|
|
///
|
|
/// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps
|
|
/// required for shadow mapping.
|
|
fn prepass_vertex_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader
|
|
/// will be used.
|
|
///
|
|
/// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps
|
|
/// required for shadow mapping.
|
|
fn prepass_fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the default deferred vertex shader
|
|
/// will be used.
|
|
fn deferred_vertex_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's deferred fragment shader. If [`ShaderRef::Default`] is returned, the default deferred fragment shader
|
|
/// will be used.
|
|
fn deferred_fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned,
|
|
/// the default meshlet mesh fragment shader will be used.
|
|
///
|
|
/// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s.
|
|
///
|
|
/// See [`crate::meshlet::MeshletMesh`] for limitations.
|
|
#[cfg(feature = "meshlet")]
|
|
fn meshlet_mesh_fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned,
|
|
/// the default meshlet mesh prepass fragment shader will be used.
|
|
///
|
|
/// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s.
|
|
///
|
|
/// See [`crate::meshlet::MeshletMesh`] for limitations.
|
|
#[cfg(feature = "meshlet")]
|
|
fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned,
|
|
/// the default meshlet mesh deferred fragment shader will be used.
|
|
///
|
|
/// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s.
|
|
///
|
|
/// See [`crate::meshlet::MeshletMesh`] for limitations.
|
|
#[cfg(feature = "meshlet")]
|
|
fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef {
|
|
ShaderRef::Default
|
|
}
|
|
|
|
/// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's
|
|
/// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input.
|
|
#[expect(
|
|
unused_variables,
|
|
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
|
|
)]
|
|
#[inline]
|
|
fn specialize(
|
|
pipeline: &MaterialPipeline<Self>,
|
|
descriptor: &mut RenderPipelineDescriptor,
|
|
layout: &MeshVertexBufferLayoutRef,
|
|
key: MaterialPipelineKey<Self>,
|
|
) -> Result<(), SpecializedMeshPipelineError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`]
|
|
/// asset type.
|
|
pub struct MaterialPlugin<M: Material> {
|
|
/// Controls if the prepass is enabled for the Material.
|
|
/// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs.
|
|
///
|
|
/// When it is enabled, it will automatically add the [`PrepassPlugin`]
|
|
/// required to make the prepass work on this Material.
|
|
pub prepass_enabled: bool,
|
|
/// Controls if shadows are enabled for the Material.
|
|
pub shadows_enabled: bool,
|
|
pub _marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M: Material> Default for MaterialPlugin<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
prepass_enabled: true,
|
|
shadows_enabled: true,
|
|
_marker: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M: Material> Plugin for MaterialPlugin<M>
|
|
where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
fn build(&self, app: &mut App) {
|
|
app.init_asset::<M>()
|
|
.register_type::<MeshMaterial3d<M>>()
|
|
.init_resource::<EntitiesNeedingSpecialization<M>>()
|
|
.add_plugins((RenderAssetPlugin::<PreparedMaterial<M>>::default(),))
|
|
.add_systems(
|
|
PostUpdate,
|
|
(
|
|
mark_meshes_as_changed_if_their_materials_changed::<M>.ambiguous_with_all(),
|
|
check_entities_needing_specialization::<M>.after(AssetEvents),
|
|
)
|
|
.after(mark_3d_meshes_as_changed_if_their_assets_changed),
|
|
);
|
|
|
|
if self.shadows_enabled {
|
|
app.add_systems(
|
|
PostUpdate,
|
|
check_light_entities_needing_specialization::<M>
|
|
.after(check_entities_needing_specialization::<M>),
|
|
);
|
|
}
|
|
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<EntitySpecializationTicks<M>>()
|
|
.init_resource::<SpecializedMaterialPipelineCache<M>>()
|
|
.init_resource::<DrawFunctions<Shadow>>()
|
|
.init_resource::<RenderMaterialInstances<M>>()
|
|
.add_render_command::<Shadow, DrawPrepass<M>>()
|
|
.add_render_command::<Transmissive3d, DrawMaterial<M>>()
|
|
.add_render_command::<Transparent3d, DrawMaterial<M>>()
|
|
.add_render_command::<Opaque3d, DrawMaterial<M>>()
|
|
.add_render_command::<AlphaMask3d, DrawMaterial<M>>()
|
|
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
|
|
.add_systems(
|
|
ExtractSchedule,
|
|
(
|
|
extract_mesh_materials::<M>.before(ExtractMeshesSet),
|
|
extract_entities_needs_specialization::<M>,
|
|
),
|
|
)
|
|
.add_systems(
|
|
Render,
|
|
(
|
|
specialize_material_meshes::<M>
|
|
.in_set(RenderSet::PrepareAssets)
|
|
.after(prepare_assets::<PreparedMaterial<M>>)
|
|
.after(prepare_assets::<RenderMesh>),
|
|
queue_material_meshes::<M>
|
|
.in_set(RenderSet::QueueMeshes)
|
|
.after(prepare_assets::<PreparedMaterial<M>>),
|
|
),
|
|
)
|
|
.add_systems(
|
|
Render,
|
|
prepare_material_bind_groups::<M>
|
|
.in_set(RenderSet::PrepareBindGroups)
|
|
.after(prepare_assets::<PreparedMaterial<M>>),
|
|
);
|
|
|
|
if self.shadows_enabled {
|
|
render_app
|
|
.init_resource::<LightKeyCache>()
|
|
.init_resource::<LightSpecializationTicks>()
|
|
.init_resource::<SpecializedShadowMaterialPipelineCache<M>>()
|
|
.add_systems(
|
|
Render,
|
|
(
|
|
check_views_lights_need_specialization.in_set(RenderSet::PrepareAssets),
|
|
specialize_shadows::<M>
|
|
.in_set(RenderSet::PrepareAssets)
|
|
.after(prepare_assets::<PreparedMaterial<M>>),
|
|
queue_shadows::<M>
|
|
.in_set(RenderSet::QueueMeshes)
|
|
.after(prepare_assets::<PreparedMaterial<M>>),
|
|
),
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "meshlet")]
|
|
render_app.add_systems(
|
|
Render,
|
|
queue_material_meshlet_meshes::<M>
|
|
.in_set(RenderSet::QueueMeshes)
|
|
.run_if(resource_exists::<InstanceManager>),
|
|
);
|
|
|
|
#[cfg(feature = "meshlet")]
|
|
render_app.add_systems(
|
|
Render,
|
|
prepare_material_meshlet_meshes_main_opaque_pass::<M>
|
|
.in_set(RenderSet::QueueMeshes)
|
|
.after(prepare_assets::<PreparedMaterial<M>>)
|
|
.before(queue_material_meshlet_meshes::<M>)
|
|
.run_if(resource_exists::<InstanceManager>),
|
|
);
|
|
}
|
|
|
|
if self.shadows_enabled || self.prepass_enabled {
|
|
// PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin
|
|
app.add_plugins(PrepassPipelinePlugin::<M>::default());
|
|
}
|
|
|
|
if self.prepass_enabled {
|
|
app.add_plugins(PrepassPlugin::<M>::default());
|
|
}
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<MaterialPipeline<M>>()
|
|
.init_resource::<MaterialBindGroupAllocator<M>>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A key uniquely identifying a specialized [`MaterialPipeline`].
|
|
pub struct MaterialPipelineKey<M: Material> {
|
|
pub mesh_key: MeshPipelineKey,
|
|
pub bind_group_data: M::Data,
|
|
}
|
|
|
|
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: core::hash::Hasher>(&self, state: &mut H) {
|
|
self.mesh_key.hash(state);
|
|
self.bind_group_data.hash(state);
|
|
}
|
|
}
|
|
|
|
/// Render pipeline data for a given [`Material`].
|
|
#[derive(Resource)]
|
|
pub struct MaterialPipeline<M: Material> {
|
|
pub mesh_pipeline: MeshPipeline,
|
|
pub material_layout: BindGroupLayout,
|
|
pub vertex_shader: Option<Handle<Shader>>,
|
|
pub fragment_shader: Option<Handle<Shader>>,
|
|
/// Whether this material *actually* uses bindless resources, taking the
|
|
/// platform support (or lack thereof) of bindless resources into account.
|
|
pub bindless: bool,
|
|
pub marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M: Material> Clone for MaterialPipeline<M> {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
mesh_pipeline: self.mesh_pipeline.clone(),
|
|
material_layout: self.material_layout.clone(),
|
|
vertex_shader: self.vertex_shader.clone(),
|
|
fragment_shader: self.fragment_shader.clone(),
|
|
bindless: self.bindless,
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M: Material> SpecializedMeshPipeline for MaterialPipeline<M>
|
|
where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
type Key = MaterialPipelineKey<M>;
|
|
|
|
fn specialize(
|
|
&self,
|
|
key: Self::Key,
|
|
layout: &MeshVertexBufferLayoutRef,
|
|
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
|
|
let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?;
|
|
if let Some(vertex_shader) = &self.vertex_shader {
|
|
descriptor.vertex.shader = vertex_shader.clone();
|
|
}
|
|
|
|
if let Some(fragment_shader) = &self.fragment_shader {
|
|
descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone();
|
|
}
|
|
|
|
descriptor.layout.insert(2, self.material_layout.clone());
|
|
|
|
M::specialize(self, &mut descriptor, layout, key)?;
|
|
|
|
// If bindless mode is on, add a `BINDLESS` define.
|
|
if self.bindless {
|
|
descriptor.vertex.shader_defs.push("BINDLESS".into());
|
|
if let Some(ref mut fragment) = descriptor.fragment {
|
|
fragment.shader_defs.push("BINDLESS".into());
|
|
}
|
|
}
|
|
|
|
Ok(descriptor)
|
|
}
|
|
}
|
|
|
|
impl<M: Material> FromWorld for MaterialPipeline<M> {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let asset_server = world.resource::<AssetServer>();
|
|
let render_device = world.resource::<RenderDevice>();
|
|
|
|
MaterialPipeline {
|
|
mesh_pipeline: world.resource::<MeshPipeline>().clone(),
|
|
material_layout: M::bind_group_layout(render_device),
|
|
vertex_shader: match M::vertex_shader() {
|
|
ShaderRef::Default => None,
|
|
ShaderRef::Handle(handle) => Some(handle),
|
|
ShaderRef::Path(path) => Some(asset_server.load(path)),
|
|
},
|
|
fragment_shader: match M::fragment_shader() {
|
|
ShaderRef::Default => None,
|
|
ShaderRef::Handle(handle) => Some(handle),
|
|
ShaderRef::Path(path) => Some(asset_server.load(path)),
|
|
},
|
|
bindless: material_bind_groups::material_uses_bindless_resources::<M>(render_device),
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
type DrawMaterial<M> = (
|
|
SetItemPipeline,
|
|
SetMeshViewBindGroup<0>,
|
|
SetMeshBindGroup<1>,
|
|
SetMaterialBindGroup<M, 2>,
|
|
DrawMesh,
|
|
);
|
|
|
|
/// Sets the bind group for a given [`Material`] at the configured `I` index.
|
|
pub struct SetMaterialBindGroup<M: Material, const I: usize>(PhantomData<M>);
|
|
impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterialBindGroup<M, I> {
|
|
type Param = (
|
|
SRes<RenderAssets<PreparedMaterial<M>>>,
|
|
SRes<RenderMaterialInstances<M>>,
|
|
SRes<MaterialBindGroupAllocator<M>>,
|
|
);
|
|
type ViewQuery = ();
|
|
type ItemQuery = ();
|
|
|
|
#[inline]
|
|
fn render<'w>(
|
|
item: &P,
|
|
_view: (),
|
|
_item_query: Option<()>,
|
|
(materials, material_instances, material_bind_group_allocator): SystemParamItem<
|
|
'w,
|
|
'_,
|
|
Self::Param,
|
|
>,
|
|
pass: &mut TrackedRenderPass<'w>,
|
|
) -> RenderCommandResult {
|
|
let materials = materials.into_inner();
|
|
let material_instances = material_instances.into_inner();
|
|
let material_bind_group_allocator = material_bind_group_allocator.into_inner();
|
|
|
|
let Some(material_asset_id) = material_instances.get(&item.main_entity()) else {
|
|
return RenderCommandResult::Skip;
|
|
};
|
|
let Some(material) = materials.get(*material_asset_id) else {
|
|
return RenderCommandResult::Skip;
|
|
};
|
|
let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group)
|
|
else {
|
|
return RenderCommandResult::Skip;
|
|
};
|
|
let Some(bind_group) = material_bind_group.get_bind_group() else {
|
|
return RenderCommandResult::Skip;
|
|
};
|
|
pass.set_bind_group(I, bind_group, &[]);
|
|
RenderCommandResult::Success
|
|
}
|
|
}
|
|
|
|
/// Stores all extracted instances of a [`Material`] in the render world.
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
pub struct RenderMaterialInstances<M: Material>(pub MainEntityHashMap<AssetId<M>>);
|
|
|
|
impl<M: Material> Default for RenderMaterialInstances<M> {
|
|
fn default() -> Self {
|
|
Self(Default::default())
|
|
}
|
|
}
|
|
|
|
pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey {
|
|
match alpha_mode {
|
|
// Premultiplied and Add share the same pipeline key
|
|
// They're made distinct in the PBR shader, via `premultiply_alpha()`
|
|
AlphaMode::Premultiplied | AlphaMode::Add => MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA,
|
|
AlphaMode::Blend => MeshPipelineKey::BLEND_ALPHA,
|
|
AlphaMode::Multiply => MeshPipelineKey::BLEND_MULTIPLY,
|
|
AlphaMode::Mask(_) => MeshPipelineKey::MAY_DISCARD,
|
|
AlphaMode::AlphaToCoverage => match *msaa {
|
|
Msaa::Off => MeshPipelineKey::MAY_DISCARD,
|
|
_ => MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE,
|
|
},
|
|
_ => MeshPipelineKey::NONE,
|
|
}
|
|
}
|
|
|
|
pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey {
|
|
match tonemapping {
|
|
Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE,
|
|
Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD,
|
|
Tonemapping::ReinhardLuminance => MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE,
|
|
Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED,
|
|
Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX,
|
|
Tonemapping::SomewhatBoringDisplayTransform => {
|
|
MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
|
|
}
|
|
Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE,
|
|
Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC,
|
|
}
|
|
}
|
|
|
|
pub const fn screen_space_specular_transmission_pipeline_key(
|
|
screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality,
|
|
) -> MeshPipelineKey {
|
|
match screen_space_transmissive_blur_quality {
|
|
ScreenSpaceTransmissionQuality::Low => {
|
|
MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW
|
|
}
|
|
ScreenSpaceTransmissionQuality::Medium => {
|
|
MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM
|
|
}
|
|
ScreenSpaceTransmissionQuality::High => {
|
|
MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH
|
|
}
|
|
ScreenSpaceTransmissionQuality::Ultra => {
|
|
MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A system that ensures that
|
|
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes
|
|
/// whose materials changed.
|
|
///
|
|
/// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers
|
|
/// meshes that were newly extracted, and it writes information from the
|
|
/// [`RenderMeshMaterialIds`] into the
|
|
/// [`crate::render::mesh::MeshInputUniform`], we must tell
|
|
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a
|
|
/// mesh if its material changed. Otherwise, the material binding information in
|
|
/// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly.
|
|
/// The easiest way to ensure that
|
|
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh
|
|
/// is to mark its [`Mesh3d`] as changed, so that's what this system does.
|
|
fn mark_meshes_as_changed_if_their_materials_changed<M>(
|
|
mut changed_meshes_query: Query<&mut Mesh3d, Changed<MeshMaterial3d<M>>>,
|
|
) where
|
|
M: Material,
|
|
{
|
|
for mut mesh in &mut changed_meshes_query {
|
|
mesh.set_changed();
|
|
}
|
|
}
|
|
|
|
/// Fills the [`RenderMaterialInstances`] and [`RenderMeshMaterialIds`]
|
|
/// resources from the meshes in the scene.
|
|
fn extract_mesh_materials<M: Material>(
|
|
mut material_instances: ResMut<RenderMaterialInstances<M>>,
|
|
mut material_ids: ResMut<RenderMeshMaterialIds>,
|
|
changed_meshes_query: Extract<
|
|
Query<
|
|
(Entity, &ViewVisibility, &MeshMaterial3d<M>),
|
|
Or<(Changed<ViewVisibility>, Changed<MeshMaterial3d<M>>)>,
|
|
>,
|
|
>,
|
|
mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
|
|
mut removed_materials_query: Extract<RemovedComponents<MeshMaterial3d<M>>>,
|
|
) {
|
|
for (entity, view_visibility, material) in &changed_meshes_query {
|
|
if view_visibility.get() {
|
|
material_instances.insert(entity.into(), material.id());
|
|
material_ids.insert(entity.into(), material.id().into());
|
|
} else {
|
|
material_instances.remove(&MainEntity::from(entity));
|
|
material_ids.remove(entity.into());
|
|
}
|
|
}
|
|
|
|
for entity in removed_visibilities_query
|
|
.read()
|
|
.chain(removed_materials_query.read())
|
|
{
|
|
// Only queue a mesh for removal if we didn't pick it up above.
|
|
// It's possible that a necessary component was removed and re-added in
|
|
// the same frame.
|
|
if !changed_meshes_query.contains(entity) {
|
|
material_instances.remove(&MainEntity::from(entity));
|
|
material_ids.remove(entity.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn extract_entities_needs_specialization<M>(
|
|
entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<M>>>,
|
|
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks<M>>,
|
|
ticks: SystemChangeTick,
|
|
) where
|
|
M: Material,
|
|
{
|
|
for entity in entities_needing_specialization.iter() {
|
|
// Update the entity's specialization tick with this run's tick
|
|
entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Deref, DerefMut, Clone, Debug)]
|
|
pub struct EntitiesNeedingSpecialization<M> {
|
|
#[deref]
|
|
pub entities: Vec<Entity>,
|
|
_marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M> Default for EntitiesNeedingSpecialization<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
entities: Default::default(),
|
|
_marker: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Deref, DerefMut, Clone, Debug)]
|
|
pub struct EntitySpecializationTicks<M> {
|
|
#[deref]
|
|
pub entities: MainEntityHashMap<Tick>,
|
|
_marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M> Default for EntitySpecializationTicks<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
entities: MainEntityHashMap::default(),
|
|
_marker: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
pub struct SpecializedMaterialPipelineCache<M> {
|
|
// (view_entity, material_entity) -> (tick, pipeline_id)
|
|
#[deref]
|
|
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
|
|
marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M> Default for SpecializedMaterialPipelineCache<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
map: HashMap::default(),
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn check_entities_needing_specialization<M>(
|
|
needs_specialization: Query<
|
|
Entity,
|
|
Or<(
|
|
Changed<Mesh3d>,
|
|
AssetChanged<Mesh3d>,
|
|
Changed<MeshMaterial3d<M>>,
|
|
AssetChanged<MeshMaterial3d<M>>,
|
|
)>,
|
|
>,
|
|
mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<M>>,
|
|
) where
|
|
M: Material,
|
|
{
|
|
entities_needing_specialization.clear();
|
|
for entity in &needs_specialization {
|
|
entities_needing_specialization.push(entity);
|
|
}
|
|
}
|
|
|
|
pub fn specialize_material_meshes<M: Material>(
|
|
render_meshes: Res<RenderAssets<RenderMesh>>,
|
|
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
|
|
render_mesh_instances: Res<RenderMeshInstances>,
|
|
render_material_instances: Res<RenderMaterialInstances<M>>,
|
|
render_lightmaps: Res<RenderLightmaps>,
|
|
render_visibility_ranges: Res<RenderVisibilityRanges>,
|
|
(
|
|
material_bind_group_allocator,
|
|
opaque_render_phases,
|
|
alpha_mask_render_phases,
|
|
transmissive_render_phases,
|
|
transparent_render_phases,
|
|
): (
|
|
Res<MaterialBindGroupAllocator<M>>,
|
|
Res<ViewBinnedRenderPhases<Opaque3d>>,
|
|
Res<ViewBinnedRenderPhases<AlphaMask3d>>,
|
|
Res<ViewSortedRenderPhases<Transmissive3d>>,
|
|
Res<ViewSortedRenderPhases<Transparent3d>>,
|
|
),
|
|
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
|
|
view_key_cache: Res<ViewKeyCache>,
|
|
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
|
|
view_specialization_ticks: Res<ViewSpecializationTicks>,
|
|
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache<M>>,
|
|
mut pipelines: ResMut<SpecializedMeshPipelines<MaterialPipeline<M>>>,
|
|
pipeline: Res<MaterialPipeline<M>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
ticks: SystemChangeTick,
|
|
) where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
for (view_entity, view, visible_entities) in &views {
|
|
if !transparent_render_phases.contains_key(&view.retained_view_entity)
|
|
&& !opaque_render_phases.contains_key(&view.retained_view_entity)
|
|
&& !alpha_mask_render_phases.contains_key(&view.retained_view_entity)
|
|
&& !transmissive_render_phases.contains_key(&view.retained_view_entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let Some(view_key) = view_key_cache.get(view_entity) else {
|
|
continue;
|
|
};
|
|
|
|
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
|
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
|
|
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
|
|
let last_specialized_tick = specialized_material_pipeline_cache
|
|
.get(&(*view_entity, *visible_entity))
|
|
.map(|(tick, _)| *tick);
|
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
|
view_tick.is_newer_than(tick, ticks.this_run())
|
|
|| entity_tick.is_newer_than(tick, ticks.this_run())
|
|
});
|
|
if !needs_specialization {
|
|
continue;
|
|
}
|
|
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
|
|
continue;
|
|
};
|
|
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
|
|
continue;
|
|
};
|
|
let Some(material) = render_materials.get(*material_asset_id) else {
|
|
continue;
|
|
};
|
|
let Some(material_bind_group) =
|
|
material_bind_group_allocator.get(material.binding.group)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits;
|
|
mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(
|
|
material.properties.alpha_mode,
|
|
&Msaa::from_samples(view_key.msaa_samples()),
|
|
));
|
|
let mut mesh_key = *view_key
|
|
| MeshPipelineKey::from_bits_retain(mesh.key_bits.bits())
|
|
| mesh_pipeline_key_bits;
|
|
|
|
if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) {
|
|
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
|
|
|
|
if lightmap.bicubic_sampling {
|
|
mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING;
|
|
}
|
|
}
|
|
|
|
if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) {
|
|
mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER;
|
|
}
|
|
|
|
if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
|
|
// If the previous frame have skins or morph targets, note that.
|
|
if mesh_instance
|
|
.flags
|
|
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN)
|
|
{
|
|
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN;
|
|
}
|
|
if mesh_instance
|
|
.flags
|
|
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH)
|
|
{
|
|
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH;
|
|
}
|
|
}
|
|
|
|
let key = MaterialPipelineKey {
|
|
mesh_key,
|
|
bind_group_data: material_bind_group
|
|
.get_extra_data(material.binding.slot)
|
|
.clone(),
|
|
};
|
|
let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout);
|
|
let pipeline_id = match pipeline_id {
|
|
Ok(id) => id,
|
|
Err(err) => {
|
|
error!("{}", err);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
specialized_material_pipeline_cache.insert(
|
|
(*view_entity, *visible_entity),
|
|
(ticks.this_run(), pipeline_id),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// For each view, iterates over all the meshes visible from that view and adds
|
|
/// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate.
|
|
pub fn queue_material_meshes<M: Material>(
|
|
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
|
|
render_mesh_instances: Res<RenderMeshInstances>,
|
|
render_material_instances: Res<RenderMaterialInstances<M>>,
|
|
mesh_allocator: Res<MeshAllocator>,
|
|
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
|
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
|
|
mut alpha_mask_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
|
|
mut transmissive_render_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
|
|
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
|
|
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
|
|
specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache<M>>,
|
|
) where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
for (view_entity, view, visible_entities) in &views {
|
|
let (
|
|
Some(opaque_phase),
|
|
Some(alpha_mask_phase),
|
|
Some(transmissive_phase),
|
|
Some(transparent_phase),
|
|
) = (
|
|
opaque_render_phases.get_mut(&view.retained_view_entity),
|
|
alpha_mask_render_phases.get_mut(&view.retained_view_entity),
|
|
transmissive_render_phases.get_mut(&view.retained_view_entity),
|
|
transparent_render_phases.get_mut(&view.retained_view_entity),
|
|
)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let rangefinder = view.rangefinder3d();
|
|
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
|
let Some(pipeline_id) = specialized_material_pipeline_cache
|
|
.get(&(*view_entity, *visible_entity))
|
|
.map(|(_, pipeline_id)| *pipeline_id)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
|
|
continue;
|
|
};
|
|
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(material) = render_materials.get(*material_asset_id) else {
|
|
continue;
|
|
};
|
|
|
|
// Fetch the slabs that this mesh resides in.
|
|
let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
|
|
|
|
match material.properties.render_phase_type {
|
|
RenderPhaseType::Transmissive => {
|
|
let distance = rangefinder.distance_translation(&mesh_instance.translation)
|
|
+ material.properties.depth_bias;
|
|
transmissive_phase.add(Transmissive3d {
|
|
entity: (*render_entity, *visible_entity),
|
|
draw_function: material.properties.draw_function_id,
|
|
pipeline: pipeline_id,
|
|
distance,
|
|
batch_range: 0..1,
|
|
extra_index: PhaseItemExtraIndex::None,
|
|
indexed: index_slab.is_some(),
|
|
});
|
|
}
|
|
RenderPhaseType::Opaque => {
|
|
if material.properties.render_method == OpaqueRendererMethod::Deferred {
|
|
continue;
|
|
}
|
|
let batch_set_key = Opaque3dBatchSetKey {
|
|
pipeline: pipeline_id,
|
|
draw_function: material.properties.draw_function_id,
|
|
material_bind_group_index: Some(material.binding.group.0),
|
|
vertex_slab: vertex_slab.unwrap_or_default(),
|
|
index_slab,
|
|
lightmap_slab: mesh_instance.shared.lightmap_slab_index.map(|index| *index),
|
|
};
|
|
let bin_key = Opaque3dBinKey {
|
|
asset_id: mesh_instance.mesh_asset_id.into(),
|
|
};
|
|
opaque_phase.add(
|
|
batch_set_key,
|
|
bin_key,
|
|
(*render_entity, *visible_entity),
|
|
BinnedRenderPhaseType::mesh(
|
|
mesh_instance.should_batch(),
|
|
&gpu_preprocessing_support,
|
|
),
|
|
);
|
|
}
|
|
// Alpha mask
|
|
RenderPhaseType::AlphaMask => {
|
|
let batch_set_key = OpaqueNoLightmap3dBatchSetKey {
|
|
draw_function: material.properties.draw_function_id,
|
|
pipeline: pipeline_id,
|
|
material_bind_group_index: Some(material.binding.group.0),
|
|
vertex_slab: vertex_slab.unwrap_or_default(),
|
|
index_slab,
|
|
};
|
|
let bin_key = OpaqueNoLightmap3dBinKey {
|
|
asset_id: mesh_instance.mesh_asset_id.into(),
|
|
};
|
|
alpha_mask_phase.add(
|
|
batch_set_key,
|
|
bin_key,
|
|
(*render_entity, *visible_entity),
|
|
BinnedRenderPhaseType::mesh(
|
|
mesh_instance.should_batch(),
|
|
&gpu_preprocessing_support,
|
|
),
|
|
);
|
|
}
|
|
RenderPhaseType::Transparent => {
|
|
let distance = rangefinder.distance_translation(&mesh_instance.translation)
|
|
+ material.properties.depth_bias;
|
|
transparent_phase.add(Transparent3d {
|
|
entity: (*render_entity, *visible_entity),
|
|
draw_function: material.properties.draw_function_id,
|
|
pipeline: pipeline_id,
|
|
distance,
|
|
batch_range: 0..1,
|
|
extra_index: PhaseItemExtraIndex::None,
|
|
indexed: index_slab.is_some(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Default render method used for opaque materials.
|
|
#[derive(Default, Resource, Clone, Debug, ExtractResource, Reflect)]
|
|
#[reflect(Resource, Default, Debug)]
|
|
pub struct DefaultOpaqueRendererMethod(OpaqueRendererMethod);
|
|
|
|
impl DefaultOpaqueRendererMethod {
|
|
pub fn forward() -> Self {
|
|
DefaultOpaqueRendererMethod(OpaqueRendererMethod::Forward)
|
|
}
|
|
|
|
pub fn deferred() -> Self {
|
|
DefaultOpaqueRendererMethod(OpaqueRendererMethod::Deferred)
|
|
}
|
|
|
|
pub fn set_to_forward(&mut self) {
|
|
self.0 = OpaqueRendererMethod::Forward;
|
|
}
|
|
|
|
pub fn set_to_deferred(&mut self) {
|
|
self.0 = OpaqueRendererMethod::Deferred;
|
|
}
|
|
}
|
|
|
|
/// Render method used for opaque materials.
|
|
///
|
|
/// The forward rendering main pass draws each mesh entity and shades it according to its
|
|
/// corresponding material and the lights that affect it. Some render features like Screen Space
|
|
/// Ambient Occlusion require running depth and normal prepasses, that are 'deferred'-like
|
|
/// prepasses over all mesh entities to populate depth and normal textures. This means that when
|
|
/// using render features that require running prepasses, multiple passes over all visible geometry
|
|
/// are required. This can be slow if there is a lot of geometry that cannot be batched into few
|
|
/// draws.
|
|
///
|
|
/// Deferred rendering runs a prepass to gather not only geometric information like depth and
|
|
/// normals, but also all the material properties like base color, emissive color, reflectance,
|
|
/// metalness, etc, and writes them into a deferred 'g-buffer' texture. The deferred main pass is
|
|
/// then a fullscreen pass that reads data from these textures and executes shading. This allows
|
|
/// for one pass over geometry, but is at the cost of not being able to use MSAA, and has heavier
|
|
/// bandwidth usage which can be unsuitable for low end mobile or other bandwidth-constrained devices.
|
|
///
|
|
/// If a material indicates `OpaqueRendererMethod::Auto`, `DefaultOpaqueRendererMethod` will be used.
|
|
#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect)]
|
|
pub enum OpaqueRendererMethod {
|
|
#[default]
|
|
Forward,
|
|
Deferred,
|
|
Auto,
|
|
}
|
|
|
|
/// Common [`Material`] properties, calculated for a specific material instance.
|
|
pub struct MaterialProperties {
|
|
/// Is this material should be rendered by the deferred renderer when.
|
|
/// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`]
|
|
pub render_method: OpaqueRendererMethod,
|
|
/// The [`AlphaMode`] of this material.
|
|
pub alpha_mode: AlphaMode,
|
|
/// The bits in the [`MeshPipelineKey`] for this material.
|
|
///
|
|
/// These are precalculated so that we can just "or" them together in
|
|
/// [`queue_material_meshes`].
|
|
pub mesh_pipeline_key_bits: MeshPipelineKey,
|
|
/// 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.
|
|
/// The bias is in depth-texture units so large values may be needed to overcome small depth differences.
|
|
pub depth_bias: f32,
|
|
/// Whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture).
|
|
///
|
|
/// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires
|
|
/// rendering to take place in a separate [`Transmissive3d`] pass.
|
|
pub reads_view_transmission_texture: bool,
|
|
pub render_phase_type: RenderPhaseType,
|
|
pub draw_function_id: DrawFunctionId,
|
|
pub prepass_draw_function_id: Option<DrawFunctionId>,
|
|
pub deferred_draw_function_id: Option<DrawFunctionId>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum RenderPhaseType {
|
|
Opaque,
|
|
AlphaMask,
|
|
Transmissive,
|
|
Transparent,
|
|
}
|
|
|
|
/// A resource that maps each untyped material ID to its binding.
|
|
///
|
|
/// This duplicates information in `RenderAssets<M>`, but it doesn't have the
|
|
/// `M` type parameter, so it can be used in untyped contexts like
|
|
/// [`crate::render::mesh::collect_meshes_for_gpu_building`].
|
|
#[derive(Resource, Default, Deref, DerefMut)]
|
|
pub struct RenderMaterialBindings(HashMap<UntypedAssetId, MaterialBindingId>);
|
|
|
|
/// Data prepared for a [`Material`] instance.
|
|
pub struct PreparedMaterial<M: Material> {
|
|
pub binding: MaterialBindingId,
|
|
pub properties: MaterialProperties,
|
|
pub phantom: PhantomData<M>,
|
|
}
|
|
|
|
impl<M: Material> RenderAsset for PreparedMaterial<M> {
|
|
type SourceAsset = M;
|
|
|
|
type Param = (
|
|
SRes<RenderDevice>,
|
|
SRes<MaterialPipeline<M>>,
|
|
SRes<DefaultOpaqueRendererMethod>,
|
|
SResMut<MaterialBindGroupAllocator<M>>,
|
|
SResMut<RenderMaterialBindings>,
|
|
SRes<DrawFunctions<Opaque3d>>,
|
|
SRes<DrawFunctions<AlphaMask3d>>,
|
|
SRes<DrawFunctions<Transmissive3d>>,
|
|
SRes<DrawFunctions<Transparent3d>>,
|
|
SRes<DrawFunctions<Opaque3dPrepass>>,
|
|
SRes<DrawFunctions<AlphaMask3dPrepass>>,
|
|
SRes<DrawFunctions<Opaque3dDeferred>>,
|
|
SRes<DrawFunctions<AlphaMask3dDeferred>>,
|
|
M::Param,
|
|
);
|
|
|
|
fn prepare_asset(
|
|
material: Self::SourceAsset,
|
|
material_id: AssetId<Self::SourceAsset>,
|
|
(
|
|
render_device,
|
|
pipeline,
|
|
default_opaque_render_method,
|
|
ref mut bind_group_allocator,
|
|
ref mut render_material_bindings,
|
|
opaque_draw_functions,
|
|
alpha_mask_draw_functions,
|
|
transmissive_draw_functions,
|
|
transparent_draw_functions,
|
|
opaque_prepass_draw_functions,
|
|
alpha_mask_prepass_draw_functions,
|
|
opaque_deferred_draw_functions,
|
|
alpha_mask_deferred_draw_functions,
|
|
ref mut material_param,
|
|
): &mut SystemParamItem<Self::Param>,
|
|
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
|
// Allocate a material binding ID if needed.
|
|
let material_binding_id = *render_material_bindings
|
|
.entry(material_id.into())
|
|
.or_insert_with(|| bind_group_allocator.allocate());
|
|
|
|
let draw_opaque_pbr = opaque_draw_functions.read().id::<DrawMaterial<M>>();
|
|
let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::<DrawMaterial<M>>();
|
|
let draw_transmissive_pbr = transmissive_draw_functions.read().id::<DrawMaterial<M>>();
|
|
let draw_transparent_pbr = transparent_draw_functions.read().id::<DrawMaterial<M>>();
|
|
let draw_opaque_prepass = opaque_prepass_draw_functions
|
|
.read()
|
|
.get_id::<DrawPrepass<M>>();
|
|
let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions
|
|
.read()
|
|
.get_id::<DrawPrepass<M>>();
|
|
let draw_opaque_deferred = opaque_deferred_draw_functions
|
|
.read()
|
|
.get_id::<DrawPrepass<M>>();
|
|
let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions
|
|
.read()
|
|
.get_id::<DrawPrepass<M>>();
|
|
|
|
let render_method = match material.opaque_render_method() {
|
|
OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward,
|
|
OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred,
|
|
OpaqueRendererMethod::Auto => default_opaque_render_method.0,
|
|
};
|
|
|
|
let mut mesh_pipeline_key_bits = MeshPipelineKey::empty();
|
|
mesh_pipeline_key_bits.set(
|
|
MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE,
|
|
material.reads_view_transmission_texture(),
|
|
);
|
|
|
|
let reads_view_transmission_texture =
|
|
mesh_pipeline_key_bits.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE);
|
|
|
|
let render_phase_type = match material.alpha_mode() {
|
|
AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => {
|
|
RenderPhaseType::Transparent
|
|
}
|
|
_ if reads_view_transmission_texture => RenderPhaseType::Transmissive,
|
|
AlphaMode::Opaque | AlphaMode::AlphaToCoverage => RenderPhaseType::Opaque,
|
|
AlphaMode::Mask(_) => RenderPhaseType::AlphaMask,
|
|
};
|
|
|
|
let draw_function_id = match render_phase_type {
|
|
RenderPhaseType::Opaque => draw_opaque_pbr,
|
|
RenderPhaseType::AlphaMask => draw_alpha_mask_pbr,
|
|
RenderPhaseType::Transmissive => draw_transmissive_pbr,
|
|
RenderPhaseType::Transparent => draw_transparent_pbr,
|
|
};
|
|
let prepass_draw_function_id = match render_phase_type {
|
|
RenderPhaseType::Opaque => draw_opaque_prepass,
|
|
RenderPhaseType::AlphaMask => draw_alpha_mask_prepass,
|
|
_ => None,
|
|
};
|
|
let deferred_draw_function_id = match render_phase_type {
|
|
RenderPhaseType::Opaque => draw_opaque_deferred,
|
|
RenderPhaseType::AlphaMask => draw_alpha_mask_deferred,
|
|
_ => None,
|
|
};
|
|
|
|
match material.unprepared_bind_group(
|
|
&pipeline.material_layout,
|
|
render_device,
|
|
material_param,
|
|
false,
|
|
) {
|
|
Ok(unprepared) => {
|
|
bind_group_allocator.init(render_device, material_binding_id, unprepared);
|
|
|
|
Ok(PreparedMaterial {
|
|
binding: material_binding_id,
|
|
properties: MaterialProperties {
|
|
alpha_mode: material.alpha_mode(),
|
|
depth_bias: material.depth_bias(),
|
|
reads_view_transmission_texture,
|
|
render_phase_type,
|
|
draw_function_id,
|
|
prepass_draw_function_id,
|
|
render_method,
|
|
mesh_pipeline_key_bits,
|
|
deferred_draw_function_id,
|
|
},
|
|
phantom: PhantomData,
|
|
})
|
|
}
|
|
|
|
Err(AsBindGroupError::RetryNextUpdate) => {
|
|
Err(PrepareAssetError::RetryNextUpdate(material))
|
|
}
|
|
|
|
Err(AsBindGroupError::CreateBindGroupDirectly) => {
|
|
// This material has opted out of automatic bind group creation
|
|
// and is requesting a fully-custom bind group. Invoke
|
|
// `as_bind_group` as requested, and store the resulting bind
|
|
// group in the slot.
|
|
match material.as_bind_group(
|
|
&pipeline.material_layout,
|
|
render_device,
|
|
material_param,
|
|
) {
|
|
Ok(prepared_bind_group) => {
|
|
// Store the resulting bind group directly in the slot.
|
|
bind_group_allocator.init_custom(
|
|
material_binding_id,
|
|
prepared_bind_group.bind_group,
|
|
prepared_bind_group.data,
|
|
);
|
|
|
|
Ok(PreparedMaterial {
|
|
binding: material_binding_id,
|
|
properties: MaterialProperties {
|
|
alpha_mode: material.alpha_mode(),
|
|
depth_bias: material.depth_bias(),
|
|
reads_view_transmission_texture,
|
|
render_phase_type,
|
|
draw_function_id,
|
|
prepass_draw_function_id,
|
|
render_method,
|
|
mesh_pipeline_key_bits,
|
|
deferred_draw_function_id,
|
|
},
|
|
phantom: PhantomData,
|
|
})
|
|
}
|
|
|
|
Err(AsBindGroupError::RetryNextUpdate) => {
|
|
Err(PrepareAssetError::RetryNextUpdate(material))
|
|
}
|
|
|
|
Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),
|
|
}
|
|
}
|
|
|
|
Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),
|
|
}
|
|
}
|
|
|
|
fn unload_asset(
|
|
source_asset: AssetId<Self::SourceAsset>,
|
|
(
|
|
_,
|
|
_,
|
|
_,
|
|
ref mut bind_group_allocator,
|
|
ref mut render_material_bindings,
|
|
..,
|
|
): &mut SystemParamItem<Self::Param>,
|
|
) {
|
|
let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped())
|
|
else {
|
|
return;
|
|
};
|
|
bind_group_allocator.free(material_binding_id);
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)]
|
|
pub struct MaterialBindGroupId(pub Option<BindGroupId>);
|
|
|
|
impl MaterialBindGroupId {
|
|
pub fn new(id: BindGroupId) -> Self {
|
|
Self(Some(id))
|
|
}
|
|
}
|
|
|
|
impl From<BindGroup> for MaterialBindGroupId {
|
|
fn from(value: BindGroup) -> Self {
|
|
Self::new(value.id())
|
|
}
|
|
}
|
|
|
|
/// A system that creates and/or recreates any bind groups that contain
|
|
/// materials that were modified this frame.
|
|
pub fn prepare_material_bind_groups<M>(
|
|
mut allocator: ResMut<MaterialBindGroupAllocator<M>>,
|
|
render_device: Res<RenderDevice>,
|
|
fallback_image: Res<FallbackImage>,
|
|
fallback_resources: Res<FallbackBindlessResources>,
|
|
) where
|
|
M: Material,
|
|
{
|
|
allocator.prepare_bind_groups(&render_device, &fallback_image, &fallback_resources);
|
|
}
|