mirror of
https://github.com/bevyengine/bevy
synced 2025-01-02 00:08:53 +00:00
54006b107b
# 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 https://github.com/bevyengine/bevy/issues/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>
397 lines
13 KiB
Rust
397 lines
13 KiB
Rust
// FIXME(15321): solve CI failures, then replace with `#![expect()]`.
|
|
#![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
#![forbid(unsafe_code)]
|
|
#![doc(
|
|
html_logo_url = "https://bevyengine.org/assets/icon.png",
|
|
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
|
)]
|
|
|
|
//! Provides 2D sprite rendering functionality.
|
|
|
|
extern crate alloc;
|
|
|
|
mod bundle;
|
|
mod dynamic_texture_atlas_builder;
|
|
mod mesh2d;
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
mod picking_backend;
|
|
mod render;
|
|
mod sprite;
|
|
mod texture_atlas;
|
|
mod texture_atlas_builder;
|
|
mod texture_slice;
|
|
|
|
/// The sprite prelude.
|
|
///
|
|
/// This includes the most common types in this crate, re-exported for your convenience.
|
|
#[expect(deprecated)]
|
|
pub mod prelude {
|
|
#[doc(hidden)]
|
|
pub use crate::{
|
|
bundle::SpriteBundle,
|
|
sprite::{ImageScaleMode, Sprite},
|
|
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
|
|
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
|
|
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
|
|
};
|
|
}
|
|
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
pub use bundle::*;
|
|
pub use dynamic_texture_atlas_builder::*;
|
|
pub use mesh2d::*;
|
|
pub use render::*;
|
|
pub use sprite::*;
|
|
pub use texture_atlas::*;
|
|
pub use texture_atlas_builder::*;
|
|
pub use texture_slice::*;
|
|
|
|
use bevy_app::prelude::*;
|
|
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
|
|
use bevy_core_pipeline::core_2d::Transparent2d;
|
|
use bevy_ecs::{prelude::*, query::QueryItem};
|
|
use bevy_render::{
|
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
|
mesh::{Mesh, Mesh2d},
|
|
primitives::Aabb,
|
|
render_phase::AddRenderCommand,
|
|
render_resource::{Shader, SpecializedRenderPipelines},
|
|
texture::Image,
|
|
view::{check_visibility, NoFrustumCulling, VisibilitySystems},
|
|
ExtractSchedule, Render, RenderApp, RenderSet,
|
|
};
|
|
|
|
/// Adds support for 2D sprite rendering.
|
|
#[derive(Default)]
|
|
pub struct SpritePlugin;
|
|
|
|
pub const SPRITE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(2763343953151597127);
|
|
pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle<Shader> =
|
|
Handle::weak_from_u128(8846920112458963210);
|
|
|
|
/// System set for sprite rendering.
|
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
|
pub enum SpriteSystem {
|
|
ExtractSprites,
|
|
ComputeSlices,
|
|
}
|
|
|
|
/// A component that marks entities that aren't themselves sprites but become
|
|
/// sprites during rendering.
|
|
///
|
|
/// Right now, this is used for `Text`.
|
|
#[derive(Component, Reflect, Clone, Copy, Debug, Default)]
|
|
#[reflect(Component, Default, Debug)]
|
|
pub struct SpriteSource;
|
|
|
|
/// A convenient alias for `Or<With<Sprite>, With<SpriteSource>>`, for use with
|
|
/// [`bevy_render::view::VisibleEntities`].
|
|
pub type WithSprite = Or<(With<Sprite>, With<SpriteSource>)>;
|
|
|
|
impl Plugin for SpritePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
load_internal_asset!(
|
|
app,
|
|
SPRITE_SHADER_HANDLE,
|
|
"render/sprite.wgsl",
|
|
Shader::from_wgsl
|
|
);
|
|
load_internal_asset!(
|
|
app,
|
|
SPRITE_VIEW_BINDINGS_SHADER_HANDLE,
|
|
"render/sprite_view_bindings.wgsl",
|
|
Shader::from_wgsl
|
|
);
|
|
app.init_asset::<TextureAtlasLayout>()
|
|
.register_asset_reflect::<TextureAtlasLayout>()
|
|
.register_type::<Sprite>()
|
|
.register_type::<ImageScaleMode>()
|
|
.register_type::<TextureSlicer>()
|
|
.register_type::<Anchor>()
|
|
.register_type::<TextureAtlas>()
|
|
.register_type::<Mesh2d>()
|
|
.register_type::<SpriteSource>()
|
|
.add_plugins((
|
|
Mesh2dRenderPlugin,
|
|
ColorMaterialPlugin,
|
|
ExtractComponentPlugin::<SpriteSource>::default(),
|
|
))
|
|
.add_systems(
|
|
PostUpdate,
|
|
(
|
|
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
|
|
(
|
|
compute_slices_on_asset_event,
|
|
compute_slices_on_sprite_change,
|
|
)
|
|
.in_set(SpriteSystem::ComputeSlices),
|
|
(
|
|
check_visibility::<With<Mesh2d>>,
|
|
check_visibility::<WithSprite>,
|
|
)
|
|
.in_set(VisibilitySystems::CheckVisibility),
|
|
),
|
|
);
|
|
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
app.add_plugins(picking_backend::SpritePickingBackend);
|
|
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<ImageBindGroups>()
|
|
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
|
|
.init_resource::<SpriteMeta>()
|
|
.init_resource::<ExtractedSprites>()
|
|
.init_resource::<SpriteAssetEvents>()
|
|
.add_render_command::<Transparent2d, DrawSprite>()
|
|
.add_systems(
|
|
ExtractSchedule,
|
|
(
|
|
extract_sprites.in_set(SpriteSystem::ExtractSprites),
|
|
extract_sprite_events,
|
|
),
|
|
)
|
|
.add_systems(
|
|
Render,
|
|
(
|
|
queue_sprites
|
|
.in_set(RenderSet::Queue)
|
|
.ambiguous_with(queue_material2d_meshes::<ColorMaterial>),
|
|
prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups),
|
|
prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
|
|
),
|
|
);
|
|
};
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app.init_resource::<SpritePipeline>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// System calculating and inserting an [`Aabb`] component to entities with either:
|
|
/// - a `Mesh2d` component,
|
|
/// - a `Sprite` and `Handle<Image>` components,
|
|
/// and without a [`NoFrustumCulling`] component.
|
|
///
|
|
/// Used in system set [`VisibilitySystems::CalculateBounds`].
|
|
pub fn calculate_bounds_2d(
|
|
mut commands: Commands,
|
|
meshes: Res<Assets<Mesh>>,
|
|
images: Res<Assets<Image>>,
|
|
atlases: Res<Assets<TextureAtlasLayout>>,
|
|
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
|
sprites_to_recalculate_aabb: Query<
|
|
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
|
|
(
|
|
Or<(Without<Aabb>, Changed<Sprite>, Changed<TextureAtlas>)>,
|
|
Without<NoFrustumCulling>,
|
|
),
|
|
>,
|
|
) {
|
|
for (entity, mesh_handle) in &meshes_without_aabb {
|
|
if let Some(mesh) = meshes.get(&mesh_handle.0) {
|
|
if let Some(aabb) = mesh.compute_aabb() {
|
|
commands.entity(entity).try_insert(aabb);
|
|
}
|
|
}
|
|
}
|
|
for (entity, sprite, texture_handle, atlas) in &sprites_to_recalculate_aabb {
|
|
if let Some(size) = sprite
|
|
.custom_size
|
|
.or_else(|| sprite.rect.map(|rect| rect.size()))
|
|
.or_else(|| match atlas {
|
|
// We default to the texture size for regular sprites
|
|
None => images.get(texture_handle).map(Image::size_f32),
|
|
// We default to the drawn rect for atlas sprites
|
|
Some(atlas) => atlas
|
|
.texture_rect(&atlases)
|
|
.map(|rect| rect.size().as_vec2()),
|
|
})
|
|
{
|
|
let aabb = Aabb {
|
|
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
|
|
half_extents: (0.5 * size).extend(0.0).into(),
|
|
};
|
|
commands.entity(entity).try_insert(aabb);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ExtractComponent for SpriteSource {
|
|
type QueryData = ();
|
|
|
|
type QueryFilter = With<SpriteSource>;
|
|
|
|
type Out = SpriteSource;
|
|
|
|
fn extract_component(_: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
|
|
Some(SpriteSource)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use bevy_math::{Rect, Vec2, Vec3A};
|
|
use bevy_utils::default;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn((Sprite::default(), image_handle))
|
|
.id();
|
|
|
|
// Verify that the entity does not have an AABB
|
|
assert!(!app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.contains::<Aabb>());
|
|
|
|
// Run system
|
|
app.update();
|
|
|
|
// Verify the AABB exists
|
|
assert!(app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.contains::<Aabb>());
|
|
}
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn((
|
|
Sprite {
|
|
custom_size: Some(Vec2::ZERO),
|
|
..default()
|
|
},
|
|
image_handle,
|
|
))
|
|
.id();
|
|
|
|
// Create initial AABB
|
|
app.update();
|
|
|
|
// Get the initial AABB
|
|
let first_aabb = *app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find initial AABB");
|
|
|
|
// Change `custom_size` of sprite
|
|
let mut binding = app
|
|
.world_mut()
|
|
.get_entity_mut(entity)
|
|
.expect("Could not find entity");
|
|
let mut sprite = binding
|
|
.get_mut::<Sprite>()
|
|
.expect("Could not find sprite component of entity");
|
|
sprite.custom_size = Some(Vec2::ONE);
|
|
|
|
// Re-run the `calculate_bounds_2d` system to get the new AABB
|
|
app.update();
|
|
|
|
// Get the re-calculated AABB
|
|
let second_aabb = *app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find second AABB");
|
|
|
|
// Check that the AABBs are not equal
|
|
assert_ne!(first_aabb, second_aabb);
|
|
}
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn((
|
|
Sprite {
|
|
rect: Some(Rect::new(0., 0., 0.5, 1.)),
|
|
anchor: Anchor::TopRight,
|
|
..default()
|
|
},
|
|
image_handle,
|
|
))
|
|
.id();
|
|
|
|
// Create AABB
|
|
app.update();
|
|
|
|
// Get the AABB
|
|
let aabb = *app
|
|
.world_mut()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find AABB");
|
|
|
|
// Verify that the AABB is at the expected position
|
|
assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
|
|
|
|
// Verify that the AABB has the expected size
|
|
assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
|
|
}
|
|
}
|