Path: blob/main/crates/bevy_sprite_render/src/render/mod.rs
6604 views
use core::ops::Range;12use crate::ComputedTextureSlices;3use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, AssetServer, Assets, Handle};4use bevy_camera::visibility::ViewVisibility;5use bevy_color::{ColorToComponents, LinearRgba};6use bevy_core_pipeline::{7core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT},8tonemapping::{9get_lut_bind_group_layout_entries, get_lut_bindings, DebandDither, Tonemapping,10TonemappingLuts,11},12};13use bevy_derive::{Deref, DerefMut};14use bevy_ecs::{15prelude::*,16query::ROQueryItem,17system::{lifetimeless::*, SystemParamItem},18};19use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo};20use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4};21use bevy_mesh::VertexBufferLayout;22use bevy_platform::collections::HashMap;23use bevy_render::view::{RenderVisibleEntities, RetainedViewEntity};24use bevy_render::{25render_asset::RenderAssets,26render_phase::{27DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,28SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases,29},30render_resource::{31binding_types::{sampler, texture_2d, uniform_buffer},32*,33},34renderer::{RenderDevice, RenderQueue},35sync_world::RenderEntity,36texture::{DefaultImageSampler, FallbackImage, GpuImage},37view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms},38Extract,39};40use bevy_shader::{Shader, ShaderDefVal};41use bevy_sprite::{Anchor, ScalingMode, Sprite};42use bevy_transform::components::GlobalTransform;43use bevy_utils::default;44use bytemuck::{Pod, Zeroable};45use fixedbitset::FixedBitSet;4647#[derive(Resource)]48pub struct SpritePipeline {49view_layout: BindGroupLayout,50material_layout: BindGroupLayout,51shader: Handle<Shader>,52pub dummy_white_gpu_image: GpuImage,53}5455pub fn init_sprite_pipeline(56mut commands: Commands,57render_device: Res<RenderDevice>,58default_sampler: Res<DefaultImageSampler>,59render_queue: Res<RenderQueue>,60asset_server: Res<AssetServer>,61) {62let tonemapping_lut_entries = get_lut_bind_group_layout_entries();63let view_layout = render_device.create_bind_group_layout(64"sprite_view_layout",65&BindGroupLayoutEntries::sequential(66ShaderStages::VERTEX_FRAGMENT,67(68uniform_buffer::<ViewUniform>(true),69tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT),70tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT),71),72),73);7475let material_layout = render_device.create_bind_group_layout(76"sprite_material_layout",77&BindGroupLayoutEntries::sequential(78ShaderStages::FRAGMENT,79(80texture_2d(TextureSampleType::Float { filterable: true }),81sampler(SamplerBindingType::Filtering),82),83),84);85let dummy_white_gpu_image = {86let image = Image::default();87let texture = render_device.create_texture(&image.texture_descriptor);88let sampler = match image.sampler {89ImageSampler::Default => (**default_sampler).clone(),90ImageSampler::Descriptor(ref descriptor) => {91render_device.create_sampler(&descriptor.as_wgpu())92}93};9495if let Ok(format_size) = image.texture_descriptor.format.pixel_size() {96render_queue.write_texture(97texture.as_image_copy(),98image.data.as_ref().expect("Image has no data"),99TexelCopyBufferLayout {100offset: 0,101bytes_per_row: Some(image.width() * format_size as u32),102rows_per_image: None,103},104image.texture_descriptor.size,105);106}107let texture_view = texture.create_view(&TextureViewDescriptor::default());108GpuImage {109texture,110texture_view,111texture_format: image.texture_descriptor.format,112sampler,113size: image.texture_descriptor.size,114mip_level_count: image.texture_descriptor.mip_level_count,115}116};117118commands.insert_resource(SpritePipeline {119view_layout,120material_layout,121dummy_white_gpu_image,122shader: load_embedded_asset!(asset_server.as_ref(), "sprite.wgsl"),123});124}125126bitflags::bitflags! {127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]128#[repr(transparent)]129// NOTE: Apparently quadro drivers support up to 64x MSAA.130// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA.131pub struct SpritePipelineKey: u32 {132const NONE = 0;133const HDR = 1 << 0;134const TONEMAP_IN_SHADER = 1 << 1;135const DEBAND_DITHER = 1 << 2;136const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;137const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS;138const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS;139const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS;140const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS;141const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS;142const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS;143const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS;144const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;145const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;146}147}148149impl SpritePipelineKey {150const MSAA_MASK_BITS: u32 = 0b111;151const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();152const TONEMAP_METHOD_MASK_BITS: u32 = 0b111;153const TONEMAP_METHOD_SHIFT_BITS: u32 =154Self::MSAA_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones();155156#[inline]157pub const fn from_msaa_samples(msaa_samples: u32) -> Self {158let msaa_bits =159(msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS;160Self::from_bits_retain(msaa_bits)161}162163#[inline]164pub const fn msaa_samples(&self) -> u32 {1651 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS)166}167168#[inline]169pub const fn from_hdr(hdr: bool) -> Self {170if hdr {171SpritePipelineKey::HDR172} else {173SpritePipelineKey::NONE174}175}176}177178impl SpecializedRenderPipeline for SpritePipeline {179type Key = SpritePipelineKey;180181fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {182let mut shader_defs = Vec::new();183if key.contains(SpritePipelineKey::TONEMAP_IN_SHADER) {184shader_defs.push("TONEMAP_IN_SHADER".into());185shader_defs.push(ShaderDefVal::UInt(186"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),1871,188));189shader_defs.push(ShaderDefVal::UInt(190"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),1912,192));193194let method = key.intersection(SpritePipelineKey::TONEMAP_METHOD_RESERVED_BITS);195196if method == SpritePipelineKey::TONEMAP_METHOD_NONE {197shader_defs.push("TONEMAP_METHOD_NONE".into());198} else if method == SpritePipelineKey::TONEMAP_METHOD_REINHARD {199shader_defs.push("TONEMAP_METHOD_REINHARD".into());200} else if method == SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE {201shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());202} else if method == SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED {203shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into());204} else if method == SpritePipelineKey::TONEMAP_METHOD_AGX {205shader_defs.push("TONEMAP_METHOD_AGX".into());206} else if method == SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM207{208shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());209} else if method == SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC {210shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());211} else if method == SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE {212shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());213}214215// Debanding is tied to tonemapping in the shader, cannot run without it.216if key.contains(SpritePipelineKey::DEBAND_DITHER) {217shader_defs.push("DEBAND_DITHER".into());218}219}220221let format = match key.contains(SpritePipelineKey::HDR) {222true => ViewTarget::TEXTURE_FORMAT_HDR,223false => TextureFormat::bevy_default(),224};225226let instance_rate_vertex_buffer_layout = VertexBufferLayout {227array_stride: 80,228step_mode: VertexStepMode::Instance,229attributes: vec![230// @location(0) i_model_transpose_col0: vec4<f32>,231VertexAttribute {232format: VertexFormat::Float32x4,233offset: 0,234shader_location: 0,235},236// @location(1) i_model_transpose_col1: vec4<f32>,237VertexAttribute {238format: VertexFormat::Float32x4,239offset: 16,240shader_location: 1,241},242// @location(2) i_model_transpose_col2: vec4<f32>,243VertexAttribute {244format: VertexFormat::Float32x4,245offset: 32,246shader_location: 2,247},248// @location(3) i_color: vec4<f32>,249VertexAttribute {250format: VertexFormat::Float32x4,251offset: 48,252shader_location: 3,253},254// @location(4) i_uv_offset_scale: vec4<f32>,255VertexAttribute {256format: VertexFormat::Float32x4,257offset: 64,258shader_location: 4,259},260],261};262263RenderPipelineDescriptor {264vertex: VertexState {265shader: self.shader.clone(),266shader_defs: shader_defs.clone(),267buffers: vec![instance_rate_vertex_buffer_layout],268..default()269},270fragment: Some(FragmentState {271shader: self.shader.clone(),272shader_defs,273targets: vec![Some(ColorTargetState {274format,275blend: Some(BlendState::ALPHA_BLENDING),276write_mask: ColorWrites::ALL,277})],278..default()279}),280layout: vec![self.view_layout.clone(), self.material_layout.clone()],281// Sprites are always alpha blended so they never need to write to depth.282// They just need to read it in case an opaque mesh2d283// that wrote to depth is present.284depth_stencil: Some(DepthStencilState {285format: CORE_2D_DEPTH_FORMAT,286depth_write_enabled: false,287depth_compare: CompareFunction::GreaterEqual,288stencil: StencilState {289front: StencilFaceState::IGNORE,290back: StencilFaceState::IGNORE,291read_mask: 0,292write_mask: 0,293},294bias: DepthBiasState {295constant: 0,296slope_scale: 0.0,297clamp: 0.0,298},299}),300multisample: MultisampleState {301count: key.msaa_samples(),302mask: !0,303alpha_to_coverage_enabled: false,304},305label: Some("sprite_pipeline".into()),306..default()307}308}309}310311pub struct ExtractedSlice {312pub offset: Vec2,313pub rect: Rect,314pub size: Vec2,315}316317pub struct ExtractedSprite {318pub main_entity: Entity,319pub render_entity: Entity,320pub transform: GlobalTransform,321pub color: LinearRgba,322/// Change the on-screen size of the sprite323/// Asset ID of the [`Image`] of this sprite324/// PERF: storing an `AssetId` instead of `Handle<Image>` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped)325pub image_handle_id: AssetId<Image>,326pub flip_x: bool,327pub flip_y: bool,328pub kind: ExtractedSpriteKind,329}330331pub enum ExtractedSpriteKind {332/// A single sprite with custom sizing and scaling options333Single {334anchor: Vec2,335rect: Option<Rect>,336scaling_mode: Option<ScalingMode>,337custom_size: Option<Vec2>,338},339/// Indexes into the list of [`ExtractedSlice`]s stored in the [`ExtractedSlices`] resource340/// Used for elements composed from multiple sprites such as text or nine-patched borders341Slices { indices: Range<usize> },342}343344#[derive(Resource, Default)]345pub struct ExtractedSprites {346pub sprites: Vec<ExtractedSprite>,347}348349#[derive(Resource, Default)]350pub struct ExtractedSlices {351pub slices: Vec<ExtractedSlice>,352}353354#[derive(Resource, Default)]355pub struct SpriteAssetEvents {356pub images: Vec<AssetEvent<Image>>,357}358359pub fn extract_sprite_events(360mut events: ResMut<SpriteAssetEvents>,361mut image_events: Extract<EventReader<AssetEvent<Image>>>,362) {363let SpriteAssetEvents { ref mut images } = *events;364images.clear();365366for event in image_events.read() {367images.push(*event);368}369}370371pub fn extract_sprites(372mut extracted_sprites: ResMut<ExtractedSprites>,373mut extracted_slices: ResMut<ExtractedSlices>,374texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,375sprite_query: Extract<376Query<(377Entity,378RenderEntity,379&ViewVisibility,380&Sprite,381&GlobalTransform,382&Anchor,383Option<&ComputedTextureSlices>,384)>,385>,386) {387extracted_sprites.sprites.clear();388extracted_slices.slices.clear();389for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in390sprite_query.iter()391{392if !view_visibility.get() {393continue;394}395396if let Some(slices) = slices {397let start = extracted_slices.slices.len();398extracted_slices399.slices400.extend(slices.extract_slices(sprite, anchor.as_vec()));401let end = extracted_slices.slices.len();402extracted_sprites.sprites.push(ExtractedSprite {403main_entity,404render_entity,405color: sprite.color.into(),406transform: *transform,407flip_x: sprite.flip_x,408flip_y: sprite.flip_y,409image_handle_id: sprite.image.id(),410kind: ExtractedSpriteKind::Slices {411indices: start..end,412},413});414} else {415let atlas_rect = sprite416.texture_atlas417.as_ref()418.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));419let rect = match (atlas_rect, sprite.rect) {420(None, None) => None,421(None, Some(sprite_rect)) => Some(sprite_rect),422(Some(atlas_rect), None) => Some(atlas_rect),423(Some(atlas_rect), Some(mut sprite_rect)) => {424sprite_rect.min += atlas_rect.min;425sprite_rect.max += atlas_rect.min;426Some(sprite_rect)427}428};429430// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive431extracted_sprites.sprites.push(ExtractedSprite {432main_entity,433render_entity,434color: sprite.color.into(),435transform: *transform,436flip_x: sprite.flip_x,437flip_y: sprite.flip_y,438image_handle_id: sprite.image.id(),439kind: ExtractedSpriteKind::Single {440anchor: anchor.as_vec(),441rect,442scaling_mode: sprite.image_mode.scale(),443// Pass the custom size444custom_size: sprite.custom_size,445},446});447}448}449}450451#[repr(C)]452#[derive(Copy, Clone, Pod, Zeroable)]453struct SpriteInstance {454// Affine 4x3 transposed to 3x4455pub i_model_transpose: [Vec4; 3],456pub i_color: [f32; 4],457pub i_uv_offset_scale: [f32; 4],458}459460impl SpriteInstance {461#[inline]462fn from(transform: &Affine3A, color: &LinearRgba, uv_offset_scale: &Vec4) -> Self {463let transpose_model_3x3 = transform.matrix3.transpose();464Self {465i_model_transpose: [466transpose_model_3x3.x_axis.extend(transform.translation.x),467transpose_model_3x3.y_axis.extend(transform.translation.y),468transpose_model_3x3.z_axis.extend(transform.translation.z),469],470i_color: color.to_f32_array(),471i_uv_offset_scale: uv_offset_scale.to_array(),472}473}474}475476#[derive(Resource)]477pub struct SpriteMeta {478sprite_index_buffer: RawBufferVec<u32>,479sprite_instance_buffer: RawBufferVec<SpriteInstance>,480}481482impl Default for SpriteMeta {483fn default() -> Self {484Self {485sprite_index_buffer: RawBufferVec::<u32>::new(BufferUsages::INDEX),486sprite_instance_buffer: RawBufferVec::<SpriteInstance>::new(BufferUsages::VERTEX),487}488}489}490491#[derive(Component)]492pub struct SpriteViewBindGroup {493pub value: BindGroup,494}495496#[derive(Resource, Deref, DerefMut, Default)]497pub struct SpriteBatches(HashMap<(RetainedViewEntity, Entity), SpriteBatch>);498499#[derive(PartialEq, Eq, Clone, Debug)]500pub struct SpriteBatch {501image_handle_id: AssetId<Image>,502range: Range<u32>,503}504505#[derive(Resource, Default)]506pub struct ImageBindGroups {507values: HashMap<AssetId<Image>, BindGroup>,508}509510pub fn queue_sprites(511mut view_entities: Local<FixedBitSet>,512draw_functions: Res<DrawFunctions<Transparent2d>>,513sprite_pipeline: Res<SpritePipeline>,514mut pipelines: ResMut<SpecializedRenderPipelines<SpritePipeline>>,515pipeline_cache: Res<PipelineCache>,516extracted_sprites: Res<ExtractedSprites>,517mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,518mut views: Query<(519&RenderVisibleEntities,520&ExtractedView,521&Msaa,522Option<&Tonemapping>,523Option<&DebandDither>,524)>,525) {526let draw_sprite_function = draw_functions.read().id::<DrawSprite>();527528for (visible_entities, view, msaa, tonemapping, dither) in &mut views {529let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)530else {531continue;532};533534let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples());535let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key;536537if !view.hdr {538if let Some(tonemapping) = tonemapping {539view_key |= SpritePipelineKey::TONEMAP_IN_SHADER;540view_key |= match tonemapping {541Tonemapping::None => SpritePipelineKey::TONEMAP_METHOD_NONE,542Tonemapping::Reinhard => SpritePipelineKey::TONEMAP_METHOD_REINHARD,543Tonemapping::ReinhardLuminance => {544SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE545}546Tonemapping::AcesFitted => SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED,547Tonemapping::AgX => SpritePipelineKey::TONEMAP_METHOD_AGX,548Tonemapping::SomewhatBoringDisplayTransform => {549SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM550}551Tonemapping::TonyMcMapface => SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE,552Tonemapping::BlenderFilmic => SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC,553};554}555if let Some(DebandDither::Enabled) = dither {556view_key |= SpritePipelineKey::DEBAND_DITHER;557}558}559560let pipeline = pipelines.specialize(&pipeline_cache, &sprite_pipeline, view_key);561562view_entities.clear();563view_entities.extend(564visible_entities565.iter::<Sprite>()566.map(|(_, e)| e.index() as usize),567);568569transparent_phase570.items571.reserve(extracted_sprites.sprites.len());572573for (index, extracted_sprite) in extracted_sprites.sprites.iter().enumerate() {574let view_index = extracted_sprite.main_entity.index();575576if !view_entities.contains(view_index as usize) {577continue;578}579580// These items will be sorted by depth with other phase items581let sort_key = FloatOrd(extracted_sprite.transform.translation().z);582583// Add the item to the render phase584transparent_phase.add(Transparent2d {585draw_function: draw_sprite_function,586pipeline,587entity: (588extracted_sprite.render_entity,589extracted_sprite.main_entity.into(),590),591sort_key,592// `batch_range` is calculated in `prepare_sprite_image_bind_groups`593batch_range: 0..0,594extra_index: PhaseItemExtraIndex::None,595extracted_index: index,596indexed: true,597});598}599}600}601602pub fn prepare_sprite_view_bind_groups(603mut commands: Commands,604render_device: Res<RenderDevice>,605sprite_pipeline: Res<SpritePipeline>,606view_uniforms: Res<ViewUniforms>,607views: Query<(Entity, &Tonemapping), With<ExtractedView>>,608tonemapping_luts: Res<TonemappingLuts>,609images: Res<RenderAssets<GpuImage>>,610fallback_image: Res<FallbackImage>,611) {612let Some(view_binding) = view_uniforms.uniforms.binding() else {613return;614};615616for (entity, tonemapping) in &views {617let lut_bindings =618get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image);619let view_bind_group = render_device.create_bind_group(620"mesh2d_view_bind_group",621&sprite_pipeline.view_layout,622&BindGroupEntries::sequential((view_binding.clone(), lut_bindings.0, lut_bindings.1)),623);624625commands.entity(entity).insert(SpriteViewBindGroup {626value: view_bind_group,627});628}629}630631pub fn prepare_sprite_image_bind_groups(632render_device: Res<RenderDevice>,633render_queue: Res<RenderQueue>,634mut sprite_meta: ResMut<SpriteMeta>,635sprite_pipeline: Res<SpritePipeline>,636mut image_bind_groups: ResMut<ImageBindGroups>,637gpu_images: Res<RenderAssets<GpuImage>>,638extracted_sprites: Res<ExtractedSprites>,639extracted_slices: Res<ExtractedSlices>,640mut phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,641events: Res<SpriteAssetEvents>,642mut batches: ResMut<SpriteBatches>,643) {644// If an image has changed, the GpuImage has (probably) changed645for event in &events.images {646match event {647AssetEvent::Added { .. } |648// Images don't have dependencies649AssetEvent::LoadedWithDependencies { .. } => {}650AssetEvent::Unused { id } | AssetEvent::Modified { id } | AssetEvent::Removed { id } => {651image_bind_groups.values.remove(id);652}653};654}655656batches.clear();657658// Clear the sprite instances659sprite_meta.sprite_instance_buffer.clear();660661// Index buffer indices662let mut index = 0;663664let image_bind_groups = &mut *image_bind_groups;665666for (retained_view, transparent_phase) in phases.iter_mut() {667let mut current_batch = None;668let mut batch_item_index = 0;669let mut batch_image_size = Vec2::ZERO;670let mut batch_image_handle = AssetId::invalid();671672// Iterate through the phase items and detect when successive sprites that can be batched.673// Spawn an entity with a `SpriteBatch` component for each possible batch.674// Compatible items share the same entity.675for item_index in 0..transparent_phase.items.len() {676let item = &transparent_phase.items[item_index];677678let Some(extracted_sprite) = extracted_sprites679.sprites680.get(item.extracted_index)681.filter(|extracted_sprite| extracted_sprite.render_entity == item.entity())682else {683// If there is a phase item that is not a sprite, then we must start a new684// batch to draw the other phase item(s) and to respect draw order. This can be685// done by invalidating the batch_image_handle686batch_image_handle = AssetId::invalid();687continue;688};689690if batch_image_handle != extracted_sprite.image_handle_id {691let Some(gpu_image) = gpu_images.get(extracted_sprite.image_handle_id) else {692continue;693};694695batch_image_size = gpu_image.size_2d().as_vec2();696batch_image_handle = extracted_sprite.image_handle_id;697image_bind_groups698.values699.entry(batch_image_handle)700.or_insert_with(|| {701render_device.create_bind_group(702"sprite_material_bind_group",703&sprite_pipeline.material_layout,704&BindGroupEntries::sequential((705&gpu_image.texture_view,706&gpu_image.sampler,707)),708)709});710711batch_item_index = item_index;712current_batch = Some(batches.entry((*retained_view, item.entity())).insert(713SpriteBatch {714image_handle_id: batch_image_handle,715range: index..index,716},717));718}719match extracted_sprite.kind {720ExtractedSpriteKind::Single {721anchor,722rect,723scaling_mode,724custom_size,725} => {726// By default, the size of the quad is the size of the texture727let mut quad_size = batch_image_size;728let mut texture_size = batch_image_size;729730// Calculate vertex data for this item731// If a rect is specified, adjust UVs and the size of the quad732let mut uv_offset_scale = if let Some(rect) = rect {733let rect_size = rect.size();734quad_size = rect_size;735// Update texture size to the rect size736// It will help scale properly only portion of the image737texture_size = rect_size;738Vec4::new(739rect.min.x / batch_image_size.x,740rect.max.y / batch_image_size.y,741rect_size.x / batch_image_size.x,742-rect_size.y / batch_image_size.y,743)744} else {745Vec4::new(0.0, 1.0, 1.0, -1.0)746};747748if extracted_sprite.flip_x {749uv_offset_scale.x += uv_offset_scale.z;750uv_offset_scale.z *= -1.0;751}752if extracted_sprite.flip_y {753uv_offset_scale.y += uv_offset_scale.w;754uv_offset_scale.w *= -1.0;755}756757// Override the size if a custom one is specified758quad_size = custom_size.unwrap_or(quad_size);759760// Used for translation of the quad if `TextureScale::Fit...` is specified.761let mut quad_translation = Vec2::ZERO;762763// Scales the texture based on the `texture_scale` field.764if let Some(scaling_mode) = scaling_mode {765apply_scaling(766scaling_mode,767texture_size,768&mut quad_size,769&mut quad_translation,770&mut uv_offset_scale,771);772}773774let transform = extracted_sprite.transform.affine()775* Affine3A::from_scale_rotation_translation(776quad_size.extend(1.0),777Quat::IDENTITY,778((quad_size + quad_translation) * (-anchor - Vec2::splat(0.5)))779.extend(0.0),780);781782// Store the vertex data and add the item to the render phase783sprite_meta784.sprite_instance_buffer785.push(SpriteInstance::from(786&transform,787&extracted_sprite.color,788&uv_offset_scale,789));790791current_batch.as_mut().unwrap().get_mut().range.end += 1;792index += 1;793}794ExtractedSpriteKind::Slices { ref indices } => {795for i in indices.clone() {796let slice = &extracted_slices.slices[i];797let rect = slice.rect;798let rect_size = rect.size();799800// Calculate vertex data for this item801let mut uv_offset_scale: Vec4;802803// If a rect is specified, adjust UVs and the size of the quad804uv_offset_scale = Vec4::new(805rect.min.x / batch_image_size.x,806rect.max.y / batch_image_size.y,807rect_size.x / batch_image_size.x,808-rect_size.y / batch_image_size.y,809);810811if extracted_sprite.flip_x {812uv_offset_scale.x += uv_offset_scale.z;813uv_offset_scale.z *= -1.0;814}815if extracted_sprite.flip_y {816uv_offset_scale.y += uv_offset_scale.w;817uv_offset_scale.w *= -1.0;818}819820let transform = extracted_sprite.transform.affine()821* Affine3A::from_scale_rotation_translation(822slice.size.extend(1.0),823Quat::IDENTITY,824(slice.size * -Vec2::splat(0.5) + slice.offset).extend(0.0),825);826827// Store the vertex data and add the item to the render phase828sprite_meta829.sprite_instance_buffer830.push(SpriteInstance::from(831&transform,832&extracted_sprite.color,833&uv_offset_scale,834));835836current_batch.as_mut().unwrap().get_mut().range.end += 1;837index += 1;838}839}840}841transparent_phase.items[batch_item_index]842.batch_range_mut()843.end += 1;844}845sprite_meta846.sprite_instance_buffer847.write_buffer(&render_device, &render_queue);848849if sprite_meta.sprite_index_buffer.len() != 6 {850sprite_meta.sprite_index_buffer.clear();851852// NOTE: This code is creating 6 indices pointing to 4 vertices.853// The vertices form the corners of a quad based on their two least significant bits.854// 10 11855//856// 00 01857// The sprite shader can then use the two least significant bits as the vertex index.858// The rest of the properties to transform the vertex positions and UVs (which are859// implicit) are baked into the instance transform, and UV offset and scale.860// See bevy_sprite_render/src/render/sprite.wgsl for the details.861sprite_meta.sprite_index_buffer.push(2);862sprite_meta.sprite_index_buffer.push(0);863sprite_meta.sprite_index_buffer.push(1);864sprite_meta.sprite_index_buffer.push(1);865sprite_meta.sprite_index_buffer.push(3);866sprite_meta.sprite_index_buffer.push(2);867868sprite_meta869.sprite_index_buffer870.write_buffer(&render_device, &render_queue);871}872}873}874/// [`RenderCommand`] for sprite rendering.875pub type DrawSprite = (876SetItemPipeline,877SetSpriteViewBindGroup<0>,878SetSpriteTextureBindGroup<1>,879DrawSpriteBatch,880);881882pub struct SetSpriteViewBindGroup<const I: usize>;883impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSpriteViewBindGroup<I> {884type Param = ();885type ViewQuery = (Read<ViewUniformOffset>, Read<SpriteViewBindGroup>);886type ItemQuery = ();887888fn render<'w>(889_item: &P,890(view_uniform, sprite_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>,891_entity: Option<()>,892_param: SystemParamItem<'w, '_, Self::Param>,893pass: &mut TrackedRenderPass<'w>,894) -> RenderCommandResult {895pass.set_bind_group(I, &sprite_view_bind_group.value, &[view_uniform.offset]);896RenderCommandResult::Success897}898}899pub struct SetSpriteTextureBindGroup<const I: usize>;900impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSpriteTextureBindGroup<I> {901type Param = (SRes<ImageBindGroups>, SRes<SpriteBatches>);902type ViewQuery = Read<ExtractedView>;903type ItemQuery = ();904905fn render<'w>(906item: &P,907view: ROQueryItem<'w, '_, Self::ViewQuery>,908_entity: Option<()>,909(image_bind_groups, batches): SystemParamItem<'w, '_, Self::Param>,910pass: &mut TrackedRenderPass<'w>,911) -> RenderCommandResult {912let image_bind_groups = image_bind_groups.into_inner();913let Some(batch) = batches.get(&(view.retained_view_entity, item.entity())) else {914return RenderCommandResult::Skip;915};916917pass.set_bind_group(918I,919image_bind_groups920.values921.get(&batch.image_handle_id)922.unwrap(),923&[],924);925RenderCommandResult::Success926}927}928929pub struct DrawSpriteBatch;930impl<P: PhaseItem> RenderCommand<P> for DrawSpriteBatch {931type Param = (SRes<SpriteMeta>, SRes<SpriteBatches>);932type ViewQuery = Read<ExtractedView>;933type ItemQuery = ();934935fn render<'w>(936item: &P,937view: ROQueryItem<'w, '_, Self::ViewQuery>,938_entity: Option<()>,939(sprite_meta, batches): SystemParamItem<'w, '_, Self::Param>,940pass: &mut TrackedRenderPass<'w>,941) -> RenderCommandResult {942let sprite_meta = sprite_meta.into_inner();943let Some(batch) = batches.get(&(view.retained_view_entity, item.entity())) else {944return RenderCommandResult::Skip;945};946947pass.set_index_buffer(948sprite_meta.sprite_index_buffer.buffer().unwrap().slice(..),9490,950IndexFormat::Uint32,951);952pass.set_vertex_buffer(9530,954sprite_meta955.sprite_instance_buffer956.buffer()957.unwrap()958.slice(..),959);960pass.draw_indexed(0..6, 0, batch.range.clone());961RenderCommandResult::Success962}963}964965/// Scales a texture to fit within a given quad size with keeping the aspect ratio.966fn apply_scaling(967scaling_mode: ScalingMode,968texture_size: Vec2,969quad_size: &mut Vec2,970quad_translation: &mut Vec2,971uv_offset_scale: &mut Vec4,972) {973let quad_ratio = quad_size.x / quad_size.y;974let texture_ratio = texture_size.x / texture_size.y;975let tex_quad_scale = texture_ratio / quad_ratio;976let quad_tex_scale = quad_ratio / texture_ratio;977978match scaling_mode {979ScalingMode::FillCenter => {980if quad_ratio > texture_ratio {981// offset texture to center by y coordinate982uv_offset_scale.y += (uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale) * 0.5;983// sum up scales984uv_offset_scale.w *= tex_quad_scale;985} else {986// offset texture to center by x coordinate987uv_offset_scale.x += (uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale) * 0.5;988uv_offset_scale.z *= quad_tex_scale;989};990}991ScalingMode::FillStart => {992if quad_ratio > texture_ratio {993uv_offset_scale.y += uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale;994uv_offset_scale.w *= tex_quad_scale;995} else {996uv_offset_scale.z *= quad_tex_scale;997}998}999ScalingMode::FillEnd => {1000if quad_ratio > texture_ratio {1001uv_offset_scale.w *= tex_quad_scale;1002} else {1003uv_offset_scale.x += uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale;1004uv_offset_scale.z *= quad_tex_scale;1005}1006}1007ScalingMode::FitCenter => {1008if texture_ratio > quad_ratio {1009// Scale based on width1010quad_size.y *= quad_tex_scale;1011} else {1012// Scale based on height1013quad_size.x *= tex_quad_scale;1014}1015}1016ScalingMode::FitStart => {1017if texture_ratio > quad_ratio {1018// The quad is scaled to match the image ratio, and the quad translation is adjusted1019// to start of the quad within the original quad size.1020let scale = Vec2::new(1.0, quad_tex_scale);1021let new_quad = *quad_size * scale;1022let offset = *quad_size - new_quad;1023*quad_translation = Vec2::new(0.0, -offset.y);1024*quad_size = new_quad;1025} else {1026let scale = Vec2::new(tex_quad_scale, 1.0);1027let new_quad = *quad_size * scale;1028let offset = *quad_size - new_quad;1029*quad_translation = Vec2::new(offset.x, 0.0);1030*quad_size = new_quad;1031}1032}1033ScalingMode::FitEnd => {1034if texture_ratio > quad_ratio {1035let scale = Vec2::new(1.0, quad_tex_scale);1036let new_quad = *quad_size * scale;1037let offset = *quad_size - new_quad;1038*quad_translation = Vec2::new(0.0, offset.y);1039*quad_size = new_quad;1040} else {1041let scale = Vec2::new(tex_quad_scale, 1.0);1042let new_quad = *quad_size * scale;1043let offset = *quad_size - new_quad;1044*quad_translation = Vec2::new(-offset.x, 0.0);1045*quad_size = new_quad;1046}1047}1048}1049}105010511052