Skip to content

Commit

Permalink
Migrate meshes and materials to required components (#15524)
Browse files Browse the repository at this point in the history
# Objective

A big step in the migration to required components: meshes and
materials!

## Solution

As per the [selected
proposal](https://hackmd.io/@bevy/required_components/%2Fj9-PnF-2QKK0on1KQ29UWQ):

- Deprecate `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle`.
- Add `Mesh2d` and `Mesh3d` components, which wrap a `Handle<Mesh>`.
- Add `MeshMaterial2d<M: Material2d>` and `MeshMaterial3d<M: Material>`,
which wrap a `Handle<M>`.
- Meshes *without* a mesh material should be rendered with a default
material. The existence of a material is determined by
`HasMaterial2d`/`HasMaterial3d`, which is required by
`MeshMaterial2d`/`MeshMaterial3d`. This gets around problems with the
generics.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, previously nothing was rendered. Now,
it renders a white default `ColorMaterial` in 2D and a
`StandardMaterial` in 3D (this can be overridden). Below, only every
other entity has a material:

![Näyttökuva 2024-09-29
181746](https://github.com/user-attachments/assets/5c8be029-d2fe-4b8c-ae89-17a72ff82c9a)

![Näyttökuva 2024-09-29
181918](https://github.com/user-attachments/assets/58adbc55-5a1e-4c7d-a2c7-ed456227b909)

Why white? This is still open for discussion, but I think white makes
sense for a *default* material, while *invalid* asset handles pointing
to nothing should have something like a pink material to indicate that
something is broken (I don't handle that in this PR yet). This is kind
of a mix of Godot and Unity: Godot just renders a white material for
non-existent materials, while Unity renders nothing when no materials
exist, but renders pink for invalid materials. I can also change the
default material to pink if that is preferable though.

## Testing

I ran some 2D and 3D examples to test if anything changed visually. I
have not tested all examples or features yet however. If anyone wants to
test more extensively, it would be appreciated!

## Implementation Notes

- The relationship between `bevy_render` and `bevy_pbr` is weird here.
`bevy_render` needs `Mesh3d` for its own systems, but `bevy_pbr` has all
of the material logic, and `bevy_render` doesn't depend on it. I feel
like the two crates should be refactored in some way, but I think that's
out of scope for this PR.
- I didn't migrate meshlets to required components yet. That can
probably be done in a follow-up, as this is already a huge PR.
- It is becoming increasingly clear to me that we really, *really* want
to disallow raw asset handles as components. They caused me a *ton* of
headache here already, and it took me a long time to find every place
that queried for them or inserted them directly on entities, since there
were no compiler errors for it. If we don't remove the `Component`
derive, I expect raw asset handles to be a *huge* footgun for users as
we transition to wrapper components, especially as handles as components
have been the norm so far. I personally consider this to be a blocker
for 0.15: we need to migrate to wrapper components for asset handles
everywhere, and remove the `Component` derive. Also see
#14124.

---

## Migration Guide

Asset handles for meshes and mesh materials must now be wrapped in the
`Mesh2d` and `MeshMaterial2d` or `Mesh3d` and `MeshMaterial3d`
components for 2D and 3D respectively. Raw handles as components no
longer render meshes.

Additionally, `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle` have been deprecated. Instead, use the mesh and material
components directly.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, a white default material is now used.
Previously, nothing was rendered if the material was missing.

The `WithMesh2d` and `WithMesh3d` query filter type aliases have also
been removed. Simply use `With<Mesh2d>` or `With<Mesh3d>`.

---------

Co-authored-by: Tim Blackbird <justthecooldude@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent 0fe17b8 commit 54006b1
Show file tree
Hide file tree
Showing 144 changed files with 2,044 additions and 2,088 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,6 @@ jobs:
echo -e "$errors"
echo " Avoid importing internal Bevy crates, they should not be used directly"
echo " Fix the issue by replacing 'bevy_*' with 'bevy'"
echo " Example: 'use bevy::sprite::MaterialMesh2dBundle;' instead of 'bevy_internal::sprite::MaterialMesh2dBundle;'"
echo " Example: 'use bevy::sprite::Mesh2d;' instead of 'bevy_internal::sprite::Mesh2d;'"
exit 1
fi
16 changes: 9 additions & 7 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ use bevy_ecs::{
use bevy_hierarchy::{BuildChildren, ChildBuild, WorldChildBuilder};
use bevy_math::{Affine2, Mat4, Vec3};
use bevy_pbr::{
DirectionalLight, PbrBundle, PointLight, SpotLight, StandardMaterial, UvChannel, MAX_JOINTS,
DirectionalLight, MeshMaterial3d, PointLight, SpotLight, StandardMaterial, UvChannel,
MAX_JOINTS,
};
use bevy_render::{
alpha::AlphaMode,
camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode},
mesh::{
morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights},
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
Indices, Mesh, Mesh3d, MeshVertexAttribute, VertexAttributeValues,
},
prelude::SpatialBundle,
primitives::Aabb,
Expand Down Expand Up @@ -1456,12 +1457,13 @@ fn load_node(
};
let bounds = primitive.bounding_box();

let mut mesh_entity = parent.spawn(PbrBundle {
let mut mesh_entity = parent.spawn((
// TODO: handle missing label handle errors here?
mesh: load_context.get_label_handle(primitive_label.to_string()),
material: load_context.get_label_handle(&material_label),
..Default::default()
});
Mesh3d(load_context.get_label_handle(primitive_label.to_string())),
MeshMaterial3d::<StandardMaterial>(
load_context.get_label_handle(&material_label),
),
));

let target_count = primitive.morph_targets().len();
if target_count != 0 {
Expand Down
23 changes: 15 additions & 8 deletions crates/bevy_pbr/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#![expect(deprecated)]

use crate::{
CascadeShadowConfig, Cascades, DirectionalLight, Material, PointLight, SpotLight,
StandardMaterial,
CascadeShadowConfig, Cascades, DirectionalLight, Material, MeshMaterial3d, PointLight,
SpotLight, StandardMaterial,
};
use bevy_asset::Handle;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
bundle::Bundle,
Expand All @@ -14,21 +13,29 @@ use bevy_ecs::{
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
mesh::Mesh,
mesh::Mesh3d,
primitives::{CascadesFrusta, CubemapFrusta, Frustum},
view::{InheritedVisibility, ViewVisibility, Visibility},
world_sync::SyncToRenderWorld,
};
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 [`Mesh3d`] and a [`MeshMaterial3d<StandardMaterial>`].
#[deprecated(
since = "0.15.0",
note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically."
)]
pub type PbrBundle = MaterialMeshBundle<StandardMaterial>;

/// A component bundle for entities with a [`Mesh`] and a [`Material`].
/// A component bundle for entities with a [`Mesh3d`] and a [`MeshMaterial3d`].
#[derive(Bundle, Clone)]
#[deprecated(
since = "0.15.0",
note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically."
)]
pub struct MaterialMeshBundle<M: Material> {
pub mesh: Handle<Mesh>,
pub material: Handle<M>,
pub mesh: Mesh3d,
pub material: MeshMaterial3d<M>,
pub transform: Transform,
pub global_transform: GlobalTransform,
/// User indication of whether an entity is visible
Expand Down
16 changes: 13 additions & 3 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod light;
mod light_probe;
mod lightmap;
mod material;
mod mesh_material;
mod parallax;
mod pbr_material;
mod prepass;
Expand All @@ -53,6 +54,7 @@ pub use light::*;
pub use light_probe::*;
pub use lightmap::*;
pub use material::*;
pub use mesh_material::*;
pub use parallax::*;
pub use pbr_material::*;
pub use prepass::*;
Expand Down Expand Up @@ -83,6 +85,7 @@ pub mod prelude {
LightProbe,
},
material::{Material, MaterialPlugin},
mesh_material::MeshMaterial3d,
parallax::ParallaxMappingMethod,
pbr_material::StandardMaterial,
ssao::ScreenSpaceAmbientOcclusionPlugin,
Expand Down Expand Up @@ -411,13 +414,13 @@ impl Plugin for PbrPlugin {
app.add_plugins(DeferredPbrLightingPlugin);
}

// Initialize the default material.
app.world_mut()
.resource_mut::<Assets<StandardMaterial>>()
.insert(
&Handle::<StandardMaterial>::default(),
StandardMaterial {
base_color: Color::srgb(1.0, 0.0, 0.5),
unlit: true,
base_color: Color::WHITE,
..Default::default()
},
);
Expand All @@ -428,7 +431,14 @@ impl Plugin for PbrPlugin {

// Extract the required data from the main world
render_app
.add_systems(ExtractSchedule, (extract_clusters, extract_lights))
.add_systems(
ExtractSchedule,
(
extract_clusters,
extract_lights,
extract_default_materials.after(clear_material_instances::<StandardMaterial>),
),
)
.add_systems(
Render,
(
Expand Down
12 changes: 6 additions & 6 deletions crates/bevy_pbr/src/light/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bevy_render::{
camera::{Camera, CameraProjection},
extract_component::ExtractComponent,
extract_resource::ExtractResource,
mesh::Mesh,
mesh::Mesh3d,
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
view::{
InheritedVisibility, NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange,
Expand Down Expand Up @@ -440,19 +440,19 @@ fn calculate_cascade(
texel_size: cascade_texel_size,
}
}
/// Add this component to make a [`Mesh`] not cast shadows.
/// Add this component to make a [`Mesh3d`] not cast shadows.
#[derive(Debug, Component, Reflect, Default)]
#[reflect(Component, Default, Debug)]
pub struct NotShadowCaster;
/// Add this component to make a [`Mesh`] not receive shadows.
/// Add this component to make a [`Mesh3d`] not receive shadows.
///
/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will
/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled,
/// even when [`TransmittedShadowReceiver`] is being used.
#[derive(Debug, Component, Reflect, Default)]
#[reflect(Component, Default, Debug)]
pub struct NotShadowReceiver;
/// Add this component to make a [`Mesh`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0`
/// Add this component to make a [`Mesh3d`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0`
/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”)
///
/// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness)
Expand Down Expand Up @@ -697,7 +697,7 @@ pub fn check_dir_light_mesh_visibility(
(
Without<NotShadowCaster>,
Without<DirectionalLight>,
With<Handle<Mesh>>,
With<Mesh3d>,
),
>,
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
Expand Down Expand Up @@ -866,7 +866,7 @@ pub fn check_point_light_mesh_visibility(
(
Without<NotShadowCaster>,
Without<DirectionalLight>,
With<Handle<Mesh>>,
With<Mesh3d>,
),
>,
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
Expand Down
18 changes: 10 additions & 8 deletions crates/bevy_pbr/src/lightmap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`]
//! project support other lightmap baking methods.
//!
//! When a [`Lightmap`] component is added to an entity with a [`Mesh`] and a
//! [`StandardMaterial`](crate::StandardMaterial), Bevy applies the lightmap when rendering. The brightness
//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a
//! [`MeshMaterial3d<StandardMaterial>`], Bevy applies the lightmap when rendering. The brightness
//! of the lightmap may be controlled with the `lightmap_exposure` field on
//! `StandardMaterial`.
//! [`StandardMaterial`].
//!
//! During the rendering extraction phase, we extract all lightmaps into the
//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup
Expand All @@ -25,7 +25,9 @@
//! appropriately.
//!
//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper
//!
//! [`Mesh3d`]: bevy_render::mesh::Mesh3d
//! [`MeshMaterial3d<StandardMaterial>`]: crate::StandardMaterial
//! [`StandardMaterial`]: crate::StandardMaterial
//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi

use bevy_app::{App, Plugin};
Expand Down Expand Up @@ -61,10 +63,10 @@ pub struct LightmapPlugin;
/// A component that applies baked indirect diffuse global illumination from a
/// lightmap.
///
/// When assigned to an entity that contains a [`Mesh`] and a
/// [`StandardMaterial`](crate::StandardMaterial), if the mesh has a second UV
/// layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)), then
/// the lightmap will render using those UVs.
/// When assigned to an entity that contains a [`Mesh3d`](bevy_render::mesh::Mesh3d) and a
/// [`MeshMaterial3d<StandardMaterial>`](crate::StandardMaterial), if the mesh
/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)),
/// then the lightmap will render using those UVs.
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct Lightmap {
Expand Down
85 changes: 64 additions & 21 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ use bevy_reflect::std_traits::ReflectDefault;
use bevy_reflect::Reflect;
use bevy_render::{
camera::TemporalJitter,
extract_instances::{ExtractInstancesPlugin, ExtractedInstances},
extract_instances::ExtractedInstances,
extract_resource::ExtractResource,
mesh::{MeshVertexBufferLayoutRef, RenderMesh},
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*,
render_resource::*,
renderer::RenderDevice,
view::{ExtractedView, Msaa, RenderVisibilityRanges, VisibleEntities, WithMesh},
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility, VisibleEntities},
Extract,
};
use bevy_utils::tracing::error;
use core::{
Expand All @@ -43,26 +44,28 @@ use core::{

use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight};

/// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`]
/// 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 [`Mesh`](bevy_render::mesh::Mesh) entities with custom shader logic.
/// 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,
/// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
/// check out the [`AsBindGroup`] documentation.
///
/// ```
/// # use bevy_pbr::{Material, MaterialMeshBundle};
/// # use bevy_pbr::{Material, MeshMaterial3d};
/// # use bevy_ecs::prelude::*;
/// # use bevy_reflect::TypePath;
/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image};
/// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}, texture::Image};
/// # 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
Expand All @@ -84,17 +87,23 @@ use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight};
/// }
/// }
///
/// // Spawn an entity using `CustomMaterial`.
/// fn setup(mut commands: Commands, mut materials: ResMut<Assets<CustomMaterial>>, asset_server: Res<AssetServer>) {
/// commands.spawn(MaterialMeshBundle {
/// material: materials.add(CustomMaterial {
/// // 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"),
/// }),
/// ..Default::default()
/// });
/// })),
/// ));
/// }
/// ```
///
/// In WGSL shaders, the material's binding would look like this:
///
/// ```wgsl
Expand Down Expand Up @@ -258,20 +267,25 @@ where
M::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) {
app.init_asset::<M>().add_plugins((
ExtractInstancesPlugin::<AssetId<M>>::extract_visible(),
RenderAssetPlugin::<PreparedMaterial<M>>::default(),
));
app.init_asset::<M>()
.register_type::<MeshMaterial3d<M>>()
.register_type::<HasMaterial3d>()
.add_plugins(RenderAssetPlugin::<PreparedMaterial<M>>::default());

if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<DrawFunctions<Shadow>>()
.init_resource::<ExtractedInstances<AssetId<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,
(clear_material_instances::<M>, extract_mesh_materials::<M>).chain(),
)
.add_systems(
Render,
queue_material_meshes::<M>
Expand Down Expand Up @@ -527,6 +541,35 @@ pub const fn screen_space_specular_transmission_pipeline_key(
}
}

pub(super) fn clear_material_instances<M: Material>(
mut material_instances: ResMut<RenderMaterialInstances<M>>,
) {
material_instances.clear();
}

fn extract_mesh_materials<M: Material>(
mut material_instances: ResMut<RenderMaterialInstances<M>>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMaterial3d<M>), With<Mesh3d>>>,
) {
for (entity, view_visibility, material) in &query {
if view_visibility.get() {
material_instances.insert(entity, material.id());
}
}
}

/// Extracts default materials for 3D meshes with no [`MeshMaterial3d`].
pub(super) fn extract_default_materials(
mut material_instances: ResMut<RenderMaterialInstances<StandardMaterial>>,
query: Extract<Query<(Entity, &ViewVisibility), (With<Mesh3d>, Without<HasMaterial3d>)>>,
) {
for (entity, view_visibility) in &query {
if view_visibility.get() {
material_instances.insert(entity, AssetId::default());
}
}
}

/// For each view, iterates over all the meshes visible from that view and adds
/// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate.
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -686,7 +729,7 @@ pub fn queue_material_meshes<M: Material>(
}

let rangefinder = view.rangefinder3d();
for visible_entity in visible_entities.iter::<WithMesh>() {
for visible_entity in visible_entities.iter::<With<Mesh3d>>() {
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
continue;
};
Expand Down
Loading

0 comments on commit 54006b1

Please sign in to comment.