diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 24690e071d..0653ee62f1 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -26,7 +26,6 @@ mod geometry; mod layout; mod render; mod stack; -mod texture_slice; mod ui_node; pub use focus::*; @@ -163,11 +162,6 @@ impl Plugin for UiPlugin { .before(UiSystem::Layout) .in_set(AmbiguousWithTextSystem) .in_set(AmbiguousWithUpdateText2DLayout), - ( - texture_slice::compute_slices_on_asset_event, - texture_slice::compute_slices_on_image_change, - ) - .after(UiSystem::Layout), ), ); diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index d05c0b541e..9837a696a6 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -1,6 +1,7 @@ mod pipeline; mod render_pass; mod ui_material_pipeline; +pub mod ui_texture_slice_pipeline; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d}; @@ -14,16 +15,16 @@ use bevy_render::{ view::ViewVisibility, ExtractSchedule, Render, }; -use bevy_sprite::{SpriteAssetEvents, TextureAtlas}; +use bevy_sprite::{ImageScaleMode, SpriteAssetEvents, TextureAtlas}; pub use pipeline::*; pub use render_pass::*; pub use ui_material_pipeline::*; +use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::graph::{NodeUi, SubGraphUi}; use crate::{ - texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius, - CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, - UiScale, Val, + BackgroundColor, BorderColor, BorderRadius, CalculatedClip, ContentSize, DefaultUiCamera, Node, + Outline, Style, TargetCamera, UiImage, UiScale, Val, }; use bevy_app::prelude::*; @@ -139,6 +140,8 @@ pub fn build_ui_render(app: &mut App) { graph_3d.add_node_edge(Node3d::EndMainPassPostProcessing, NodeUi::UiPass); graph_3d.add_node_edge(NodeUi::UiPass, Node3d::Upscaling); } + + app.add_plugins(UiTextureSlicerPlugin); } fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { @@ -298,19 +301,21 @@ pub fn extract_uinode_images( ui_scale: Extract>, default_ui_camera: Extract, uinode_query: Extract< - Query<( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - &UiImage, - Option<&TextureAtlas>, - Option<&ComputedTextureSlices>, - Option<&BorderRadius>, - Option<&Parent>, - &Style, - )>, + Query< + ( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + Option<&TextureAtlas>, + Option<&BorderRadius>, + Option<&Parent>, + &Style, + ), + Without, + >, >, node_query: Extract>, ) { @@ -322,7 +327,6 @@ pub fn extract_uinode_images( camera, image, atlas, - slices, border_radius, parent, style, @@ -338,15 +342,6 @@ pub fn extract_uinode_images( continue; } - if let Some(slices) = slices { - extracted_uinodes.uinodes.extend( - slices - .extract_ui_nodes(transform, uinode, image, clip, camera_entity) - .map(|e| (commands.spawn_empty().id(), e)), - ); - continue; - } - let (rect, atlas_size) = match atlas { Some(atlas) => { let Some(layout) = texture_atlases.get(&atlas.layout) else { diff --git a/crates/bevy_ui/src/render/ui_texture_slice.wgsl b/crates/bevy_ui/src/render/ui_texture_slice.wgsl new file mode 100644 index 0000000000..edcc992154 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_texture_slice.wgsl @@ -0,0 +1,127 @@ +#import bevy_render::view::View; +#import bevy_render::globals::Globals; + +@group(0) @binding(0) +var view: View; +@group(0) @binding(1) +var globals: Globals; + +@group(1) @binding(0) var sprite_texture: texture_2d; +@group(1) @binding(1) var sprite_sampler: sampler; + +struct UiVertexOutput { + @location(0) uv: vec2, + @location(1) color: vec4, + + // Defines the dividing line that are used to split the texture atlas rect into corner, side and center slices + // The distances are normalized and from the top left corner of the texture atlas rect + // x = distance of the left vertical dividing line + // y = distance of the top horizontal dividing line + // z = distance of the right vertical dividing line + // w = distance of the bottom horizontal dividing line + @location(2) @interpolate(flat) texture_slices: vec4, + + // Defines the dividing line that are used to split the render target into into corner, side and center slices + // The distances are normalized and from the top left corner of the render target + // x = distance of left vertical dividing line + // y = distance of top horizontal dividing line + // z = distance of right vertical dividing line + // w = distance of bottom horizontal dividing line + @location(3) @interpolate(flat) target_slices: vec4, + + // The number of times the side or center texture slices should be repeated when mapping them to the border slices + // x = number of times to repeat along the horizontal axis for the side textures + // y = number of times to repeat along the vertical axis for the side textures + // z = number of times to repeat along the horizontal axis for the center texture + // w = number of times to repeat along the vertical axis for the center texture + @location(4) @interpolate(flat) repeat: vec4, + + // normalized texture atlas rect coordinates + // x, y = top, left corner of the atlas rect + // z, w = bottom, right corner of the atlas rect + @location(5) @interpolate(flat) atlas_rect: vec4, + @builtin(position) position: vec4, +} + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) vertex_color: vec4, + @location(3) texture_slices: vec4, + @location(4) target_slices: vec4, + @location(5) repeat: vec4, + @location(6) atlas_rect: vec4, +) -> UiVertexOutput { + var out: UiVertexOutput; + out.uv = vertex_uv; + out.color = vertex_color; + out.position = view.clip_from_world * vec4(vertex_position, 1.0); + out.texture_slices = texture_slices; + out.target_slices = target_slices; + out.repeat = repeat; + out.atlas_rect = atlas_rect; + return out; +} + +/// maps a point along the axis of the render target to slice coordinates +fn map_axis_with_repeat( + // normalized distance along the axis + p: f32, + // target min dividing point + il: f32, + // target max dividing point + ih: f32, + // slice min dividing point + tl: f32, + // slice max dividing point + th: f32, + // number of times to repeat the slice for sides and the center + r: f32, +) -> f32 { + if p < il { + // inside one of the two left (horizontal axis) or top (vertical axis) corners + return (p / il) * tl; + } else if ih < p { + // inside one of the two (horizontal axis) or top (vertical axis) corners + return th + ((p - ih) / (1 - ih)) * (1 - th); + } else { + // not inside a corner, repeat the texture + return tl + fract((r * (p - il)) / (ih - il)) * (th - tl); + } +} + +fn map_uvs_to_slice( + uv: vec2, + target_slices: vec4, + texture_slices: vec4, + repeat: vec4, +) -> vec2 { + var r: vec2; + if target_slices.x <= uv.x && uv.x <= target_slices.z && target_slices.y <= uv.y && uv.y <= target_slices.w { + // use the center repeat values if the uv coords are inside the center slice of the target + r = repeat.zw; + } else { + // use the side repeat values if the uv coords are outside the center slice + r = repeat.xy; + } + + // map horizontal axis + let x = map_axis_with_repeat(uv.x, target_slices.x, target_slices.z, texture_slices.x, texture_slices.z, r.x); + + // map vertical axis + let y = map_axis_with_repeat(uv.y, target_slices.y, target_slices.w, texture_slices.y, texture_slices.w, r.y); + + return vec2(x, y); +} + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + // map the target uvs to slice coords + let uv = map_uvs_to_slice(in.uv, in.target_slices, in.texture_slices, in.repeat); + + // map the slice coords to texture coords + let atlas_uv = in.atlas_rect.xy + uv * (in.atlas_rect.zw - in.atlas_rect.xy); + + return in.color * textureSample(sprite_texture, sprite_sampler, atlas_uv); +} diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs new file mode 100644 index 0000000000..1bfb5c4978 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -0,0 +1,759 @@ +use std::{hash::Hash, ops::Range}; + +use bevy_asset::*; +use bevy_color::{Alpha, ColorToComponents, LinearRgba}; +use bevy_ecs::{ + prelude::Component, + storage::SparseSet, + system::{ + lifetimeless::{Read, SRes}, + *, + }, +}; +use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_render::{ + render_asset::RenderAssets, + render_phase::*, + render_resource::{binding_types::uniform_buffer, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, Image, TRANSPARENT_IMAGE_HANDLE}, + view::*, + Extract, ExtractSchedule, Render, RenderSet, +}; +use bevy_sprite::{ + ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlas, TextureAtlasLayout, + TextureSlicer, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::HashMap; +use binding_types::{sampler, texture_2d}; +use bytemuck::{Pod, Zeroable}; + +use crate::*; + +pub const UI_SLICER_SHADER_HANDLE: Handle = Handle::weak_from_u128(11156288772117983964); + +pub struct UiTextureSlicerPlugin; + +impl Plugin for UiTextureSlicerPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + UI_SLICER_SHADER_HANDLE, + "ui_texture_slice.wgsl", + Shader::from_wgsl + ); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_render_command::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_ui_texture_slices.after(extract_uinode_images), + ) + .add_systems( + Render, + ( + queue_ui_slices.in_set(RenderSet::Queue), + prepare_ui_slices.in_set(RenderSet::PrepareBindGroups), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::(); + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct UiTextureSliceVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub color: [f32; 4], + pub slices: [f32; 4], + pub border: [f32; 4], + pub repeat: [f32; 4], + pub atlas: [f32; 4], +} + +#[derive(Component)] +pub struct UiTextureSlicerBatch { + pub range: Range, + pub image: AssetId, + pub camera: Entity, +} + +#[derive(Resource)] +pub struct UiTextureSliceMeta { + vertices: RawBufferVec, + indices: RawBufferVec, + view_bind_group: Option, +} + +impl Default for UiTextureSliceMeta { + fn default() -> Self { + Self { + vertices: RawBufferVec::new(BufferUsages::VERTEX), + indices: RawBufferVec::new(BufferUsages::INDEX), + view_bind_group: None, + } + } +} + +#[derive(Resource, Default)] +pub struct UiTextureSliceImageBindGroups { + pub values: HashMap, BindGroup>, +} + +#[derive(Resource)] +pub struct UiTextureSlicePipeline { + pub view_layout: BindGroupLayout, + pub image_layout: BindGroupLayout, +} + +impl FromWorld for UiTextureSlicePipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let view_layout = render_device.create_bind_group_layout( + "ui_texture_slice_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); + + let image_layout = render_device.create_bind_group_layout( + "ui_texture_slice_image_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + ), + ), + ); + + UiTextureSlicePipeline { + view_layout, + image_layout, + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct UiTextureSlicePipelineKey { + pub hdr: bool, +} + +impl SpecializedRenderPipeline for UiTextureSlicePipeline { + type Key = UiTextureSlicePipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let vertex_layout = VertexBufferLayout::from_vertex_formats( + VertexStepMode::Vertex, + vec![ + // position + VertexFormat::Float32x3, + // uv + VertexFormat::Float32x2, + // color + VertexFormat::Float32x4, + // normalized texture slicing lines (left, top, right, bottom) + VertexFormat::Float32x4, + // normalized target slicing lines (left, top, right, bottom) + VertexFormat::Float32x4, + // repeat values (horizontal side, vertical side, horizontal center, vertical center) + VertexFormat::Float32x4, + // normalized texture atlas rect (left, top, right, bottom) + VertexFormat::Float32x4, + ], + ); + let shader_defs = Vec::new(); + + RenderPipelineDescriptor { + vertex: VertexState { + shader: UI_SLICER_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_layout], + }, + fragment: Some(FragmentState { + shader: UI_SLICER_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![self.view_layout.clone(), self.image_layout.clone()], + push_constant_ranges: Vec::new(), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("ui_texture_slice_pipeline".into()), + } + } +} + +pub struct ExtractedUiTextureSlice { + pub stack_index: u32, + pub transform: Mat4, + pub rect: Rect, + pub atlas_rect: Option, + pub image: AssetId, + pub clip: Option, + pub camera_entity: Entity, + pub color: LinearRgba, + pub image_scale_mode: ImageScaleMode, +} + +#[derive(Resource, Default)] +pub struct ExtractedUiTextureSlices { + pub slices: SparseSet, +} + +pub fn extract_ui_texture_slices( + mut commands: Commands, + mut extracted_ui_slicers: ResMut, + default_ui_camera: Extract, + texture_atlases: Extract>>, + slicers_query: Extract< + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + &ImageScaleMode, + Option<&TextureAtlas>, + )>, + >, +) { + for (uinode, transform, view_visibility, clip, camera, image, image_scale_mode, atlas) in + &slicers_query + { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + else { + continue; + }; + + // Skip invisible images + if !view_visibility.get() + || image.color.is_fully_transparent() + || image.texture.id() == TRANSPARENT_IMAGE_HANDLE.id() + { + continue; + } + + let atlas_rect = atlas.and_then(|atlas| { + texture_atlases + .get(&atlas.layout) + .map(|layout| layout.textures[atlas.index].as_rect()) + }); + + extracted_ui_slicers.slices.insert( + commands.spawn_empty().id(), + ExtractedUiTextureSlice { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: image.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + clip: clip.map(|clip| clip.clip), + image: image.texture.id(), + camera_entity, + image_scale_mode: image_scale_mode.clone(), + atlas_rect, + }, + ); + } +} + +pub fn queue_ui_slices( + extracted_ui_slicers: ResMut, + ui_slicer_pipeline: Res, + mut pipelines: ResMut>, + mut transparent_render_phases: ResMut>, + mut views: Query<(Entity, &ExtractedView)>, + pipeline_cache: Res, + draw_functions: Res>, +) { + let draw_function = draw_functions.read().id::(); + for (entity, extracted_slicer) in extracted_ui_slicers.slices.iter() { + let Ok((view_entity, view)) = views.get_mut(extracted_slicer.camera_entity) else { + continue; + }; + + let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + continue; + }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_slicer_pipeline, + UiTextureSlicePipelineKey { hdr: view.hdr }, + ); + + transparent_phase.add(TransparentUi { + draw_function, + pipeline, + entity: *entity, + sort_key: ( + FloatOrd(extracted_slicer.stack_index as f32), + entity.index(), + ), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::NONE, + }); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_ui_slices( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut ui_meta: ResMut, + mut extracted_slices: ResMut, + view_uniforms: Res, + texture_slicer_pipeline: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut phases: ResMut>, + events: Res, + mut previous_len: Local, +) { + // If an image has changed, the GpuImage has (probably) changed + for event in &events.images { + match event { + AssetEvent::Added { .. } | + AssetEvent::Unused { .. } | + // Images don't have dependencies + AssetEvent::LoadedWithDependencies { .. } => {} + AssetEvent::Modified { id } | AssetEvent::Removed { id } => { + image_bind_groups.values.remove(id); + } + }; + } + + if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); + ui_meta.indices.clear(); + ui_meta.view_bind_group = Some(render_device.create_bind_group( + "ui_texture_slice_view_bind_group", + &texture_slicer_pipeline.view_layout, + &BindGroupEntries::single(view_binding), + )); + + // Buffer indexes + let mut vertices_index = 0; + let mut indices_index = 0; + + for ui_phase in phases.values_mut() { + let mut batch_item_index = 0; + let mut batch_image_handle = AssetId::invalid(); + let mut batch_image_size = Vec2::ZERO; + + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + if let Some(texture_slices) = extracted_slices.slices.get(item.entity) { + let mut existing_batch = batches.last_mut(); + + if batch_image_handle == AssetId::invalid() + || existing_batch.is_none() + || (batch_image_handle != AssetId::default() + && texture_slices.image != AssetId::default() + && batch_image_handle != texture_slices.image) + || existing_batch.as_ref().map(|(_, b)| b.camera) + != Some(texture_slices.camera_entity) + { + if let Some(gpu_image) = gpu_images.get(texture_slices.image) { + batch_item_index = item_index; + batch_image_handle = texture_slices.image; + batch_image_size = gpu_image.size.as_vec2(); + + let new_batch = UiTextureSlicerBatch { + range: vertices_index..vertices_index, + image: texture_slices.image, + camera: texture_slices.camera_entity, + }; + + batches.push((item.entity, new_batch)); + + image_bind_groups + .values + .entry(batch_image_handle) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_texture_slice_image_layout", + &texture_slicer_pipeline.image_layout, + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + + existing_batch = batches.last_mut(); + } else { + continue; + } + } else if batch_image_handle == AssetId::default() + && texture_slices.image != AssetId::default() + { + if let Some(gpu_image) = gpu_images.get(texture_slices.image) { + batch_image_handle = texture_slices.image; + batch_image_size = gpu_image.size.as_vec2(); + existing_batch.as_mut().unwrap().1.image = texture_slices.image; + + image_bind_groups + .values + .entry(batch_image_handle) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_texture_slice_image_layout", + &texture_slicer_pipeline.image_layout, + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + } else { + continue; + } + } + + let uinode_rect = texture_slices.rect; + + let rect_size = uinode_rect.size().extend(1.0); + + // Specify the corners of the node + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + + // Calculate the effect of clipping + // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) + let positions_diff = if let Some(clip) = texture_slices.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + + let transformed_rect_size = + texture_slices.transform.transform_vector3(rect_size); + + // Don't try to cull nodes that have a rotation + // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π + // In those two cases, the culling check can proceed normally as corners will be on + // horizontal / vertical lines + // For all other angles, bypass the culling check + // This does not properly handles all rotations on all axis + if texture_slices.transform.x_axis[1] == 0.0 { + // Cull nodes that are completely clipped + if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y + { + continue; + } + } + let flags = if texture_slices.image != AssetId::default() { + shader_flags::TEXTURED + } else { + shader_flags::UNTEXTURED + }; + + let uvs = if flags == shader_flags::UNTEXTURED { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + } else { + let atlas_extent = uinode_rect.max; + [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) + }; + + let color = texture_slices.color.to_f32_array(); + + let (image_size, atlas) = if let Some(atlas) = texture_slices.atlas_rect { + ( + atlas.size(), + [ + atlas.min.x / batch_image_size.x, + atlas.min.y / batch_image_size.y, + atlas.max.x / batch_image_size.x, + atlas.max.y / batch_image_size.y, + ], + ) + } else { + (batch_image_size, [0., 0., 1., 1.]) + }; + + let [slices, border, repeat] = compute_texture_slices( + image_size, + uinode_rect.size(), + &texture_slices.image_scale_mode, + ); + + for i in 0..4 { + ui_meta.vertices.push(UiTextureSliceVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + slices, + border, + repeat, + atlas, + }); + } + + for &i in &QUAD_INDICES { + ui_meta.indices.push(indices_index + i as u32); + } + + vertices_index += 6; + indices_index += 4; + + existing_batch.unwrap().1.range.end = vertices_index; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; + } else { + batch_image_handle = AssetId::invalid(); + } + } + } + ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); + } + extracted_slices.slices.clear(); +} + +pub type DrawUiTextureSlices = ( + SetItemPipeline, + SetSlicerViewBindGroup<0>, + SetSlicerTextureBindGroup<1>, + DrawSlicer, +); + +pub struct SetSlicerViewBindGroup; +impl RenderCommand

for SetSlicerViewBindGroup { + type Param = SRes; + type ViewQuery = Read; + type ItemQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: Option<()>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else { + return RenderCommandResult::Failure; + }; + pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]); + RenderCommandResult::Success + } +} +pub struct SetSlicerTextureBindGroup; +impl RenderCommand

for SetSlicerTextureBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w UiTextureSlicerBatch>, + image_bind_groups: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let image_bind_groups = image_bind_groups.into_inner(); + let Some(batch) = batch else { + return RenderCommandResult::Success; + }; + + pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]); + RenderCommandResult::Success + } +} +pub struct DrawSlicer; +impl RenderCommand

for DrawSlicer { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w UiTextureSlicerBatch>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(batch) = batch else { + return RenderCommandResult::Success; + }; + let ui_meta = ui_meta.into_inner(); + let Some(vertices) = ui_meta.vertices.buffer() else { + return RenderCommandResult::Failure; + }; + let Some(indices) = ui_meta.indices.buffer() else { + return RenderCommandResult::Failure; + }; + + // Store the vertices + pass.set_vertex_buffer(0, vertices.slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); + RenderCommandResult::Success + } +} + +fn compute_texture_slices( + image_size: Vec2, + target_size: Vec2, + image_scale_mode: &ImageScaleMode, +) -> [[f32; 4]; 3] { + match image_scale_mode { + ImageScaleMode::Sliced(TextureSlicer { + border, + center_scale_mode, + sides_scale_mode, + max_corner_scale, + }) => { + let min_coeff = (target_size / image_size) + .min_element() + .min(*max_corner_scale); + + let slices = [ + border.left / image_size.x, + border.top / image_size.y, + 1. - border.right / image_size.x, + 1. - border.bottom / image_size.y, + ]; + + let border = [ + (border.left / target_size.x) * min_coeff, + (border.top / target_size.y) * min_coeff, + 1. - (border.right / target_size.x) * min_coeff, + 1. - (border.bottom / target_size.y) * min_coeff, + ]; + + let isx = image_size.x * (1. - slices[0] - slices[2]); + let isy = image_size.y * (1. - slices[1] - slices[3]); + let tsx = target_size.x * (1. - border[0] - border[2]); + let tsy = target_size.y * (1. - border[1] - border[3]); + + let rx = compute_tiled_subaxis(isx, tsx, sides_scale_mode); + let ry = compute_tiled_subaxis(isy, tsy, sides_scale_mode); + let cx = compute_tiled_subaxis(isx, tsx, center_scale_mode); + let cy = compute_tiled_subaxis(isy, tsy, center_scale_mode); + + [slices, border, [rx, ry, cx, cy]] + } + ImageScaleMode::Tiled { + tile_x, + tile_y, + stretch_value, + } => { + let rx = compute_tiled_axis(*tile_x, image_size.x, target_size.x, *stretch_value); + let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value); + [[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]] + } + } +} + +fn compute_tiled_axis(tile: bool, is: f32, ts: f32, stretch: f32) -> f32 { + if tile { + let s = is * stretch; + ts / s + } else { + 1. + } +} + +fn compute_tiled_subaxis(is: f32, ts: f32, mode: &SliceScaleMode) -> f32 { + match mode { + SliceScaleMode::Stretch => 1., + SliceScaleMode::Tile { stretch_value } => { + let s = is * *stretch_value; + ts / s + } + } +} diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs deleted file mode 100644 index 6712d53ad6..0000000000 --- a/crates/bevy_ui/src/texture_slice.rs +++ /dev/null @@ -1,219 +0,0 @@ -// This module is mostly copied and pasted from `bevy_sprite::texture_slice` -// -// A more centralized solution should be investigated in the future - -use bevy_asset::{AssetEvent, Assets}; -use bevy_ecs::prelude::*; -use bevy_math::{Rect, Vec2}; -use bevy_render::texture::Image; -use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice}; -use bevy_transform::prelude::*; -use bevy_utils::HashSet; - -use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage}; - -/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] -/// -/// This component is automatically inserted and updated -#[derive(Debug, Clone, Component)] -pub struct ComputedTextureSlices { - slices: Vec, - image_size: Vec2, -} - -impl ComputedTextureSlices { - /// Computes [`ExtractedUiNode`] iterator from the sprite slices - /// - /// # Arguments - /// - /// * `transform` - the sprite entity global transform - /// * `original_entity` - the sprite entity - /// * `sprite` - The sprite component - /// * `handle` - The sprite texture handle - #[must_use] - pub(crate) fn extract_ui_nodes<'a>( - &'a self, - transform: &'a GlobalTransform, - node: &'a Node, - image: &'a UiImage, - clip: Option<&'a CalculatedClip>, - camera_entity: Entity, - ) -> impl ExactSizeIterator + 'a { - let mut flip = Vec2::new(1.0, -1.0); - let [mut flip_x, mut flip_y] = [false; 2]; - if image.flip_x { - flip.x *= -1.0; - flip_x = true; - } - if image.flip_y { - flip.y *= -1.0; - flip_y = true; - } - self.slices.iter().map(move |slice| { - let offset = (slice.offset * flip).extend(0.0); - let transform = transform.mul_transform(Transform::from_translation(offset)); - let scale = slice.draw_size / slice.texture_rect.size(); - let mut rect = slice.texture_rect; - rect.min *= scale; - rect.max *= scale; - let atlas_size = Some(self.image_size * scale); - ExtractedUiNode { - stack_index: node.stack_index, - color: image.color.into(), - transform: transform.compute_matrix(), - rect, - flip_x, - flip_y, - image: image.texture.id(), - atlas_size, - clip: clip.map(|clip| clip.clip), - camera_entity, - border: [0.; 4], - border_radius: [0.; 4], - node_type: NodeType::Rect, - } - }) - } -} - -/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices -/// will be computed according to the `image_handle` dimensions. -/// -/// Returns `None` if the image asset is not loaded -/// -/// # Arguments -/// -/// * `draw_area` - The size of the drawing area the slices will have to fit into -/// * `scale_mode` - The image scaling component -/// * `image_handle` - The texture to slice or tile -/// * `images` - The image assets, use to retrieve the image dimensions -/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section -/// of the texture -/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect -#[must_use] -fn compute_texture_slices( - draw_area: Vec2, - scale_mode: &ImageScaleMode, - image_handle: &UiImage, - images: &Assets, - atlas: Option<&TextureAtlas>, - atlas_layouts: &Assets, -) -> Option { - let (image_size, texture_rect) = match 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(&image_handle.texture)?; - let size = Vec2::new( - image.texture_descriptor.size.width as f32, - image.texture_descriptor.size.height as f32, - ); - let rect = Rect { - min: Vec2::ZERO, - max: size, - }; - (size, rect) - } - }; - let slices = match scale_mode { - ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), - ImageScaleMode::Tiled { - tile_x, - tile_y, - stretch_value, - } => { - let slice = TextureSlice { - texture_rect, - draw_size: draw_area, - offset: Vec2::ZERO, - }; - slice.tiled(*stretch_value, (*tile_x, *tile_y)) - } - }; - Some(ComputedTextureSlices { slices, image_size }) -} - -/// System reacting to added or modified [`Image`] handles, and recompute sprite slices -/// on matching sprite entities with a [`ImageScaleMode`] component -pub(crate) fn compute_slices_on_asset_event( - mut commands: Commands, - mut events: EventReader>, - images: Res>, - atlas_layouts: Res>, - ui_nodes: Query<( - Entity, - &ImageScaleMode, - &Node, - &UiImage, - Option<&TextureAtlas>, - )>, -) { - // 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, scale_mode, ui_node, image, atlas) in &ui_nodes { - if !added_handles.contains(&image.texture.id()) { - continue; - } - if let Some(slices) = compute_texture_slices( - ui_node.size(), - scale_mode, - image, - &images, - atlas, - &atlas_layouts, - ) { - commands.entity(entity).try_insert(slices); - } - } -} - -/// System reacting to changes on relevant sprite bundle components to compute the sprite slices -/// on matching sprite entities with a [`ImageScaleMode`] component -pub(crate) fn compute_slices_on_image_change( - mut commands: Commands, - images: Res>, - atlas_layouts: Res>, - changed_nodes: Query< - ( - Entity, - &ImageScaleMode, - &Node, - &UiImage, - Option<&TextureAtlas>, - ), - Or<( - Changed, - Changed, - Changed, - Changed, - )>, - >, -) { - for (entity, scale_mode, ui_node, image, atlas) in &changed_nodes { - if let Some(slices) = compute_texture_slices( - ui_node.size(), - scale_mode, - image, - &images, - atlas, - &atlas_layouts, - ) { - commands.entity(entity).try_insert(slices); - } - } -}