1use 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
41pub 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#[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 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 VertexFormat::Float32x3,
147 VertexFormat::Float32x2,
149 VertexFormat::Float32x4,
151 VertexFormat::Float32x2,
153 VertexFormat::Float32x4,
155 VertexFormat::Float32,
157 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
194pub 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#[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 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 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 let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
399 box_shadow
400 .transform
401 .transform_point2(pos * rect_size)
402 .extend(0.)
403 });
404
405 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 if box_shadow.transform.x_axis[1] == 0.0 {
446 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 *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 pass.set_vertex_buffer(0, vertices.slice(..));
561 pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32);
563 pass.draw_indexed(batch.range.clone(), 0, 0..1);
565 RenderCommandResult::Success
566 }
567}