Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

358
vendor/bevy_sprite/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,358 @@
#![expect(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 mesh2d;
#[cfg(feature = "bevy_sprite_picking_backend")]
mod picking_backend;
mod render;
mod sprite;
mod texture_slice;
/// The sprite prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_sprite_picking_backend")]
#[doc(hidden)]
pub use crate::picking_backend::{
SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings,
};
#[doc(hidden)]
pub use crate::{
sprite::{Sprite, SpriteImageMode},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, MeshMaterial2d, ScalingMode,
};
}
pub use mesh2d::*;
#[cfg(feature = "bevy_sprite_picking_backend")]
pub use picking_backend::*;
pub use render::*;
pub use sprite::*;
pub use texture_slice::*;
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, AssetEvents, Assets, Handle};
use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d};
use bevy_ecs::prelude::*;
use bevy_image::{prelude::*, TextureAtlasPlugin};
use bevy_render::{
batching::sort_binned_render_phase,
mesh::{Mesh, Mesh2d, MeshAabb},
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{Shader, SpecializedRenderPipelines},
view::{NoFrustumCulling, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
/// Adds support for 2D sprite rendering.
#[derive(Default)]
pub struct SpritePlugin;
pub const SPRITE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2");
pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("43947210-8df6-459a-8f2a-12f350d174cc");
/// System set for sprite rendering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SpriteSystem {
ExtractSprites,
ComputeSlices,
}
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
);
if !app.is_plugin_added::<TextureAtlasPlugin>() {
app.add_plugins(TextureAtlasPlugin);
}
app.register_type::<Sprite>()
.register_type::<SpriteImageMode>()
.register_type::<TextureSlicer>()
.register_type::<Anchor>()
.register_type::<Mesh2d>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems(
PostUpdate,
(
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
compute_slices_on_asset_event.before(AssetEvents),
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
),
);
#[cfg(feature = "bevy_sprite_picking_backend")]
app.add_plugins(SpritePickingPlugin);
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::<ExtractedSlices>()
.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),
sort_binned_render_phase::<Opaque2d>.in_set(RenderSet::PhaseSort),
sort_binned_render_phase::<AlphaMask2d>.in_set(RenderSet::PhaseSort),
),
);
};
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<SpriteBatches>()
.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),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
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) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| sprite.rect.map(|rect| rect.size()))
.or_else(|| match &sprite.texture_atlas {
// We default to the texture size for regular sprites
None => images.get(&sprite.image).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);
}
}
}
#[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::from_image(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),
image: image_handle,
..default()
})
.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,
image: image_handle,
..default()
})
.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.));
}
}

View File

@@ -0,0 +1,161 @@
use crate::{AlphaMode2d, Material2d, Material2dPlugin};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle};
use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba};
use bevy_image::Image;
use bevy_math::{Affine2, Mat3, Vec4};
use bevy_reflect::prelude::*;
use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage};
pub const COLOR_MATERIAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("92e0e6e9-ed0b-4db3-89ab-5f65d3678250");
#[derive(Default)]
pub struct ColorMaterialPlugin;
impl Plugin for ColorMaterialPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
COLOR_MATERIAL_SHADER_HANDLE,
"color_material.wgsl",
Shader::from_wgsl
);
app.add_plugins(Material2dPlugin::<ColorMaterial>::default())
.register_asset_reflect::<ColorMaterial>();
// Initialize the default material handle.
app.world_mut()
.resource_mut::<Assets<ColorMaterial>>()
.insert(
&Handle::<ColorMaterial>::default(),
ColorMaterial {
color: Color::srgb(1.0, 0.0, 1.0),
..Default::default()
},
);
}
}
/// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2d) with a texture tinted by a uniform color
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[reflect(Default, Debug, Clone)]
#[uniform(0, ColorMaterialUniform)]
pub struct ColorMaterial {
pub color: Color,
pub alpha_mode: AlphaMode2d,
pub uv_transform: Affine2,
#[texture(1)]
#[sampler(2)]
pub texture: Option<Handle<Image>>,
}
impl ColorMaterial {
/// Creates a new material from a given color
pub fn from_color(color: impl Into<Color>) -> Self {
Self::from(color.into())
}
}
impl Default for ColorMaterial {
fn default() -> Self {
ColorMaterial {
color: Color::WHITE,
uv_transform: Affine2::default(),
texture: None,
// TODO should probably default to AlphaMask once supported?
alpha_mode: AlphaMode2d::Blend,
}
}
}
impl From<Color> for ColorMaterial {
fn from(color: Color) -> Self {
ColorMaterial {
color,
alpha_mode: if color.alpha() < 1.0 {
AlphaMode2d::Blend
} else {
AlphaMode2d::Opaque
},
..Default::default()
}
}
}
impl From<Handle<Image>> for ColorMaterial {
fn from(texture: Handle<Image>) -> Self {
ColorMaterial {
texture: Some(texture),
..Default::default()
}
}
}
// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/color_material.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
pub struct ColorMaterialFlags: u32 {
const TEXTURE = 1 << 0;
/// Bitmask reserving bits for the [`AlphaMode2d`]
/// Values are just sequential values bitshifted into
/// the bitmask, and can range from 0 to 3.
const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS;
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
impl ColorMaterialFlags {
const ALPHA_MODE_MASK_BITS: u32 = 0b11;
const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones();
}
/// The GPU representation of the uniform data of a [`ColorMaterial`].
#[derive(Clone, Default, ShaderType)]
pub struct ColorMaterialUniform {
pub color: Vec4,
pub uv_transform: Mat3,
pub flags: u32,
pub alpha_cutoff: f32,
}
impl AsBindGroupShaderType<ColorMaterialUniform> for ColorMaterial {
fn as_bind_group_shader_type(&self, _images: &RenderAssets<GpuImage>) -> ColorMaterialUniform {
let mut flags = ColorMaterialFlags::NONE;
if self.texture.is_some() {
flags |= ColorMaterialFlags::TEXTURE;
}
// Defaults to 0.5 like in 3d
let mut alpha_cutoff = 0.5;
match self.alpha_mode {
AlphaMode2d::Opaque => flags |= ColorMaterialFlags::ALPHA_MODE_OPAQUE,
AlphaMode2d::Mask(c) => {
alpha_cutoff = c;
flags |= ColorMaterialFlags::ALPHA_MODE_MASK;
}
AlphaMode2d::Blend => flags |= ColorMaterialFlags::ALPHA_MODE_BLEND,
};
ColorMaterialUniform {
color: LinearRgba::from(self.color).to_f32_array().into(),
uv_transform: self.uv_transform.into(),
flags: flags.bits(),
alpha_cutoff,
}
}
}
impl Material2d for ColorMaterial {
fn fragment_shader() -> ShaderRef {
COLOR_MATERIAL_SHADER_HANDLE.into()
}
fn alpha_mode(&self) -> AlphaMode2d {
self.alpha_mode
}
}

View File

@@ -0,0 +1,72 @@
#import bevy_sprite::{
mesh2d_vertex_output::VertexOutput,
mesh2d_view_bindings::view,
}
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
struct ColorMaterial {
color: vec4<f32>,
uv_transform: mat3x3<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
alpha_cutoff: f32,
};
const COLOR_MATERIAL_FLAGS_TEXTURE_BIT: u32 = 1u;
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3221225472u; // (0b11u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1073741824u; // (1u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2147483648u; // (2u32 << 30)
@group(2) @binding(0) var<uniform> material: ColorMaterial;
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@fragment
fn fragment(
mesh: VertexOutput,
) -> @location(0) vec4<f32> {
var output_color: vec4<f32> = material.color;
#ifdef VERTEX_COLORS
output_color = output_color * mesh.color;
#endif
let uv = (material.uv_transform * vec3(mesh.uv, 1.0)).xy;
if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) {
output_color = output_color * textureSample(texture, texture_sampler, uv);
}
output_color = alpha_discard(material, output_color);
#ifdef TONEMAP_IN_SHADER
output_color = tonemapping::tone_mapping(output_color, view.color_grading);
#endif
return output_color;
}
fn alpha_discard(material: ColorMaterial, output_color: vec4<f32>) -> vec4<f32> {
var color = output_color;
let alpha_mode = material.flags & COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
if alpha_mode == COLOR_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
// NOTE: If rendering as opaque, alpha should be ignored so set to 1.0
color.a = 1.0;
}
#ifdef MAY_DISCARD
else if alpha_mode == COLOR_MATERIAL_FLAGS_ALPHA_MODE_MASK {
if color.a >= material.alpha_cutoff {
// NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque
color.a = 1.0;
} else {
// NOTE: output_color.a < in.material.alpha_cutoff should not be rendered
discard;
}
}
#endif // MAY_DISCARD
return color;
}

1004
vendor/bevy_sprite/src/mesh2d/material.rs vendored Normal file

File diff suppressed because it is too large Load Diff

921
vendor/bevy_sprite/src/mesh2d/mesh.rs vendored Normal file
View File

@@ -0,0 +1,921 @@
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
use crate::{tonemapping_pipeline_key, Material2dBindGroupId};
use bevy_core_pipeline::tonemapping::DebandDither;
use bevy_core_pipeline::{
core_2d::{AlphaMask2d, Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT},
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::component::Tick;
use bevy_ecs::system::SystemChangeTick;
use bevy_ecs::{
prelude::*,
query::ROQueryItem,
system::{lifetimeless::*, SystemParamItem, SystemState},
};
use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo};
use bevy_math::{Affine3, Vec4};
use bevy_render::mesh::MeshTag;
use bevy_render::prelude::Msaa;
use bevy_render::RenderSet::PrepareAssets;
use bevy_render::{
batching::{
gpu_preprocessing::IndirectParametersCpuMetadata,
no_gpu_preprocessing::{
self, batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase,
write_batched_instance_buffer, BatchedInstanceBuffer,
},
GetBatchData, GetFullBatchData, NoAutomaticBatching,
},
globals::{GlobalsBuffer, GlobalsUniform},
mesh::{
allocator::MeshAllocator, Mesh, Mesh2d, MeshVertexBufferLayoutRef, RenderMesh,
RenderMeshBufferInfo,
},
render_asset::RenderAssets,
render_phase::{
sweep_old_entities, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
TrackedRenderPass,
},
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::{MainEntity, MainEntityHashMap},
texture::{DefaultImageSampler, FallbackImage, GpuImage},
view::{
ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility,
},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::GlobalTransform;
use nonmax::NonMaxU32;
use tracing::error;
#[derive(Default)]
pub struct Mesh2dRenderPlugin;
pub const MESH2D_VERTEX_OUTPUT: Handle<Shader> =
weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0");
pub const MESH2D_VIEW_TYPES_HANDLE: Handle<Shader> =
weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83");
pub const MESH2D_VIEW_BINDINGS_HANDLE: Handle<Shader> =
weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2");
pub const MESH2D_TYPES_HANDLE: Handle<Shader> =
weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f");
pub const MESH2D_BINDINGS_HANDLE: Handle<Shader> =
weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4");
pub const MESH2D_FUNCTIONS_HANDLE: Handle<Shader> =
weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51");
pub const MESH2D_SHADER_HANDLE: Handle<Shader> =
weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91");
impl Plugin for Mesh2dRenderPlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(
app,
MESH2D_VERTEX_OUTPUT,
"mesh2d_vertex_output.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_VIEW_TYPES_HANDLE,
"mesh2d_view_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_VIEW_BINDINGS_HANDLE,
"mesh2d_view_bindings.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_TYPES_HANDLE,
"mesh2d_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_FUNCTIONS_HANDLE,
"mesh2d_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ViewKeyCache>()
.init_resource::<RenderMesh2dInstances>()
.init_resource::<SpecializedMeshPipelines<Mesh2dPipeline>>()
.add_systems(ExtractSchedule, extract_mesh2d)
.add_systems(
Render,
(
(
sweep_old_entities::<Opaque2d>,
sweep_old_entities::<AlphaMask2d>,
)
.in_set(RenderSet::QueueSweep),
batch_and_prepare_binned_render_phase::<Opaque2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_binned_render_phase::<AlphaMask2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
write_batched_instance_buffer::<Mesh2dPipeline>
.in_set(RenderSet::PrepareResourcesFlush),
prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups),
prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
no_gpu_preprocessing::clear_batched_cpu_instance_buffers::<Mesh2dPipeline>
.in_set(RenderSet::Cleanup)
.after(RenderSet::Render),
),
);
}
}
fn finish(&self, app: &mut bevy_app::App) {
let mut mesh_bindings_shader_defs = Vec::with_capacity(1);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
let render_device = render_app.world().resource::<RenderDevice>();
let batched_instance_buffer =
BatchedInstanceBuffer::<Mesh2dUniform>::new(render_device);
if let Some(per_object_buffer_batch_size) =
GpuArrayBuffer::<Mesh2dUniform>::batch_size(render_device)
{
mesh_bindings_shader_defs.push(ShaderDefVal::UInt(
"PER_OBJECT_BUFFER_BATCH_SIZE".into(),
per_object_buffer_batch_size,
));
}
render_app
.insert_resource(batched_instance_buffer)
.init_resource::<Mesh2dPipeline>()
.init_resource::<ViewKeyCache>()
.init_resource::<ViewSpecializationTicks>()
.add_systems(
Render,
check_views_need_specialization.in_set(PrepareAssets),
);
}
// Load the mesh_bindings shader module here as it depends on runtime information about
// whether storage buffers are supported, or the maximum uniform buffer binding size.
load_internal_asset!(
app,
MESH2D_BINDINGS_HANDLE,
"mesh2d_bindings.wgsl",
Shader::from_wgsl_with_defs,
mesh_bindings_shader_defs
);
}
}
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
pub struct ViewKeyCache(MainEntityHashMap<Mesh2dPipelineKey>);
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
pub struct ViewSpecializationTicks(MainEntityHashMap<Tick>);
pub fn check_views_need_specialization(
mut view_key_cache: ResMut<ViewKeyCache>,
mut view_specialization_ticks: ResMut<ViewSpecializationTicks>,
views: Query<(
&MainEntity,
&ExtractedView,
&Msaa,
Option<&Tonemapping>,
Option<&DebandDither>,
)>,
ticks: SystemChangeTick,
) {
for (view_entity, view, msaa, tonemapping, dither) in &views {
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;
}
}
if !view_key_cache
.get_mut(view_entity)
.is_some_and(|current_key| *current_key == view_key)
{
view_key_cache.insert(*view_entity, view_key);
view_specialization_ticks.insert(*view_entity, ticks.this_run());
}
}
}
#[derive(Component)]
pub struct Mesh2dTransforms {
pub world_from_local: Affine3,
pub flags: u32,
}
#[derive(ShaderType, Clone, Copy)]
pub struct Mesh2dUniform {
// Affine 4x3 matrix transposed to 3x4
pub world_from_local: [Vec4; 3],
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
// [2].z
pub local_from_world_transpose_a: [Vec4; 2],
pub local_from_world_transpose_b: f32,
pub flags: u32,
pub tag: u32,
}
impl Mesh2dUniform {
fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32) -> Self {
let (local_from_world_transpose_a, local_from_world_transpose_b) =
mesh_transforms.world_from_local.inverse_transpose_3x3();
Self {
world_from_local: mesh_transforms.world_from_local.to_transpose(),
local_from_world_transpose_a,
local_from_world_transpose_b,
flags: mesh_transforms.flags,
tag,
}
}
}
// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
pub struct MeshFlags: u32 {
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
pub struct RenderMesh2dInstance {
pub transforms: Mesh2dTransforms,
pub mesh_asset_id: AssetId<Mesh>,
pub material_bind_group_id: Material2dBindGroupId,
pub automatic_batching: bool,
pub tag: u32,
}
#[derive(Default, Resource, Deref, DerefMut)]
pub struct RenderMesh2dInstances(MainEntityHashMap<RenderMesh2dInstance>);
#[derive(Component, Default)]
pub struct Mesh2dMarker;
pub fn extract_mesh2d(
mut render_mesh_instances: ResMut<RenderMesh2dInstances>,
query: Extract<
Query<(
Entity,
&ViewVisibility,
&GlobalTransform,
&Mesh2d,
Option<&MeshTag>,
Has<NoAutomaticBatching>,
)>,
>,
) {
render_mesh_instances.clear();
for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query {
if !view_visibility.get() {
continue;
}
render_mesh_instances.insert(
entity.into(),
RenderMesh2dInstance {
transforms: Mesh2dTransforms {
world_from_local: (&transform.affine()).into(),
flags: MeshFlags::empty().bits(),
},
mesh_asset_id: handle.0.id(),
material_bind_group_id: Material2dBindGroupId::default(),
automatic_batching: !no_automatic_batching,
tag: tag.map_or(0, |i| **i),
},
);
}
}
#[derive(Resource, Clone)]
pub struct Mesh2dPipeline {
pub view_layout: BindGroupLayout,
pub mesh_layout: BindGroupLayout,
// This dummy white texture is to be used in place of optional textures
pub dummy_white_gpu_image: GpuImage,
pub per_object_buffer_batch_size: Option<u32>,
}
impl FromWorld for Mesh2dPipeline {
fn from_world(world: &mut World) -> Self {
let mut system_state: SystemState<(
Res<RenderDevice>,
Res<RenderQueue>,
Res<DefaultImageSampler>,
)> = SystemState::new(world);
let (render_device, render_queue, default_sampler) = system_state.get_mut(world);
let render_device = render_device.into_inner();
let tonemapping_lut_entries = get_lut_bind_group_layout_entries();
let view_layout = render_device.create_bind_group_layout(
"mesh2d_view_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX_FRAGMENT,
(
(0, uniform_buffer::<ViewUniform>(true)),
(1, uniform_buffer::<GlobalsUniform>(false)),
(
2,
tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT),
),
(
3,
tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT),
),
),
),
);
let mesh_layout = render_device.create_bind_group_layout(
"mesh2d_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
GpuArrayBuffer::<Mesh2dUniform>::binding_layout(render_device),
),
);
// A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures
let dummy_white_gpu_image = {
let image = Image::default();
let texture = render_device.create_texture(&image.texture_descriptor);
let sampler = match image.sampler {
ImageSampler::Default => (**default_sampler).clone(),
ImageSampler::Descriptor(ref descriptor) => {
render_device.create_sampler(&descriptor.as_wgpu())
}
};
let format_size = image.texture_descriptor.format.pixel_size();
render_queue.write_texture(
texture.as_image_copy(),
image.data.as_ref().expect("Image has no data"),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(image.width() * format_size as u32),
rows_per_image: None,
},
image.texture_descriptor.size,
);
let texture_view = texture.create_view(&TextureViewDescriptor::default());
GpuImage {
texture,
texture_view,
texture_format: image.texture_descriptor.format,
sampler,
size: image.texture_descriptor.size,
mip_level_count: image.texture_descriptor.mip_level_count,
}
};
Mesh2dPipeline {
view_layout,
mesh_layout,
dummy_white_gpu_image,
per_object_buffer_batch_size: GpuArrayBuffer::<Mesh2dUniform>::batch_size(
render_device,
),
}
}
}
impl Mesh2dPipeline {
pub fn get_image_texture<'a>(
&'a self,
gpu_images: &'a RenderAssets<GpuImage>,
handle_option: &Option<Handle<Image>>,
) -> Option<(&'a TextureView, &'a Sampler)> {
if let Some(handle) = handle_option {
let gpu_image = gpu_images.get(handle)?;
Some((&gpu_image.texture_view, &gpu_image.sampler))
} else {
Some((
&self.dummy_white_gpu_image.texture_view,
&self.dummy_white_gpu_image.sampler,
))
}
}
}
impl GetBatchData for Mesh2dPipeline {
type Param = (
SRes<RenderMesh2dInstances>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
);
type CompareData = (Material2dBindGroupId, AssetId<Mesh>);
type BufferData = Mesh2dUniform;
fn get_batch_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
(_entity, main_entity): (Entity, MainEntity),
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
let mesh_instance = mesh_instances.get(&main_entity)?;
Some((
Mesh2dUniform::from_components(&mesh_instance.transforms, mesh_instance.tag),
mesh_instance.automatic_batching.then_some((
mesh_instance.material_bind_group_id,
mesh_instance.mesh_asset_id,
)),
))
}
}
impl GetFullBatchData for Mesh2dPipeline {
type BufferInputData = ();
fn get_binned_batch_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
main_entity: MainEntity,
) -> Option<Self::BufferData> {
let mesh_instance = mesh_instances.get(&main_entity)?;
Some(Mesh2dUniform::from_components(
&mesh_instance.transforms,
mesh_instance.tag,
))
}
fn get_index_and_compare_data(
_: &SystemParamItem<Self::Param>,
_query_item: MainEntity,
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
error!(
"`get_index_and_compare_data` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn get_binned_index(
_: &SystemParamItem<Self::Param>,
_query_item: MainEntity,
) -> Option<NonMaxU32> {
error!(
"`get_binned_index` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn write_batch_indirect_parameters_metadata(
indexed: bool,
base_output_index: u32,
batch_set_index: Option<NonMaxU32>,
indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::UntypedPhaseIndirectParametersBuffers,
indirect_parameters_offset: u32,
) {
// Note that `IndirectParameters` covers both of these structures, even
// though they actually have distinct layouts. See the comment above that
// type for more information.
let indirect_parameters = IndirectParametersCpuMetadata {
base_output_index,
batch_set_index: match batch_set_index {
None => !0,
Some(batch_set_index) => u32::from(batch_set_index),
},
};
if indexed {
indirect_parameters_buffer
.indexed
.set(indirect_parameters_offset, indirect_parameters);
} else {
indirect_parameters_buffer
.non_indexed
.set(indirect_parameters_offset, indirect_parameters);
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
// NOTE: Apparently quadro drivers support up to 64x MSAA.
// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA.
// FIXME: make normals optional?
pub struct Mesh2dPipelineKey: u32 {
const NONE = 0;
const HDR = 1 << 0;
const TONEMAP_IN_SHADER = 1 << 1;
const DEBAND_DITHER = 1 << 2;
const BLEND_ALPHA = 1 << 3;
const MAY_DISCARD = 1 << 4;
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
}
}
impl Mesh2dPipelineKey {
const MSAA_MASK_BITS: u32 = 0b111;
const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();
const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3;
const TONEMAP_METHOD_MASK_BITS: u32 = 0b111;
const TONEMAP_METHOD_SHIFT_BITS: u32 =
Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones();
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
let msaa_bits =
(msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS;
Self::from_bits_retain(msaa_bits)
}
pub fn from_hdr(hdr: bool) -> Self {
if hdr {
Mesh2dPipelineKey::HDR
} else {
Mesh2dPipelineKey::NONE
}
}
pub fn msaa_samples(&self) -> u32 {
1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS)
}
pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self {
let primitive_topology_bits = ((primitive_topology as u32)
& Self::PRIMITIVE_TOPOLOGY_MASK_BITS)
<< Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
Self::from_bits_retain(primitive_topology_bits)
}
pub fn primitive_topology(&self) -> PrimitiveTopology {
let primitive_topology_bits = (self.bits() >> Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS)
& Self::PRIMITIVE_TOPOLOGY_MASK_BITS;
match primitive_topology_bits {
x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList,
x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList,
x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip,
x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList,
x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip,
_ => PrimitiveTopology::default(),
}
}
}
impl SpecializedMeshPipeline for Mesh2dPipeline {
type Key = Mesh2dPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut shader_defs = Vec::new();
let mut vertex_attributes = Vec::new();
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
shader_defs.push("VERTEX_POSITIONS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
}
if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) {
shader_defs.push("VERTEX_NORMALS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1));
}
if layout.0.contains(Mesh::ATTRIBUTE_UV_0) {
shader_defs.push("VERTEX_UVS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2));
}
if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) {
shader_defs.push("VERTEX_TANGENTS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3));
}
if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
shader_defs.push("VERTEX_COLORS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4));
}
if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
2,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
3,
));
let method = key.intersection(Mesh2dPipelineKey::TONEMAP_METHOD_RESERVED_BITS);
match method {
Mesh2dPipelineKey::TONEMAP_METHOD_NONE => {
shader_defs.push("TONEMAP_METHOD_NONE".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD => {
shader_defs.push("TONEMAP_METHOD_REINHARD".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE => {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED => {
shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_AGX => {
shader_defs.push("TONEMAP_METHOD_AGX".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM => {
shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC => {
shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE => {
shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
}
_ => {}
}
// Debanding is tied to tonemapping in the shader, cannot run without it.
if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) {
shader_defs.push("DEBAND_DITHER".into());
}
}
if key.contains(Mesh2dPipelineKey::MAY_DISCARD) {
shader_defs.push("MAY_DISCARD".into());
}
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
let format = match key.contains(Mesh2dPipelineKey::HDR) {
true => ViewTarget::TEXTURE_FORMAT_HDR,
false => TextureFormat::bevy_default(),
};
let (depth_write_enabled, label, blend);
if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) {
label = "transparent_mesh2d_pipeline";
blend = Some(BlendState::ALPHA_BLENDING);
depth_write_enabled = false;
} else {
label = "opaque_mesh2d_pipeline";
blend = None;
depth_write_enabled = true;
}
Ok(RenderPipelineDescriptor {
vertex: VertexState {
shader: MESH2D_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: MESH2D_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend,
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone(), self.mesh_layout.clone()],
push_constant_ranges: vec![],
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: key.primitive_topology(),
strip_index_format: None,
},
depth_stencil: Some(DepthStencilState {
format: CORE_2D_DEPTH_FORMAT,
depth_write_enabled,
depth_compare: CompareFunction::GreaterEqual,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
read_mask: 0,
write_mask: 0,
},
bias: DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: MultisampleState {
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some(label.into()),
zero_initialize_workgroup_memory: false,
})
}
}
#[derive(Resource)]
pub struct Mesh2dBindGroup {
pub value: BindGroup,
}
pub fn prepare_mesh2d_bind_group(
mut commands: Commands,
mesh2d_pipeline: Res<Mesh2dPipeline>,
render_device: Res<RenderDevice>,
mesh2d_uniforms: Res<BatchedInstanceBuffer<Mesh2dUniform>>,
) {
if let Some(binding) = mesh2d_uniforms.instance_data_binding() {
commands.insert_resource(Mesh2dBindGroup {
value: render_device.create_bind_group(
"mesh2d_bind_group",
&mesh2d_pipeline.mesh_layout,
&BindGroupEntries::single(binding),
),
});
}
}
#[derive(Component)]
pub struct Mesh2dViewBindGroup {
pub value: BindGroup,
}
pub fn prepare_mesh2d_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
mesh2d_pipeline: Res<Mesh2dPipeline>,
view_uniforms: Res<ViewUniforms>,
views: Query<(Entity, &Tonemapping), (With<ExtractedView>, With<Camera2d>)>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
images: Res<RenderAssets<GpuImage>>,
fallback_image: Res<FallbackImage>,
) {
let (Some(view_binding), Some(globals)) = (
view_uniforms.uniforms.binding(),
globals_buffer.buffer.binding(),
) else {
return;
};
for (entity, tonemapping) in &views {
let lut_bindings =
get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image);
let view_bind_group = render_device.create_bind_group(
"mesh2d_view_bind_group",
&mesh2d_pipeline.view_layout,
&BindGroupEntries::with_indices((
(0, view_binding.clone()),
(1, globals.clone()),
(2, lut_bindings.0),
(3, lut_bindings.1),
)),
);
commands.entity(entity).insert(Mesh2dViewBindGroup {
value: view_bind_group,
});
}
}
pub struct SetMesh2dViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMesh2dViewBindGroup<I> {
type Param = ();
type ViewQuery = (Read<ViewUniformOffset>, Read<Mesh2dViewBindGroup>);
type ItemQuery = ();
#[inline]
fn render<'w>(
_item: &P,
(view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>,
_view: Option<()>,
_param: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(I, &mesh2d_view_bind_group.value, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct SetMesh2dBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMesh2dBindGroup<I> {
type Param = SRes<Mesh2dBindGroup>;
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
mesh2d_bind_group: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let mut dynamic_offsets: [u32; 1] = Default::default();
let mut offset_count = 0;
if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() {
dynamic_offsets[offset_count] = dynamic_offset;
offset_count += 1;
}
pass.set_bind_group(
I,
&mesh2d_bind_group.into_inner().value,
&dynamic_offsets[..offset_count],
);
RenderCommandResult::Success
}
}
pub struct DrawMesh2d;
impl<P: PhaseItem> RenderCommand<P> for DrawMesh2d {
type Param = (
SRes<RenderAssets<RenderMesh>>,
SRes<RenderMesh2dInstances>,
SRes<MeshAllocator>,
);
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
(meshes, render_mesh2d_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let meshes = meshes.into_inner();
let render_mesh2d_instances = render_mesh2d_instances.into_inner();
let mesh_allocator = mesh_allocator.into_inner();
let Some(RenderMesh2dInstance { mesh_asset_id, .. }) =
render_mesh2d_instances.get(&item.main_entity())
else {
return RenderCommandResult::Skip;
};
let Some(gpu_mesh) = meshes.get(*mesh_asset_id) else {
return RenderCommandResult::Skip;
};
let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(mesh_asset_id) else {
return RenderCommandResult::Skip;
};
pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
let batch_range = item.batch_range();
match &gpu_mesh.buffer_info {
RenderMeshBufferInfo::Indexed {
index_format,
count,
} => {
let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(mesh_asset_id)
else {
return RenderCommandResult::Skip;
};
pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format);
pass.draw_indexed(
index_buffer_slice.range.start..(index_buffer_slice.range.start + count),
vertex_buffer_slice.range.start as i32,
batch_range.clone(),
);
}
RenderMeshBufferInfo::NonIndexed => {
pass.draw(vertex_buffer_slice.range, batch_range.clone());
}
}
RenderCommandResult::Success
}
}

View File

@@ -0,0 +1,76 @@
#import bevy_sprite::{
mesh2d_functions as mesh_functions,
mesh2d_vertex_output::VertexOutput,
mesh2d_view_bindings::view,
}
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
struct Vertex {
@builtin(instance_index) instance_index: u32,
#ifdef VERTEX_POSITIONS
@location(0) position: vec3<f32>,
#endif
#ifdef VERTEX_NORMALS
@location(1) normal: vec3<f32>,
#endif
#ifdef VERTEX_UVS
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(3) tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(4) color: vec4<f32>,
#endif
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef VERTEX_UVS
out.uv = vertex.uv;
#endif
#ifdef VERTEX_POSITIONS
var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
out.world_position = mesh_functions::mesh2d_position_local_to_world(
world_from_local,
vec4<f32>(vertex.position, 1.0)
);
out.position = mesh_functions::mesh2d_position_world_to_clip(out.world_position);
#endif
#ifdef VERTEX_NORMALS
out.world_normal = mesh_functions::mesh2d_normal_local_to_world(vertex.normal, vertex.instance_index);
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh2d_tangent_local_to_world(
world_from_local,
vertex.tangent
);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
return out;
}
@fragment
fn fragment(
in: VertexOutput,
) -> @location(0) vec4<f32> {
#ifdef VERTEX_COLORS
var color = in.color;
#ifdef TONEMAP_IN_SHADER
color = tonemapping::tone_mapping(color, view.color_grading);
#endif
return color;
#else
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
#endif
}

View File

@@ -0,0 +1,9 @@
#define_import_path bevy_sprite::mesh2d_bindings
#import bevy_sprite::mesh2d_types::Mesh2d
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(1) @binding(0) var<uniform> mesh: array<Mesh2d, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else
@group(1) @binding(0) var<storage> mesh: array<Mesh2d>;
#endif // PER_OBJECT_BUFFER_BATCH_SIZE

View File

@@ -0,0 +1,49 @@
#define_import_path bevy_sprite::mesh2d_functions
#import bevy_sprite::{
mesh2d_view_bindings::view,
mesh2d_bindings::mesh,
}
#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack}
fn get_world_from_local(instance_index: u32) -> mat4x4<f32> {
return affine3_to_square(mesh[instance_index].world_from_local);
}
fn mesh2d_position_local_to_world(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
return world_from_local * vertex_position;
}
fn mesh2d_position_world_to_clip(world_position: vec4<f32>) -> vec4<f32> {
return view.clip_from_world * world_position;
}
// NOTE: The intermediate world_position assignment is important
// for precision purposes when using the 'equals' depth comparison
// function.
fn mesh2d_position_local_to_clip(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
let world_position = mesh2d_position_local_to_world(world_from_local, vertex_position);
return mesh2d_position_world_to_clip(world_position);
}
fn mesh2d_normal_local_to_world(vertex_normal: vec3<f32>, instance_index: u32) -> vec3<f32> {
return mat2x4_f32_to_mat3x3_unpack(
mesh[instance_index].local_from_world_transpose_a,
mesh[instance_index].local_from_world_transpose_b,
) * vertex_normal;
}
fn mesh2d_tangent_local_to_world(world_from_local: mat4x4<f32>, vertex_tangent: vec4<f32>) -> vec4<f32> {
return vec4<f32>(
mat3x3<f32>(
world_from_local[0].xyz,
world_from_local[1].xyz,
world_from_local[2].xyz
) * vertex_tangent.xyz,
vertex_tangent.w
);
}
fn get_tag(instance_index: u32) -> u32 {
return mesh[instance_index].tag;
}

View File

@@ -0,0 +1,17 @@
#define_import_path bevy_sprite::mesh2d_types
struct Mesh2d {
// Affine 4x3 matrix transposed to 3x4
// Use bevy_render::maths::affine3_to_square to unpack
world_from_local: mat3x4<f32>,
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
// [2].z
// Use bevy_render::maths::mat2x4_f32_to_mat3x3_unpack to unpack
local_from_world_transpose_a: mat2x4<f32>,
local_from_world_transpose_b: f32,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
tag: u32,
};

View File

@@ -0,0 +1,16 @@
#define_import_path bevy_sprite::mesh2d_vertex_output
struct VertexOutput {
// this is `clip position` when the struct is used as a vertex stage output
// and `frag coord` when used as a fragment stage input
@builtin(position) position: vec4<f32>,
@location(0) world_position: vec4<f32>,
@location(1) world_normal: vec3<f32>,
@location(2) uv: vec2<f32>,
#ifdef VERTEX_TANGENTS
@location(3) world_tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(4) color: vec4<f32>,
#endif
}

View File

@@ -0,0 +1,11 @@
#define_import_path bevy_sprite::mesh2d_view_bindings
#import bevy_render::view::View
#import bevy_render::globals::Globals
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> globals: Globals;
@group(0) @binding(2) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(3) var dt_lut_sampler: sampler;

View File

@@ -0,0 +1,4 @@
#define_import_path bevy_sprite::mesh2d_view_types
#import bevy_render::view
#import bevy_render::globals

9
vendor/bevy_sprite/src/mesh2d/mod.rs vendored Normal file
View File

@@ -0,0 +1,9 @@
mod color_material;
mod material;
mod mesh;
mod wireframe2d;
pub use color_material::*;
pub use material::*;
pub use mesh::*;
pub use wireframe2d::*;

View File

@@ -0,0 +1,879 @@
use crate::{
DrawMesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup,
SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks,
};
use bevy_app::{App, Plugin, PostUpdate, Startup, Update};
use bevy_asset::{
load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp,
AssetEvents, AssetId, Assets, Handle, UntypedAssetId,
};
use bevy_color::{Color, ColorToComponents};
use bevy_core_pipeline::core_2d::{
graph::{Core2d, Node2d},
Camera2d,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Tick,
prelude::*,
query::QueryItem,
system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem},
};
use bevy_platform::{
collections::{HashMap, HashSet},
hash::FixedHasher,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingMode,
camera::ExtractedCamera,
extract_resource::ExtractResource,
mesh::{
allocator::{MeshAllocator, SlabId},
Mesh2d, MeshVertexBufferLayoutRef, RenderMesh,
},
prelude::*,
render_asset::{
prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_phase::{
AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType,
CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem,
PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases,
},
render_resource::*,
renderer::RenderContext,
sync_world::{MainEntity, MainEntityHashMap},
view::{
ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget,
},
Extract, Render, RenderApp, RenderDebugFlags, RenderSet,
};
use core::{hash::Hash, ops::Range};
use tracing::error;
pub const WIREFRAME_2D_SHADER_HANDLE: Handle<Shader> =
weak_handle!("2d8a3853-2927-4de2-9dc7-3971e7e40970");
/// A [`Plugin`] that draws wireframes for 2D meshes.
///
/// Wireframes currently do not work when using webgl or webgpu.
/// Supported rendering backends:
/// - DX12
/// - Vulkan
/// - Metal
///
/// This is a native only feature.
#[derive(Debug, Default)]
pub struct Wireframe2dPlugin {
/// Debugging flags that can optionally be set when constructing the renderer.
pub debug_flags: RenderDebugFlags,
}
impl Wireframe2dPlugin {
/// Creates a new [`Wireframe2dPlugin`] with the given debug flags.
pub fn new(debug_flags: RenderDebugFlags) -> Self {
Self { debug_flags }
}
}
impl Plugin for Wireframe2dPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
WIREFRAME_2D_SHADER_HANDLE,
"wireframe2d.wgsl",
Shader::from_wgsl
);
app.add_plugins((
BinnedRenderPhasePlugin::<Wireframe2dPhaseItem, Mesh2dPipeline>::new(self.debug_flags),
RenderAssetPlugin::<RenderWireframeMaterial>::default(),
))
.init_asset::<Wireframe2dMaterial>()
.init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
.register_type::<NoWireframe2d>()
.register_type::<Wireframe2dConfig>()
.register_type::<Wireframe2dColor>()
.init_resource::<Wireframe2dConfig>()
.init_resource::<WireframeEntitiesNeedingSpecialization>()
.add_systems(Startup, setup_global_wireframe_material)
.add_systems(
Update,
(
global_color_changed.run_if(resource_changed::<Wireframe2dConfig>),
wireframe_color_changed,
// Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global
// wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed.
(apply_wireframe_material, apply_global_wireframe_material).chain(),
),
)
.add_systems(
PostUpdate,
check_wireframe_entities_needing_specialization
.after(AssetEvents)
.run_if(resource_exists::<Wireframe2dConfig>),
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<WireframeEntitySpecializationTicks>()
.init_resource::<SpecializedWireframePipelineCache>()
.init_resource::<DrawFunctions<Wireframe2dPhaseItem>>()
.add_render_command::<Wireframe2dPhaseItem, DrawWireframe2d>()
.init_resource::<RenderWireframeInstances>()
.init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
.add_render_graph_node::<ViewNodeRunner<Wireframe2dNode>>(Core2d, Node2d::Wireframe)
.add_render_graph_edges(
Core2d,
(
Node2d::EndMainPass,
Node2d::Wireframe,
Node2d::PostProcessing,
),
)
.add_systems(
ExtractSchedule,
(
extract_wireframe_2d_camera,
extract_wireframe_entities_needing_specialization,
extract_wireframe_materials,
),
)
.add_systems(
Render,
(
specialize_wireframes
.in_set(RenderSet::PrepareMeshes)
.after(prepare_assets::<RenderWireframeMaterial>)
.after(prepare_assets::<RenderMesh>),
queue_wireframes
.in_set(RenderSet::QueueMeshes)
.after(prepare_assets::<RenderWireframeMaterial>),
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<Wireframe2dPipeline>();
}
}
/// Enables wireframe rendering for any entity it is attached to.
/// It will ignore the [`Wireframe2dConfig`] global setting.
///
/// This requires the [`Wireframe2dPlugin`] to be enabled.
#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct Wireframe2d;
pub struct Wireframe2dPhaseItem {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: Wireframe2dBatchSetKey,
/// The key, which determines which can be batched.
pub bin_key: Wireframe2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for Wireframe2dPhaseItem {
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl CachedRenderPipelinePhaseItem for Wireframe2dPhaseItem {
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
impl BinnedPhaseItem for Wireframe2dPhaseItem {
type BinKey = Wireframe2dBinKey;
type BatchSetKey = Wireframe2dBatchSetKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Wireframe2dBatchSetKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The wireframe material asset ID.
pub asset_id: UntypedAssetId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The ID of the slab of GPU memory that contains vertex data.
///
/// For non-mesh items, you can fill this with 0 if your items can be
/// multi-drawn, or with a unique value if they can't.
pub vertex_slab: SlabId,
/// The ID of the slab of GPU memory that contains index data, if present.
///
/// For non-mesh items, you can safely fill this with `None`.
pub index_slab: Option<SlabId>,
}
impl PhaseItemBatchSetKey for Wireframe2dBatchSetKey {
fn indexed(&self) -> bool {
self.index_slab.is_some()
}
}
/// Data that must be identical in order to *batch* phase items together.
///
/// Note that a *batch set* (if multi-draw is in use) contains multiple batches.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Wireframe2dBinKey {
/// The wireframe mesh asset ID.
pub asset_id: UntypedAssetId,
}
pub struct SetWireframe2dPushConstants;
impl<P: PhaseItem> RenderCommand<P> for SetWireframe2dPushConstants {
type Param = (
SRes<RenderWireframeInstances>,
SRes<RenderAssets<RenderWireframeMaterial>>,
);
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
(wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else {
return RenderCommandResult::Failure("No wireframe material found for entity");
};
let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else {
return RenderCommandResult::Failure("No wireframe material found for entity");
};
pass.set_push_constants(
ShaderStages::FRAGMENT,
0,
bytemuck::bytes_of(&wireframe_material.color),
);
RenderCommandResult::Success
}
}
pub type DrawWireframe2d = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMesh2dBindGroup<1>,
SetWireframe2dPushConstants,
DrawMesh2d,
);
#[derive(Resource, Clone)]
pub struct Wireframe2dPipeline {
mesh_pipeline: Mesh2dPipeline,
shader: Handle<Shader>,
}
impl FromWorld for Wireframe2dPipeline {
fn from_world(render_world: &mut World) -> Self {
Wireframe2dPipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
shader: WIREFRAME_2D_SHADER_HANDLE,
}
}
}
impl SpecializedMeshPipeline for Wireframe2dPipeline {
type Key = Mesh2dPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
descriptor.label = Some("wireframe_2d_pipeline".into());
descriptor.push_constant_ranges.push(PushConstantRange {
stages: ShaderStages::FRAGMENT,
range: 0..16,
});
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader = self.shader.clone();
descriptor.primitive.polygon_mode = PolygonMode::Line;
descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
Ok(descriptor)
}
}
#[derive(Default)]
struct Wireframe2dNode;
impl ViewNode for Wireframe2dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, view, target, depth): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(wireframe_phase) =
world.get_resource::<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>()
else {
return Ok(());
};
let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else {
return Ok(());
};
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("wireframe_2d_pass"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) {
error!("Error encountered while rendering the stencil phase {err:?}");
return Err(NodeRunError::DrawError(err));
}
Ok(())
}
}
/// Sets the color of the [`Wireframe2d`] of the entity it is attached to.
///
/// If this component is present but there's no [`Wireframe2d`] component,
/// it will still affect the color of the wireframe when [`Wireframe2dConfig::global`] is set to true.
///
/// This overrides the [`Wireframe2dConfig::default_color`].
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct Wireframe2dColor {
pub color: Color,
}
#[derive(Component, Debug, Clone, Default)]
pub struct ExtractedWireframeColor {
pub color: [f32; 4],
}
/// Disables wireframe rendering for any entity it is attached to.
/// It will ignore the [`Wireframe2dConfig`] global setting.
///
/// This requires the [`Wireframe2dPlugin`] to be enabled.
#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct NoWireframe2d;
#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)]
#[reflect(Resource, Debug, Default)]
pub struct Wireframe2dConfig {
/// Whether to show wireframes for all meshes.
/// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component.
pub global: bool,
/// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have
/// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe2d`],
/// but no [`Wireframe2dColor`].
pub default_color: Color,
}
#[derive(Asset, Reflect, Clone, Debug, Default)]
#[reflect(Clone, Default)]
pub struct Wireframe2dMaterial {
pub color: Color,
}
pub struct RenderWireframeMaterial {
pub color: [f32; 4],
}
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)]
#[reflect(Component, Default, Clone, PartialEq)]
pub struct Mesh2dWireframe(pub Handle<Wireframe2dMaterial>);
impl AsAssetId for Mesh2dWireframe {
type Asset = Wireframe2dMaterial;
fn as_asset_id(&self) -> AssetId<Self::Asset> {
self.0.id()
}
}
impl RenderAsset for RenderWireframeMaterial {
type SourceAsset = Wireframe2dMaterial;
type Param = ();
fn prepare_asset(
source_asset: Self::SourceAsset,
_asset_id: AssetId<Self::SourceAsset>,
_param: &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
Ok(RenderWireframeMaterial {
color: source_asset.color.to_linear().to_f32_array(),
})
}
}
#[derive(Resource, Deref, DerefMut, Default)]
pub struct RenderWireframeInstances(MainEntityHashMap<AssetId<Wireframe2dMaterial>>);
#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)]
pub struct WireframeEntitiesNeedingSpecialization {
#[deref]
pub entities: Vec<Entity>,
}
#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)]
pub struct WireframeEntitySpecializationTicks {
pub entities: MainEntityHashMap<Tick>,
}
/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct SpecializedWireframePipelineCache {
// view entity -> view pipeline cache
#[deref]
map: HashMap<RetainedViewEntity, SpecializedWireframeViewPipelineCache>,
}
/// Stores the cached render pipeline ID for each entity in a single view, as
/// well as the last time it was changed.
#[derive(Deref, DerefMut, Default)]
pub struct SpecializedWireframeViewPipelineCache {
// material entity -> (tick, pipeline_id)
#[deref]
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
}
#[derive(Resource)]
struct GlobalWireframeMaterial {
// This handle will be reused when the global config is enabled
handle: Handle<Wireframe2dMaterial>,
}
pub fn extract_wireframe_materials(
mut material_instances: ResMut<RenderWireframeInstances>,
changed_meshes_query: Extract<
Query<
(Entity, &ViewVisibility, &Mesh2dWireframe),
Or<(Changed<ViewVisibility>, Changed<Mesh2dWireframe>)>,
>,
>,
mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
mut removed_materials_query: Extract<RemovedComponents<Mesh2dWireframe>>,
) {
for (entity, view_visibility, material) in &changed_meshes_query {
if view_visibility.get() {
material_instances.insert(entity.into(), material.id());
} else {
material_instances.remove(&MainEntity::from(entity));
}
}
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));
}
}
}
fn setup_global_wireframe_material(
mut commands: Commands,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
config: Res<Wireframe2dConfig>,
) {
// Create the handle used for the global material
commands.insert_resource(GlobalWireframeMaterial {
handle: materials.add(Wireframe2dMaterial {
color: config.default_color,
}),
});
}
/// Updates the wireframe material of all entities without a [`Wireframe2dColor`] or without a [`Wireframe2d`] component
fn global_color_changed(
config: Res<Wireframe2dConfig>,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
global_material: Res<GlobalWireframeMaterial>,
) {
if let Some(global_material) = materials.get_mut(&global_material.handle) {
global_material.color = config.default_color;
}
}
/// Updates the wireframe material when the color in [`Wireframe2dColor`] changes
fn wireframe_color_changed(
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
mut colors_changed: Query<
(&mut Mesh2dWireframe, &Wireframe2dColor),
(With<Wireframe2d>, Changed<Wireframe2dColor>),
>,
) {
for (mut handle, wireframe_color) in &mut colors_changed {
handle.0 = materials.add(Wireframe2dMaterial {
color: wireframe_color.color,
});
}
}
/// Applies or remove the wireframe material to any mesh with a [`Wireframe2d`] component, and removes it
/// for any mesh with a [`NoWireframe2d`] component.
fn apply_wireframe_material(
mut commands: Commands,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
wireframes: Query<
(Entity, Option<&Wireframe2dColor>),
(With<Wireframe2d>, Without<Mesh2dWireframe>),
>,
no_wireframes: Query<Entity, (With<NoWireframe2d>, With<Mesh2dWireframe>)>,
mut removed_wireframes: RemovedComponents<Wireframe2d>,
global_material: Res<GlobalWireframeMaterial>,
) {
for e in removed_wireframes.read().chain(no_wireframes.iter()) {
if let Ok(mut commands) = commands.get_entity(e) {
commands.remove::<Mesh2dWireframe>();
}
}
let mut material_to_spawn = vec![];
for (e, maybe_color) in &wireframes {
let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
material_to_spawn.push((e, Mesh2dWireframe(material)));
}
commands.try_insert_batch(material_to_spawn);
}
type WireframeFilter = (With<Mesh2d>, Without<Wireframe2d>, Without<NoWireframe2d>);
/// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component.
fn apply_global_wireframe_material(
mut commands: Commands,
config: Res<Wireframe2dConfig>,
meshes_without_material: Query<
(Entity, Option<&Wireframe2dColor>),
(WireframeFilter, Without<Mesh2dWireframe>),
>,
meshes_with_global_material: Query<Entity, (WireframeFilter, With<Mesh2dWireframe>)>,
global_material: Res<GlobalWireframeMaterial>,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
) {
if config.global {
let mut material_to_spawn = vec![];
for (e, maybe_color) in &meshes_without_material {
let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
// We only add the material handle but not the Wireframe component
// This makes it easy to detect which mesh is using the global material and which ones are user specified
material_to_spawn.push((e, Mesh2dWireframe(material)));
}
commands.try_insert_batch(material_to_spawn);
} else {
for e in &meshes_with_global_material {
commands.entity(e).remove::<Mesh2dWireframe>();
}
}
}
/// Gets a handle to a wireframe material with a fallback on the default material
fn get_wireframe_material(
maybe_color: Option<&Wireframe2dColor>,
wireframe_materials: &mut Assets<Wireframe2dMaterial>,
global_material: &GlobalWireframeMaterial,
) -> Handle<Wireframe2dMaterial> {
if let Some(wireframe_color) = maybe_color {
wireframe_materials.add(Wireframe2dMaterial {
color: wireframe_color.color,
})
} else {
// If there's no color specified we can use the global material since it's already set to use the default_color
global_material.handle.clone()
}
}
fn extract_wireframe_2d_camera(
mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
cameras: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<HashSet<RetainedViewEntity>>,
) {
live_entities.clear();
for (main_entity, camera) in &cameras {
if !camera.is_active {
continue;
}
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
wireframe_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
live_entities.insert(retained_view_entity);
}
// Clear out all dead views.
wireframe_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
}
pub fn extract_wireframe_entities_needing_specialization(
entities_needing_specialization: Extract<Res<WireframeEntitiesNeedingSpecialization>>,
mut entity_specialization_ticks: ResMut<WireframeEntitySpecializationTicks>,
views: Query<&ExtractedView>,
mut specialized_wireframe_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
mut removed_meshes_query: Extract<RemovedComponents<Mesh2d>>,
ticks: SystemChangeTick,
) {
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());
}
for entity in removed_meshes_query.read() {
for view in &views {
if let Some(specialized_wireframe_pipeline_cache) =
specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity)
{
specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity));
}
}
}
}
pub fn check_wireframe_entities_needing_specialization(
needs_specialization: Query<
Entity,
Or<(
Changed<Mesh2d>,
AssetChanged<Mesh2d>,
Changed<Mesh2dWireframe>,
AssetChanged<Mesh2dWireframe>,
)>,
>,
mut entities_needing_specialization: ResMut<WireframeEntitiesNeedingSpecialization>,
) {
entities_needing_specialization.clear();
for entity in &needs_specialization {
entities_needing_specialization.push(entity);
}
}
pub fn specialize_wireframes(
render_meshes: Res<RenderAssets<RenderMesh>>,
render_mesh_instances: Res<RenderMesh2dInstances>,
render_wireframe_instances: Res<RenderWireframeInstances>,
wireframe_phases: Res<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
view_key_cache: Res<ViewKeyCache>,
entity_specialization_ticks: Res<WireframeEntitySpecializationTicks>,
view_specialization_ticks: Res<ViewSpecializationTicks>,
mut specialized_material_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
mut pipelines: ResMut<SpecializedMeshPipelines<Wireframe2dPipeline>>,
pipeline: Res<Wireframe2dPipeline>,
pipeline_cache: Res<PipelineCache>,
ticks: SystemChangeTick,
) {
// Record the retained IDs of all views so that we can expire old
// pipeline IDs.
let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
for (view, visible_entities) in &views {
all_views.insert(view.retained_view_entity);
if !wireframe_phases.contains_key(&view.retained_view_entity) {
continue;
}
let Some(view_key) = view_key_cache.get(&view.retained_view_entity.main_entity) else {
continue;
};
let view_tick = view_specialization_ticks
.get(&view.retained_view_entity.main_entity)
.unwrap();
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
.entry(view.retained_view_entity)
.or_default();
for (_, visible_entity) in visible_entities.iter::<Mesh2d>() {
if !render_wireframe_instances.contains_key(visible_entity) {
continue;
};
let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
continue;
};
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
let last_specialized_tick = view_specialized_material_pipeline_cache
.get(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(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
let mut mesh_key = *view_key;
mesh_key |= Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology());
let pipeline_id =
pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout);
let pipeline_id = match pipeline_id {
Ok(id) => id,
Err(err) => {
error!("{}", err);
continue;
}
};
view_specialized_material_pipeline_cache
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
}
}
// Delete specialized pipelines belonging to views that have expired.
specialized_material_pipeline_cache
.retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
}
fn queue_wireframes(
custom_draw_functions: Res<DrawFunctions<Wireframe2dPhaseItem>>,
render_mesh_instances: Res<RenderMesh2dInstances>,
mesh_allocator: Res<MeshAllocator>,
specialized_wireframe_pipeline_cache: Res<SpecializedWireframePipelineCache>,
render_wireframe_instances: Res<RenderWireframeInstances>,
mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
) {
for (view, visible_entities) in &mut views {
let Some(wireframe_phase) = wireframe_2d_phases.get_mut(&view.retained_view_entity) else {
continue;
};
let draw_wireframe = custom_draw_functions.read().id::<DrawWireframe2d>();
let Some(view_specialized_material_pipeline_cache) =
specialized_wireframe_pipeline_cache.get(&view.retained_view_entity)
else {
continue;
};
for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() {
let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else {
continue;
};
let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
.get(visible_entity)
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
else {
continue;
};
// Skip the entity if it's cached in a bin and up to date.
if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) {
continue;
}
let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
continue;
};
let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
let bin_key = Wireframe2dBinKey {
asset_id: mesh_instance.mesh_asset_id.untyped(),
};
let batch_set_key = Wireframe2dBatchSetKey {
pipeline: pipeline_id,
asset_id: wireframe_instance.untyped(),
draw_function: draw_wireframe,
vertex_slab: vertex_slab.unwrap_or_default(),
index_slab,
};
wireframe_phase.add(
batch_set_key,
bin_key,
(*render_entity, *visible_entity),
InputUniformIndex::default(),
if mesh_instance.automatic_batching {
BinnedRenderPhaseType::BatchableMesh
} else {
BinnedRenderPhaseType::UnbatchableMesh
},
current_change_tick,
);
}
}
}

View File

@@ -0,0 +1,12 @@
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
struct PushConstants {
color: vec4<f32>
}
var<push_constant> push_constants: PushConstants;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return push_constants.color;
}

View File

@@ -0,0 +1,251 @@
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for
//! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels.
//! This means a partially transparent sprite is pickable even in its transparent areas.
//!
//! ## Implementation Notes
//!
//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized
//! vector provided by the target's `GlobalTransform::back()`.
use crate::Sprite;
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_color::Alpha;
use bevy_ecs::prelude::*;
use bevy_image::prelude::*;
use bevy_math::{prelude::*, FloatExt};
use bevy_picking::backend::prelude::*;
use bevy_reflect::prelude::*;
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].
///
/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored
/// otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component, Clone)]
pub struct SpritePickingCamera;
/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels
#[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Debug, Clone)]
pub enum SpritePickingMode {
/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.
/// Only consider the rect of a given sprite.
BoundingBox,
/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)
/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value
AlphaThreshold(f32),
}
/// Runtime settings for the [`SpritePickingPlugin`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct SpritePickingSettings {
/// When set to `true` sprite picking will only consider cameras marked with
/// [`SpritePickingCamera`].
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the sprite picking backend at runtime.
pub require_markers: bool,
/// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone.
///
/// Defaults to an inclusive alpha threshold of 0.1
pub picking_mode: SpritePickingMode,
}
impl Default for SpritePickingSettings {
fn default() -> Self {
Self {
require_markers: false,
picking_mode: SpritePickingMode::AlphaThreshold(0.1),
}
}
}
/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.
#[derive(Clone)]
pub struct SpritePickingPlugin;
impl Plugin for SpritePickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SpritePickingSettings>()
.register_type::<SpritePickingCamera>()
.register_type::<SpritePickingMode>()
.register_type::<SpritePickingSettings>()
.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend));
}
}
fn sprite_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
cameras: Query<(
Entity,
&Camera,
&GlobalTransform,
&Projection,
Has<SpritePickingCamera>,
)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
images: Res<Assets<Image>>,
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
settings: Res<SpritePickingSettings>,
sprite_query: Query<(
Entity,
&Sprite,
&GlobalTransform,
&Pickable,
&ViewVisibility,
)>,
mut output: EventWriter<PointerHits>,
) {
let mut sorted_sprites: Vec<_> = sprite_query
.iter()
.filter_map(|(entity, sprite, transform, pickable, vis)| {
if !transform.affine().is_nan() && vis.get() {
Some((entity, sprite, transform, pickable))
} else {
None
}
})
.collect();
// radsort is a stable radix sort that performed better than `slice::sort_by_key`
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| {
-transform.translation().z
});
let primary_window = primary_window.single().ok();
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
pointer_location.location().map(|loc| (pointer, loc))
}) {
let mut blocked = false;
let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) =
cameras
.iter()
.filter(|(_, camera, _, _, cam_can_pick)| {
let marker_requirement = !settings.require_markers || *cam_can_pick;
camera.is_active && marker_requirement
})
.find(|(_, camera, _, _, _)| {
camera
.target
.normalize(primary_window)
.is_some_and(|x| x == location.target)
})
else {
continue;
};
let viewport_pos = camera
.logical_viewport_rect()
.map(|v| v.min)
.unwrap_or_default();
let pos_in_viewport = location.position - viewport_pos;
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
continue;
};
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
let picks: Vec<(Entity, HitData)> = sorted_sprites
.iter()
.copied()
.filter_map(|(entity, sprite, sprite_transform, pickable)| {
if blocked {
return None;
}
// Transform cursor line segment to sprite coordinate system
let world_to_sprite = sprite_transform.affine().inverse();
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
// plane in sprite-local space). It may not intersect if, for example, we're
// viewing the sprite side-on
if cursor_start_sprite.z == cursor_end_sprite.z {
// Cursor ray is parallel to the sprite and misses it
return None;
}
let lerp_factor =
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
if !(0.0..=1.0).contains(&lerp_factor) {
// Lerp factor is out of range, meaning that while an infinite line cast by
// the cursor would intersect the sprite, the sprite is not between the
// camera's near and far planes
return None;
}
// Otherwise we can interpolate the xy of the start and end positions by the
// lerp factor to get the cursor position in sprite space!
let cursor_pos_sprite = cursor_start_sprite
.lerp(cursor_end_sprite, lerp_factor)
.xy();
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
cursor_pos_sprite,
&images,
&texture_atlas_layout,
) else {
return None;
};
// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of
// the sprite.
let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {
match settings.picking_mode {
SpritePickingMode::AlphaThreshold(cutoff) => {
let Some(image) = images.get(&sprite.image) else {
// [`Sprite::from_color`] returns a defaulted handle.
// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible
break 'valid_pixel true;
};
// grab pixel and check alpha
let Ok(color) = image.get_color_at(
cursor_pixel_space.x as u32,
cursor_pixel_space.y as u32,
) else {
// We don't know how to interpret the pixel.
break 'valid_pixel false;
};
// Check the alpha is above the cutoff.
color.alpha() > cutoff
}
SpritePickingMode::BoundingBox => true,
}
};
blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;
cursor_in_valid_pixels_of_sprite.then(|| {
let hit_pos_world =
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
// Transform point from world to camera space to get the Z distance
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(hit_pos_world);
// HitData requires a depth as calculated from the camera's near clipping plane
let depth = -cam_ortho.near - hit_pos_cam.z;
(
entity,
HitData::new(
cam_entity,
depth,
Some(hit_pos_world),
Some(*sprite_transform.back()),
),
)
})
})
.collect();
let order = camera.order as f32;
output.write(PointerHits::new(*pointer, picks, order));
}
}

1068
vendor/bevy_sprite/src/render/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
#import bevy_render::{
maths::affine3_to_square,
view::View,
}
#import bevy_sprite::sprite_view_bindings::view
struct VertexInput {
@builtin(vertex_index) index: u32,
// NOTE: Instance-rate vertex buffer members prefixed with i_
// NOTE: i_model_transpose_colN are the 3 columns of a 3x4 matrix that is the transpose of the
// affine 4x3 model matrix.
@location(0) i_model_transpose_col0: vec4<f32>,
@location(1) i_model_transpose_col1: vec4<f32>,
@location(2) i_model_transpose_col2: vec4<f32>,
@location(3) i_color: vec4<f32>,
@location(4) i_uv_offset_scale: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
};
@vertex
fn vertex(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
let vertex_position = vec3<f32>(
f32(in.index & 0x1u),
f32((in.index & 0x2u) >> 1u),
0.0
);
out.clip_position = view.clip_from_world * affine3_to_square(mat3x4<f32>(
in.i_model_transpose_col0,
in.i_model_transpose_col1,
in.i_model_transpose_col2,
)) * vec4<f32>(vertex_position, 1.0);
out.uv = vec2<f32>(vertex_position.xy) * in.i_uv_offset_scale.zw + in.i_uv_offset_scale.xy;
out.color = in.i_color;
return out;
}
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
@group(1) @binding(1) var sprite_sampler: sampler;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var color = in.color * textureSample(sprite_texture, sprite_sampler, in.uv);
#ifdef TONEMAP_IN_SHADER
color = tonemapping::tone_mapping(color, view.color_grading);
#endif
return color;
}

View File

@@ -0,0 +1,9 @@
#define_import_path bevy_sprite::sprite_view_bindings
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(2) var dt_lut_sampler: sampler;

542
vendor/bevy_sprite/src/sprite.rs vendored Normal file
View File

@@ -0,0 +1,542 @@
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
sync_world::SyncToRenderWorld,
view::{self, Visibility, VisibilityClass},
};
use bevy_transform::components::Transform;
use crate::TextureSlicer;
/// Describes a sprite to be rendered to a 2D camera
#[derive(Component, Debug, Default, Clone, Reflect)]
#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)]
#[reflect(Component, Default, Debug, Clone)]
#[component(on_add = view::add_visibility_class::<Sprite>)]
pub struct Sprite {
/// The image used to render the sprite
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the sprite
pub texture_atlas: Option<TextureAtlas>,
/// The sprite's color tint
pub color: Color,
/// Flip the sprite along the `X` axis
pub flip_x: bool,
/// Flip the sprite along the `Y` axis
pub flip_y: bool,
/// An optional custom size for the sprite that will be used when rendering, instead of the size
/// of the sprite's image
pub custom_size: Option<Vec2>,
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
pub anchor: Anchor,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
}
impl Sprite {
/// Create a Sprite with a custom size
pub fn sized(custom_size: Vec2) -> Self {
Sprite {
custom_size: Some(custom_size),
..Default::default()
}
}
/// Create a sprite from an image
pub fn from_image(image: Handle<Image>) -> Self {
Self {
image,
..Default::default()
}
}
/// Create a sprite from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}
/// Create a sprite from a solid color
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
Self {
color: color.into(),
custom_size: Some(size),
..Default::default()
}
}
/// Computes the pixel point where `point_relative_to_sprite` is sampled
/// from in this sprite. `point_relative_to_sprite` must be in the sprite's
/// local frame. Returns an Ok if the point is inside the bounds of the
/// sprite (not just the image), and returns an Err otherwise.
pub fn compute_pixel_space_point(
&self,
point_relative_to_sprite: Vec2,
images: &Assets<Image>,
texture_atlases: &Assets<TextureAtlasLayout>,
) -> Result<Vec2, Vec2> {
let image_size = images
.get(&self.image)
.map(Image::size)
.unwrap_or(UVec2::ONE);
let atlas_rect = self
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(texture_atlases))
.map(|r| r.as_rect());
let texture_rect = match (atlas_rect, self.rect) {
(None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32),
(None, Some(sprite_rect)) => sprite_rect,
(Some(atlas_rect), None) => atlas_rect,
(Some(atlas_rect), Some(mut sprite_rect)) => {
// Make the sprite rect relative to the atlas rect.
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;
sprite_rect
}
};
let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
let sprite_center = -self.anchor.as_vec() * sprite_size;
let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;
if self.flip_x {
point_relative_to_sprite_center.x *= -1.0;
}
// Texture coordinates start at the top left, whereas world coordinates start at the bottom
// left. So flip by default, and then don't flip if `flip_y` is set.
if !self.flip_y {
point_relative_to_sprite_center.y *= -1.0;
}
let sprite_to_texture_ratio = {
let texture_size = texture_rect.size();
let div_or_zero = |a, b| if b == 0.0 { 0.0 } else { a / b };
Vec2::new(
div_or_zero(texture_size.x, sprite_size.x),
div_or_zero(texture_size.y, sprite_size.y),
)
};
let point_relative_to_texture =
point_relative_to_sprite_center * sprite_to_texture_ratio + texture_rect.center();
// TODO: Support `SpriteImageMode`.
if texture_rect.contains(point_relative_to_texture) {
Ok(point_relative_to_texture)
} else {
Err(point_relative_to_texture)
}
}
}
impl From<Handle<Image>> for Sprite {
fn from(image: Handle<Image>) -> Self {
Self::from_image(image)
}
}
/// Controls how the image is altered when scaled.
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
#[reflect(Debug, Default, Clone)]
pub enum SpriteImageMode {
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
#[default]
Auto,
/// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`].
/// Otherwise no scaling will be applied.
Scale(ScalingMode),
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
impl SpriteImageMode {
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
)
}
/// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise.
#[inline]
#[must_use]
pub const fn scale(&self) -> Option<ScalingMode> {
if let SpriteImageMode::Scale(scale) = self {
Some(*scale)
} else {
None
}
}
}
/// Represents various modes for proportional scaling of a texture.
///
/// Can be used in [`SpriteImageMode::Scale`].
#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
#[reflect(Debug, Default, Clone)]
pub enum ScalingMode {
/// Scale the texture uniformly (maintain the texture's aspect ratio)
/// so that both dimensions (width and height) of the texture will be equal
/// to or larger than the corresponding dimension of the target rectangle.
/// Fill sprite with a centered texture.
#[default]
FillCenter,
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
/// One dimension of the texture will match the rectangle's size,
/// while the other dimension may exceed it.
/// The exceeding portion is aligned to the start:
/// * Horizontal overflow is left-aligned if the width exceeds the rectangle.
/// * Vertical overflow is top-aligned if the height exceeds the rectangle.
FillStart,
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
/// One dimension of the texture will match the rectangle's size,
/// while the other dimension may exceed it.
/// The exceeding portion is aligned to the end:
/// * Horizontal overflow is right-aligned if the width exceeds the rectangle.
/// * Vertical overflow is bottom-aligned if the height exceeds the rectangle.
FillEnd,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside the rect.
/// At least one axis (x or y) will fit exactly. The result is centered inside the rect.
FitCenter,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside rect.
/// At least one axis (x or y) will fit exactly.
/// Aligns the result to the left and top edges of rect.
FitStart,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside rect.
/// At least one axis (x or y) will fit exactly.
/// Aligns the result to the right and bottom edges of rect.
FitEnd,
}
/// How a sprite is positioned relative to its [`Transform`].
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[doc(alias = "pivot")]
pub enum Anchor {
#[default]
Center,
BottomLeft,
BottomCenter,
BottomRight,
CenterLeft,
CenterRight,
TopLeft,
TopCenter,
TopRight,
/// Custom anchor point. Top left is `(-0.5, 0.5)`, center is `(0.0, 0.0)`. The value will
/// be scaled with the sprite size.
Custom(Vec2),
}
impl Anchor {
pub fn as_vec(&self) -> Vec2 {
match self {
Anchor::Center => Vec2::ZERO,
Anchor::BottomLeft => Vec2::new(-0.5, -0.5),
Anchor::BottomCenter => Vec2::new(0.0, -0.5),
Anchor::BottomRight => Vec2::new(0.5, -0.5),
Anchor::CenterLeft => Vec2::new(-0.5, 0.0),
Anchor::CenterRight => Vec2::new(0.5, 0.0),
Anchor::TopLeft => Vec2::new(-0.5, 0.5),
Anchor::TopCenter => Vec2::new(0.0, 0.5),
Anchor::TopRight => Vec2::new(0.5, 0.5),
Anchor::Custom(point) => *point,
}
}
}
#[cfg(test)]
mod tests {
use bevy_asset::{Assets, RenderAssetUsages};
use bevy_color::Color;
use bevy_image::Image;
use bevy_image::{TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, URect, UVec2, Vec2};
use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use crate::Anchor;
use super::Sprite;
/// Makes a new image of the specified size.
fn make_image(size: UVec2) -> Image {
Image::new_fill(
Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::Rgba8Unorm,
RenderAssetUsages::all(),
)
}
#[test]
fn compute_pixel_space_point_for_regular_sprite() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(3.0, 0.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-3.0, 0.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_color_sprite() {
let image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
// This also tests the `custom_size` field.
let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0));
let compute = |point| {
sprite
.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets)
// Round to remove floating point errors.
.map(|x| (x * 1e5).round() / 1e5)
.map_err(|x| (x * 1e5).round() / 1e5)
};
assert_eq!(compute(Vec2::new(-20.0, -40.0)), Ok(Vec2::new(0.1, 0.9)));
assert_eq!(compute(Vec2::new(0.0, 10.0)), Ok(Vec2::new(0.5, 0.4)));
assert_eq!(compute(Vec2::new(75.0, 100.0)), Err(Vec2::new(2.0, -0.5)));
assert_eq!(compute(Vec2::new(-75.0, -100.0)), Err(Vec2::new(-1.0, 1.5)));
assert_eq!(compute(Vec2::new(-30.0, -40.0)), Err(Vec2::new(-0.1, 0.9)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_top_right() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::TopRight,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
flip_x: true,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::TopRight,
flip_y: true,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5)));
assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_rect() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)),
anchor: Anchor::BottomLeft,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0)));
// The pixel is outside the rect, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0)));
}
#[test]
fn compute_pixel_space_point_for_texture_atlas_sprite() {
let mut image_assets = Assets::<Image>::default();
let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
size: UVec2::new(5, 10),
textures: vec![URect::new(1, 1, 4, 4)],
});
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
}),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5)));
}
#[test]
fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() {
let mut image_assets = Assets::<Image>::default();
let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
size: UVec2::new(5, 10),
textures: vec![URect::new(1, 1, 4, 4)],
});
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
}),
// The rect is relative to the texture atlas sprite.
rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
custom_size: Some(Vec2::new(100.0, 50.0)),
rect: Some(Rect::new(0.0, 0.0, 5.0, 5.0)),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0)));
assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0)));
}
}

View File

@@ -0,0 +1,64 @@
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Defines the extents of the border of a rectangle.
///
/// This struct is used to represent thickness or offsets from the edges
/// of a rectangle (left, right, top, and bottom), with values increasing inwards.
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq, Default)]
pub struct BorderRect {
/// Extent of the border along the left edge
pub left: f32,
/// Extent of the border along the right edge
pub right: f32,
/// Extent of the border along the top edge
pub top: f32,
/// Extent of the border along the bottom edge
pub bottom: f32,
}
impl BorderRect {
/// An empty border with zero thickness along each edge
pub const ZERO: Self = Self::all(0.);
/// Creates a border with the same `extent` along each edge
#[must_use]
#[inline]
pub const fn all(extent: f32) -> Self {
Self {
left: extent,
right: extent,
top: extent,
bottom: extent,
}
}
/// Creates a new border with the `left` and `right` extents equal to `horizontal`, and `top` and `bottom` extents equal to `vertical`.
#[must_use]
#[inline]
pub const fn axes(horizontal: f32, vertical: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
}
impl From<f32> for BorderRect {
fn from(extent: f32) -> Self {
Self::all(extent)
}
}
impl From<[f32; 4]> for BorderRect {
fn from([left, right, top, bottom]: [f32; 4]) -> Self {
Self {
left,
right,
top,
bottom,
}
}
}

View File

@@ -0,0 +1,156 @@
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets};
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{Rect, Vec2};
use bevy_platform::collections::HashSet;
/// Component storing texture slices for tiled or sliced sprite entities
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
pub struct ComputedTextureSlices(Vec<TextureSlice>);
impl ComputedTextureSlices {
/// Computes [`ExtractedSlice`] iterator from the sprite slices
///
/// # Arguments
///
/// * `sprite` - The sprite component
#[must_use]
pub(crate) fn extract_slices<'a>(
&'a self,
sprite: &'a Sprite,
) -> impl ExactSizeIterator<Item = ExtractedSlice> + 'a {
let mut flip = Vec2::ONE;
if sprite.flip_x {
flip.x *= -1.0;
}
if sprite.flip_y {
flip.y *= -1.0;
}
let anchor = sprite.anchor.as_vec()
* sprite
.custom_size
.unwrap_or(sprite.rect.unwrap_or_default().size());
self.0.iter().map(move |slice| ExtractedSlice {
offset: slice.offset * flip - anchor,
rect: slice.texture_rect,
size: slice.draw_size,
})
}
}
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if the image asset is not loaded
///
/// # Arguments
///
/// * `sprite` - The sprite component with the image handle and image mode
/// * `images` - The image assets, use to retrieve the image dimensions
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
images: &Assets<Image>,
atlas_layouts: &Assets<TextureAtlasLayout>,
) -> Option<ComputedTextureSlices> {
let (image_size, texture_rect) = match &sprite.texture_atlas {
Some(a) => {
let layout = atlas_layouts.get(&a.layout)?;
(
layout.size.as_vec2(),
layout.textures.get(a.index)?.as_rect(),
)
}
None => {
let image = images.get(&sprite.image)?;
let size = Vec2::new(
image.texture_descriptor.size.width as f32,
image.texture_descriptor.size.height as f32,
);
let rect = sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
max: size,
});
(size, rect)
}
};
let slices = match &sprite.image_mode {
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => {
let slice = TextureSlice {
texture_rect,
draw_size: sprite.custom_size.unwrap_or(image_size),
offset: Vec2::ZERO,
};
slice.tiled(*stretch_value, (*tile_x, *tile_y))
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
}
SpriteImageMode::Scale(_) => {
unreachable!("Slices should not be computed for SpriteImageMode::Scale")
}
};
Some(ComputedTextureSlices(slices))
}
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on sprite entities with a matching [`SpriteImageMode`]
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
sprites: Query<(Entity, &Sprite)>,
) {
// We store the asset ids of added/modified image assets
let added_handles: HashSet<_> = events
.read()
.filter_map(|e| match e {
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.collect();
if added_handles.is_empty() {
return;
}
// We recompute the sprite slices for sprite entities with a matching asset handle id
for (entity, sprite) in &sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if !added_handles.contains(&sprite.image.id()) {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
) {
for (entity, sprite) in &changed_sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}

View File

@@ -0,0 +1,93 @@
mod border_rect;
mod computed_slices;
mod slicer;
use bevy_math::{Rect, Vec2};
pub use border_rect::BorderRect;
pub use slicer::{SliceScaleMode, TextureSlicer};
pub(crate) use computed_slices::{
compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices,
};
/// Single texture slice, representing a texture rect to draw in a given area
#[derive(Debug, Clone, PartialEq)]
pub struct TextureSlice {
/// texture area to draw
pub texture_rect: Rect,
/// slice draw size
pub draw_size: Vec2,
/// offset of the slice
pub offset: Vec2,
}
impl TextureSlice {
/// Transforms the given slice in a collection of tiled subdivisions.
///
/// # Arguments
///
/// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* (rect) are above `stretch_value`.
/// * `tile_x` - should the slice be tiled horizontally
/// * `tile_y` - should the slice be tiled vertically
#[must_use]
pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec<Self> {
if !tile_x && !tile_y {
return vec![self];
}
let stretch_value = stretch_value.max(0.001);
let rect_size = self.texture_rect.size();
// Each tile expected size
let expected_size = Vec2::new(
if tile_x {
// No slice should be less than 1 pixel wide
(rect_size.x * stretch_value).max(1.0)
} else {
self.draw_size.x
},
if tile_y {
// No slice should be less than 1 pixel high
(rect_size.y * stretch_value).max(1.0)
} else {
self.draw_size.y
},
)
.min(self.draw_size);
let mut slices = Vec::new();
let base_offset = Vec2::new(
-self.draw_size.x / 2.0,
self.draw_size.y / 2.0, // Start from top
);
let mut offset = base_offset;
let mut remaining_columns = self.draw_size.y;
while remaining_columns > 0.0 {
let size_y = expected_size.y.min(remaining_columns);
offset.x = base_offset.x;
offset.y -= size_y / 2.0;
let mut remaining_rows = self.draw_size.x;
while remaining_rows > 0.0 {
let size_x = expected_size.x.min(remaining_rows);
offset.x += size_x / 2.0;
let draw_size = Vec2::new(size_x, size_y);
let delta = draw_size / expected_size;
slices.push(Self {
texture_rect: Rect {
min: self.texture_rect.min,
max: self.texture_rect.min + self.texture_rect.size() * delta,
},
draw_size,
offset: self.offset + offset,
});
offset.x += size_x / 2.0;
remaining_rows -= size_x;
}
offset.y -= size_y / 2.0;
remaining_columns -= size_y;
}
if slices.len() > 1_000 {
tracing::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len());
}
slices
}
}

View File

@@ -0,0 +1,436 @@
use super::{BorderRect, TextureSlice};
use bevy_math::{vec2, Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes
/// without needing to prepare multiple assets. The associated texture will be split into nine portions,
/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion.
///
/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect, PartialEq)]
#[reflect(Clone, PartialEq)]
pub struct TextureSlicer {
/// Inset values in pixels that define the four slicing lines dividing the texture into nine sections.
pub border: BorderRect,
/// Defines how the center part of the 9 slices will scale
pub center_scale_mode: SliceScaleMode,
/// Defines how the 4 side parts of the 9 slices will scale
pub sides_scale_mode: SliceScaleMode,
/// Defines the maximum scale of the 4 corner slices (default to `1.0`)
pub max_corner_scale: f32,
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
#[reflect(Clone, PartialEq, Default)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]
Stretch,
/// The slice will be tiled to fit the area
Tile {
/// The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above `stretch_value`.
///
/// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels.
/// `2.0` means it would repeat after 20 screen pixels.
///
/// Note: The value should be inferior or equal to `1.0` to avoid quality loss.
///
/// Note: the value will be clamped to `0.001` if lower
stretch_value: f32,
},
}
impl TextureSlicer {
/// Computes the 4 corner slices: top left, top right, bottom left, bottom right.
#[must_use]
fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {
let coef = render_size / base_rect.size();
let BorderRect {
left,
right,
top,
bottom,
} = self.border;
let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);
[
// Top Left Corner
TextureSlice {
texture_rect: Rect {
min: base_rect.min,
max: base_rect.min + vec2(left, top),
},
draw_size: vec2(left, top) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Top Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.min.y),
max: vec2(base_rect.max.x, base_rect.min.y + top),
},
draw_size: vec2(right, top) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Bottom Left
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.min.x, base_rect.max.y - bottom),
max: vec2(base_rect.min.x + left, base_rect.max.y),
},
draw_size: vec2(left, bottom) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
// Bottom Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),
max: base_rect.max,
},
draw_size: vec2(right, bottom) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
]
}
/// Computes the 2 horizontal side slices (left and right borders)
#[must_use]
fn horizontal_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Left
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(0.0, self.border.top),
max: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
},
draw_size: vec2(
tl_corner.draw_size.x,
render_size.y - (tl_corner.draw_size.y + bl_corner.draw_size.y),
),
offset: vec2(
tl_corner.draw_size.x - render_size.x,
bl_corner.draw_size.y - tl_corner.draw_size.y,
) / 2.0,
},
// Right
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
max: base_rect.max - vec2(0.0, self.border.bottom),
},
draw_size: vec2(
tr_corner.draw_size.x,
render_size.y - (tr_corner.draw_size.y + br_corner.draw_size.y),
),
offset: vec2(
render_size.x - tr_corner.draw_size.x,
br_corner.draw_size.y - tr_corner.draw_size.y,
) / 2.0,
},
]
}
/// Computes the 2 vertical side slices (top and bottom borders)
#[must_use]
fn vertical_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Top
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(self.border.left, 0.0),
max: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
},
draw_size: vec2(
render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),
tl_corner.draw_size.y,
),
offset: vec2(
tl_corner.draw_size.x - tr_corner.draw_size.x,
render_size.y - tl_corner.draw_size.y,
) / 2.0,
},
// Bottom
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
max: base_rect.max - vec2(self.border.right, 0.0),
},
draw_size: vec2(
render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),
bl_corner.draw_size.y,
),
offset: vec2(
bl_corner.draw_size.x - br_corner.draw_size.x,
bl_corner.draw_size.y - render_size.y,
) / 2.0,
},
]
}
/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,
/// a bigger number of sections will be computed.
///
/// # Arguments
///
/// * `rect` - The section of the texture to slice in 9 parts
/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.
// TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`)
#[must_use]
pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {
let render_size = render_size.unwrap_or_else(|| rect.size());
if self.border.left + self.border.right >= rect.size().x
|| self.border.top + self.border.bottom >= rect.size().y
{
tracing::error!(
"TextureSlicer::border has out of bounds values. No slicing will be applied"
);
return vec![TextureSlice {
texture_rect: rect,
draw_size: render_size,
offset: Vec2::ZERO,
}];
}
let mut slices = Vec::with_capacity(9);
// Corners are in this order: [TL, TR, BL, BR]
let corners = self.corner_slices(rect, render_size);
// Vertical Sides: [T, B]
let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);
// Horizontal Sides: [L, R]
let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);
// Center
let center = TextureSlice {
texture_rect: Rect {
min: rect.min + vec2(self.border.left, self.border.top),
max: rect.max - vec2(self.border.right, self.border.bottom),
},
draw_size: vec2(
render_size.x - (corners[0].draw_size.x + corners[1].draw_size.x),
render_size.y - (corners[0].draw_size.y + corners[2].draw_size.y),
),
offset: vec2(vertical_sides[0].offset.x, horizontal_sides[0].offset.y),
};
slices.extend(corners);
match self.center_scale_mode {
SliceScaleMode::Stretch => {
slices.push(center);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(center.tiled(stretch_value, (true, true)));
}
}
match self.sides_scale_mode {
SliceScaleMode::Stretch => {
slices.extend(horizontal_sides);
slices.extend(vertical_sides);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(
horizontal_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (false, true))),
);
slices.extend(
vertical_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (true, false))),
);
}
}
slices
}
}
impl Default for TextureSlicer {
fn default() -> Self {
Self {
border: Default::default(),
center_scale_mode: Default::default(),
sides_scale_mode: Default::default(),
max_corner_scale: 1.0,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_horizontal_sizes_uniform() {
let slicer = TextureSlicer {
border: BorderRect {
left: 10.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::splat(10.0)
},
draw_size: Vec2::new(10.0, 10.0),
offset: Vec2::new(-45.0, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_bigger() {
let slicer = TextureSlicer {
border: BorderRect {
left: 20.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(20.0, 10.0)
},
draw_size: Vec2::new(20.0, 10.0),
offset: Vec2::new(-40.0, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_smaller() {
let slicer = TextureSlicer {
border: BorderRect {
left: 5.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_size = Vec2::splat(100.);
let corners = slicer.corner_slices(rect, render_size);
let vertical_sides = slicer.vertical_side_slices(&corners, rect, render_size);
assert_eq!(
corners[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(5.0, 10.0)
},
draw_size: Vec2::new(5.0, 10.0),
offset: Vec2::new(-47.5, 45.0),
}
);
assert_eq!(
vertical_sides[0], // top
TextureSlice {
texture_rect: Rect {
min: Vec2::new(5.0, 0.0),
max: Vec2::new(40.0, 10.0)
},
draw_size: Vec2::new(85.0, 10.0),
offset: Vec2::new(-2.5, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_zero() {
let slicer = TextureSlicer {
border: BorderRect {
left: 0.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(0.0, 10.0)
},
draw_size: Vec2::new(0.0, 10.0),
offset: Vec2::new(-50.0, 45.0),
}
);
}
}