UI material border radius (#15171)
# Objective I wrote a box shadow UI material naively thinking I could use the border widths attribute to hold the border radius but it doesn't work as the border widths are automatically set in the extraction function. Need to send border radius to the shader seperately for it to be viable. ## Solution Add a `border_radius` vertex attribute to the ui material. This PR also removes the normalization of border widths for custom UI materials. The regular UI shader doesn't do this so it's a bit confusing and means you can't use the logic from `ui.wgsl` in your custom UI materials. ## Testing / Showcase Made a change to the `ui_material` example to display border radius: ```cargo run --example ui_material``` <img width="569" alt="corners" src="https://github.com/user-attachments/assets/36412736-a9ee-4042-aadd-68b9cafb17cb" />
This commit is contained in:
parent
37893a3f9e
commit
c0ccc87738
@ -10,20 +10,42 @@
|
||||
|
||||
@fragment
|
||||
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
|
||||
// normalized position relative to the center of the UI node
|
||||
let r = in.uv - 0.5;
|
||||
let output_color = textureSample(material_color_texture, material_color_sampler, in.uv) * color;
|
||||
|
||||
// normalized size of the border closest to the current position
|
||||
// half size of the UI node
|
||||
let half_size = 0.5 * in.size;
|
||||
|
||||
// position relative to the center of the UI node
|
||||
let p = in.uv * in.size - half_size;
|
||||
|
||||
// thickness of the border closest to the current position
|
||||
let b = vec2(
|
||||
select(in.border_widths.x, in.border_widths.y, 0. < r.x),
|
||||
select(in.border_widths.z, in.border_widths.w, 0. < r.y)
|
||||
select(in.border_widths.x, in.border_widths.z, 0. < p.x),
|
||||
select(in.border_widths.y, in.border_widths.w, 0. < p.y)
|
||||
);
|
||||
|
||||
// select radius for the nearest corner
|
||||
let rs = select(in.border_radius.xy, in.border_radius.wz, 0.0 < p.y);
|
||||
let radius = select(rs.x, rs.y, 0.0 < p.x);
|
||||
|
||||
// distance along each axis from the corner
|
||||
let d = half_size - abs(p);
|
||||
|
||||
// if the distance to the edge from the current position on any axis
|
||||
// is less than the border width on that axis then the position is within
|
||||
// the border and we return the border color
|
||||
if any(0.5 - b < abs(r)) {
|
||||
return border_color;
|
||||
if d.x < b.x || d.y < b.y {
|
||||
// select radius for the nearest corner
|
||||
let rs = select(in.border_radius.xy, in.border_radius.wz, 0.0 < p.y);
|
||||
let radius = select(rs.x, rs.y, 0.0 < p.x);
|
||||
|
||||
// determine if the point is inside the curved corner and return the corresponding color
|
||||
let q = radius - d;
|
||||
if radius < min(max(q.x, q.y), 0.0) + length(vec2(max(q.x, 0.0), max(q.y, 0.0))) {
|
||||
return vec4(0.0);
|
||||
} else {
|
||||
return border_color;
|
||||
}
|
||||
}
|
||||
|
||||
// sample the texture at this position if it's to the left of the slider value
|
||||
|
@ -15,12 +15,14 @@ fn vertex(
|
||||
@location(1) vertex_uv: vec2<f32>,
|
||||
@location(2) size: vec2<f32>,
|
||||
@location(3) border_widths: vec4<f32>,
|
||||
@location(4) border_radius: vec4<f32>,
|
||||
) -> UiVertexOutput {
|
||||
var out: UiVertexOutput;
|
||||
out.uv = vertex_uv;
|
||||
out.position = view.clip_from_world * vec4<f32>(vertex_position, 1.0);
|
||||
out.size = size;
|
||||
out.border_widths = border_widths;
|
||||
out.border_radius = border_radius;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ use bevy_ecs::{
|
||||
};
|
||||
use bevy_image::BevyDefault as _;
|
||||
use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles};
|
||||
use bevy_render::sync_world::MainEntity;
|
||||
use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
|
||||
use bevy_render::{
|
||||
extract_component::ExtractComponentPlugin,
|
||||
globals::{GlobalsBuffer, GlobalsUniform},
|
||||
@ -21,10 +21,11 @@ use bevy_render::{
|
||||
render_phase::*,
|
||||
render_resource::{binding_types::uniform_buffer, *},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
sync_world::{RenderEntity, TemporaryRenderEntity},
|
||||
sync_world::RenderEntity,
|
||||
view::*,
|
||||
Extract, ExtractSchedule, Render, RenderSet,
|
||||
};
|
||||
use bevy_sprite::BorderRect;
|
||||
use bevy_transform::prelude::GlobalTransform;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
@ -116,7 +117,8 @@ pub struct UiMaterialVertex {
|
||||
pub position: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
pub size: [f32; 2],
|
||||
pub border_widths: [f32; 4],
|
||||
pub border: [f32; 4],
|
||||
pub radius: [f32; 4],
|
||||
}
|
||||
|
||||
// in this [`UiMaterialPipeline`] there is (currently) no batching going on.
|
||||
@ -154,7 +156,9 @@ where
|
||||
VertexFormat::Float32x2,
|
||||
// size
|
||||
VertexFormat::Float32x2,
|
||||
// border_widths
|
||||
// border widths
|
||||
VertexFormat::Float32x4,
|
||||
// border radius
|
||||
VertexFormat::Float32x4,
|
||||
],
|
||||
);
|
||||
@ -335,7 +339,8 @@ pub struct ExtractedUiMaterialNode<M: UiMaterial> {
|
||||
pub stack_index: u32,
|
||||
pub transform: Mat4,
|
||||
pub rect: Rect,
|
||||
pub border: [f32; 4],
|
||||
pub border: BorderRect,
|
||||
pub border_radius: ResolvedBorderRadius,
|
||||
pub material: AssetId<M>,
|
||||
pub clip: Option<Rect>,
|
||||
// Camera to render this UI node to. By the time it is extracted,
|
||||
@ -379,7 +384,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
|
||||
// If there is only one camera, we use it as default
|
||||
let default_single_camera = default_ui_camera.get();
|
||||
|
||||
for (entity, uinode, transform, handle, inherited_visibility, clip, camera) in
|
||||
for (entity, computed_node, transform, handle, inherited_visibility, clip, camera) in
|
||||
uinode_query.iter()
|
||||
{
|
||||
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_single_camera)
|
||||
@ -392,7 +397,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
|
||||
};
|
||||
|
||||
// skip invisible nodes
|
||||
if !inherited_visibility.get() || uinode.is_empty() {
|
||||
if !inherited_visibility.get() || computed_node.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -401,24 +406,18 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
|
||||
continue;
|
||||
}
|
||||
|
||||
let border = [
|
||||
uinode.border.left / uinode.size().x,
|
||||
uinode.border.right / uinode.size().x,
|
||||
uinode.border.top / uinode.size().y,
|
||||
uinode.border.bottom / uinode.size().y,
|
||||
];
|
||||
|
||||
extracted_uinodes.uinodes.insert(
|
||||
commands.spawn(TemporaryRenderEntity).id(),
|
||||
ExtractedUiMaterialNode {
|
||||
stack_index: uinode.stack_index,
|
||||
stack_index: computed_node.stack_index,
|
||||
transform: transform.compute_matrix(),
|
||||
material: handle.id(),
|
||||
rect: Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: uinode.size(),
|
||||
max: computed_node.size(),
|
||||
},
|
||||
border,
|
||||
border: computed_node.border(),
|
||||
border_radius: computed_node.border_radius(),
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
extracted_camera_entity,
|
||||
main_entity: entity.into(),
|
||||
@ -558,7 +557,18 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
|
||||
position: positions_clipped[i].into(),
|
||||
uv: uvs[i].into(),
|
||||
size: extracted_uinode.rect.size().into(),
|
||||
border_widths: extracted_uinode.border,
|
||||
radius: [
|
||||
extracted_uinode.border_radius.top_left,
|
||||
extracted_uinode.border_radius.top_right,
|
||||
extracted_uinode.border_radius.bottom_right,
|
||||
extracted_uinode.border_radius.bottom_left,
|
||||
],
|
||||
border: [
|
||||
extracted_uinode.border.left,
|
||||
extracted_uinode.border.top,
|
||||
extracted_uinode.border.right,
|
||||
extracted_uinode.border.bottom,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,9 @@ struct UiVertexOutput {
|
||||
@location(0) uv: vec2<f32>,
|
||||
// The size of the borders in UV space. Order is Left, Right, Top, Bottom.
|
||||
@location(1) border_widths: vec4<f32>,
|
||||
// The size of the borders in pixels. Order is top left, top right, bottom right, bottom left.
|
||||
@location(2) border_radius: vec4<f32>,
|
||||
// The size of the node in pixels. Order is width, height.
|
||||
@location(2) @interpolate(flat) size: vec2<f32>,
|
||||
@location(3) @interpolate(flat) size: vec2<f32>,
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
//! Demonstrates the use of [`UiMaterials`](UiMaterial) and how to change material values
|
||||
|
||||
use bevy::{prelude::*, reflect::TypePath, render::render_resource::*};
|
||||
use bevy::{
|
||||
color::palettes::css::DARK_BLUE, prelude::*, reflect::TypePath, render::render_resource::*,
|
||||
};
|
||||
|
||||
/// This example uses a shader source file from the assets subdirectory
|
||||
const SHADER_ASSET_PATH: &str = "shaders/custom_ui_material.wgsl";
|
||||
@ -33,18 +35,25 @@ fn setup(
|
||||
.with_children(|parent| {
|
||||
let banner_scale_factor = 0.5;
|
||||
parent.spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: Val::Px(905.0 * banner_scale_factor),
|
||||
height: Val::Px(363.0 * banner_scale_factor),
|
||||
border: UiRect::all(Val::Px(20.)),
|
||||
..default()
|
||||
},
|
||||
MaterialNode(ui_materials.add(CustomUiMaterial {
|
||||
color: LinearRgba::WHITE.to_f32_array().into(),
|
||||
slider: 0.5,
|
||||
color_texture: asset_server.load("branding/banner.png"),
|
||||
border_color: LinearRgba::WHITE.to_f32_array().into(),
|
||||
})),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: Val::Px(905.0 * banner_scale_factor),
|
||||
height: Val::Px(363.0 * banner_scale_factor),
|
||||
border: UiRect::all(Val::Px(10.)),
|
||||
..default()
|
||||
BorderRadius::all(Val::Px(20.)),
|
||||
// UI material nodes can have outlines and shadows like any other UI node
|
||||
Outline {
|
||||
width: Val::Px(2.),
|
||||
offset: Val::Px(100.),
|
||||
color: DARK_BLUE.into(),
|
||||
},
|
||||
));
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user