Path: blob/main/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs
9390 views
use core::{hash::Hash, ops::Range};12use crate::*;3use bevy_asset::*;4use bevy_color::{ColorToComponents, LinearRgba};5use bevy_ecs::{6prelude::Component,7system::{8lifetimeless::{Read, SRes},9*,10},11};12use bevy_image::prelude::*;13use bevy_math::{Affine2, FloatOrd, Rect, Vec2};14use bevy_mesh::VertexBufferLayout;15use bevy_platform::collections::HashMap;16use bevy_render::{17render_asset::RenderAssets,18render_phase::*,19render_resource::{binding_types::uniform_buffer, *},20renderer::{RenderDevice, RenderQueue},21texture::GpuImage,22view::*,23Extract, ExtractSchedule, Render, RenderSystems,24};25use bevy_render::{sync_world::MainEntity, RenderStartup};26use bevy_shader::Shader;27use bevy_sprite::{SliceScaleMode, SpriteImageMode, TextureSlicer};28use bevy_sprite_render::SpriteAssetEvents;29use bevy_ui::widget;30use bevy_utils::default;31use binding_types::{sampler, texture_2d};32use bytemuck::{Pod, Zeroable};3334pub struct UiTextureSlicerPlugin;3536impl Plugin for UiTextureSlicerPlugin {37fn build(&self, app: &mut App) {38embedded_asset!(app, "ui_texture_slice.wgsl");3940if let Some(render_app) = app.get_sub_app_mut(RenderApp) {41render_app42.add_render_command::<TransparentUi, DrawUiTextureSlices>()43.init_resource::<ExtractedUiTextureSlices>()44.init_resource::<UiTextureSliceMeta>()45.init_resource::<UiTextureSliceImageBindGroups>()46.init_resource::<SpecializedRenderPipelines<UiTextureSlicePipeline>>()47.add_systems(RenderStartup, init_ui_texture_slice_pipeline)48.add_systems(49ExtractSchedule,50extract_ui_texture_slices.in_set(RenderUiSystems::ExtractTextureSlice),51)52.add_systems(53Render,54(55queue_ui_slices.in_set(RenderSystems::Queue),56prepare_ui_slices.in_set(RenderSystems::PrepareBindGroups),57),58);59}60}61}6263#[repr(C)]64#[derive(Copy, Clone, Pod, Zeroable)]65struct UiTextureSliceVertex {66pub position: [f32; 3],67pub uv: [f32; 2],68pub color: [f32; 4],69pub slices: [f32; 4],70pub border: [f32; 4],71pub repeat: [f32; 4],72pub atlas: [f32; 4],73}7475#[derive(Component)]76pub struct UiTextureSlicerBatch {77pub range: Range<u32>,78pub image: AssetId<Image>,79}8081#[derive(Resource)]82pub struct UiTextureSliceMeta {83vertices: RawBufferVec<UiTextureSliceVertex>,84indices: RawBufferVec<u32>,85view_bind_group: Option<BindGroup>,86}8788impl Default for UiTextureSliceMeta {89fn default() -> Self {90Self {91vertices: RawBufferVec::new(BufferUsages::VERTEX),92indices: RawBufferVec::new(BufferUsages::INDEX),93view_bind_group: None,94}95}96}9798#[derive(Resource, Default)]99pub struct UiTextureSliceImageBindGroups {100pub values: HashMap<AssetId<Image>, BindGroup>,101}102103#[derive(Resource)]104pub struct UiTextureSlicePipeline {105pub view_layout: BindGroupLayoutDescriptor,106pub image_layout: BindGroupLayoutDescriptor,107pub shader: Handle<Shader>,108}109110pub fn init_ui_texture_slice_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {111let view_layout = BindGroupLayoutDescriptor::new(112"ui_texture_slice_view_layout",113&BindGroupLayoutEntries::single(114ShaderStages::VERTEX_FRAGMENT,115uniform_buffer::<ViewUniform>(true),116),117);118119let image_layout = BindGroupLayoutDescriptor::new(120"ui_texture_slice_image_layout",121&BindGroupLayoutEntries::sequential(122ShaderStages::FRAGMENT,123(124texture_2d(TextureSampleType::Float { filterable: true }),125sampler(SamplerBindingType::Filtering),126),127),128);129130commands.insert_resource(UiTextureSlicePipeline {131view_layout,132image_layout,133shader: load_embedded_asset!(asset_server.as_ref(), "ui_texture_slice.wgsl"),134});135}136137#[derive(Clone, Copy, Hash, PartialEq, Eq)]138pub struct UiTextureSlicePipelineKey {139pub hdr: bool,140}141142impl SpecializedRenderPipeline for UiTextureSlicePipeline {143type Key = UiTextureSlicePipelineKey;144145fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {146let vertex_layout = VertexBufferLayout::from_vertex_formats(147VertexStepMode::Vertex,148vec![149// position150VertexFormat::Float32x3,151// uv152VertexFormat::Float32x2,153// color154VertexFormat::Float32x4,155// normalized texture slicing lines (left, top, right, bottom)156VertexFormat::Float32x4,157// normalized target slicing lines (left, top, right, bottom)158VertexFormat::Float32x4,159// repeat values (horizontal side, vertical side, horizontal center, vertical center)160VertexFormat::Float32x4,161// normalized texture atlas rect (left, top, right, bottom)162VertexFormat::Float32x4,163],164);165let shader_defs = Vec::new();166167RenderPipelineDescriptor {168vertex: VertexState {169shader: self.shader.clone(),170shader_defs: shader_defs.clone(),171buffers: vec![vertex_layout],172..default()173},174fragment: Some(FragmentState {175shader: self.shader.clone(),176shader_defs,177targets: vec![Some(ColorTargetState {178format: if key.hdr {179ViewTarget::TEXTURE_FORMAT_HDR180} else {181TextureFormat::bevy_default()182},183blend: Some(BlendState::ALPHA_BLENDING),184write_mask: ColorWrites::ALL,185})],186..default()187}),188layout: vec![self.view_layout.clone(), self.image_layout.clone()],189label: Some("ui_texture_slice_pipeline".into()),190..default()191}192}193}194195pub struct ExtractedUiTextureSlice {196pub stack_index: u32,197pub transform: Affine2,198pub rect: Rect,199pub atlas_rect: Option<Rect>,200pub image: AssetId<Image>,201pub clip: Option<Rect>,202pub extracted_camera_entity: Entity,203pub color: LinearRgba,204pub image_scale_mode: SpriteImageMode,205pub flip_x: bool,206pub flip_y: bool,207pub inverse_scale_factor: f32,208pub main_entity: MainEntity,209pub render_entity: Entity,210}211212#[derive(Resource, Default)]213pub struct ExtractedUiTextureSlices {214pub slices: Vec<ExtractedUiTextureSlice>,215}216217pub fn extract_ui_texture_slices(218mut commands: Commands,219mut extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,220texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,221slicers_query: Extract<222Query<(223Entity,224&ComputedNode,225&UiGlobalTransform,226&InheritedVisibility,227Option<&CalculatedClip>,228&ComputedUiTargetCamera,229&ImageNode,230)>,231>,232camera_map: Extract<UiCameraMap>,233) {234let mut camera_mapper = camera_map.get_mapper();235236for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &slicers_query {237// Skip invisible images238if !inherited_visibility.get()239|| image.color.is_fully_transparent()240|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()241{242continue;243}244245let image_scale_mode = match image.image_mode.clone() {246widget::NodeImageMode::Sliced(texture_slicer) => {247SpriteImageMode::Sliced(texture_slicer)248}249widget::NodeImageMode::Tiled {250tile_x,251tile_y,252stretch_value,253} => SpriteImageMode::Tiled {254tile_x,255tile_y,256stretch_value,257},258_ => continue,259};260261let Some(extracted_camera_entity) = camera_mapper.map(camera) else {262continue;263};264265let atlas_rect = image266.texture_atlas267.as_ref()268.and_then(|s| s.texture_rect(&texture_atlases))269.map(|r| r.as_rect());270271let atlas_rect = match (atlas_rect, image.rect) {272(None, None) => None,273(None, Some(image_rect)) => Some(image_rect),274(Some(atlas_rect), None) => Some(atlas_rect),275(Some(atlas_rect), Some(mut image_rect)) => {276image_rect.min += atlas_rect.min;277image_rect.max += atlas_rect.min;278Some(image_rect)279}280};281282extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {283render_entity: commands.spawn(TemporaryRenderEntity).id(),284stack_index: uinode.stack_index,285transform: transform.into(),286color: image.color.into(),287rect: Rect {288min: Vec2::ZERO,289max: uinode.size,290},291clip: clip.map(|clip| clip.clip),292image: image.image.id(),293extracted_camera_entity,294image_scale_mode,295atlas_rect,296flip_x: image.flip_x,297flip_y: image.flip_y,298inverse_scale_factor: uinode.inverse_scale_factor,299main_entity: entity.into(),300});301}302}303304#[expect(305clippy::too_many_arguments,306reason = "it's a system that needs a lot of them"307)]308pub fn queue_ui_slices(309extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,310ui_slicer_pipeline: Res<UiTextureSlicePipeline>,311mut pipelines: ResMut<SpecializedRenderPipelines<UiTextureSlicePipeline>>,312mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,313mut render_views: Query<&UiCameraView, With<ExtractedView>>,314camera_views: Query<&ExtractedView>,315pipeline_cache: Res<PipelineCache>,316draw_functions: Res<DrawFunctions<TransparentUi>>,317) {318let draw_function = draw_functions.read().id::<DrawUiTextureSlices>();319for (index, extracted_slicer) in extracted_ui_slicers.slices.iter().enumerate() {320let Ok(default_camera_view) =321render_views.get_mut(extracted_slicer.extracted_camera_entity)322else {323continue;324};325326let Ok(view) = camera_views.get(default_camera_view.0) else {327continue;328};329330let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)331else {332continue;333};334335let pipeline = pipelines.specialize(336&pipeline_cache,337&ui_slicer_pipeline,338UiTextureSlicePipelineKey { hdr: view.hdr },339);340341transparent_phase.add(TransparentUi {342draw_function,343pipeline,344entity: (extracted_slicer.render_entity, extracted_slicer.main_entity),345sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE),346batch_range: 0..0,347extra_index: PhaseItemExtraIndex::None,348index,349indexed: true,350});351}352}353354pub fn prepare_ui_slices(355mut commands: Commands,356render_device: Res<RenderDevice>,357render_queue: Res<RenderQueue>,358pipeline_cache: Res<PipelineCache>,359mut ui_meta: ResMut<UiTextureSliceMeta>,360mut extracted_slices: ResMut<ExtractedUiTextureSlices>,361view_uniforms: Res<ViewUniforms>,362texture_slicer_pipeline: Res<UiTextureSlicePipeline>,363mut image_bind_groups: ResMut<UiTextureSliceImageBindGroups>,364gpu_images: Res<RenderAssets<GpuImage>>,365mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,366events: Res<SpriteAssetEvents>,367mut previous_len: Local<usize>,368) {369// If an image has changed, the GpuImage has (probably) changed370for event in &events.images {371match event {372AssetEvent::Added { .. } |373AssetEvent::Unused { .. } |374// Images don't have dependencies375AssetEvent::LoadedWithDependencies { .. } => {}376AssetEvent::Modified { id } | AssetEvent::Removed { id } => {377image_bind_groups.values.remove(id);378}379};380}381382if let Some(view_binding) = view_uniforms.uniforms.binding() {383let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len);384385ui_meta.vertices.clear();386ui_meta.indices.clear();387ui_meta.view_bind_group = Some(render_device.create_bind_group(388"ui_texture_slice_view_bind_group",389&pipeline_cache.get_bind_group_layout(&texture_slicer_pipeline.view_layout),390&BindGroupEntries::single(view_binding),391));392393// Buffer indexes394let mut vertices_index = 0;395let mut indices_index = 0;396397for ui_phase in phases.values_mut() {398let mut batch_item_index = 0;399let mut batch_image_handle = AssetId::invalid();400let mut batch_image_size = Vec2::ZERO;401402for item_index in 0..ui_phase.items.len() {403let item = &mut ui_phase.items[item_index];404if let Some(texture_slices) = extracted_slices405.slices406.get(item.index)407.filter(|n| item.entity() == n.render_entity)408{409let mut existing_batch = batches.last_mut();410411if batch_image_handle == AssetId::invalid()412|| existing_batch.is_none()413|| (batch_image_handle != AssetId::default()414&& texture_slices.image != AssetId::default()415&& batch_image_handle != texture_slices.image)416{417if let Some(gpu_image) = gpu_images.get(texture_slices.image) {418batch_item_index = item_index;419batch_image_handle = texture_slices.image;420batch_image_size = gpu_image.size_2d().as_vec2();421422let new_batch = UiTextureSlicerBatch {423range: vertices_index..vertices_index,424image: texture_slices.image,425};426427batches.push((item.entity(), new_batch));428429image_bind_groups430.values431.entry(batch_image_handle)432.or_insert_with(|| {433render_device.create_bind_group(434"ui_texture_slice_image_layout",435&pipeline_cache.get_bind_group_layout(436&texture_slicer_pipeline.image_layout,437),438&BindGroupEntries::sequential((439&gpu_image.texture_view,440&gpu_image.sampler,441)),442)443});444445existing_batch = batches.last_mut();446} else {447continue;448}449} else if let Some(ref mut existing_batch) = existing_batch450&& batch_image_handle == AssetId::default()451&& texture_slices.image != AssetId::default()452{453if let Some(gpu_image) = gpu_images.get(texture_slices.image) {454batch_image_handle = texture_slices.image;455batch_image_size = gpu_image.size_2d().as_vec2();456existing_batch.1.image = texture_slices.image;457458image_bind_groups459.values460.entry(batch_image_handle)461.or_insert_with(|| {462render_device.create_bind_group(463"ui_texture_slice_image_layout",464&pipeline_cache.get_bind_group_layout(465&texture_slicer_pipeline.image_layout,466),467&BindGroupEntries::sequential((468&gpu_image.texture_view,469&gpu_image.sampler,470)),471)472});473} else {474continue;475}476}477478let uinode_rect = texture_slices.rect;479480let rect_size = uinode_rect.size();481482// Specify the corners of the node483let positions = QUAD_VERTEX_POSITIONS.map(|pos| {484(texture_slices.transform.transform_point2(pos * rect_size)).extend(0.)485});486487// Calculate the effect of clipping488// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)489let positions_diff = if let Some(clip) = texture_slices.clip {490[491Vec2::new(492f32::max(clip.min.x - positions[0].x, 0.),493f32::max(clip.min.y - positions[0].y, 0.),494),495Vec2::new(496f32::min(clip.max.x - positions[1].x, 0.),497f32::max(clip.min.y - positions[1].y, 0.),498),499Vec2::new(500f32::min(clip.max.x - positions[2].x, 0.),501f32::min(clip.max.y - positions[2].y, 0.),502),503Vec2::new(504f32::max(clip.min.x - positions[3].x, 0.),505f32::min(clip.max.y - positions[3].y, 0.),506),507]508} else {509[Vec2::ZERO; 4]510};511512let positions_clipped = [513positions[0] + positions_diff[0].extend(0.),514positions[1] + positions_diff[1].extend(0.),515positions[2] + positions_diff[2].extend(0.),516positions[3] + positions_diff[3].extend(0.),517];518519let transformed_rect_size =520texture_slices.transform.transform_vector2(rect_size);521522// Don't try to cull nodes that have a rotation523// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π524// In those two cases, the culling check can proceed normally as corners will be on525// horizontal / vertical lines526// For all other angles, bypass the culling check527// This does not properly handles all rotations on all axis528if texture_slices.transform.x_axis[1] == 0.0 {529// Cull nodes that are completely clipped530if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x531|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y532{533continue;534}535}536let flags = if texture_slices.image != AssetId::default() {537shader_flags::TEXTURED538} else {539shader_flags::UNTEXTURED540};541542let uvs = if flags == shader_flags::UNTEXTURED {543[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]544} else {545let atlas_extent = uinode_rect.max;546[547Vec2::new(548uinode_rect.min.x + positions_diff[0].x,549uinode_rect.min.y + positions_diff[0].y,550),551Vec2::new(552uinode_rect.max.x + positions_diff[1].x,553uinode_rect.min.y + positions_diff[1].y,554),555Vec2::new(556uinode_rect.max.x + positions_diff[2].x,557uinode_rect.max.y + positions_diff[2].y,558),559Vec2::new(560uinode_rect.min.x + positions_diff[3].x,561uinode_rect.max.y + positions_diff[3].y,562),563]564.map(|pos| pos / atlas_extent)565};566567let color = texture_slices.color.to_f32_array();568569let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect {570(571atlas.size(),572[573atlas.min.x / batch_image_size.x,574atlas.min.y / batch_image_size.y,575atlas.max.x / batch_image_size.x,576atlas.max.y / batch_image_size.y,577],578)579} else {580(batch_image_size, [0., 0., 1., 1.])581};582583if texture_slices.flip_x {584atlas.swap(0, 2);585}586587if texture_slices.flip_y {588atlas.swap(1, 3);589}590591let [slices, border, repeat] = compute_texture_slices(592image_size,593uinode_rect.size() * texture_slices.inverse_scale_factor,594&texture_slices.image_scale_mode,595);596597for i in 0..4 {598ui_meta.vertices.push(UiTextureSliceVertex {599position: positions_clipped[i].into(),600uv: uvs[i].into(),601color,602slices,603border,604repeat,605atlas,606});607}608609for &i in &QUAD_INDICES {610ui_meta.indices.push(indices_index + i as u32);611}612613vertices_index += 6;614indices_index += 4;615616existing_batch.unwrap().1.range.end = vertices_index;617ui_phase.items[batch_item_index].batch_range_mut().end += 1;618} else {619batch_image_handle = AssetId::invalid();620}621}622}623ui_meta.vertices.write_buffer(&render_device, &render_queue);624ui_meta.indices.write_buffer(&render_device, &render_queue);625*previous_len = batches.len();626commands.try_insert_batch(batches);627}628extracted_slices.slices.clear();629}630631pub type DrawUiTextureSlices = (632SetItemPipeline,633SetSlicerViewBindGroup<0>,634SetSlicerTextureBindGroup<1>,635DrawSlicer,636);637638pub struct SetSlicerViewBindGroup<const I: usize>;639impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerViewBindGroup<I> {640type Param = SRes<UiTextureSliceMeta>;641type ViewQuery = Read<ViewUniformOffset>;642type ItemQuery = ();643644fn render<'w>(645_item: &P,646view_uniform: &'w ViewUniformOffset,647_entity: Option<()>,648ui_meta: SystemParamItem<'w, '_, Self::Param>,649pass: &mut TrackedRenderPass<'w>,650) -> RenderCommandResult {651let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {652return RenderCommandResult::Failure("view_bind_group not available");653};654pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);655RenderCommandResult::Success656}657}658pub struct SetSlicerTextureBindGroup<const I: usize>;659impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerTextureBindGroup<I> {660type Param = SRes<UiTextureSliceImageBindGroups>;661type ViewQuery = ();662type ItemQuery = Read<UiTextureSlicerBatch>;663664#[inline]665fn render<'w>(666_item: &P,667_view: (),668batch: Option<&'w UiTextureSlicerBatch>,669image_bind_groups: SystemParamItem<'w, '_, Self::Param>,670pass: &mut TrackedRenderPass<'w>,671) -> RenderCommandResult {672let image_bind_groups = image_bind_groups.into_inner();673let Some(batch) = batch else {674return RenderCommandResult::Skip;675};676677pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]);678RenderCommandResult::Success679}680}681pub struct DrawSlicer;682impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {683type Param = SRes<UiTextureSliceMeta>;684type ViewQuery = ();685type ItemQuery = Read<UiTextureSlicerBatch>;686687#[inline]688fn render<'w>(689_item: &P,690_view: (),691batch: Option<&'w UiTextureSlicerBatch>,692ui_meta: SystemParamItem<'w, '_, Self::Param>,693pass: &mut TrackedRenderPass<'w>,694) -> RenderCommandResult {695let Some(batch) = batch else {696return RenderCommandResult::Skip;697};698let ui_meta = ui_meta.into_inner();699let Some(vertices) = ui_meta.vertices.buffer() else {700return RenderCommandResult::Failure("missing vertices to draw ui");701};702let Some(indices) = ui_meta.indices.buffer() else {703return RenderCommandResult::Failure("missing indices to draw ui");704};705706// Store the vertices707pass.set_vertex_buffer(0, vertices.slice(..));708// Define how to "connect" the vertices709pass.set_index_buffer(indices.slice(..), IndexFormat::Uint32);710// Draw the vertices711pass.draw_indexed(batch.range.clone(), 0, 0..1);712RenderCommandResult::Success713}714}715716fn compute_texture_slices(717image_size: Vec2,718target_size: Vec2,719image_scale_mode: &SpriteImageMode,720) -> [[f32; 4]; 3] {721match image_scale_mode {722SpriteImageMode::Sliced(TextureSlicer {723border: border_rect,724center_scale_mode,725sides_scale_mode,726max_corner_scale,727}) => {728let min_coeff = (target_size / image_size)729.min_element()730.min(*max_corner_scale);731732// calculate the normalized extents of the nine-patched image slices733let slices = [734border_rect.min_inset.x / image_size.x,735border_rect.min_inset.y / image_size.y,7361. - border_rect.max_inset.x / image_size.x,7371. - border_rect.max_inset.y / image_size.y,738];739740// calculate the normalized extents of the target slices741let border = [742(border_rect.min_inset.x / target_size.x) * min_coeff,743(border_rect.min_inset.y / target_size.y) * min_coeff,7441. - (border_rect.max_inset.x / target_size.x) * min_coeff,7451. - (border_rect.max_inset.y / target_size.y) * min_coeff,746];747748let image_side_width = image_size.x * (slices[2] - slices[0]);749let image_side_height = image_size.y * (slices[3] - slices[1]);750let target_side_width = target_size.x * (border[2] - border[0]);751let target_side_height = target_size.y * (border[3] - border[1]);752753// compute the number of times to repeat the side and center slices when tiling along each axis754// if the returned value is `1.` the slice will be stretched to fill the axis.755let repeat_side_x =756compute_tiled_subaxis(image_side_width, target_side_width, sides_scale_mode);757let repeat_side_y =758compute_tiled_subaxis(image_side_height, target_side_height, sides_scale_mode);759let repeat_center_x =760compute_tiled_subaxis(image_side_width, target_side_width, center_scale_mode);761let repeat_center_y =762compute_tiled_subaxis(image_side_height, target_side_height, center_scale_mode);763764[765slices,766border,767[768repeat_side_x,769repeat_side_y,770repeat_center_x,771repeat_center_y,772],773]774}775SpriteImageMode::Tiled {776tile_x,777tile_y,778stretch_value,779} => {780let rx = compute_tiled_axis(*tile_x, image_size.x, target_size.x, *stretch_value);781let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);782[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]783}784SpriteImageMode::Auto => {785unreachable!("Slices can not be computed for SpriteImageMode::Stretch")786}787SpriteImageMode::Scale(_) => {788unreachable!("Slices can not be computed for SpriteImageMode::Scale")789}790}791}792793fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 {794if tile {795let s = image_extent * stretch;796target_extent / s797} else {7981.799}800}801802fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 {803match mode {804SliceScaleMode::Stretch => 1.,805SliceScaleMode::Tile { stretch_value } => {806let s = image_extent * *stretch_value;807target_extent / s808}809}810}811812813