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