Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui_render/src/box_shadow.rs
6596 views
1
//! Box shadows rendering
2
3
use core::{hash::Hash, ops::Range};
4
5
use bevy_app::prelude::*;
6
use bevy_asset::*;
7
use bevy_camera::visibility::InheritedVisibility;
8
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
9
use bevy_ecs::prelude::*;
10
use bevy_ecs::{
11
prelude::Component,
12
system::{
13
lifetimeless::{Read, SRes},
14
*,
15
},
16
};
17
use bevy_image::BevyDefault as _;
18
use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};
19
use bevy_mesh::VertexBufferLayout;
20
use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
21
use bevy_render::{
22
render_phase::*,
23
render_resource::{binding_types::uniform_buffer, *},
24
renderer::{RenderDevice, RenderQueue},
25
view::*,
26
Extract, ExtractSchedule, Render, RenderSystems,
27
};
28
use bevy_render::{RenderApp, RenderStartup};
29
use bevy_shader::{Shader, ShaderDefVal};
30
use bevy_ui::{
31
BoxShadow, CalculatedClip, ComputedNode, ComputedUiRenderTargetInfo, ComputedUiTargetCamera,
32
ResolvedBorderRadius, UiGlobalTransform, Val,
33
};
34
use bevy_utils::default;
35
use bytemuck::{Pod, Zeroable};
36
37
use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap};
38
39
use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
40
41
/// A plugin that enables the rendering of box shadows.
42
pub struct BoxShadowPlugin;
43
44
impl 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)]
72
struct 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)]
83
pub 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)]
90
pub struct BoxShadowMeta {
91
vertices: RawBufferVec<BoxShadowVertex>,
92
indices: RawBufferVec<u32>,
93
view_bind_group: Option<BindGroup>,
94
}
95
96
impl 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)]
107
pub struct BoxShadowPipeline {
108
pub view_layout: BindGroupLayout,
109
pub shader: Handle<Shader>,
110
}
111
112
pub 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)]
132
pub struct BoxShadowPipelineKey {
133
pub hdr: bool,
134
/// Number of samples, a higher value results in better quality shadows.
135
pub samples: u32,
136
}
137
138
impl 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
195
pub 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)]
211
pub struct ExtractedBoxShadows {
212
pub box_shadows: Vec<ExtractedBoxShadow>,
213
}
214
215
pub 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
)]
308
pub 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
359
pub 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
511
pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow);
512
513
pub struct SetBoxShadowViewBindGroup<const I: usize>;
514
impl<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
534
pub struct DrawBoxShadow;
535
impl<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
}
568
569