Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui_render/src/gradient.rs
9346 views
1
use core::{
2
f32::consts::{FRAC_PI_2, TAU},
3
hash::Hash,
4
ops::Range,
5
};
6
7
use super::shader_flags::BORDER_ALL;
8
use crate::*;
9
use bevy_asset::*;
10
use bevy_color::{ColorToComponents, Hsla, Hsva, LinearRgba, Oklaba, Oklcha, Srgba};
11
use bevy_ecs::{
12
prelude::Component,
13
system::{
14
lifetimeless::{Read, SRes},
15
*,
16
},
17
};
18
use bevy_image::prelude::*;
19
use bevy_math::{
20
ops::{cos, sin},
21
FloatOrd, Rect, Vec2,
22
};
23
use bevy_math::{Affine2, Vec2Swizzles};
24
use bevy_mesh::VertexBufferLayout;
25
use bevy_render::{
26
render_phase::*,
27
render_resource::{binding_types::uniform_buffer, *},
28
renderer::{RenderDevice, RenderQueue},
29
sync_world::TemporaryRenderEntity,
30
view::*,
31
Extract, ExtractSchedule, Render, RenderSystems,
32
};
33
use bevy_render::{sync_world::MainEntity, RenderStartup};
34
use bevy_shader::Shader;
35
use bevy_sprite::BorderRect;
36
use bevy_ui::{
37
BackgroundGradient, BorderGradient, ColorStop, ComputedUiRenderTargetInfo, ConicGradient,
38
Gradient, InterpolationColorSpace, LinearGradient, RadialGradient, ResolvedBorderRadius, Val,
39
};
40
use bevy_utils::default;
41
use bytemuck::{Pod, Zeroable};
42
43
pub struct GradientPlugin;
44
45
impl Plugin for GradientPlugin {
46
fn build(&self, app: &mut App) {
47
embedded_asset!(app, "gradient.wgsl");
48
49
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
50
render_app
51
.add_render_command::<TransparentUi, DrawGradientFns>()
52
.init_resource::<ExtractedGradients>()
53
.init_resource::<ExtractedColorStops>()
54
.init_resource::<GradientMeta>()
55
.init_resource::<SpecializedRenderPipelines<GradientPipeline>>()
56
.add_systems(RenderStartup, init_gradient_pipeline)
57
.add_systems(
58
ExtractSchedule,
59
extract_gradients
60
.in_set(RenderUiSystems::ExtractGradient)
61
.after(extract_uinode_background_colors),
62
)
63
.add_systems(
64
Render,
65
(
66
queue_gradient.in_set(RenderSystems::Queue),
67
prepare_gradient.in_set(RenderSystems::PrepareBindGroups),
68
),
69
);
70
}
71
}
72
}
73
74
#[derive(Component)]
75
pub struct GradientBatch {
76
pub range: Range<u32>,
77
}
78
79
#[derive(Resource)]
80
pub struct GradientMeta {
81
vertices: RawBufferVec<UiGradientVertex>,
82
indices: RawBufferVec<u32>,
83
view_bind_group: Option<BindGroup>,
84
}
85
86
impl Default for GradientMeta {
87
fn default() -> Self {
88
Self {
89
vertices: RawBufferVec::new(BufferUsages::VERTEX),
90
indices: RawBufferVec::new(BufferUsages::INDEX),
91
view_bind_group: None,
92
}
93
}
94
}
95
96
#[derive(Resource)]
97
pub struct GradientPipeline {
98
pub view_layout: BindGroupLayoutDescriptor,
99
pub shader: Handle<Shader>,
100
}
101
102
pub fn init_gradient_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {
103
let view_layout = BindGroupLayoutDescriptor::new(
104
"ui_gradient_view_layout",
105
&BindGroupLayoutEntries::single(
106
ShaderStages::VERTEX_FRAGMENT,
107
uniform_buffer::<ViewUniform>(true),
108
),
109
);
110
111
commands.insert_resource(GradientPipeline {
112
view_layout,
113
shader: load_embedded_asset!(asset_server.as_ref(), "gradient.wgsl"),
114
});
115
}
116
117
pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 {
118
let center = 0.5 * size;
119
let v = Vec2::new(sin(angle), -cos(angle));
120
121
let (pos_corner, neg_corner) = if v.x >= 0.0 && v.y <= 0.0 {
122
(size.with_y(0.), size.with_x(0.))
123
} else if v.x >= 0.0 && v.y > 0.0 {
124
(size, Vec2::ZERO)
125
} else if v.x < 0.0 && v.y <= 0.0 {
126
(Vec2::ZERO, size)
127
} else {
128
(size.with_x(0.), size.with_y(0.))
129
};
130
131
let t_pos = (pos_corner - center).dot(v);
132
let t_neg = (neg_corner - center).dot(v);
133
134
(t_pos - t_neg).abs()
135
}
136
137
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
138
pub struct UiGradientPipelineKey {
139
anti_alias: bool,
140
color_space: InterpolationColorSpace,
141
pub hdr: bool,
142
}
143
144
impl SpecializedRenderPipeline for GradientPipeline {
145
type Key = UiGradientPipelineKey;
146
147
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
148
let vertex_layout = VertexBufferLayout::from_vertex_formats(
149
VertexStepMode::Vertex,
150
vec![
151
// position
152
VertexFormat::Float32x3,
153
// uv
154
VertexFormat::Float32x2,
155
// flags
156
VertexFormat::Uint32,
157
// radius
158
VertexFormat::Float32x4,
159
// border
160
VertexFormat::Float32x4,
161
// size
162
VertexFormat::Float32x2,
163
// point
164
VertexFormat::Float32x2,
165
// start_point
166
VertexFormat::Float32x2,
167
// dir
168
VertexFormat::Float32x2,
169
// start_color
170
VertexFormat::Float32x4,
171
// start_len
172
VertexFormat::Float32,
173
// end_len
174
VertexFormat::Float32,
175
// end color
176
VertexFormat::Float32x4,
177
// hint
178
VertexFormat::Float32,
179
],
180
);
181
let color_space = match key.color_space {
182
InterpolationColorSpace::Oklaba => "IN_OKLAB",
183
InterpolationColorSpace::Oklcha => "IN_OKLCH",
184
InterpolationColorSpace::OklchaLong => "IN_OKLCH_LONG",
185
InterpolationColorSpace::Srgba => "IN_SRGB",
186
InterpolationColorSpace::LinearRgba => "IN_LINEAR_RGB",
187
InterpolationColorSpace::Hsla => "IN_HSL",
188
InterpolationColorSpace::HslaLong => "IN_HSL_LONG",
189
InterpolationColorSpace::Hsva => "IN_HSV",
190
InterpolationColorSpace::HsvaLong => "IN_HSV_LONG",
191
};
192
193
let shader_defs = if key.anti_alias {
194
vec![color_space.into(), "ANTI_ALIAS".into()]
195
} else {
196
vec![color_space.into()]
197
};
198
199
RenderPipelineDescriptor {
200
vertex: VertexState {
201
shader: self.shader.clone(),
202
shader_defs: shader_defs.clone(),
203
buffers: vec![vertex_layout],
204
..default()
205
},
206
fragment: Some(FragmentState {
207
shader: self.shader.clone(),
208
shader_defs,
209
targets: vec![Some(ColorTargetState {
210
format: if key.hdr {
211
ViewTarget::TEXTURE_FORMAT_HDR
212
} else {
213
TextureFormat::bevy_default()
214
},
215
blend: Some(BlendState::ALPHA_BLENDING),
216
write_mask: ColorWrites::ALL,
217
})],
218
..default()
219
}),
220
layout: vec![self.view_layout.clone()],
221
label: Some("ui_gradient_pipeline".into()),
222
..default()
223
}
224
}
225
}
226
227
pub enum ResolvedGradient {
228
Linear { angle: f32 },
229
Conic { center: Vec2, start: f32 },
230
Radial { center: Vec2, size: Vec2 },
231
}
232
233
pub struct ExtractedGradient {
234
pub stack_index: u32,
235
pub transform: Affine2,
236
pub rect: Rect,
237
pub clip: Option<Rect>,
238
pub extracted_camera_entity: Entity,
239
/// range into `ExtractedColorStops`
240
pub stops_range: Range<usize>,
241
pub node_type: NodeType,
242
pub main_entity: MainEntity,
243
pub render_entity: Entity,
244
/// Border radius of the UI node.
245
/// Ordering: top left, top right, bottom right, bottom left.
246
pub border_radius: ResolvedBorderRadius,
247
/// Border thickness of the UI node.
248
/// Ordering: left, top, right, bottom.
249
pub border: BorderRect,
250
pub resolved_gradient: ResolvedGradient,
251
pub color_space: InterpolationColorSpace,
252
}
253
254
#[derive(Resource, Default)]
255
pub struct ExtractedGradients {
256
pub items: Vec<ExtractedGradient>,
257
}
258
259
#[derive(Resource, Default)]
260
pub struct ExtractedColorStops(pub Vec<(LinearRgba, f32, f32)>);
261
262
// Interpolate implicit stops (where position is `f32::NAN`)
263
// If the first and last stops are implicit set them to the `min` and `max` values
264
// so that we always have explicit start and end points to interpolate between.
265
fn interpolate_color_stops(stops: &mut [(LinearRgba, f32, f32)], min: f32, max: f32) {
266
if stops[0].1.is_nan() {
267
stops[0].1 = min;
268
}
269
if stops.last().unwrap().1.is_nan() {
270
stops.last_mut().unwrap().1 = max;
271
}
272
273
let mut i = 1;
274
275
while i < stops.len() - 1 {
276
let point = stops[i].1;
277
if point.is_nan() {
278
let start = i;
279
let mut end = i + 1;
280
while end < stops.len() - 1 && stops[end].1.is_nan() {
281
end += 1;
282
}
283
let start_point = stops[start - 1].1;
284
let end_point = stops[end].1;
285
let steps = end - start;
286
let step = (end_point - start_point) / (steps + 1) as f32;
287
for j in 0..steps {
288
stops[i + j].1 = start_point + step * (j + 1) as f32;
289
}
290
i = end;
291
}
292
i += 1;
293
}
294
}
295
296
fn compute_color_stops(
297
stops: &[ColorStop],
298
scale_factor: f32,
299
length: f32,
300
target_size: Vec2,
301
scratch: &mut Vec<(LinearRgba, f32, f32)>,
302
extracted_color_stops: &mut Vec<(LinearRgba, f32, f32)>,
303
) {
304
// resolve the physical distances of explicit stops and sort them
305
scratch.extend(stops.iter().filter_map(|stop| {
306
stop.point
307
.resolve(scale_factor, length, target_size)
308
.ok()
309
.map(|physical_point| (stop.color.to_linear(), physical_point, stop.hint))
310
}));
311
scratch.sort_by_key(|(_, point, _)| FloatOrd(*point));
312
313
let min = scratch
314
.first()
315
.map(|(_, min, _)| *min)
316
.unwrap_or(0.)
317
.min(0.);
318
319
// get the position of the last explicit stop and use the full length of the gradient if no explicit stops
320
let max = scratch
321
.last()
322
.map(|(_, max, _)| *max)
323
.unwrap_or(length)
324
.max(length);
325
326
let mut sorted_stops_drain = scratch.drain(..);
327
328
let range_start = extracted_color_stops.len();
329
330
// Fill the extracted color stops buffer
331
extracted_color_stops.extend(stops.iter().map(|stop| {
332
if stop.point == Val::Auto {
333
(stop.color.to_linear(), f32::NAN, stop.hint)
334
} else {
335
sorted_stops_drain.next().unwrap()
336
}
337
}));
338
339
interpolate_color_stops(&mut extracted_color_stops[range_start..], min, max);
340
}
341
342
pub fn extract_gradients(
343
mut commands: Commands,
344
mut extracted_gradients: ResMut<ExtractedGradients>,
345
mut extracted_color_stops: ResMut<ExtractedColorStops>,
346
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
347
gradients_query: Extract<
348
Query<(
349
Entity,
350
&ComputedNode,
351
&ComputedUiTargetCamera,
352
&ComputedUiRenderTargetInfo,
353
&UiGlobalTransform,
354
&InheritedVisibility,
355
Option<&CalculatedClip>,
356
AnyOf<(&BackgroundGradient, &BorderGradient)>,
357
)>,
358
>,
359
camera_map: Extract<UiCameraMap>,
360
) {
361
let mut camera_mapper = camera_map.get_mapper();
362
let mut sorted_stops = vec![];
363
364
for (
365
entity,
366
uinode,
367
camera,
368
target,
369
transform,
370
inherited_visibility,
371
clip,
372
(gradient, gradient_border),
373
) in &gradients_query
374
{
375
// Skip invisible images
376
if !inherited_visibility.get() {
377
continue;
378
}
379
380
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
381
continue;
382
};
383
384
for (gradients, node_type) in [
385
(gradient.map(|g| &g.0), NodeType::Rect),
386
(gradient_border.map(|g| &g.0), NodeType::Border(BORDER_ALL)),
387
]
388
.iter()
389
.filter_map(|(g, n)| g.map(|g| (g, *n)))
390
{
391
for gradient in gradients.iter() {
392
if gradient.is_empty() {
393
continue;
394
}
395
if let Some(color) = gradient.get_single() {
396
// With a single color stop there's no gradient, fill the node with the color
397
extracted_uinodes.uinodes.push(ExtractedUiNode {
398
z_order: uinode.stack_index as f32
399
+ match node_type {
400
NodeType::Rect => stack_z_offsets::GRADIENT,
401
NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT,
402
},
403
image: AssetId::default(),
404
clip: clip.map(|clip| clip.clip),
405
extracted_camera_entity,
406
transform: transform.into(),
407
item: ExtractedUiItem::Node {
408
color: color.into(),
409
rect: Rect {
410
min: Vec2::ZERO,
411
max: uinode.size,
412
},
413
atlas_scaling: None,
414
flip_x: false,
415
flip_y: false,
416
border_radius: uinode.border_radius,
417
border: uinode.border,
418
node_type,
419
},
420
main_entity: entity.into(),
421
render_entity: commands.spawn(TemporaryRenderEntity).id(),
422
});
423
continue;
424
}
425
match gradient {
426
Gradient::Linear(LinearGradient {
427
color_space,
428
angle,
429
stops,
430
}) => {
431
let length = compute_gradient_line_length(*angle, uinode.size);
432
433
let range_start = extracted_color_stops.0.len();
434
435
compute_color_stops(
436
stops,
437
target.scale_factor(),
438
length,
439
target.physical_size().as_vec2(),
440
&mut sorted_stops,
441
&mut extracted_color_stops.0,
442
);
443
444
extracted_gradients.items.push(ExtractedGradient {
445
render_entity: commands.spawn(TemporaryRenderEntity).id(),
446
stack_index: uinode.stack_index,
447
transform: transform.into(),
448
stops_range: range_start..extracted_color_stops.0.len(),
449
rect: Rect {
450
min: Vec2::ZERO,
451
max: uinode.size,
452
},
453
clip: clip.map(|clip| clip.clip),
454
extracted_camera_entity,
455
main_entity: entity.into(),
456
node_type,
457
border_radius: uinode.border_radius,
458
border: uinode.border,
459
resolved_gradient: ResolvedGradient::Linear { angle: *angle },
460
color_space: *color_space,
461
});
462
}
463
Gradient::Radial(RadialGradient {
464
color_space,
465
position: center,
466
shape,
467
stops,
468
}) => {
469
let c = center.resolve(
470
target.scale_factor(),
471
uinode.size,
472
target.physical_size().as_vec2(),
473
);
474
475
let size = shape.resolve(
476
c,
477
target.scale_factor(),
478
uinode.size,
479
target.physical_size().as_vec2(),
480
);
481
482
let length = size.x;
483
484
let range_start = extracted_color_stops.0.len();
485
compute_color_stops(
486
stops,
487
target.scale_factor(),
488
length,
489
target.physical_size().as_vec2(),
490
&mut sorted_stops,
491
&mut extracted_color_stops.0,
492
);
493
494
extracted_gradients.items.push(ExtractedGradient {
495
render_entity: commands.spawn(TemporaryRenderEntity).id(),
496
stack_index: uinode.stack_index,
497
transform: transform.into(),
498
stops_range: range_start..extracted_color_stops.0.len(),
499
rect: Rect {
500
min: Vec2::ZERO,
501
max: uinode.size,
502
},
503
clip: clip.map(|clip| clip.clip),
504
extracted_camera_entity,
505
main_entity: entity.into(),
506
node_type,
507
border_radius: uinode.border_radius,
508
border: uinode.border,
509
resolved_gradient: ResolvedGradient::Radial { center: c, size },
510
color_space: *color_space,
511
});
512
}
513
Gradient::Conic(ConicGradient {
514
color_space,
515
start,
516
position: center,
517
stops,
518
}) => {
519
let g_start = center.resolve(
520
target.scale_factor(),
521
uinode.size,
522
target.physical_size().as_vec2(),
523
);
524
let range_start = extracted_color_stops.0.len();
525
526
// sort the explicit stops
527
sorted_stops.extend(stops.iter().filter_map(|stop| {
528
stop.angle.map(|angle| {
529
(stop.color.to_linear(), angle.clamp(0., TAU), stop.hint)
530
})
531
}));
532
sorted_stops.sort_by_key(|(_, angle, _)| FloatOrd(*angle));
533
let mut sorted_stops_drain = sorted_stops.drain(..);
534
535
// fill the extracted stops buffer
536
extracted_color_stops.0.extend(stops.iter().map(|stop| {
537
if stop.angle.is_none() {
538
(stop.color.to_linear(), f32::NAN, stop.hint)
539
} else {
540
sorted_stops_drain.next().unwrap()
541
}
542
}));
543
544
interpolate_color_stops(
545
&mut extracted_color_stops.0[range_start..],
546
0.,
547
TAU,
548
);
549
550
extracted_gradients.items.push(ExtractedGradient {
551
render_entity: commands.spawn(TemporaryRenderEntity).id(),
552
stack_index: uinode.stack_index,
553
transform: transform.into(),
554
stops_range: range_start..extracted_color_stops.0.len(),
555
rect: Rect {
556
min: Vec2::ZERO,
557
max: uinode.size,
558
},
559
clip: clip.map(|clip| clip.clip),
560
extracted_camera_entity,
561
main_entity: entity.into(),
562
node_type,
563
border_radius: uinode.border_radius,
564
border: uinode.border,
565
resolved_gradient: ResolvedGradient::Conic {
566
start: *start,
567
center: g_start,
568
},
569
color_space: *color_space,
570
});
571
}
572
}
573
}
574
}
575
}
576
}
577
578
#[expect(
579
clippy::too_many_arguments,
580
reason = "it's a system that needs a lot of them"
581
)]
582
pub fn queue_gradient(
583
extracted_gradients: ResMut<ExtractedGradients>,
584
gradients_pipeline: Res<GradientPipeline>,
585
mut pipelines: ResMut<SpecializedRenderPipelines<GradientPipeline>>,
586
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
587
mut render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With<ExtractedView>>,
588
camera_views: Query<&ExtractedView>,
589
pipeline_cache: Res<PipelineCache>,
590
draw_functions: Res<DrawFunctions<TransparentUi>>,
591
) {
592
let draw_function = draw_functions.read().id::<DrawGradientFns>();
593
for (index, gradient) in extracted_gradients.items.iter().enumerate() {
594
let Ok((default_camera_view, ui_anti_alias)) =
595
render_views.get_mut(gradient.extracted_camera_entity)
596
else {
597
continue;
598
};
599
600
let Ok(view) = camera_views.get(default_camera_view.0) else {
601
continue;
602
};
603
604
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
605
else {
606
continue;
607
};
608
609
let pipeline = pipelines.specialize(
610
&pipeline_cache,
611
&gradients_pipeline,
612
UiGradientPipelineKey {
613
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
614
color_space: gradient.color_space,
615
hdr: view.hdr,
616
},
617
);
618
619
transparent_phase.add(TransparentUi {
620
draw_function,
621
pipeline,
622
entity: (gradient.render_entity, gradient.main_entity),
623
sort_key: FloatOrd(
624
gradient.stack_index as f32
625
+ match gradient.node_type {
626
NodeType::Rect => stack_z_offsets::GRADIENT,
627
NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT,
628
},
629
),
630
batch_range: 0..0,
631
extra_index: PhaseItemExtraIndex::None,
632
index,
633
indexed: true,
634
});
635
}
636
}
637
638
#[repr(C)]
639
#[derive(Copy, Clone, Pod, Zeroable)]
640
struct UiGradientVertex {
641
position: [f32; 3],
642
uv: [f32; 2],
643
flags: u32,
644
radius: [f32; 4],
645
border: [f32; 4],
646
size: [f32; 2],
647
point: [f32; 2],
648
g_start: [f32; 2],
649
g_dir: [f32; 2],
650
start_color: [f32; 4],
651
start_len: f32,
652
end_len: f32,
653
end_color: [f32; 4],
654
hint: f32,
655
}
656
657
fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] {
658
match space {
659
InterpolationColorSpace::Oklaba => {
660
let oklaba: Oklaba = color.into();
661
[oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha]
662
}
663
InterpolationColorSpace::Oklcha | InterpolationColorSpace::OklchaLong => {
664
let oklcha: Oklcha = color.into();
665
[
666
oklcha.lightness,
667
oklcha.chroma,
668
// The shader expects normalized hues
669
oklcha.hue / 360.,
670
oklcha.alpha,
671
]
672
}
673
InterpolationColorSpace::Srgba => {
674
let srgba: Srgba = color.into();
675
[srgba.red, srgba.green, srgba.blue, srgba.alpha]
676
}
677
InterpolationColorSpace::LinearRgba => color.to_f32_array(),
678
InterpolationColorSpace::Hsla | InterpolationColorSpace::HslaLong => {
679
let hsla: Hsla = color.into();
680
// The shader expects normalized hues
681
[hsla.hue / 360., hsla.saturation, hsla.lightness, hsla.alpha]
682
}
683
InterpolationColorSpace::Hsva | InterpolationColorSpace::HsvaLong => {
684
let hsva: Hsva = color.into();
685
// The shader expects normalized hues
686
[hsva.hue / 360., hsva.saturation, hsva.value, hsva.alpha]
687
}
688
}
689
}
690
691
pub fn prepare_gradient(
692
mut commands: Commands,
693
render_device: Res<RenderDevice>,
694
render_queue: Res<RenderQueue>,
695
pipeline_cache: Res<PipelineCache>,
696
mut ui_meta: ResMut<GradientMeta>,
697
mut extracted_gradients: ResMut<ExtractedGradients>,
698
mut extracted_color_stops: ResMut<ExtractedColorStops>,
699
view_uniforms: Res<ViewUniforms>,
700
gradients_pipeline: Res<GradientPipeline>,
701
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
702
mut previous_len: Local<usize>,
703
) {
704
if let Some(view_binding) = view_uniforms.uniforms.binding() {
705
let mut batches: Vec<(Entity, GradientBatch)> = Vec::with_capacity(*previous_len);
706
707
ui_meta.vertices.clear();
708
ui_meta.indices.clear();
709
ui_meta.view_bind_group = Some(render_device.create_bind_group(
710
"gradient_view_bind_group",
711
&pipeline_cache.get_bind_group_layout(&gradients_pipeline.view_layout),
712
&BindGroupEntries::single(view_binding),
713
));
714
715
// Buffer indexes
716
let mut vertices_index = 0;
717
let mut indices_index = 0;
718
719
for ui_phase in phases.values_mut() {
720
for item_index in 0..ui_phase.items.len() {
721
let item = &mut ui_phase.items[item_index];
722
if let Some(gradient) = extracted_gradients
723
.items
724
.get(item.index)
725
.filter(|n| item.entity() == n.render_entity)
726
{
727
*item.batch_range_mut() = item_index as u32..item_index as u32 + 1;
728
let uinode_rect = gradient.rect;
729
730
let rect_size = uinode_rect.size();
731
732
// Specify the corners of the node
733
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
734
gradient
735
.transform
736
.transform_point2(pos * rect_size)
737
.extend(0.)
738
});
739
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
740
741
// Calculate the effect of clipping
742
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
743
let positions_diff = if let Some(clip) = gradient.clip {
744
[
745
Vec2::new(
746
f32::max(clip.min.x - positions[0].x, 0.),
747
f32::max(clip.min.y - positions[0].y, 0.),
748
),
749
Vec2::new(
750
f32::min(clip.max.x - positions[1].x, 0.),
751
f32::max(clip.min.y - positions[1].y, 0.),
752
),
753
Vec2::new(
754
f32::min(clip.max.x - positions[2].x, 0.),
755
f32::min(clip.max.y - positions[2].y, 0.),
756
),
757
Vec2::new(
758
f32::max(clip.min.x - positions[3].x, 0.),
759
f32::min(clip.max.y - positions[3].y, 0.),
760
),
761
]
762
} else {
763
[Vec2::ZERO; 4]
764
};
765
766
let positions_clipped = [
767
positions[0] + positions_diff[0].extend(0.),
768
positions[1] + positions_diff[1].extend(0.),
769
positions[2] + positions_diff[2].extend(0.),
770
positions[3] + positions_diff[3].extend(0.),
771
];
772
773
let points = [
774
corner_points[0] + positions_diff[0],
775
corner_points[1] + positions_diff[1],
776
corner_points[2] + positions_diff[2],
777
corner_points[3] + positions_diff[3],
778
];
779
780
let transformed_rect_size = gradient.transform.transform_vector2(rect_size);
781
782
// Don't try to cull nodes that have a rotation
783
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
784
// In those two cases, the culling check can proceed normally as corners will be on
785
// horizontal / vertical lines
786
// For all other angles, bypass the culling check
787
// This does not properly handles all rotations on all axis
788
if gradient.transform.x_axis[1] == 0.0 {
789
// Cull nodes that are completely clipped
790
if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
791
|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
792
{
793
continue;
794
}
795
}
796
797
let uvs = { [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] };
798
799
let mut flags = if let NodeType::Border(borders) = gradient.node_type {
800
borders
801
} else {
802
0
803
};
804
805
let (g_start, g_dir, g_flags) = match gradient.resolved_gradient {
806
ResolvedGradient::Linear { angle } => {
807
let corner_index = (angle - FRAC_PI_2).rem_euclid(TAU) / FRAC_PI_2;
808
(
809
corner_points[corner_index as usize].into(),
810
// CSS angles increase in a clockwise direction
811
[sin(angle), -cos(angle)],
812
0,
813
)
814
}
815
ResolvedGradient::Conic { center, start } => {
816
(center.into(), [start, 0.], shader_flags::CONIC)
817
}
818
ResolvedGradient::Radial { center, size } => (
819
center.into(),
820
Vec2::splat(if size.y != 0. { size.x / size.y } else { 1. }).into(),
821
shader_flags::RADIAL,
822
),
823
};
824
825
flags |= g_flags;
826
827
let range = gradient.stops_range.start..gradient.stops_range.end - 1;
828
let mut segment_count = 0;
829
830
for stop_index in range {
831
let mut start_stop = extracted_color_stops.0[stop_index];
832
let end_stop = extracted_color_stops.0[stop_index + 1];
833
if start_stop.1 == end_stop.1 {
834
if stop_index == gradient.stops_range.end - 2 {
835
if 0 < segment_count {
836
start_stop.0 = LinearRgba::NONE;
837
}
838
} else {
839
continue;
840
}
841
}
842
let start_color =
843
convert_color_to_space(start_stop.0, gradient.color_space);
844
let end_color = convert_color_to_space(end_stop.0, gradient.color_space);
845
let mut stop_flags = flags;
846
if 0. < start_stop.1
847
&& (stop_index == gradient.stops_range.start || segment_count == 0)
848
{
849
stop_flags |= shader_flags::FILL_START;
850
}
851
if stop_index == gradient.stops_range.end - 2 {
852
stop_flags |= shader_flags::FILL_END;
853
}
854
855
for i in 0..4 {
856
ui_meta.vertices.push(UiGradientVertex {
857
position: positions_clipped[i].into(),
858
uv: uvs[i].into(),
859
flags: stop_flags | shader_flags::CORNERS[i],
860
radius: [
861
gradient.border_radius.top_left,
862
gradient.border_radius.top_right,
863
gradient.border_radius.bottom_right,
864
gradient.border_radius.bottom_left,
865
],
866
border: [
867
gradient.border.min_inset.x,
868
gradient.border.min_inset.y,
869
gradient.border.max_inset.x,
870
gradient.border.max_inset.y,
871
],
872
size: rect_size.xy().into(),
873
g_start,
874
g_dir,
875
point: points[i].into(),
876
start_color,
877
start_len: start_stop.1,
878
end_len: end_stop.1,
879
end_color,
880
hint: start_stop.2,
881
});
882
}
883
884
for &i in &QUAD_INDICES {
885
ui_meta.indices.push(indices_index + i as u32);
886
}
887
indices_index += 4;
888
segment_count += 1;
889
}
890
891
if 0 < segment_count {
892
let vertices_count = 6 * segment_count;
893
894
batches.push((
895
item.entity(),
896
GradientBatch {
897
range: vertices_index..(vertices_index + vertices_count),
898
},
899
));
900
901
vertices_index += vertices_count;
902
}
903
}
904
}
905
}
906
ui_meta.vertices.write_buffer(&render_device, &render_queue);
907
ui_meta.indices.write_buffer(&render_device, &render_queue);
908
*previous_len = batches.len();
909
commands.try_insert_batch(batches);
910
}
911
extracted_gradients.items.clear();
912
extracted_color_stops.0.clear();
913
}
914
915
pub type DrawGradientFns = (SetItemPipeline, SetGradientViewBindGroup<0>, DrawGradient);
916
917
pub struct SetGradientViewBindGroup<const I: usize>;
918
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetGradientViewBindGroup<I> {
919
type Param = SRes<GradientMeta>;
920
type ViewQuery = Read<ViewUniformOffset>;
921
type ItemQuery = ();
922
923
fn render<'w>(
924
_item: &P,
925
view_uniform: &'w ViewUniformOffset,
926
_entity: Option<()>,
927
ui_meta: SystemParamItem<'w, '_, Self::Param>,
928
pass: &mut TrackedRenderPass<'w>,
929
) -> RenderCommandResult {
930
let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
931
return RenderCommandResult::Failure("view_bind_group not available");
932
};
933
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
934
RenderCommandResult::Success
935
}
936
}
937
938
pub struct DrawGradient;
939
impl<P: PhaseItem> RenderCommand<P> for DrawGradient {
940
type Param = SRes<GradientMeta>;
941
type ViewQuery = ();
942
type ItemQuery = Read<GradientBatch>;
943
944
#[inline]
945
fn render<'w>(
946
_item: &P,
947
_view: (),
948
batch: Option<&'w GradientBatch>,
949
ui_meta: SystemParamItem<'w, '_, Self::Param>,
950
pass: &mut TrackedRenderPass<'w>,
951
) -> RenderCommandResult {
952
let Some(batch) = batch else {
953
return RenderCommandResult::Skip;
954
};
955
let ui_meta = ui_meta.into_inner();
956
let Some(vertices) = ui_meta.vertices.buffer() else {
957
return RenderCommandResult::Failure("missing vertices to draw ui");
958
};
959
let Some(indices) = ui_meta.indices.buffer() else {
960
return RenderCommandResult::Failure("missing indices to draw ui");
961
};
962
963
// Store the vertices
964
pass.set_vertex_buffer(0, vertices.slice(..));
965
// Define how to "connect" the vertices
966
pass.set_index_buffer(indices.slice(..), IndexFormat::Uint32);
967
// Draw the vertices
968
pass.draw_indexed(batch.range.clone(), 0, 0..1);
969
RenderCommandResult::Success
970
}
971
}
972
973