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