Skip to main content

bevy_ui_render/
box_shadow.rs

1//! Box shadows rendering
2
3use core::{hash::Hash, ops::Range};
4
5use bevy_app::prelude::*;
6use bevy_asset::*;
7use bevy_camera::visibility::InheritedVisibility;
8use bevy_color::{Alpha, ColorToComponents, LinearRgba};
9use bevy_ecs::prelude::*;
10use bevy_ecs::{
11    prelude::Component,
12    system::{
13        lifetimeless::{Read, SRes},
14        *,
15    },
16};
17use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};
18use bevy_mesh::VertexBufferLayout;
19use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
20use bevy_render::{
21    render_phase::*,
22    render_resource::{binding_types::uniform_buffer, *},
23    renderer::{RenderDevice, RenderQueue},
24    view::*,
25    Extract, ExtractSchedule, Render, RenderSystems,
26};
27use bevy_render::{GpuResourceAppExt, RenderApp, RenderStartup};
28use bevy_shader::{Shader, ShaderDefVal};
29use bevy_ui::{
30    BoxShadow, CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiRenderTargetInfo,
31    ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, Val,
32};
33use bevy_utils::default;
34use bytemuck::{Pod, Zeroable};
35
36use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap};
37
38use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
39
40/// A plugin that enables the rendering of box shadows.
41pub struct BoxShadowPlugin;
42
43impl Plugin for BoxShadowPlugin {
44    fn build(&self, app: &mut App) {
45        {
    {
        let mut embedded =
            app.world_mut().resource_mut::<::bevy_asset::io::embedded::EmbeddedAssetRegistry>();
        let path =
            {
                let crate_name =
                    "bevy_ui_render::box_shadow".split(':').next().unwrap();
                ::bevy_asset::io::embedded::_embedded_asset_path(crate_name,
                    "src".as_ref(), "src/box_shadow.rs".as_ref(),
                    "box_shadow.wgsl".as_ref())
            };
        let watched_path =
            ::bevy_asset::io::embedded::watched_path("src/box_shadow.rs",
                "box_shadow.wgsl");
        embedded.insert_asset(watched_path, &path,
            b"#import bevy_render::view::View;\n#import bevy_render::globals::Globals;\n\nconst PI: f32 = 3.14159265358979323846;\nconst SAMPLES: i32 = #SHADOW_SAMPLES;\n\n@group(0) @binding(0) var<uniform> view: View;\n\nstruct BoxShadowVertexOutput {\n    @builtin(position) position: vec4<f32>,\n    @location(0) point: vec2<f32>,\n    @location(1) color: vec4<f32>,\n    @location(2) @interpolate(flat) size: vec2<f32>,\n    @location(3) @interpolate(flat) radius: vec4<f32>,    \n    @location(4) @interpolate(flat) blur: f32,\n}\n\nfn gaussian(x: f32, sigma: f32) -> f32 {\n    return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * PI) * sigma);\n}\n\n// Approximates the Gauss error function: https://en.wikipedia.org/wiki/Error_function\nfn erf(p: vec2<f32>) -> vec2<f32> {\n    let s = sign(p);\n    let a = abs(p);\n    // fourth degree polynomial approximation for erf\n    var result = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;\n    result = result * result;\n    return s - s / (result * result);\n}\n\n// returns the closest corner radius based on the signs of the components of p\nfn selectCorner(p: vec2<f32>, c: vec4<f32>) -> f32 {\n    return mix(mix(c.x, c.y, step(0., p.x)), mix(c.w, c.z, step(0., p.x)), step(0., p.y));\n}\n\nfn horizontalRoundedBoxShadow(x: f32, y: f32, blur: f32, corner: f32, half_size: vec2<f32>) -> f32 {\n    let d = min(half_size.y - corner - abs(y), 0.);\n    let c = half_size.x - corner + sqrt(max(0., corner * corner - d * d));\n    let integral = 0.5 + 0.5 * erf((x + vec2(-c, c)) * (sqrt(0.5) / blur));\n    return integral.y - integral.x;\n}\n\nfn roundedBoxShadow(\n    lower: vec2<f32>,\n    upper: vec2<f32>,\n    point: vec2<f32>,\n    blur: f32,\n    corners: vec4<f32>,\n) -> f32 {\n    let center = (lower + upper) * 0.5;\n    let half_size = (upper - lower) * 0.5;\n    let p = point - center;\n    let low = p.y - half_size.y;\n    let high = p.y + half_size.y;\n    let start = clamp(-3. * blur, low, high);\n    let end = clamp(3. * blur, low, high);\n    let step = (end - start) / f32(SAMPLES);\n    var y = start + step * 0.5;\n    var value: f32 = 0.0;\n    for (var i = 0; i < SAMPLES; i++) {\n        let corner = selectCorner(p, corners);\n        value += horizontalRoundedBoxShadow(p.x, p.y - y, blur, corner, half_size) * gaussian(y, blur) * step;\n        y += step;\n    }\n    return value;\n}\n\n@vertex\nfn vertex(\n    @location(0) vertex_position: vec3<f32>,\n    @location(1) uv: vec2<f32>,\n    @location(2) vertex_color: vec4<f32>,\n    @location(3) size: vec2<f32>,\n    @location(4) radius: vec4<f32>,\n    @location(5) blur: f32,\n    @location(6) bounds: vec2<f32>,\n) -> BoxShadowVertexOutput {\n    var out: BoxShadowVertexOutput;\n    out.position = view.clip_from_world * vec4(vertex_position, 1.0);\n    out.point = (uv.xy - 0.5) * bounds;\n    out.color = vertex_color;\n    out.size = size;\n    out.radius = radius;\n    out.blur = blur;\n    return out;\n}\n\n@fragment\nfn fragment(\n    in: BoxShadowVertexOutput,\n) -> @location(0) vec4<f32> {\n    let g = in.color.a * roundedBoxShadow(-0.5 * in.size, 0.5 * in.size, in.point, max(in.blur, 0.01), in.radius);\n    return vec4(in.color.rgb, g);\n}\n\n\n\n");
    }
};embedded_asset!(app, "box_shadow.wgsl");
46
47        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
48            render_app
49                .add_render_command::<TransparentUi, DrawBoxShadows>()
50                .init_resource::<ExtractedBoxShadows>()
51                .init_gpu_resource::<BoxShadowMeta>()
52                .init_gpu_resource::<SpecializedRenderPipelines<BoxShadowPipeline>>()
53                .add_systems(RenderStartup, init_box_shadow_pipeline)
54                .add_systems(
55                    ExtractSchedule,
56                    extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows),
57                )
58                .add_systems(
59                    Render,
60                    (
61                        queue_shadows.in_set(RenderSystems::Queue),
62                        prepare_shadows.in_set(RenderSystems::PrepareBindGroups),
63                    ),
64                );
65        }
66    }
67}
68
69#[repr(C)]
70#[derive(#[automatically_derived]
impl ::core::marker::Copy for BoxShadowVertex { }Copy, #[automatically_derived]
impl ::core::clone::Clone for BoxShadowVertex {
    #[inline]
    fn clone(&self) -> BoxShadowVertex {
        let _: ::core::clone::AssertParamIsClone<[f32; 3]>;
        let _: ::core::clone::AssertParamIsClone<[f32; 2]>;
        let _: ::core::clone::AssertParamIsClone<[f32; 4]>;
        let _: ::core::clone::AssertParamIsClone<[f32; 2]>;
        let _: ::core::clone::AssertParamIsClone<[f32; 4]>;
        let _: ::core::clone::AssertParamIsClone<f32>;
        let _: ::core::clone::AssertParamIsClone<[f32; 2]>;
        *self
    }
}Clone, unsafe impl ::bytemuck::Pod for BoxShadowVertex {}Pod, unsafe impl ::bytemuck::Zeroable for BoxShadowVertex {}Zeroable)]
71struct BoxShadowVertex {
72    position: [f32; 3],
73    uvs: [f32; 2],
74    vertex_color: [f32; 4],
75    size: [f32; 2],
76    radius: [f32; 4],
77    blur: f32,
78    bounds: [f32; 2],
79}
80
81#[derive(impl bevy_ecs::component::Component for UiShadowsBatch where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}Component)]
82pub struct UiShadowsBatch {
83    pub range: Range<u32>,
84    pub camera: Entity,
85}
86
87/// Contains the vertices and bind groups to be sent to the GPU
88#[derive(impl bevy_ecs::resource::Resource for BoxShadowMeta where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource)]
89pub struct BoxShadowMeta {
90    vertices: RawBufferVec<BoxShadowVertex>,
91    indices: RawBufferVec<u32>,
92    view_bind_group: Option<BindGroup>,
93}
94
95impl Default for BoxShadowMeta {
96    fn default() -> Self {
97        Self {
98            vertices: RawBufferVec::new(BufferUsages::VERTEX),
99            indices: RawBufferVec::new(BufferUsages::INDEX),
100            view_bind_group: None,
101        }
102    }
103}
104
105#[derive(impl bevy_ecs::resource::Resource for BoxShadowPipeline where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource)]
106pub struct BoxShadowPipeline {
107    pub view_layout: BindGroupLayoutDescriptor,
108    pub shader: Handle<Shader>,
109}
110
111pub fn init_box_shadow_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {
112    let view_layout = BindGroupLayoutDescriptor::new(
113        "box_shadow_view_layout",
114        &BindGroupLayoutEntries::single(
115            ShaderStages::VERTEX_FRAGMENT,
116            uniform_buffer::<ViewUniform>(true),
117        ),
118    );
119
120    commands.insert_resource(BoxShadowPipeline {
121        view_layout,
122        shader: {
    let (path, asset_server) =
        {
            let path =
                {
                    {
                        let crate_name =
                            "bevy_ui_render::box_shadow".split(':').next().unwrap();
                        ::bevy_asset::io::embedded::_embedded_asset_path(crate_name,
                            "src".as_ref(), "src/box_shadow.rs".as_ref(),
                            "box_shadow.wgsl".as_ref())
                    }
                };
            let path =
                ::bevy_asset::AssetPath::from_path_buf(path).with_source("embedded");
            let asset_server =
                ::bevy_asset::io::embedded::GetAssetServer::get_asset_server(asset_server.as_ref());
            (path, asset_server)
        };
    asset_server.load(path)
}load_embedded_asset!(asset_server.as_ref(), "box_shadow.wgsl"),
123    });
124}
125
126#[derive(#[automatically_derived]
impl ::core::clone::Clone for BoxShadowPipelineKey {
    #[inline]
    fn clone(&self) -> BoxShadowPipelineKey {
        let _: ::core::clone::AssertParamIsClone<TextureFormat>;
        let _: ::core::clone::AssertParamIsClone<u32>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for BoxShadowPipelineKey { }Copy, #[automatically_derived]
impl ::core::hash::Hash for BoxShadowPipelineKey {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.target_format, state);
        ::core::hash::Hash::hash(&self.samples, state)
    }
}Hash, #[automatically_derived]
impl ::core::cmp::PartialEq for BoxShadowPipelineKey {
    #[inline]
    fn eq(&self, other: &BoxShadowPipelineKey) -> bool {
        self.samples == other.samples &&
            self.target_format == other.target_format
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BoxShadowPipelineKey {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<TextureFormat>;
        let _: ::core::cmp::AssertParamIsEq<u32>;
    }
}Eq)]
127pub struct BoxShadowPipelineKey {
128    pub target_format: TextureFormat,
129    /// Number of samples, a higher value results in better quality shadows.
130    pub samples: u32,
131}
132
133impl SpecializedRenderPipeline for BoxShadowPipeline {
134    type Key = BoxShadowPipelineKey;
135
136    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
137        let vertex_layout = VertexBufferLayout::from_vertex_formats(
138            VertexStepMode::Vertex,
139            ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [VertexFormat::Float32x3, VertexFormat::Float32x2,
                VertexFormat::Float32x4, VertexFormat::Float32x2,
                VertexFormat::Float32x4, VertexFormat::Float32,
                VertexFormat::Float32x2]))vec![
140                // position
141                VertexFormat::Float32x3,
142                // uv
143                VertexFormat::Float32x2,
144                // color
145                VertexFormat::Float32x4,
146                // target rect size
147                VertexFormat::Float32x2,
148                // corner radius values (top left, top right, bottom right, bottom left)
149                VertexFormat::Float32x4,
150                // blur radius
151                VertexFormat::Float32,
152                // outer size
153                VertexFormat::Float32x2,
154            ],
155        );
156        let shader_defs = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [ShaderDefVal::UInt("SHADOW_SAMPLES".to_string(), key.samples)]))vec![ShaderDefVal::UInt(
157            "SHADOW_SAMPLES".to_string(),
158            key.samples,
159        )];
160
161        RenderPipelineDescriptor {
162            vertex: VertexState {
163                shader: self.shader.clone(),
164                shader_defs: shader_defs.clone(),
165                buffers: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [vertex_layout]))vec![vertex_layout],
166                ..default()
167            },
168            fragment: Some(FragmentState {
169                shader: self.shader.clone(),
170                shader_defs,
171                targets: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [Some(ColorTargetState {
                        format: key.target_format,
                        blend: Some(BlendState::ALPHA_BLENDING),
                        write_mask: ColorWrites::ALL,
                    })]))vec![Some(ColorTargetState {
172                    format: key.target_format,
173                    blend: Some(BlendState::ALPHA_BLENDING),
174                    write_mask: ColorWrites::ALL,
175                })],
176                ..default()
177            }),
178            layout: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [self.view_layout.clone()]))vec![self.view_layout.clone()],
179            label: Some("box_shadow_pipeline".into()),
180            ..default()
181        }
182    }
183}
184
185/// Description of a shadow to be sorted and queued for rendering
186pub struct ExtractedBoxShadow {
187    pub stack_index: u32,
188    pub transform: Affine2,
189    pub bounds: Vec2,
190    pub clip: Option<Rect>,
191    pub extracted_camera_entity: Entity,
192    pub color: LinearRgba,
193    pub radius: ResolvedBorderRadius,
194    pub blur_radius: f32,
195    pub size: Vec2,
196    pub main_entity: MainEntity,
197    pub render_entity: Entity,
198}
199
200/// List of extracted shadows to be sorted and queued for rendering
201#[derive(impl bevy_ecs::resource::Resource for ExtractedBoxShadows where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource, #[automatically_derived]
impl ::core::default::Default for ExtractedBoxShadows {
    #[inline]
    fn default() -> ExtractedBoxShadows {
        ExtractedBoxShadows {
            box_shadows: ::core::default::Default::default(),
        }
    }
}Default)]
202pub struct ExtractedBoxShadows {
203    pub box_shadows: Vec<ExtractedBoxShadow>,
204}
205
206pub fn extract_shadows(
207    mut commands: Commands,
208    mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
209    box_shadow_query: Extract<
210        Query<(
211            Entity,
212            &ComputedNode,
213            &ComputedStackIndex,
214            &UiGlobalTransform,
215            &InheritedVisibility,
216            &BoxShadow,
217            Option<&CalculatedClip>,
218            &ComputedUiTargetCamera,
219            &ComputedUiRenderTargetInfo,
220        )>,
221    >,
222    camera_map: Extract<UiCameraMap>,
223) {
224    let mut mapping = camera_map.get_mapper();
225
226    for (entity, uinode, stack_index, transform, visibility, box_shadow, clip, camera, target) in
227        &box_shadow_query
228    {
229        // Skip if no visible shadows
230        if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() {
231            continue;
232        }
233
234        let Some(extracted_camera_entity) = mapping.map(camera) else {
235            continue;
236        };
237
238        let ui_physical_viewport_size = target.physical_size().as_vec2();
239        let scale_factor = target.scale_factor();
240
241        for drop_shadow in box_shadow.iter() {
242            if drop_shadow.color.is_fully_transparent() {
243                continue;
244            }
245
246            let resolve_val = |val, base, scale_factor| match val {
247                Val::Auto => 0.,
248                Val::Px(px) => px * scale_factor,
249                Val::Percent(percent) => percent / 100. * base,
250                Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x,
251                Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y,
252                Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(),
253                Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(),
254            };
255
256            let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor);
257            let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x;
258
259            let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y);
260
261            let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor);
262            let offset = vec2(
263                resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor),
264                resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor),
265            );
266
267            let shadow_size = uinode.size() + spread;
268            if shadow_size.cmple(Vec2::ZERO).any() {
269                continue;
270            }
271
272            let radius = ResolvedBorderRadius {
273                top_left: uinode.border_radius.top_left * spread_ratio,
274                top_right: uinode.border_radius.top_right * spread_ratio,
275                bottom_left: uinode.border_radius.bottom_left * spread_ratio,
276                bottom_right: uinode.border_radius.bottom_right * spread_ratio,
277            };
278
279            extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {
280                render_entity: commands.spawn(TemporaryRenderEntity).id(),
281                stack_index: stack_index.0,
282                transform: Affine2::from(transform) * Affine2::from_translation(offset),
283                color: drop_shadow.color.into(),
284                bounds: shadow_size + 6. * blur_radius,
285                clip: clip.map(|clip| clip.clip),
286                extracted_camera_entity,
287                radius,
288                blur_radius,
289                size: shadow_size,
290                main_entity: entity.into(),
291            });
292        }
293    }
294}
295
296#[expect(
297    clippy::too_many_arguments,
298    reason = "it's a system that needs a lot of them"
299)]
300pub fn queue_shadows(
301    extracted_box_shadows: ResMut<ExtractedBoxShadows>,
302    box_shadow_pipeline: Res<BoxShadowPipeline>,
303    mut pipelines: ResMut<SpecializedRenderPipelines<BoxShadowPipeline>>,
304    mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
305    mut render_views: Query<(&UiCameraView, Option<&BoxShadowSamples>), With<ExtractedView>>,
306    camera_views: Query<&ExtractedView>,
307    pipeline_cache: Res<PipelineCache>,
308    draw_functions: Res<DrawFunctions<TransparentUi>>,
309) {
310    let draw_function = draw_functions.read().id::<DrawBoxShadows>();
311    for (index, extracted_shadow) in extracted_box_shadows.box_shadows.iter().enumerate() {
312        let entity = extracted_shadow.render_entity;
313        let Ok((default_camera_view, shadow_samples)) =
314            render_views.get_mut(extracted_shadow.extracted_camera_entity)
315        else {
316            continue;
317        };
318
319        let Ok(view) = camera_views.get(default_camera_view.0) else {
320            continue;
321        };
322
323        let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
324        else {
325            continue;
326        };
327
328        let pipeline = pipelines.specialize(
329            &pipeline_cache,
330            &box_shadow_pipeline,
331            BoxShadowPipelineKey {
332                target_format: view.target_format,
333                samples: shadow_samples.copied().unwrap_or_default().0,
334            },
335        );
336
337        transparent_phase.add_transient(TransparentUi {
338            draw_function,
339            pipeline,
340            entity: (entity, extracted_shadow.main_entity),
341            sort_key: FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW),
342
343            batch_range: 0..0,
344            extra_index: PhaseItemExtraIndex::None,
345            index,
346            indexed: true,
347        });
348    }
349}
350
351pub fn prepare_shadows(
352    mut commands: Commands,
353    render_device: Res<RenderDevice>,
354    render_queue: Res<RenderQueue>,
355    pipeline_cache: Res<PipelineCache>,
356    mut ui_meta: ResMut<BoxShadowMeta>,
357    mut extracted_shadows: ResMut<ExtractedBoxShadows>,
358    view_uniforms: Res<ViewUniforms>,
359    box_shadow_pipeline: Res<BoxShadowPipeline>,
360    mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
361    mut previous_len: Local<usize>,
362) {
363    if let Some(view_binding) = view_uniforms.uniforms.binding() {
364        let mut batches: Vec<(Entity, UiShadowsBatch)> = Vec::with_capacity(*previous_len);
365
366        ui_meta.vertices.clear();
367        ui_meta.indices.clear();
368        ui_meta.view_bind_group = Some(render_device.create_bind_group(
369            "box_shadow_view_bind_group",
370            &pipeline_cache.get_bind_group_layout(&box_shadow_pipeline.view_layout),
371            &BindGroupEntries::single(view_binding),
372        ));
373
374        // Buffer indexes
375        let mut vertices_index = 0;
376        let mut indices_index = 0;
377
378        for ui_phase in phases.values_mut() {
379            for item_index in 0..ui_phase.items.len() {
380                let item = &mut ui_phase.items[item_index];
381                let Some(box_shadow) = extracted_shadows
382                    .box_shadows
383                    .get(item.index)
384                    .filter(|n| item.entity() == n.render_entity)
385                else {
386                    continue;
387                };
388                let rect_size = box_shadow.bounds;
389
390                // Specify the corners of the node
391                let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
392                    box_shadow
393                        .transform
394                        .transform_point2(pos * rect_size)
395                        .extend(0.)
396                });
397
398                // Calculate the effect of clipping
399                // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
400                let positions_diff = if let Some(clip) = box_shadow.clip {
401                    [
402                        Vec2::new(
403                            f32::max(clip.min.x - positions[0].x, 0.),
404                            f32::max(clip.min.y - positions[0].y, 0.),
405                        ),
406                        Vec2::new(
407                            f32::min(clip.max.x - positions[1].x, 0.),
408                            f32::max(clip.min.y - positions[1].y, 0.),
409                        ),
410                        Vec2::new(
411                            f32::min(clip.max.x - positions[2].x, 0.),
412                            f32::min(clip.max.y - positions[2].y, 0.),
413                        ),
414                        Vec2::new(
415                            f32::max(clip.min.x - positions[3].x, 0.),
416                            f32::min(clip.max.y - positions[3].y, 0.),
417                        ),
418                    ]
419                } else {
420                    [Vec2::ZERO; 4]
421                };
422
423                let positions_clipped = [
424                    positions[0] + positions_diff[0].extend(0.),
425                    positions[1] + positions_diff[1].extend(0.),
426                    positions[2] + positions_diff[2].extend(0.),
427                    positions[3] + positions_diff[3].extend(0.),
428                ];
429
430                let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size).abs();
431
432                // Don't try to cull nodes that have a rotation
433                // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
434                // In those two cases, the culling check can proceed normally as corners will be on
435                // horizontal / vertical lines
436                // For all other angles, bypass the culling check
437                // This does not properly handles all rotations on all axis
438                if box_shadow.transform.x_axis[1] == 0.0 {
439                    // Cull nodes that are completely clipped
440                    if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
441                        || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
442                    {
443                        continue;
444                    }
445                }
446
447                let uvs = [
448                    Vec2::new(positions_diff[0].x, positions_diff[0].y),
449                    Vec2::new(
450                        box_shadow.bounds.x + positions_diff[1].x,
451                        positions_diff[1].y,
452                    ),
453                    Vec2::new(
454                        box_shadow.bounds.x + positions_diff[2].x,
455                        box_shadow.bounds.y + positions_diff[2].y,
456                    ),
457                    Vec2::new(
458                        positions_diff[3].x,
459                        box_shadow.bounds.y + positions_diff[3].y,
460                    ),
461                ]
462                .map(|pos| pos / box_shadow.bounds);
463
464                for i in 0..4 {
465                    ui_meta.vertices.push(BoxShadowVertex {
466                        position: positions_clipped[i].into(),
467                        uvs: uvs[i].into(),
468                        vertex_color: box_shadow.color.to_f32_array(),
469                        size: box_shadow.size.into(),
470                        radius: box_shadow.radius.into(),
471                        blur: box_shadow.blur_radius,
472                        bounds: rect_size.into(),
473                    });
474                }
475
476                for &i in &QUAD_INDICES {
477                    ui_meta.indices.push(indices_index + i as u32);
478                }
479
480                batches.push((
481                    item.entity(),
482                    UiShadowsBatch {
483                        range: vertices_index..vertices_index + 6,
484                        camera: box_shadow.extracted_camera_entity,
485                    },
486                ));
487
488                vertices_index += 6;
489                indices_index += 4;
490
491                // shadows are sent to the gpu non-batched
492                *ui_phase.items[item_index].batch_range_mut() =
493                    item_index as u32..item_index as u32 + 1;
494            }
495        }
496        ui_meta.vertices.write_buffer(&render_device, &render_queue);
497        ui_meta.indices.write_buffer(&render_device, &render_queue);
498        *previous_len = batches.len();
499        commands.try_insert_batch(batches);
500    }
501    extracted_shadows.box_shadows.clear();
502}
503
504pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow);
505
506pub struct SetBoxShadowViewBindGroup<const I: usize>;
507impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetBoxShadowViewBindGroup<I> {
508    type Param = SRes<BoxShadowMeta>;
509    type ViewQuery = Read<ViewUniformOffset>;
510    type ItemQuery = ();
511
512    fn render<'w>(
513        _item: &P,
514        view_uniform: &'w ViewUniformOffset,
515        _entity: Option<()>,
516        ui_meta: SystemParamItem<'w, '_, Self::Param>,
517        pass: &mut TrackedRenderPass<'w>,
518    ) -> RenderCommandResult {
519        let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
520            return RenderCommandResult::Failure("view_bind_group not available");
521        };
522        pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
523        RenderCommandResult::Success
524    }
525}
526
527pub struct DrawBoxShadow;
528impl<P: PhaseItem> RenderCommand<P> for DrawBoxShadow {
529    type Param = SRes<BoxShadowMeta>;
530    type ViewQuery = ();
531    type ItemQuery = Read<UiShadowsBatch>;
532
533    #[inline]
534    fn render<'w>(
535        _item: &P,
536        _view: (),
537        batch: Option<&'w UiShadowsBatch>,
538        ui_meta: SystemParamItem<'w, '_, Self::Param>,
539        pass: &mut TrackedRenderPass<'w>,
540    ) -> RenderCommandResult {
541        let Some(batch) = batch else {
542            return RenderCommandResult::Skip;
543        };
544        let ui_meta = ui_meta.into_inner();
545        let Some(vertices) = ui_meta.vertices.buffer() else {
546            return RenderCommandResult::Failure("missing vertices to draw ui");
547        };
548        let Some(indices) = ui_meta.indices.buffer() else {
549            return RenderCommandResult::Failure("missing indices to draw ui");
550        };
551
552        // Store the vertices
553        pass.set_vertex_buffer(0, vertices.slice(..));
554        // Define how to "connect" the vertices
555        pass.set_index_buffer(indices.slice(..), IndexFormat::Uint32);
556        // Draw the vertices
557        pass.draw_indexed(batch.range.clone(), 0, 0..1);
558        RenderCommandResult::Success
559    }
560}