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