mirror of
https://github.com/bevyengine/bevy
synced 2024-11-28 15:40:21 +00:00
UI texture atlas slice shader (#14990)
# Objective Fixes https://github.com/bevyengine/bevy/issues/14183 ## Solution Reimplement the UI texture atlas slicer using a shader. The problems with #14183 could be fixed more simply by hacking around with the coordinates and scaling but that way is very fragile and might get broken again the next time we make changes to the layout calculations. A shader based solution is more robust, it's impossible for gaps to appear between the image slices with these changes as we're only drawing a single quad. I've not tried any benchmarks yet but it should much more efficient as well, in the worst cases even hundreds or thousands of times faster. Maybe could have used the UiMaterialPipeline. I wrote the shader first and used fat vertices and then realised it wouldn't work that way with a UiMaterial. If it's rewritten it so it puts all the slice geometry in uniform buffer, then it might work? Adding the uniform buffer would probably make the shader more complicated though, so don't know if it's even worth it. Instancing is another alternative. ## Testing The examples are working and it seems to match the old API correctly but I've not used the texture atlas slicing API for anything before, I reviewed the PR but that was back in January. Needs a review by someone who knows the rendering pipeline and wgsl really well because I don't really have any idea what I'm doing.
This commit is contained in:
parent
659860dfe5
commit
01a3b0e830
6 changed files with 909 additions and 247 deletions
|
@ -29,7 +29,6 @@ mod geometry;
|
|||
mod layout;
|
||||
mod render;
|
||||
mod stack;
|
||||
mod texture_slice;
|
||||
mod ui_node;
|
||||
|
||||
pub use focus::*;
|
||||
|
@ -181,11 +180,6 @@ impl Plugin for UiPlugin {
|
|||
.in_set(UiSystem::Prepare)
|
||||
.in_set(AmbiguousWithTextSystem)
|
||||
.in_set(AmbiguousWithUpdateText2DLayout),
|
||||
(
|
||||
texture_slice::compute_slices_on_asset_event,
|
||||
texture_slice::compute_slices_on_image_change,
|
||||
)
|
||||
.in_set(UiSystem::PostLayout),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
@ -15,16 +16,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::*;
|
||||
|
@ -140,6 +141,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 {
|
||||
|
@ -299,19 +302,21 @@ pub fn extract_uinode_images(
|
|||
ui_scale: Extract<Res<UiScale>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
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<ImageScaleMode>,
|
||||
>,
|
||||
>,
|
||||
node_query: Extract<Query<&Node>>,
|
||||
) {
|
||||
|
@ -323,7 +328,6 @@ pub fn extract_uinode_images(
|
|||
camera,
|
||||
image,
|
||||
atlas,
|
||||
slices,
|
||||
border_radius,
|
||||
parent,
|
||||
style,
|
||||
|
@ -342,15 +346,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_scaling) = match atlas {
|
||||
Some(atlas) => {
|
||||
let Some(layout) = texture_atlases.get(&atlas.layout) else {
|
||||
|
|
127
crates/bevy_ui/src/render/ui_texture_slice.wgsl
Normal file
127
crates/bevy_ui/src/render/ui_texture_slice.wgsl
Normal file
|
@ -0,0 +1,127 @@
|
|||
#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(1) @binding(0) var sprite_texture: texture_2d<f32>;
|
||||
@group(1) @binding(1) var sprite_sampler: sampler;
|
||||
|
||||
struct UiVertexOutput {
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
|
||||
// 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<f32>,
|
||||
|
||||
// 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<f32>,
|
||||
|
||||
// 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<f32>,
|
||||
|
||||
// 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<f32>,
|
||||
@builtin(position) position: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(
|
||||
@location(0) vertex_position: vec3<f32>,
|
||||
@location(1) vertex_uv: vec2<f32>,
|
||||
@location(2) vertex_color: vec4<f32>,
|
||||
@location(3) texture_slices: vec4<f32>,
|
||||
@location(4) target_slices: vec4<f32>,
|
||||
@location(5) repeat: vec4<f32>,
|
||||
@location(6) atlas_rect: vec4<f32>,
|
||||
) -> UiVertexOutput {
|
||||
var out: UiVertexOutput;
|
||||
out.uv = vertex_uv;
|
||||
out.color = vertex_color;
|
||||
out.position = view.clip_from_world * vec4<f32>(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<f32>,
|
||||
target_slices: vec4<f32>,
|
||||
texture_slices: vec4<f32>,
|
||||
repeat: vec4<f32>,
|
||||
) -> vec2<f32> {
|
||||
var r: vec2<f32>;
|
||||
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<f32> {
|
||||
// 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);
|
||||
}
|
759
crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs
Normal file
759
crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs
Normal file
|
@ -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<Shader> = 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::<TransparentUi, DrawUiTextureSlices>()
|
||||
.init_resource::<ExtractedUiTextureSlices>()
|
||||
.init_resource::<UiTextureSliceMeta>()
|
||||
.init_resource::<UiTextureSliceImageBindGroups>()
|
||||
.init_resource::<SpecializedRenderPipelines<UiTextureSlicePipeline>>()
|
||||
.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::<UiTextureSlicePipeline>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<u32>,
|
||||
pub image: AssetId<Image>,
|
||||
pub camera: Entity,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct UiTextureSliceMeta {
|
||||
vertices: RawBufferVec<UiTextureSliceVertex>,
|
||||
indices: RawBufferVec<u32>,
|
||||
view_bind_group: Option<BindGroup>,
|
||||
}
|
||||
|
||||
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<AssetId<Image>, 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::<RenderDevice>();
|
||||
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"ui_texture_slice_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
ShaderStages::VERTEX_FRAGMENT,
|
||||
uniform_buffer::<ViewUniform>(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<Rect>,
|
||||
pub image: AssetId<Image>,
|
||||
pub clip: Option<Rect>,
|
||||
pub camera_entity: Entity,
|
||||
pub color: LinearRgba,
|
||||
pub image_scale_mode: ImageScaleMode,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct ExtractedUiTextureSlices {
|
||||
pub slices: SparseSet<Entity, ExtractedUiTextureSlice>,
|
||||
}
|
||||
|
||||
pub fn extract_ui_texture_slices(
|
||||
mut commands: Commands,
|
||||
mut extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
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<ExtractedUiTextureSlices>,
|
||||
ui_slicer_pipeline: Res<UiTextureSlicePipeline>,
|
||||
mut pipelines: ResMut<SpecializedRenderPipelines<UiTextureSlicePipeline>>,
|
||||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
mut views: Query<(Entity, &ExtractedView)>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
draw_functions: Res<DrawFunctions<TransparentUi>>,
|
||||
) {
|
||||
let draw_function = draw_functions.read().id::<DrawUiTextureSlices>();
|
||||
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<RenderDevice>,
|
||||
render_queue: Res<RenderQueue>,
|
||||
mut ui_meta: ResMut<UiTextureSliceMeta>,
|
||||
mut extracted_slices: ResMut<ExtractedUiTextureSlices>,
|
||||
view_uniforms: Res<ViewUniforms>,
|
||||
texture_slicer_pipeline: Res<UiTextureSlicePipeline>,
|
||||
mut image_bind_groups: ResMut<UiTextureSliceImageBindGroups>,
|
||||
gpu_images: Res<RenderAssets<GpuImage>>,
|
||||
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
events: Res<SpriteAssetEvents>,
|
||||
mut previous_len: Local<usize>,
|
||||
) {
|
||||
// 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<const I: usize>;
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerViewBindGroup<I> {
|
||||
type Param = SRes<UiTextureSliceMeta>;
|
||||
type ViewQuery = Read<ViewUniformOffset>;
|
||||
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("view_bind_group not available");
|
||||
};
|
||||
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
pub struct SetSlicerTextureBindGroup<const I: usize>;
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerTextureBindGroup<I> {
|
||||
type Param = SRes<UiTextureSliceImageBindGroups>;
|
||||
type ViewQuery = ();
|
||||
type ItemQuery = Read<UiTextureSlicerBatch>;
|
||||
|
||||
#[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::Skip;
|
||||
};
|
||||
|
||||
pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
pub struct DrawSlicer;
|
||||
impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
|
||||
type Param = SRes<UiTextureSliceMeta>;
|
||||
type ViewQuery = ();
|
||||
type ItemQuery = Read<UiTextureSlicerBatch>;
|
||||
|
||||
#[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::Skip;
|
||||
};
|
||||
let ui_meta = ui_meta.into_inner();
|
||||
let Some(vertices) = ui_meta.vertices.buffer() else {
|
||||
return RenderCommandResult::Failure("missing vertices to draw ui");
|
||||
};
|
||||
let Some(indices) = ui_meta.indices.buffer() else {
|
||||
return RenderCommandResult::Failure("missing indices to draw ui");
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,213 +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<TextureSlice>,
|
||||
}
|
||||
|
||||
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<Item = ExtractedUiNode> + '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;
|
||||
ExtractedUiNode {
|
||||
stack_index: node.stack_index,
|
||||
color: image.color.into(),
|
||||
transform: transform.compute_matrix(),
|
||||
rect,
|
||||
flip_x,
|
||||
flip_y,
|
||||
image: image.texture.id(),
|
||||
atlas_scaling: Some(scale),
|
||||
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<Image>,
|
||||
atlas: Option<&TextureAtlas>,
|
||||
atlas_layouts: &Assets<TextureAtlasLayout>,
|
||||
) -> Option<ComputedTextureSlices> {
|
||||
let texture_rect = match atlas {
|
||||
Some(a) => {
|
||||
let layout = atlas_layouts.get(&a.layout)?;
|
||||
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,
|
||||
);
|
||||
Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: size,
|
||||
}
|
||||
}
|
||||
};
|
||||
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 })
|
||||
}
|
||||
|
||||
/// 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<AssetEvent<Image>>,
|
||||
images: Res<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
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<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
changed_nodes: Query<
|
||||
(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Node,
|
||||
&UiImage,
|
||||
Option<&TextureAtlas>,
|
||||
),
|
||||
Or<(
|
||||
Changed<ImageScaleMode>,
|
||||
Changed<UiImage>,
|
||||
Changed<Node>,
|
||||
Changed<TextureAtlas>,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ fn main() {
|
|||
// Over time, we are working to reduce the number: your PR should not increase it.
|
||||
// If you decrease this by fixing an ambiguity, reduce the number to prevent regressions.
|
||||
// See https://github.com/bevyengine/bevy/issues/7386 for progress.
|
||||
46,
|
||||
44,
|
||||
"Main app has unexpected ambiguities among the following schedules: \n{:#?}.",
|
||||
main_app_ambiguities,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue