mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
box shadow (#15204)
# Objective UI box shadow support Adds a new component `BoxShadow`: ```rust pub struct BoxShadow { /// The shadow's color pub color: Color, /// Horizontal offset pub x_offset: Val, /// Vertical offset pub y_offset: Val, /// Horizontal difference in size from the occluding uninode pub spread_radius: Val, /// Blurriness of the shadow pub blur_radius: Val, } ``` To use `BoxShadow`, add the component to any Bevy UI node and a shadow will be drawn beneath that node. Also adds a resource `BoxShadowSamples` that can be used to adjust the shadow quality. #### Notes * I'm not super happy with the field names. Maybe we need a `struct Size { width: Val, height: Val }` type or something. * The shader isn't very optimised but I don't see that it's too important for now as the number of shadows being rendered is not going to be massive most of the time. I think it's more important to get the API and geometry correct with this PR. * I didn't implement an inset property, it's not essential and can easily be added in a follow up. * Shadows are only rendered for uinodes, not for images or text. * Batching isn't supported, it would need out-of-the-scope-of-this-pr changes to the way the UI handles z-ordering for it to be effective. # Showcase ```cargo run --example box_shadow -- --samples 4``` <img width="391" alt="br" src="https://github.com/user-attachments/assets/4e8add96-dc93-46e0-9e35-d995eb0943ad"> ```cargo run --example box_shadow -- --samples 10``` <img width="391" alt="s10" src="https://github.com/user-attachments/assets/ecb384c9-4012-4cd6-9dea-5180904bf28e">
This commit is contained in:
parent
f1fbb668f9
commit
99b9a2fcd7
8 changed files with 984 additions and 3 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -2896,6 +2896,17 @@ description = "Demonstrates how to create a node with a border"
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "box_shadow"
|
||||
path = "examples/ui/box_shadow.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.box_shadow]
|
||||
name = "Box Shadow"
|
||||
description = "Demonstrates how to create a node with a shadow"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "button"
|
||||
path = "examples/ui/button.rs"
|
||||
|
|
|
@ -149,6 +149,7 @@ impl Plugin for UiPlugin {
|
|||
.register_type::<widget::Label>()
|
||||
.register_type::<ZIndex>()
|
||||
.register_type::<Outline>()
|
||||
.register_type::<UiBoxShadowSamples>()
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
(
|
||||
|
|
569
crates/bevy_ui/src/render/box_shadow.rs
Normal file
569
crates/bevy_ui/src/render/box_shadow.rs
Normal file
|
@ -0,0 +1,569 @@
|
|||
use core::{hash::Hash, ops::Range};
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::*;
|
||||
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::{
|
||||
prelude::Component,
|
||||
storage::SparseSet,
|
||||
system::{
|
||||
lifetimeless::{Read, SRes},
|
||||
*,
|
||||
},
|
||||
};
|
||||
use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles};
|
||||
use bevy_render::RenderApp;
|
||||
use bevy_render::{
|
||||
camera::Camera,
|
||||
render_phase::*,
|
||||
render_resource::{binding_types::uniform_buffer, *},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
texture::BevyDefault,
|
||||
view::*,
|
||||
world_sync::{RenderEntity, TemporaryRenderEntity},
|
||||
Extract, ExtractSchedule, Render, RenderSet,
|
||||
};
|
||||
use bevy_transform::prelude::GlobalTransform;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
use crate::{
|
||||
BoxShadow, CalculatedClip, DefaultUiCamera, Node, RenderUiSystem, ResolvedBorderRadius,
|
||||
TargetCamera, TransparentUi, UiBoxShadowSamples, UiScale, Val,
|
||||
};
|
||||
|
||||
use super::{QUAD_INDICES, QUAD_VERTEX_POSITIONS};
|
||||
|
||||
pub const BOX_SHADOW_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(17717747047134343426);
|
||||
|
||||
/// A plugin that enables the rendering of box shadows.
|
||||
pub struct BoxShadowPlugin;
|
||||
|
||||
impl Plugin for BoxShadowPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
load_internal_asset!(
|
||||
app,
|
||||
BOX_SHADOW_SHADER_HANDLE,
|
||||
"box_shadow.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app
|
||||
.add_render_command::<TransparentUi, DrawBoxShadows>()
|
||||
.init_resource::<ExtractedBoxShadows>()
|
||||
.init_resource::<BoxShadowMeta>()
|
||||
.init_resource::<SpecializedRenderPipelines<BoxShadowPipeline>>()
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
extract_shadows.in_set(RenderUiSystem::ExtractBoxShadows),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
queue_shadows.in_set(RenderSet::Queue),
|
||||
prepare_shadows.in_set(RenderSet::PrepareBindGroups),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<BoxShadowPipeline>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
struct BoxShadowVertex {
|
||||
position: [f32; 3],
|
||||
uvs: [f32; 2],
|
||||
vertex_color: [f32; 4],
|
||||
size: [f32; 2],
|
||||
radius: [f32; 4],
|
||||
blur: f32,
|
||||
bounds: [f32; 2],
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct UiShadowsBatch {
|
||||
pub range: Range<u32>,
|
||||
pub camera: Entity,
|
||||
}
|
||||
|
||||
/// Contains the vertices and bind groups to be sent to the GPU
|
||||
#[derive(Resource)]
|
||||
pub struct BoxShadowMeta {
|
||||
vertices: RawBufferVec<BoxShadowVertex>,
|
||||
indices: RawBufferVec<u32>,
|
||||
view_bind_group: Option<BindGroup>,
|
||||
}
|
||||
|
||||
impl Default for BoxShadowMeta {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vertices: RawBufferVec::new(BufferUsages::VERTEX),
|
||||
indices: RawBufferVec::new(BufferUsages::INDEX),
|
||||
view_bind_group: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct BoxShadowPipeline {
|
||||
pub view_layout: BindGroupLayout,
|
||||
}
|
||||
|
||||
impl FromWorld for BoxShadowPipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"box_shadow_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
ShaderStages::VERTEX_FRAGMENT,
|
||||
uniform_buffer::<ViewUniform>(true),
|
||||
),
|
||||
);
|
||||
|
||||
BoxShadowPipeline { view_layout }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct UiTextureSlicePipelineKey {
|
||||
pub hdr: bool,
|
||||
/// Number of samples, a higher value results in better quality shadows.
|
||||
pub samples: u32,
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for BoxShadowPipeline {
|
||||
type Key = UiTextureSlicePipelineKey;
|
||||
|
||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||
let vertex_layout = VertexBufferLayout::from_vertex_formats(
|
||||
VertexStepMode::Vertex,
|
||||
vec![
|
||||
// position
|
||||
VertexFormat::Float32x3,
|
||||
// uv
|
||||
VertexFormat::Float32x2,
|
||||
// color
|
||||
VertexFormat::Float32x4,
|
||||
// target rect size
|
||||
VertexFormat::Float32x2,
|
||||
// corner radius values (top left, top right, bottom right, bottom left)
|
||||
VertexFormat::Float32x4,
|
||||
// blur radius
|
||||
VertexFormat::Float32,
|
||||
// outer size
|
||||
VertexFormat::Float32x2,
|
||||
],
|
||||
);
|
||||
let shader_defs = vec![ShaderDefVal::UInt(
|
||||
"SHADOW_SAMPLES".to_string(),
|
||||
key.samples,
|
||||
)];
|
||||
|
||||
RenderPipelineDescriptor {
|
||||
vertex: VertexState {
|
||||
shader: BOX_SHADOW_SHADER_HANDLE,
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: shader_defs.clone(),
|
||||
buffers: vec![vertex_layout],
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: BOX_SHADOW_SHADER_HANDLE,
|
||||
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("box_shadow_pipeline".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of a shadow to be sorted and queued for rendering
|
||||
pub struct ExtractedBoxShadow {
|
||||
pub stack_index: u32,
|
||||
pub transform: Mat4,
|
||||
pub rect: Rect,
|
||||
pub clip: Option<Rect>,
|
||||
pub camera_entity: Entity,
|
||||
pub color: LinearRgba,
|
||||
pub radius: ResolvedBorderRadius,
|
||||
pub blur_radius: f32,
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
/// List of extracted shadows to be sorted and queued for rendering
|
||||
#[derive(Resource, Default)]
|
||||
pub struct ExtractedBoxShadows {
|
||||
pub box_shadows: SparseSet<Entity, ExtractedBoxShadow>,
|
||||
}
|
||||
|
||||
pub fn extract_shadows(
|
||||
mut commands: Commands,
|
||||
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
camera_query: Extract<Query<(Entity, &Camera)>>,
|
||||
box_shadow_query: Extract<
|
||||
Query<(
|
||||
&Node,
|
||||
&GlobalTransform,
|
||||
&ViewVisibility,
|
||||
&BoxShadow,
|
||||
Option<&CalculatedClip>,
|
||||
Option<&TargetCamera>,
|
||||
)>,
|
||||
>,
|
||||
mapping: Extract<Query<&RenderEntity>>,
|
||||
) {
|
||||
for (uinode, transform, view_visibility, box_shadow, clip, camera) in &box_shadow_query {
|
||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(&camera_entity) = mapping.get(camera_entity) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip invisible images
|
||||
if !view_visibility.get() || box_shadow.color.is_fully_transparent() || uinode.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ui_logical_viewport_size = camera_query
|
||||
.get(camera_entity.id())
|
||||
.ok()
|
||||
.and_then(|(_, c)| c.logical_viewport_size())
|
||||
.unwrap_or(Vec2::ZERO)
|
||||
// The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`,
|
||||
// so we have to divide by `UiScale` to get the size of the UI viewport.
|
||||
/ ui_scale.0;
|
||||
|
||||
let resolve_val = |val, base| match val {
|
||||
Val::Auto => 0.,
|
||||
Val::Px(px) => px,
|
||||
Val::Percent(percent) => percent / 100. * base,
|
||||
Val::Vw(percent) => percent / 100. * ui_logical_viewport_size.x,
|
||||
Val::Vh(percent) => percent / 100. * ui_logical_viewport_size.y,
|
||||
Val::VMin(percent) => percent / 100. * ui_logical_viewport_size.min_element(),
|
||||
Val::VMax(percent) => percent / 100. * ui_logical_viewport_size.max_element(),
|
||||
};
|
||||
|
||||
let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x);
|
||||
let spread_ratio_x = (spread_x + uinode.size().x) / uinode.size().x;
|
||||
|
||||
let spread = vec2(
|
||||
spread_x,
|
||||
(spread_ratio_x * uinode.size().y) - uinode.size().y,
|
||||
);
|
||||
|
||||
let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x);
|
||||
let offset = vec2(
|
||||
resolve_val(box_shadow.x_offset, uinode.size().x),
|
||||
resolve_val(box_shadow.y_offset, uinode.size().y),
|
||||
);
|
||||
|
||||
let shadow_size = uinode.size() + spread;
|
||||
if shadow_size.cmple(Vec2::ZERO).any() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let radius = ResolvedBorderRadius {
|
||||
top_left: uinode.border_radius.top_left * spread_ratio_x,
|
||||
top_right: uinode.border_radius.top_right * spread_ratio_x,
|
||||
bottom_left: uinode.border_radius.bottom_left * spread_ratio_x,
|
||||
bottom_right: uinode.border_radius.bottom_right * spread_ratio_x,
|
||||
};
|
||||
|
||||
extracted_box_shadows.box_shadows.insert(
|
||||
commands.spawn(TemporaryRenderEntity).id(),
|
||||
ExtractedBoxShadow {
|
||||
stack_index: uinode.stack_index,
|
||||
transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)),
|
||||
color: box_shadow.color.into(),
|
||||
rect: Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: shadow_size + 6. * blur_radius,
|
||||
},
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
camera_entity: camera_entity.id(),
|
||||
radius,
|
||||
blur_radius,
|
||||
size: shadow_size,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue_shadows(
|
||||
extracted_ui_slicers: ResMut<ExtractedBoxShadows>,
|
||||
ui_slicer_pipeline: Res<BoxShadowPipeline>,
|
||||
mut pipelines: ResMut<SpecializedRenderPipelines<BoxShadowPipeline>>,
|
||||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
mut views: Query<(Entity, &ExtractedView, Option<&UiBoxShadowSamples>)>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
draw_functions: Res<DrawFunctions<TransparentUi>>,
|
||||
) {
|
||||
let draw_function = draw_functions.read().id::<DrawBoxShadows>();
|
||||
for (entity, extracted_shadow) in extracted_ui_slicers.box_shadows.iter() {
|
||||
let Ok((view_entity, view, shadow_samples)) = views.get_mut(extracted_shadow.camera_entity)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let pipeline = pipelines.specialize(
|
||||
&pipeline_cache,
|
||||
&ui_slicer_pipeline,
|
||||
UiTextureSlicePipelineKey {
|
||||
hdr: view.hdr,
|
||||
samples: shadow_samples.map(|samples| samples.0).unwrap_or_default(),
|
||||
},
|
||||
);
|
||||
|
||||
transparent_phase.add(TransparentUi {
|
||||
draw_function,
|
||||
pipeline,
|
||||
entity: *entity,
|
||||
sort_key: (
|
||||
FloatOrd(extracted_shadow.stack_index as f32 - 0.1),
|
||||
entity.index(),
|
||||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn prepare_shadows(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
render_queue: Res<RenderQueue>,
|
||||
mut ui_meta: ResMut<BoxShadowMeta>,
|
||||
mut extracted_shadows: ResMut<ExtractedBoxShadows>,
|
||||
view_uniforms: Res<ViewUniforms>,
|
||||
texture_slicer_pipeline: Res<BoxShadowPipeline>,
|
||||
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
mut previous_len: Local<usize>,
|
||||
) {
|
||||
if let Some(view_binding) = view_uniforms.uniforms.binding() {
|
||||
let mut batches: Vec<(Entity, UiShadowsBatch)> = Vec::with_capacity(*previous_len);
|
||||
|
||||
ui_meta.vertices.clear();
|
||||
ui_meta.indices.clear();
|
||||
ui_meta.view_bind_group = Some(render_device.create_bind_group(
|
||||
"ui_texture_slice_view_bind_group",
|
||||
&texture_slicer_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() {
|
||||
let mut item_index = 0;
|
||||
|
||||
while item_index < ui_phase.items.len() {
|
||||
let item = &mut ui_phase.items[item_index];
|
||||
if let Some(box_shadow) = extracted_shadows.box_shadows.get(item.entity) {
|
||||
let uinode_rect = box_shadow.rect;
|
||||
|
||||
let rect_size = uinode_rect.size().extend(1.0);
|
||||
|
||||
// Specify the corners of the node
|
||||
let positions = QUAD_VERTEX_POSITIONS
|
||||
.map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz());
|
||||
|
||||
// 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) = box_shadow.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 transformed_rect_size = box_shadow.transform.transform_vector3(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 box_shadow.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 radius = [
|
||||
box_shadow.radius.top_left,
|
||||
box_shadow.radius.top_right,
|
||||
box_shadow.radius.bottom_right,
|
||||
box_shadow.radius.bottom_left,
|
||||
];
|
||||
|
||||
let uvs = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y];
|
||||
for i in 0..4 {
|
||||
ui_meta.vertices.push(BoxShadowVertex {
|
||||
position: positions_clipped[i].into(),
|
||||
uvs: uvs[i].into(),
|
||||
vertex_color: box_shadow.color.to_f32_array(),
|
||||
size: box_shadow.size.into(),
|
||||
radius,
|
||||
blur: box_shadow.blur_radius,
|
||||
bounds: rect_size.xy().into(),
|
||||
});
|
||||
}
|
||||
|
||||
for &i in &QUAD_INDICES {
|
||||
ui_meta.indices.push(indices_index + i as u32);
|
||||
}
|
||||
|
||||
batches.push((
|
||||
item.entity,
|
||||
UiShadowsBatch {
|
||||
range: vertices_index..vertices_index + 6,
|
||||
camera: box_shadow.camera_entity,
|
||||
},
|
||||
));
|
||||
|
||||
vertices_index += 6;
|
||||
indices_index += 4;
|
||||
|
||||
// shadows are sent to the gpu non-batched
|
||||
*ui_phase.items[item_index].batch_range_mut() =
|
||||
item_index as u32..item_index as u32 + 1;
|
||||
}
|
||||
item_index += 1;
|
||||
}
|
||||
}
|
||||
ui_meta.vertices.write_buffer(&render_device, &render_queue);
|
||||
ui_meta.indices.write_buffer(&render_device, &render_queue);
|
||||
*previous_len = batches.len();
|
||||
commands.insert_or_spawn_batch(batches);
|
||||
}
|
||||
extracted_shadows.box_shadows.clear();
|
||||
}
|
||||
|
||||
pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow);
|
||||
|
||||
pub struct SetBoxShadowViewBindGroup<const I: usize>;
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetBoxShadowViewBindGroup<I> {
|
||||
type Param = SRes<BoxShadowMeta>;
|
||||
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 DrawBoxShadow;
|
||||
impl<P: PhaseItem> RenderCommand<P> for DrawBoxShadow {
|
||||
type Param = SRes<BoxShadowMeta>;
|
||||
type ViewQuery = ();
|
||||
type ItemQuery = Read<UiShadowsBatch>;
|
||||
|
||||
#[inline]
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
_view: (),
|
||||
batch: Option<&'w UiShadowsBatch>,
|
||||
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
|
||||
}
|
||||
}
|
99
crates/bevy_ui/src/render/box_shadow.wgsl
Normal file
99
crates/bevy_ui/src/render/box_shadow.wgsl
Normal file
|
@ -0,0 +1,99 @@
|
|||
#import bevy_render::view::View;
|
||||
#import bevy_render::globals::Globals;
|
||||
|
||||
const PI: f32 = 3.14159265358979323846;
|
||||
const SAMPLES: i32 = #SHADOW_SAMPLES;
|
||||
|
||||
@group(0) @binding(0) var<uniform> view: View;
|
||||
@group(0) @binding(1) var<uniform> globals: Globals;
|
||||
|
||||
struct BoxShadowVertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) point: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) @interpolate(flat) size: vec2<f32>,
|
||||
@location(3) @interpolate(flat) radius: vec4<f32>,
|
||||
@location(4) @interpolate(flat) blur: f32,
|
||||
}
|
||||
|
||||
fn gaussian(x: f32, sigma: f32) -> f32 {
|
||||
return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * PI) * sigma);
|
||||
}
|
||||
|
||||
// Approximates the Gauss error function: https://en.wikipedia.org/wiki/Error_function
|
||||
fn erf(p: vec2<f32>) -> vec2<f32> {
|
||||
let s = sign(p);
|
||||
let a = abs(p);
|
||||
// fourth degree polynomial approximation for erf
|
||||
var result = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
|
||||
result = result * result;
|
||||
return s - s / (result * result);
|
||||
}
|
||||
|
||||
// returns the closest corner radius based on the signs of the components of p
|
||||
fn selectCorner(p: vec2<f32>, c: vec4<f32>) -> f32 {
|
||||
return mix(mix(c.x, c.y, step(0., p.x)), mix(c.w, c.z, step(0., p.x)), step(0., p.y));
|
||||
}
|
||||
|
||||
fn horizontalRoundedBoxShadow(x: f32, y: f32, blur: f32, corner: f32, half_size: vec2<f32>) -> f32 {
|
||||
let d = min(half_size.y - corner - abs(y), 0.);
|
||||
let c = half_size.x - corner + sqrt(max(0., corner * corner - d * d));
|
||||
let integral = 0.5 + 0.5 * erf((x + vec2(-c, c)) * (sqrt(0.5) / blur));
|
||||
return integral.y - integral.x;
|
||||
}
|
||||
|
||||
fn roundedBoxShadow(
|
||||
lower: vec2<f32>,
|
||||
upper: vec2<f32>,
|
||||
point: vec2<f32>,
|
||||
blur: f32,
|
||||
corners: vec4<f32>,
|
||||
) -> f32 {
|
||||
let center = (lower + upper) * 0.5;
|
||||
let half_size = (upper - lower) * 0.5;
|
||||
let p = point - center;
|
||||
let low = p.y - half_size.y;
|
||||
let high = p.y + half_size.y;
|
||||
let start = clamp(-3. * blur, low, high);
|
||||
let end = clamp(3. * blur, low, high);
|
||||
let step = (end - start) / f32(SAMPLES);
|
||||
var y = start + step * 0.5;
|
||||
var value: f32 = 0.0;
|
||||
for (var i = 0; i < SAMPLES; i++) {
|
||||
let corner = selectCorner(p, corners);
|
||||
value += horizontalRoundedBoxShadow(p.x, p.y - y, blur, corner, half_size) * gaussian(y, blur) * step;
|
||||
y += step;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(
|
||||
@location(0) vertex_position: vec3<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
@location(2) vertex_color: vec4<f32>,
|
||||
@location(3) size: vec2<f32>,
|
||||
@location(4) radius: vec4<f32>,
|
||||
@location(5) blur: f32,
|
||||
@location(6) bounds: vec2<f32>,
|
||||
) -> BoxShadowVertexOutput {
|
||||
var out: BoxShadowVertexOutput;
|
||||
out.position = view.clip_from_world * vec4(vertex_position, 1.0);
|
||||
out.point = (uv.xy - 0.5) * bounds;
|
||||
out.color = vertex_color;
|
||||
out.size = size;
|
||||
out.radius = radius;
|
||||
out.blur = blur;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
in: BoxShadowVertexOutput,
|
||||
) -> @location(0) vec4<f32> {
|
||||
let g = in.color.a * roundedBoxShadow(-0.5 * in.size, 0.5 * in.size, in.point, max(in.blur, 0.01), in.radius);
|
||||
return vec4(in.color.rgb, g);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod box_shadow;
|
||||
mod pipeline;
|
||||
mod render_pass;
|
||||
mod ui_material_pipeline;
|
||||
|
@ -5,7 +6,7 @@ pub mod ui_texture_slice_pipeline;
|
|||
|
||||
use crate::{
|
||||
BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Node, Outline,
|
||||
ResolvedBorderRadius, TargetCamera, UiAntiAlias, UiImage, UiScale,
|
||||
ResolvedBorderRadius, TargetCamera, UiAntiAlias, UiBoxShadowSamples, UiImage, UiScale,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||
|
@ -46,6 +47,7 @@ use bevy_text::Text;
|
|||
use bevy_text::TextLayoutInfo;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::HashMap;
|
||||
use box_shadow::BoxShadowPlugin;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core::ops::Range;
|
||||
use graph::{NodeUi, SubGraphUi};
|
||||
|
@ -70,6 +72,7 @@ pub const UI_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(130128470471
|
|||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum RenderUiSystem {
|
||||
ExtractBoxShadows,
|
||||
ExtractBackgrounds,
|
||||
ExtractImages,
|
||||
ExtractTextureSlice,
|
||||
|
@ -96,6 +99,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||
.configure_sets(
|
||||
ExtractSchedule,
|
||||
(
|
||||
RenderUiSystem::ExtractBoxShadows,
|
||||
RenderUiSystem::ExtractBackgrounds,
|
||||
RenderUiSystem::ExtractImages,
|
||||
RenderUiSystem::ExtractTextureSlice,
|
||||
|
@ -146,6 +150,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||
}
|
||||
|
||||
app.add_plugins(UiTextureSlicerPlugin);
|
||||
app.add_plugins(BoxShadowPlugin);
|
||||
}
|
||||
|
||||
fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph {
|
||||
|
@ -453,14 +458,22 @@ pub fn extract_default_ui_camera_view(
|
|||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
query: Extract<
|
||||
Query<(&RenderEntity, &Camera, Option<&UiAntiAlias>), Or<(With<Camera2d>, With<Camera3d>)>>,
|
||||
Query<
|
||||
(
|
||||
&RenderEntity,
|
||||
&Camera,
|
||||
Option<&UiAntiAlias>,
|
||||
Option<&UiBoxShadowSamples>,
|
||||
),
|
||||
Or<(With<Camera2d>, With<Camera3d>)>,
|
||||
>,
|
||||
>,
|
||||
mut live_entities: Local<EntityHashSet>,
|
||||
) {
|
||||
live_entities.clear();
|
||||
|
||||
let scale = ui_scale.0.recip();
|
||||
for (entity, camera, ui_anti_alias) in &query {
|
||||
for (entity, camera, ui_anti_alias, shadow_samples) in &query {
|
||||
// ignore inactive cameras
|
||||
if !camera.is_active {
|
||||
continue;
|
||||
|
@ -517,6 +530,9 @@ pub fn extract_default_ui_camera_view(
|
|||
if let Some(ui_anti_alias) = ui_anti_alias {
|
||||
entity_commands.insert(*ui_anti_alias);
|
||||
}
|
||||
if let Some(shadow_samples) = shadow_samples {
|
||||
entity_commands.insert(*shadow_samples);
|
||||
}
|
||||
transparent_render_phases.insert_or_clear(entity);
|
||||
|
||||
live_entities.insert(entity);
|
||||
|
|
|
@ -2373,6 +2373,41 @@ impl ResolvedBorderRadius {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
|
||||
#[reflect(Component, PartialEq, Default)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
reflect(Serialize, Deserialize)
|
||||
)]
|
||||
pub struct BoxShadow {
|
||||
/// The shadow's color
|
||||
pub color: Color,
|
||||
/// Horizontal offset
|
||||
pub x_offset: Val,
|
||||
/// Vertical offset
|
||||
pub y_offset: Val,
|
||||
/// How much the shadow should spread outward.
|
||||
///
|
||||
/// Negative values will make the shadow shrink inwards.
|
||||
/// Percentage values are based on the width of the UI node.
|
||||
pub spread_radius: Val,
|
||||
/// Blurriness of the shadow
|
||||
pub blur_radius: Val,
|
||||
}
|
||||
|
||||
impl Default for BoxShadow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::BLACK,
|
||||
x_offset: Val::Percent(20.),
|
||||
y_offset: Val::Percent(20.),
|
||||
spread_radius: Val::ZERO,
|
||||
blur_radius: Val::Percent(10.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::GridPlacement;
|
||||
|
@ -2515,3 +2550,28 @@ pub enum UiAntiAlias {
|
|||
/// UI will render without anti-aliasing
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Number of shadow samples.
|
||||
/// A larger value will result in higher quality shadows.
|
||||
/// Default is 4, values higher than ~10 offer diminishing returns.
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_core_pipeline::prelude::*;
|
||||
/// use bevy_ecs::prelude::*;
|
||||
/// use bevy_ui::prelude::*;
|
||||
///
|
||||
/// fn spawn_camera(mut commands: Commands) {
|
||||
/// commands.spawn((
|
||||
/// Camera2d,
|
||||
/// UiBoxShadowSamples(6),
|
||||
/// ));
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Component, Clone, Copy, Debug, Reflect, Eq, PartialEq)]
|
||||
pub struct UiBoxShadowSamples(pub u32);
|
||||
|
||||
impl Default for UiBoxShadowSamples {
|
||||
fn default() -> Self {
|
||||
Self(4)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -496,6 +496,7 @@ Example | Description
|
|||
Example | Description
|
||||
--- | ---
|
||||
[Borders](../examples/ui/borders.rs) | Demonstrates how to create a node with a border
|
||||
[Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow
|
||||
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
|
||||
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
|
||||
[Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI.
|
||||
|
|
224
examples/ui/box_shadow.rs
Normal file
224
examples/ui/box_shadow.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
//! This example shows how to create a node with a shadow
|
||||
|
||||
use argh::FromArgs;
|
||||
use bevy::color::palettes::css::DEEP_SKY_BLUE;
|
||||
use bevy::color::palettes::css::LIGHT_SKY_BLUE;
|
||||
use bevy::prelude::*;
|
||||
use bevy::winit::WinitSettings;
|
||||
|
||||
#[derive(FromArgs, Resource)]
|
||||
/// `box_shadow` example
|
||||
struct Args {
|
||||
/// number of samples
|
||||
#[argh(option, default = "4")]
|
||||
samples: u32,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
// `from_env` panics on the web
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let args: Args = argh::from_env();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let args = Args::from_args(&[], &[]).unwrap();
|
||||
|
||||
// ui camera
|
||||
commands.spawn((Camera2d, UiBoxShadowSamples(args.samples)));
|
||||
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
padding: UiRect::all(Val::Px(30.)),
|
||||
column_gap: Val::Px(30.),
|
||||
flex_wrap: FlexWrap::Wrap,
|
||||
..default()
|
||||
},
|
||||
background_color: BackgroundColor(DEEP_SKY_BLUE.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.with_children(|commands| {
|
||||
let example_nodes = [
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::ZERO,
|
||||
10.,
|
||||
0.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(Vec2::new(50., 25.), Vec2::ZERO, 10., 0., BorderRadius::ZERO),
|
||||
(Vec2::splat(50.), Vec2::ZERO, 10., 0., BorderRadius::MAX),
|
||||
(Vec2::new(100., 25.), Vec2::ZERO, 10., 0., BorderRadius::MAX),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::ZERO,
|
||||
10.,
|
||||
0.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(Vec2::new(50., 25.), Vec2::ZERO, 0., 10., BorderRadius::ZERO),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::ZERO,
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(Vec2::new(100., 25.), Vec2::ZERO, 0., 10., BorderRadius::MAX),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(25.),
|
||||
0.,
|
||||
0.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::new(50., 25.),
|
||||
Vec2::splat(25.),
|
||||
0.,
|
||||
0.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
0.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(25.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::new(50., 25.),
|
||||
Vec2::splat(25.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::new(50., 25.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::ZERO,
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::bottom_right(Val::Px(10.)),
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::all(Val::Px(20.)),
|
||||
),
|
||||
(
|
||||
Vec2::new(50., 25.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::all(Val::Px(20.)),
|
||||
),
|
||||
(
|
||||
Vec2::new(25., 50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
3.,
|
||||
BorderRadius::MAX,
|
||||
),
|
||||
(
|
||||
Vec2::splat(50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::all(Val::Px(20.)),
|
||||
),
|
||||
(
|
||||
Vec2::new(50., 25.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::all(Val::Px(20.)),
|
||||
),
|
||||
(
|
||||
Vec2::new(25., 50.),
|
||||
Vec2::splat(10.),
|
||||
0.,
|
||||
10.,
|
||||
BorderRadius::MAX,
|
||||
),
|
||||
];
|
||||
|
||||
for (size, offset, spread, blur, border_radius) in example_nodes {
|
||||
commands.spawn(box_shadow_node_bundle(
|
||||
size,
|
||||
offset,
|
||||
spread,
|
||||
blur,
|
||||
border_radius,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn box_shadow_node_bundle(
|
||||
size: Vec2,
|
||||
offset: Vec2,
|
||||
spread: f32,
|
||||
blur: f32,
|
||||
border_radius: BorderRadius,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Px(size.x),
|
||||
height: Val::Px(size.y),
|
||||
border: UiRect::all(Val::Px(4.)),
|
||||
..default()
|
||||
},
|
||||
border_color: BorderColor(LIGHT_SKY_BLUE.into()),
|
||||
border_radius,
|
||||
background_color: BackgroundColor(DEEP_SKY_BLUE.into()),
|
||||
..Default::default()
|
||||
},
|
||||
BoxShadow {
|
||||
color: Color::BLACK.with_alpha(0.8),
|
||||
x_offset: Val::Percent(offset.x),
|
||||
y_offset: Val::Percent(offset.y),
|
||||
spread_radius: Val::Percent(spread),
|
||||
blur_radius: Val::Px(blur),
|
||||
},
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue