bevy/crates/bevy_ui/src/render/gradient.rs
ickshonpe 4836c7868c
Specialized UI transform (#16615)
# Objective

Add specialized UI transform `Component`s and fix some related problems:
* Animating UI elements by modifying the `Transform` component of UI
nodes doesn't work very well because `ui_layout_system` overwrites the
translations each frame. The `overflow_debug` example uses a horrible
hack where it copies the transform into the position that'll likely
cause a panic if any users naively copy it.
* Picking ignores rotation and scaling and assumes UI nodes are always
axis aligned.
* The clipping geometry stored in `CalculatedClip` is wrong for rotated
and scaled elements.
* Transform propagation is unnecessary for the UI, the transforms can be
updated during layout updates.
* The UI internals use both object-centered and top-left-corner-based
coordinates systems for UI nodes. Depending on the context you have to
add or subtract the half-size sometimes before transforming between
coordinate spaces. We should just use one system consistantly so that
the transform can always be directly applied.
* `Transform` doesn't support responsive coordinates.

## Solution

* Unrequire `Transform` from `Node`.
* New components `UiTransform`, `UiGlobalTransform`:
- `Node` requires `UiTransform`, `UiTransform` requires
`UiGlobalTransform`
- `UiTransform` is a 2d-only equivalent of `Transform` with a
translation in `Val`s.
- `UiGlobalTransform` newtypes `Affine2` and is updated in
`ui_layout_system`.
* New helper functions on `ComputedNode` for mapping between viewport
and local node space.
* The cursor position is transformed to local node space during picking
so that it respects rotations and scalings.
* To check if the cursor hovers a node recursively walk up the tree to
the root checking if any of the ancestor nodes clip the point at the
cursor. If the point is clipped the interaction is ignored.
* Use object-centered coordinates for UI nodes.
* `RelativeCursorPosition`'s coordinates are now object-centered with
(0,0) at the the center of the node and the corners at (±0.5, ±0.5).
* Replaced the `normalized_visible_node_rect: Rect` field of
`RelativeCursorPosition` with `cursor_over: bool`, which is set to true
when the cursor is over an unclipped point on the node. The visible area
of the node is not necessarily a rectangle, so the previous
implementation didn't work.

This should fix all the logical bugs with non-axis aligned interactions
and clipping. Rendering still needs changes but they are far outside the
scope of this PR.

Tried and abandoned two other approaches:
* New `transform` field on `Node`, require `GlobalTransform` on `Node`,
and unrequire `Transform` on `Node`. Unrequiring `Transform` opts out of
transform propagation so there is then no conflict with updating the
`GlobalTransform` in `ui_layout_system`. This was a nice change in its
simplicity but potentially confusing for users I think, all the
`GlobalTransform` docs mention `Transform` and having special rules for
how it's updated just for the UI is unpleasently surprising.
* New `transform` field on `Node`. Unrequire `Transform` on `Node`. New
`transform: Affine2` field on `ComputedNode`.
This was okay but I think most users want a separate specialized UI
transform components. The fat `ComputedNode` doesn't work well with
change detection.

Fixes #18929, #18930

## Testing

There is an example you can look at: 
```
cargo run --example ui_transform
```

Sometimes in the example if you press the rotate button couple of times
the first glyph from the top label disappears , I'm not sure what's
causing it yet but I don't think it's related to this PR.

##  Migration Guide
New specialized 2D UI transform components `UiTransform` and
`UiGlobalTransform`. `UiTransform` is a 2d-only equivalent of
`Transform` with a translation in `Val`s. `UiGlobalTransform` newtypes
`Affine2` and is updated in `ui_layout_system`.
`Node` now requires `UiTransform` instead of `Transform`. `UiTransform`
requires `UiGlobalTransform`.

In previous versions of Bevy `ui_layout_system` would overwrite UI
node's `Transform::translation` each frame. `UiTransform`s aren't
overwritten and there is no longer any need for systems that cache and
rewrite the transform for translated UI elements.

`RelativeCursorPosition`'s coordinates are now object-centered with
(0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its
`normalized_visible_node_rect` field has been removed and replaced with
a new `cursor_over: bool` field which is set to true when the cursor is
hovering an unclipped area of the UI node.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2025-06-09 19:05:49 +00:00

919 lines
34 KiB
Rust

use core::{
f32::consts::{FRAC_PI_2, TAU},
hash::Hash,
ops::Range,
};
use crate::*;
use bevy_asset::*;
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_ecs::{
prelude::Component,
system::{
lifetimeless::{Read, SRes},
*,
},
};
use bevy_image::prelude::*;
use bevy_math::{
ops::{cos, sin},
FloatOrd, Rect, Vec2,
};
use bevy_math::{Affine2, Vec2Swizzles};
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::TemporaryRenderEntity,
view::*,
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_sprite::BorderRect;
use bytemuck::{Pod, Zeroable};
use super::shader_flags::BORDER_ALL;
pub struct GradientPlugin;
impl Plugin for GradientPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "gradient.wgsl");
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<TransparentUi, DrawGradientFns>()
.init_resource::<ExtractedGradients>()
.init_resource::<ExtractedColorStops>()
.init_resource::<GradientMeta>()
.init_resource::<SpecializedRenderPipelines<GradientPipeline>>()
.add_systems(
ExtractSchedule,
extract_gradients
.in_set(RenderUiSystems::ExtractGradient)
.after(extract_uinode_background_colors),
)
.add_systems(
Render,
(
queue_gradient.in_set(RenderSystems::Queue),
prepare_gradient.in_set(RenderSystems::PrepareBindGroups),
),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<GradientPipeline>();
}
}
}
#[derive(Component)]
pub struct GradientBatch {
pub range: Range<u32>,
}
#[derive(Resource)]
pub struct GradientMeta {
vertices: RawBufferVec<UiGradientVertex>,
indices: RawBufferVec<u32>,
view_bind_group: Option<BindGroup>,
}
impl Default for GradientMeta {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
indices: RawBufferVec::new(BufferUsages::INDEX),
view_bind_group: None,
}
}
}
#[derive(Resource)]
pub struct GradientPipeline {
pub view_layout: BindGroupLayout,
pub shader: Handle<Shader>,
}
impl FromWorld for GradientPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_layout = render_device.create_bind_group_layout(
"ui_gradient_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
GradientPipeline {
view_layout,
shader: load_embedded_asset!(world, "gradient.wgsl"),
}
}
}
pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 {
let center = 0.5 * size;
let v = Vec2::new(sin(angle), -cos(angle));
let (pos_corner, neg_corner) = if v.x >= 0.0 && v.y <= 0.0 {
(size.with_y(0.), size.with_x(0.))
} else if v.x >= 0.0 && v.y > 0.0 {
(size, Vec2::ZERO)
} else if v.x < 0.0 && v.y <= 0.0 {
(Vec2::ZERO, size)
} else {
(size.with_x(0.), size.with_y(0.))
};
let t_pos = (pos_corner - center).dot(v);
let t_neg = (neg_corner - center).dot(v);
(t_pos - t_neg).abs()
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct UiGradientPipelineKey {
anti_alias: bool,
pub hdr: bool,
}
impl SpecializedRenderPipeline for GradientPipeline {
type Key = UiGradientPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
// position
VertexFormat::Float32x3,
// uv
VertexFormat::Float32x2,
// flags
VertexFormat::Uint32,
// radius
VertexFormat::Float32x4,
// border
VertexFormat::Float32x4,
// size
VertexFormat::Float32x2,
// point
VertexFormat::Float32x2,
// start_point
VertexFormat::Float32x2,
// dir
VertexFormat::Float32x2,
// start_color
VertexFormat::Float32x4,
// start_len
VertexFormat::Float32,
// end_len
VertexFormat::Float32,
// end color
VertexFormat::Float32x4,
// hint
VertexFormat::Float32,
],
);
let shader_defs = if key.anti_alias {
vec!["ANTI_ALIAS".into()]
} else {
Vec::new()
};
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone()],
push_constant_ranges: Vec::new(),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("ui_gradient_pipeline".into()),
zero_initialize_workgroup_memory: false,
}
}
}
pub enum ResolvedGradient {
Linear { angle: f32 },
Conic { center: Vec2, start: f32 },
Radial { center: Vec2, size: Vec2 },
}
pub struct ExtractedGradient {
pub stack_index: u32,
pub transform: Affine2,
pub rect: Rect,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
/// range into `ExtractedColorStops`
pub stops_range: Range<usize>,
pub node_type: NodeType,
pub main_entity: MainEntity,
pub render_entity: Entity,
/// Border radius of the UI node.
/// Ordering: top left, top right, bottom right, bottom left.
pub border_radius: ResolvedBorderRadius,
/// Border thickness of the UI node.
/// Ordering: left, top, right, bottom.
pub border: BorderRect,
pub resolved_gradient: ResolvedGradient,
}
#[derive(Resource, Default)]
pub struct ExtractedGradients {
pub items: Vec<ExtractedGradient>,
}
#[derive(Resource, Default)]
pub struct ExtractedColorStops(pub Vec<(LinearRgba, f32, f32)>);
// Interpolate implicit stops (where position is `f32::NAN`)
// If the first and last stops are implicit set them to the `min` and `max` values
// so that we always have explicit start and end points to interpolate between.
fn interpolate_color_stops(stops: &mut [(LinearRgba, f32, f32)], min: f32, max: f32) {
if stops[0].1.is_nan() {
stops[0].1 = min;
}
if stops.last().unwrap().1.is_nan() {
stops.last_mut().unwrap().1 = max;
}
let mut i = 1;
while i < stops.len() - 1 {
let point = stops[i].1;
if point.is_nan() {
let start = i;
let mut end = i + 1;
while end < stops.len() - 1 && stops[end].1.is_nan() {
end += 1;
}
let start_point = stops[start - 1].1;
let end_point = stops[end].1;
let steps = end - start;
let step = (end_point - start_point) / (steps + 1) as f32;
for j in 0..steps {
stops[i + j].1 = start_point + step * (j + 1) as f32;
}
i = end;
}
i += 1;
}
}
fn compute_color_stops(
stops: &[ColorStop],
scale_factor: f32,
length: f32,
target_size: Vec2,
scratch: &mut Vec<(LinearRgba, f32, f32)>,
extracted_color_stops: &mut Vec<(LinearRgba, f32, f32)>,
) {
// resolve the physical distances of explicit stops and sort them
scratch.extend(stops.iter().filter_map(|stop| {
stop.point
.resolve(scale_factor, length, target_size)
.ok()
.map(|physical_point| (stop.color.to_linear(), physical_point, stop.hint))
}));
scratch.sort_by_key(|(_, point, _)| FloatOrd(*point));
let min = scratch
.first()
.map(|(_, min, _)| *min)
.unwrap_or(0.)
.min(0.);
// get the position of the last explicit stop and use the full length of the gradient if no explicit stops
let max = scratch
.last()
.map(|(_, max, _)| *max)
.unwrap_or(length)
.max(length);
let mut sorted_stops_drain = scratch.drain(..);
let range_start = extracted_color_stops.len();
// Fill the extracted color stops buffer
extracted_color_stops.extend(stops.iter().map(|stop| {
if stop.point == Val::Auto {
(stop.color.to_linear(), f32::NAN, stop.hint)
} else {
sorted_stops_drain.next().unwrap()
}
}));
interpolate_color_stops(&mut extracted_color_stops[range_start..], min, max);
}
pub fn extract_gradients(
mut commands: Commands,
mut extracted_gradients: ResMut<ExtractedGradients>,
mut extracted_color_stops: ResMut<ExtractedColorStops>,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
gradients_query: Extract<
Query<(
Entity,
&ComputedNode,
&ComputedNodeTarget,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
AnyOf<(&BackgroundGradient, &BorderGradient)>,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
let mut camera_mapper = camera_map.get_mapper();
let mut sorted_stops = vec![];
for (
entity,
uinode,
target,
transform,
inherited_visibility,
clip,
(gradient, gradient_border),
) in &gradients_query
{
// Skip invisible images
if !inherited_visibility.get() {
continue;
}
let Some(extracted_camera_entity) = camera_mapper.map(target) else {
continue;
};
for (gradients, node_type) in [
(gradient.map(|g| &g.0), NodeType::Rect),
(gradient_border.map(|g| &g.0), NodeType::Border(BORDER_ALL)),
]
.iter()
.filter_map(|(g, n)| g.map(|g| (g, *n)))
{
for gradient in gradients.iter() {
if gradient.is_empty() {
continue;
}
if let Some(color) = gradient.get_single() {
// With a single color stop there's no gradient, fill the node with the color
extracted_uinodes.uinodes.push(ExtractedUiNode {
stack_index: uinode.stack_index,
color: color.into(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
image: AssetId::default(),
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
flip_x: false,
flip_y: false,
border_radius: uinode.border_radius,
border: uinode.border,
node_type,
transform: transform.into(),
},
main_entity: entity.into(),
render_entity: commands.spawn(TemporaryRenderEntity).id(),
});
continue;
}
match gradient {
Gradient::Linear(LinearGradient { angle, stops }) => {
let length = compute_gradient_line_length(*angle, uinode.size);
let range_start = extracted_color_stops.0.len();
compute_color_stops(
stops,
target.scale_factor,
length,
target.physical_size.as_vec2(),
&mut sorted_stops,
&mut extracted_color_stops.0,
);
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
main_entity: entity.into(),
node_type,
border_radius: uinode.border_radius,
border: uinode.border,
resolved_gradient: ResolvedGradient::Linear { angle: *angle },
});
}
Gradient::Radial(RadialGradient {
position: center,
shape,
stops,
}) => {
let c = center.resolve(
target.scale_factor,
uinode.size,
target.physical_size.as_vec2(),
);
let size = shape.resolve(
c,
target.scale_factor,
uinode.size,
target.physical_size.as_vec2(),
);
let length = size.x;
let range_start = extracted_color_stops.0.len();
compute_color_stops(
stops,
target.scale_factor,
length,
target.physical_size.as_vec2(),
&mut sorted_stops,
&mut extracted_color_stops.0,
);
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
main_entity: entity.into(),
node_type,
border_radius: uinode.border_radius,
border: uinode.border,
resolved_gradient: ResolvedGradient::Radial { center: c, size },
});
}
Gradient::Conic(ConicGradient {
start,
position: center,
stops,
}) => {
let g_start = center.resolve(
target.scale_factor(),
uinode.size,
target.physical_size().as_vec2(),
);
let range_start = extracted_color_stops.0.len();
// sort the explicit stops
sorted_stops.extend(stops.iter().filter_map(|stop| {
stop.angle.map(|angle| {
(stop.color.to_linear(), angle.clamp(0., TAU), stop.hint)
})
}));
sorted_stops.sort_by_key(|(_, angle, _)| FloatOrd(*angle));
let mut sorted_stops_drain = sorted_stops.drain(..);
// fill the extracted stops buffer
extracted_color_stops.0.extend(stops.iter().map(|stop| {
if stop.angle.is_none() {
(stop.color.to_linear(), f32::NAN, stop.hint)
} else {
sorted_stops_drain.next().unwrap()
}
}));
interpolate_color_stops(
&mut extracted_color_stops.0[range_start..],
0.,
TAU,
);
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
main_entity: entity.into(),
node_type,
border_radius: uinode.border_radius,
border: uinode.border,
resolved_gradient: ResolvedGradient::Conic {
start: *start,
center: g_start,
},
});
}
}
}
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "it's a system that needs a lot of them"
)]
pub fn queue_gradient(
extracted_gradients: ResMut<ExtractedGradients>,
gradients_pipeline: Res<GradientPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<GradientPipeline>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With<ExtractedView>>,
camera_views: Query<&ExtractedView>,
pipeline_cache: Res<PipelineCache>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawGradientFns>();
for (index, gradient) in extracted_gradients.items.iter().enumerate() {
let Ok((default_camera_view, ui_anti_alias)) =
render_views.get_mut(gradient.extracted_camera_entity)
else {
continue;
};
let Ok(view) = camera_views.get(default_camera_view.0) else {
continue;
};
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
else {
continue;
};
let pipeline = pipelines.specialize(
&pipeline_cache,
&gradients_pipeline,
UiGradientPipelineKey {
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
hdr: view.hdr,
},
);
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (gradient.render_entity, gradient.main_entity),
sort_key: FloatOrd(gradient.stack_index as f32 + stack_z_offsets::GRADIENT),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: true,
});
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct UiGradientVertex {
position: [f32; 3],
uv: [f32; 2],
flags: u32,
radius: [f32; 4],
border: [f32; 4],
size: [f32; 2],
point: [f32; 2],
g_start: [f32; 2],
g_dir: [f32; 2],
start_color: [f32; 4],
start_len: f32,
end_len: f32,
end_color: [f32; 4],
hint: f32,
}
pub fn prepare_gradient(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut ui_meta: ResMut<GradientMeta>,
mut extracted_gradients: ResMut<ExtractedGradients>,
mut extracted_color_stops: ResMut<ExtractedColorStops>,
view_uniforms: Res<ViewUniforms>,
gradients_pipeline: Res<GradientPipeline>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut previous_len: Local<usize>,
) {
if let Some(view_binding) = view_uniforms.uniforms.binding() {
let mut batches: Vec<(Entity, GradientBatch)> = Vec::with_capacity(*previous_len);
ui_meta.vertices.clear();
ui_meta.indices.clear();
ui_meta.view_bind_group = Some(render_device.create_bind_group(
"gradient_view_bind_group",
&gradients_pipeline.view_layout,
&BindGroupEntries::single(view_binding),
));
// Buffer indexes
let mut vertices_index = 0;
let mut indices_index = 0;
for ui_phase in phases.values_mut() {
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(gradient) = extracted_gradients
.items
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
*item.batch_range_mut() = item_index as u32..item_index as u32 + 1;
let uinode_rect = gradient.rect;
let rect_size = uinode_rect.size();
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
gradient
.transform
.transform_point2(pos * rect_size)
.extend(0.)
});
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
let positions_diff = if let Some(clip) = gradient.clip {
[
Vec2::new(
f32::max(clip.min.x - positions[0].x, 0.),
f32::max(clip.min.y - positions[0].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[1].x, 0.),
f32::max(clip.min.y - positions[1].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[2].x, 0.),
f32::min(clip.max.y - positions[2].y, 0.),
),
Vec2::new(
f32::max(clip.min.x - positions[3].x, 0.),
f32::min(clip.max.y - positions[3].y, 0.),
),
]
} else {
[Vec2::ZERO; 4]
};
let positions_clipped = [
positions[0] + positions_diff[0].extend(0.),
positions[1] + positions_diff[1].extend(0.),
positions[2] + positions_diff[2].extend(0.),
positions[3] + positions_diff[3].extend(0.),
];
let points = [
corner_points[0] + positions_diff[0],
corner_points[1] + positions_diff[1],
corner_points[2] + positions_diff[2],
corner_points[3] + positions_diff[3],
];
let transformed_rect_size = gradient.transform.transform_vector2(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
// In those two cases, the culling check can proceed normally as corners will be on
// horizontal / vertical lines
// For all other angles, bypass the culling check
// This does not properly handles all rotations on all axis
if gradient.transform.x_axis[1] == 0.0 {
// Cull nodes that are completely clipped
if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
{
continue;
}
}
let uvs = { [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] };
let mut flags = if let NodeType::Border(borders) = gradient.node_type {
borders
} else {
0
};
let (g_start, g_dir, g_flags) = match gradient.resolved_gradient {
ResolvedGradient::Linear { angle } => {
let corner_index = (angle - FRAC_PI_2).rem_euclid(TAU) / FRAC_PI_2;
(
corner_points[corner_index as usize].into(),
// CSS angles increase in a clockwise direction
[sin(angle), -cos(angle)],
0,
)
}
ResolvedGradient::Conic { center, start } => {
(center.into(), [start, 0.], shader_flags::CONIC)
}
ResolvedGradient::Radial { center, size } => (
center.into(),
Vec2::splat(if size.y != 0. { size.x / size.y } else { 1. }).into(),
shader_flags::RADIAL,
),
};
flags |= g_flags;
let range = gradient.stops_range.start..gradient.stops_range.end - 1;
let mut segment_count = 0;
for stop_index in range {
let mut start_stop = extracted_color_stops.0[stop_index];
let end_stop = extracted_color_stops.0[stop_index + 1];
if start_stop.1 == end_stop.1 {
if stop_index == gradient.stops_range.end - 2 {
if 0 < segment_count {
start_stop.0 = LinearRgba::NONE;
}
} else {
continue;
}
}
let start_color = start_stop.0.to_f32_array();
let end_color = end_stop.0.to_f32_array();
let mut stop_flags = flags;
if 0. < start_stop.1
&& (stop_index == gradient.stops_range.start || segment_count == 0)
{
stop_flags |= shader_flags::FILL_START;
}
if stop_index == gradient.stops_range.end - 2 {
stop_flags |= shader_flags::FILL_END;
}
for i in 0..4 {
ui_meta.vertices.push(UiGradientVertex {
position: positions_clipped[i].into(),
uv: uvs[i].into(),
flags: stop_flags | shader_flags::CORNERS[i],
radius: [
gradient.border_radius.top_left,
gradient.border_radius.top_right,
gradient.border_radius.bottom_right,
gradient.border_radius.bottom_left,
],
border: [
gradient.border.left,
gradient.border.top,
gradient.border.right,
gradient.border.bottom,
],
size: rect_size.xy().into(),
g_start,
g_dir,
point: points[i].into(),
start_color,
start_len: start_stop.1,
end_len: end_stop.1,
end_color,
hint: start_stop.2,
});
}
for &i in &QUAD_INDICES {
ui_meta.indices.push(indices_index + i as u32);
}
indices_index += 4;
segment_count += 1;
}
if 0 < segment_count {
let vertices_count = 6 * segment_count;
batches.push((
item.entity(),
GradientBatch {
range: vertices_index..(vertices_index + vertices_count),
},
));
vertices_index += vertices_count;
}
}
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
ui_meta.indices.write_buffer(&render_device, &render_queue);
*previous_len = batches.len();
commands.try_insert_batch(batches);
}
extracted_gradients.items.clear();
extracted_color_stops.0.clear();
}
pub type DrawGradientFns = (SetItemPipeline, SetGradientViewBindGroup<0>, DrawGradient);
pub struct SetGradientViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetGradientViewBindGroup<I> {
type Param = SRes<GradientMeta>;
type ViewQuery = Read<ViewUniformOffset>;
type ItemQuery = ();
fn render<'w>(
_item: &P,
view_uniform: &'w ViewUniformOffset,
_entity: Option<()>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
return RenderCommandResult::Failure("view_bind_group not available");
};
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct DrawGradient;
impl<P: PhaseItem> RenderCommand<P> for DrawGradient {
type Param = SRes<GradientMeta>;
type ViewQuery = ();
type ItemQuery = Read<GradientBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w GradientBatch>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let ui_meta = ui_meta.into_inner();
let Some(vertices) = ui_meta.vertices.buffer() else {
return RenderCommandResult::Failure("missing vertices to draw ui");
};
let Some(indices) = ui_meta.indices.buffer() else {
return RenderCommandResult::Failure("missing indices to draw ui");
};
// Store the vertices
pass.set_vertex_buffer(0, vertices.slice(..));
// Define how to "connect" the vertices
pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32);
// Draw the vertices
pass.draw_indexed(batch.range.clone(), 0, 0..1);
RenderCommandResult::Success
}
}