bevy/crates/bevy_sprite/src/mesh2d/material.rs
Patrick Walton 9da0b2a0ec
Make render phases render world resources instead of components. (#13277)
This commit makes us stop using the render world ECS for
`BinnedRenderPhase` and `SortedRenderPhase` and instead use resources
with `EntityHashMap`s inside. There are three reasons to do this:

1. We can use `clear()` to clear out the render phase collections
instead of recreating the components from scratch, allowing us to reuse
allocations.

2. This is a prerequisite for retained bins, because components can't be
retained from frame to frame in the render world, but resources can.

3. We want to move away from storing anything in components in the
render world ECS, and this is a step in that direction.

This patch results in a small performance benefit, due to point (1)
above.

## Changelog

### Changed

* The `BinnedRenderPhase` and `SortedRenderPhase` render world
components have been replaced with `ViewBinnedRenderPhases` and
`ViewSortedRenderPhases` resources.

## Migration Guide

* The `BinnedRenderPhase` and `SortedRenderPhase` render world
components have been replaced with `ViewBinnedRenderPhases` and
`ViewSortedRenderPhases` resources. Instead of querying for the
components, look the camera entity up in the
`ViewBinnedRenderPhases`/`ViewSortedRenderPhases` tables.
2024-05-21 18:23:04 +00:00

542 lines
19 KiB
Rust

use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle};
use bevy_core_pipeline::{
core_2d::Transparent2d,
tonemapping::{DebandDither, Tonemapping},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::entity::EntityHashMap;
use bevy_ecs::{
prelude::*,
system::{lifetimeless::SRes, SystemParamItem},
};
use bevy_math::FloatOrd;
use bevy_render::{
mesh::{GpuMesh, MeshVertexBufferLayoutRef},
render_asset::{
prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
},
render_phase::{
AddRenderCommand, DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand,
RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases,
},
render_resource::{
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout,
OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef,
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
},
renderer::RenderDevice,
texture::{FallbackImage, GpuImage},
view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibleEntities},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::{GlobalTransform, Transform};
use bevy_utils::tracing::error;
use std::hash::Hash;
use std::marker::PhantomData;
use crate::{
DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances,
SetMesh2dBindGroup, SetMesh2dViewBindGroup, WithMesh2d,
};
/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`]
/// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level
/// way to render [`Mesh2dHandle`] entities with custom shader logic.
///
/// Material2ds 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 Material2d implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
/// check out the [`AsBindGroup`] documentation.
/// ```
/// # use bevy_sprite::{Material2d, MaterialMesh2dBundle};
/// # use bevy_ecs::prelude::*;
/// # use bevy_reflect::TypePath;
/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image};
/// # use bevy_color::LinearRgba;
/// # use bevy_asset::{Handle, AssetServer, Assets, Asset};
///
/// #[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 `Material2d` have default impls. You only need to implement the
/// // functions that are relevant for your material.
/// impl Material2d 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(MaterialMesh2dBundle {
/// material: materials.add(CustomMaterial {
/// color: LinearRgba::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(2) @binding(0) var<uniform> material: CustomMaterial;
/// @group(2) @binding(1) var color_texture: texture_2d<f32>;
/// @group(2) @binding(2) var color_sampler: sampler;
/// ```
pub trait Material2d: AsBindGroup + Asset + 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
}
/// Add a bias to the view depth of the mesh which can be used to force a specific render order.
#[inline]
fn depth_bias(&self) -> f32 {
0.0
}
/// Customizes the default [`RenderPipelineDescriptor`].
#[allow(unused_variables)]
#[inline]
fn specialize(
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: Material2dKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
Ok(())
}
}
/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`]
/// asset type (which includes [`Material2d`] types).
pub struct Material2dPlugin<M: Material2d>(PhantomData<M>);
impl<M: Material2d> Default for Material2dPlugin<M> {
fn default() -> Self {
Self(Default::default())
}
}
impl<M: Material2d> Plugin for Material2dPlugin<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) {
app.init_asset::<M>()
.add_plugins(RenderAssetPlugin::<PreparedMaterial2d<M>>::default());
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<Transparent2d, DrawMaterial2d<M>>()
.init_resource::<RenderMaterial2dInstances<M>>()
.init_resource::<SpecializedMeshPipelines<Material2dPipeline<M>>>()
.add_systems(ExtractSchedule, extract_material_meshes_2d::<M>)
.add_systems(
Render,
queue_material2d_meshes::<M>
.in_set(RenderSet::QueueMeshes)
.after(prepare_assets::<PreparedMaterial2d<M>>),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<Material2dPipeline<M>>();
}
}
}
#[derive(Resource, Deref, DerefMut)]
pub struct RenderMaterial2dInstances<M: Material2d>(EntityHashMap<AssetId<M>>);
impl<M: Material2d> Default for RenderMaterial2dInstances<M> {
fn default() -> Self {
Self(Default::default())
}
}
fn extract_material_meshes_2d<M: Material2d>(
mut material_instances: ResMut<RenderMaterial2dInstances<M>>,
query: Extract<Query<(Entity, &ViewVisibility, &Handle<M>)>>,
) {
material_instances.clear();
for (entity, view_visibility, handle) in &query {
if view_visibility.get() {
material_instances.insert(entity, handle.id());
}
}
}
/// Render pipeline data for a given [`Material2d`]
#[derive(Resource)]
pub struct Material2dPipeline<M: Material2d> {
pub mesh2d_pipeline: Mesh2dPipeline,
pub material2d_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
marker: PhantomData<M>,
}
pub struct Material2dKey<M: Material2d> {
pub mesh_key: Mesh2dPipelineKey,
pub bind_group_data: M::Data,
}
impl<M: Material2d> Eq for Material2dKey<M> where M::Data: PartialEq {}
impl<M: Material2d> PartialEq for Material2dKey<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: Material2d> Clone for Material2dKey<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: Material2d> Hash for Material2dKey<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);
}
}
impl<M: Material2d> Clone for Material2dPipeline<M> {
fn clone(&self) -> Self {
Self {
mesh2d_pipeline: self.mesh2d_pipeline.clone(),
material2d_layout: self.material2d_layout.clone(),
vertex_shader: self.vertex_shader.clone(),
fragment_shader: self.fragment_shader.clone(),
marker: PhantomData,
}
}
}
impl<M: Material2d> SpecializedMeshPipeline for Material2dPipeline<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
type Key = Material2dKey<M>;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh2d_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 = vec![
self.mesh2d_pipeline.view_layout.clone(),
self.mesh2d_pipeline.mesh_layout.clone(),
self.material2d_layout.clone(),
];
M::specialize(&mut descriptor, layout, key)?;
Ok(descriptor)
}
}
impl<M: Material2d> FromWorld for Material2dPipeline<M> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
let render_device = world.resource::<RenderDevice>();
let material2d_layout = M::bind_group_layout(render_device);
Material2dPipeline {
mesh2d_pipeline: world.resource::<Mesh2dPipeline>().clone(),
material2d_layout,
vertex_shader: match M::vertex_shader() {
ShaderRef::Default => None,
ShaderRef::Handle(handle) => Some(handle),
ShaderRef::Path(path) => Some(asset_server.load(path)),
},
fragment_shader: match M::fragment_shader() {
ShaderRef::Default => None,
ShaderRef::Handle(handle) => Some(handle),
ShaderRef::Path(path) => Some(asset_server.load(path)),
},
marker: PhantomData,
}
}
}
type DrawMaterial2d<M> = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMesh2dBindGroup<1>,
SetMaterial2dBindGroup<M, 2>,
DrawMesh2d,
);
pub struct SetMaterial2dBindGroup<M: Material2d, const I: usize>(PhantomData<M>);
impl<P: PhaseItem, M: Material2d, const I: usize> RenderCommand<P>
for SetMaterial2dBindGroup<M, I>
{
type Param = (
SRes<RenderAssets<PreparedMaterial2d<M>>>,
SRes<RenderMaterial2dInstances<M>>,
);
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
(materials, material_instances): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let materials = materials.into_inner();
let material_instances = material_instances.into_inner();
let Some(material_instance) = material_instances.get(&item.entity()) else {
return RenderCommandResult::Failure;
};
let Some(material2d) = materials.get(*material_instance) else {
return RenderCommandResult::Failure;
};
pass.set_bind_group(I, &material2d.bind_group, &[]);
RenderCommandResult::Success
}
}
pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey {
match tonemapping {
Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE,
Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD,
Tonemapping::ReinhardLuminance => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE,
Tonemapping::AcesFitted => Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED,
Tonemapping::AgX => Mesh2dPipelineKey::TONEMAP_METHOD_AGX,
Tonemapping::SomewhatBoringDisplayTransform => {
Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
}
Tonemapping::TonyMcMapface => Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE,
Tonemapping::BlenderFilmic => Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC,
}
}
#[allow(clippy::too_many_arguments)]
pub fn queue_material2d_meshes<M: Material2d>(
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
material2d_pipeline: Res<Material2dPipeline<M>>,
mut pipelines: ResMut<SpecializedMeshPipelines<Material2dPipeline<M>>>,
pipeline_cache: Res<PipelineCache>,
msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<GpuMesh>>,
render_materials: Res<RenderAssets<PreparedMaterial2d<M>>>,
mut render_mesh_instances: ResMut<RenderMesh2dInstances>,
render_material_instances: Res<RenderMaterial2dInstances<M>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut views: Query<(
Entity,
&ExtractedView,
&VisibleEntities,
Option<&Tonemapping>,
Option<&DebandDither>,
)>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
{
if render_material_instances.is_empty() {
return;
}
for (view_entity, view, visible_entities, tonemapping, dither) in &mut views {
let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else {
continue;
};
let draw_transparent_2d = transparent_draw_functions.read().id::<DrawMaterial2d<M>>();
let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr);
if !view.hdr {
if let Some(tonemapping) = tonemapping {
view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER;
view_key |= tonemapping_pipeline_key(*tonemapping);
}
if let Some(DebandDither::Enabled) = dither {
view_key |= Mesh2dPipelineKey::DEBAND_DITHER;
}
}
for visible_entity in visible_entities.iter::<WithMesh2d>() {
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
continue;
};
let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else {
continue;
};
let Some(material_2d) = render_materials.get(*material_asset_id) else {
continue;
};
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
let mesh_key =
view_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology());
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&material2d_pipeline,
Material2dKey {
mesh_key,
bind_group_data: material_2d.key.clone(),
},
&mesh.layout,
);
let pipeline_id = match pipeline_id {
Ok(id) => id,
Err(err) => {
error!("{}", err);
continue;
}
};
mesh_instance.material_bind_group_id = material_2d.get_bind_group_id();
let mesh_z = mesh_instance.transforms.transform.translation.z;
transparent_phase.add(Transparent2d {
entity: *visible_entity,
draw_function: draw_transparent_2d,
pipeline: pipeline_id,
// NOTE: Back-to-front ordering for transparent with ascending sort means far should have the
// lowest sort key and getting closer should increase. As we have
// -z in front of the camera, the largest distance is -far with values increasing toward the
// camera. As such we can just use mesh_z as the distance
sort_key: FloatOrd(mesh_z + material_2d.depth_bias),
// Batching is done in batch_and_prepare_render_phase
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::NONE,
});
}
}
}
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)]
pub struct Material2dBindGroupId(Option<BindGroupId>);
/// Data prepared for a [`Material2d`] instance.
pub struct PreparedMaterial2d<T: Material2d> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub key: T::Data,
pub depth_bias: f32,
}
impl<T: Material2d> PreparedMaterial2d<T> {
pub fn get_bind_group_id(&self) -> Material2dBindGroupId {
Material2dBindGroupId(Some(self.bind_group.id()))
}
}
impl<M: Material2d> RenderAsset for PreparedMaterial2d<M> {
type SourceAsset = M;
type Param = (
SRes<RenderDevice>,
SRes<RenderAssets<GpuImage>>,
SRes<FallbackImage>,
SRes<Material2dPipeline<M>>,
);
fn prepare_asset(
material: Self::SourceAsset,
(render_device, images, fallback_image, pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(
&pipeline.material2d_layout,
render_device,
images,
fallback_image,
) {
Ok(prepared) => Ok(PreparedMaterial2d {
bindings: prepared.bindings,
bind_group: prepared.bind_group,
key: prepared.data,
depth_bias: material.depth_bias(),
}),
Err(AsBindGroupError::RetryNextUpdate) => {
Err(PrepareAssetError::RetryNextUpdate(material))
}
}
}
}
/// A component bundle for entities with a [`Mesh2dHandle`] and a [`Material2d`].
#[derive(Bundle, Clone)]
pub struct MaterialMesh2dBundle<M: Material2d> {
pub mesh: Mesh2dHandle,
pub material: Handle<M>,
pub transform: Transform,
pub global_transform: GlobalTransform,
/// User indication of whether an entity is visible
pub visibility: Visibility,
// Inherited visibility of an entity.
pub inherited_visibility: InheritedVisibility,
// Indication of whether an entity is visible in any view.
pub view_visibility: ViewVisibility,
}
impl<M: Material2d> Default for MaterialMesh2dBundle<M> {
fn default() -> Self {
Self {
mesh: Default::default(),
material: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
visibility: Default::default(),
inherited_visibility: Default::default(),
view_visibility: Default::default(),
}
}
}