Path: blob/main/crates/bevy_ui_render/src/box_shadow.rs
9357 views
//! Box shadows rendering12use core::{hash::Hash, ops::Range};34use bevy_app::prelude::*;5use bevy_asset::*;6use bevy_camera::visibility::InheritedVisibility;7use bevy_color::{Alpha, ColorToComponents, LinearRgba};8use bevy_ecs::prelude::*;9use bevy_ecs::{10prelude::Component,11system::{12lifetimeless::{Read, SRes},13*,14},15};16use bevy_image::BevyDefault as _;17use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};18use bevy_mesh::VertexBufferLayout;19use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};20use bevy_render::{21render_phase::*,22render_resource::{binding_types::uniform_buffer, *},23renderer::{RenderDevice, RenderQueue},24view::*,25Extract, ExtractSchedule, Render, RenderSystems,26};27use bevy_render::{RenderApp, RenderStartup};28use bevy_shader::{Shader, ShaderDefVal};29use bevy_ui::{30BoxShadow, CalculatedClip, ComputedNode, ComputedUiRenderTargetInfo, ComputedUiTargetCamera,31ResolvedBorderRadius, UiGlobalTransform, Val,32};33use bevy_utils::default;34use bytemuck::{Pod, Zeroable};3536use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap};3738use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};3940/// A plugin that enables the rendering of box shadows.41pub struct BoxShadowPlugin;4243impl Plugin for BoxShadowPlugin {44fn build(&self, app: &mut App) {45embedded_asset!(app, "box_shadow.wgsl");4647if let Some(render_app) = app.get_sub_app_mut(RenderApp) {48render_app49.add_render_command::<TransparentUi, DrawBoxShadows>()50.init_resource::<ExtractedBoxShadows>()51.init_resource::<BoxShadowMeta>()52.init_resource::<SpecializedRenderPipelines<BoxShadowPipeline>>()53.add_systems(RenderStartup, init_box_shadow_pipeline)54.add_systems(55ExtractSchedule,56extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows),57)58.add_systems(59Render,60(61queue_shadows.in_set(RenderSystems::Queue),62prepare_shadows.in_set(RenderSystems::PrepareBindGroups),63),64);65}66}67}6869#[repr(C)]70#[derive(Copy, Clone, Pod, Zeroable)]71struct BoxShadowVertex {72position: [f32; 3],73uvs: [f32; 2],74vertex_color: [f32; 4],75size: [f32; 2],76radius: [f32; 4],77blur: f32,78bounds: [f32; 2],79}8081#[derive(Component)]82pub struct UiShadowsBatch {83pub range: Range<u32>,84pub camera: Entity,85}8687/// Contains the vertices and bind groups to be sent to the GPU88#[derive(Resource)]89pub struct BoxShadowMeta {90vertices: RawBufferVec<BoxShadowVertex>,91indices: RawBufferVec<u32>,92view_bind_group: Option<BindGroup>,93}9495impl Default for BoxShadowMeta {96fn default() -> Self {97Self {98vertices: RawBufferVec::new(BufferUsages::VERTEX),99indices: RawBufferVec::new(BufferUsages::INDEX),100view_bind_group: None,101}102}103}104105#[derive(Resource)]106pub struct BoxShadowPipeline {107pub view_layout: BindGroupLayoutDescriptor,108pub shader: Handle<Shader>,109}110111pub fn init_box_shadow_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {112let view_layout = BindGroupLayoutDescriptor::new(113"box_shadow_view_layout",114&BindGroupLayoutEntries::single(115ShaderStages::VERTEX_FRAGMENT,116uniform_buffer::<ViewUniform>(true),117),118);119120commands.insert_resource(BoxShadowPipeline {121view_layout,122shader: load_embedded_asset!(asset_server.as_ref(), "box_shadow.wgsl"),123});124}125126#[derive(Clone, Copy, Hash, PartialEq, Eq)]127pub struct BoxShadowPipelineKey {128pub hdr: bool,129/// Number of samples, a higher value results in better quality shadows.130pub samples: u32,131}132133impl SpecializedRenderPipeline for BoxShadowPipeline {134type Key = BoxShadowPipelineKey;135136fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {137let vertex_layout = VertexBufferLayout::from_vertex_formats(138VertexStepMode::Vertex,139vec![140// position141VertexFormat::Float32x3,142// uv143VertexFormat::Float32x2,144// color145VertexFormat::Float32x4,146// target rect size147VertexFormat::Float32x2,148// corner radius values (top left, top right, bottom right, bottom left)149VertexFormat::Float32x4,150// blur radius151VertexFormat::Float32,152// outer size153VertexFormat::Float32x2,154],155);156let shader_defs = vec![ShaderDefVal::UInt(157"SHADOW_SAMPLES".to_string(),158key.samples,159)];160161RenderPipelineDescriptor {162vertex: VertexState {163shader: self.shader.clone(),164shader_defs: shader_defs.clone(),165buffers: vec![vertex_layout],166..default()167},168fragment: Some(FragmentState {169shader: self.shader.clone(),170shader_defs,171targets: vec![Some(ColorTargetState {172format: if key.hdr {173ViewTarget::TEXTURE_FORMAT_HDR174} else {175TextureFormat::bevy_default()176},177blend: Some(BlendState::ALPHA_BLENDING),178write_mask: ColorWrites::ALL,179})],180..default()181}),182layout: vec![self.view_layout.clone()],183label: Some("box_shadow_pipeline".into()),184..default()185}186}187}188189/// Description of a shadow to be sorted and queued for rendering190pub struct ExtractedBoxShadow {191pub stack_index: u32,192pub transform: Affine2,193pub bounds: Vec2,194pub clip: Option<Rect>,195pub extracted_camera_entity: Entity,196pub color: LinearRgba,197pub radius: ResolvedBorderRadius,198pub blur_radius: f32,199pub size: Vec2,200pub main_entity: MainEntity,201pub render_entity: Entity,202}203204/// List of extracted shadows to be sorted and queued for rendering205#[derive(Resource, Default)]206pub struct ExtractedBoxShadows {207pub box_shadows: Vec<ExtractedBoxShadow>,208}209210pub fn extract_shadows(211mut commands: Commands,212mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,213box_shadow_query: Extract<214Query<(215Entity,216&ComputedNode,217&UiGlobalTransform,218&InheritedVisibility,219&BoxShadow,220Option<&CalculatedClip>,221&ComputedUiTargetCamera,222&ComputedUiRenderTargetInfo,223)>,224>,225camera_map: Extract<UiCameraMap>,226) {227let mut mapping = camera_map.get_mapper();228229for (entity, uinode, transform, visibility, box_shadow, clip, camera, target) in230&box_shadow_query231{232// Skip if no visible shadows233if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() {234continue;235}236237let Some(extracted_camera_entity) = mapping.map(camera) else {238continue;239};240241let ui_physical_viewport_size = target.physical_size().as_vec2();242let scale_factor = target.scale_factor();243244for drop_shadow in box_shadow.iter() {245if drop_shadow.color.is_fully_transparent() {246continue;247}248249let resolve_val = |val, base, scale_factor| match val {250Val::Auto => 0.,251Val::Px(px) => px * scale_factor,252Val::Percent(percent) => percent / 100. * base,253Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x,254Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y,255Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(),256Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(),257};258259let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor);260let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x;261262let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y);263264let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor);265let offset = vec2(266resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor),267resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor),268);269270let shadow_size = uinode.size() + spread;271if shadow_size.cmple(Vec2::ZERO).any() {272continue;273}274275let radius = ResolvedBorderRadius {276top_left: uinode.border_radius.top_left * spread_ratio,277top_right: uinode.border_radius.top_right * spread_ratio,278bottom_left: uinode.border_radius.bottom_left * spread_ratio,279bottom_right: uinode.border_radius.bottom_right * spread_ratio,280};281282extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {283render_entity: commands.spawn(TemporaryRenderEntity).id(),284stack_index: uinode.stack_index,285transform: Affine2::from(transform) * Affine2::from_translation(offset),286color: drop_shadow.color.into(),287bounds: shadow_size + 6. * blur_radius,288clip: clip.map(|clip| clip.clip),289extracted_camera_entity,290radius,291blur_radius,292size: shadow_size,293main_entity: entity.into(),294});295}296}297}298299#[expect(300clippy::too_many_arguments,301reason = "it's a system that needs a lot of them"302)]303pub fn queue_shadows(304extracted_box_shadows: ResMut<ExtractedBoxShadows>,305box_shadow_pipeline: Res<BoxShadowPipeline>,306mut pipelines: ResMut<SpecializedRenderPipelines<BoxShadowPipeline>>,307mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,308mut render_views: Query<(&UiCameraView, Option<&BoxShadowSamples>), With<ExtractedView>>,309camera_views: Query<&ExtractedView>,310pipeline_cache: Res<PipelineCache>,311draw_functions: Res<DrawFunctions<TransparentUi>>,312) {313let draw_function = draw_functions.read().id::<DrawBoxShadows>();314for (index, extracted_shadow) in extracted_box_shadows.box_shadows.iter().enumerate() {315let entity = extracted_shadow.render_entity;316let Ok((default_camera_view, shadow_samples)) =317render_views.get_mut(extracted_shadow.extracted_camera_entity)318else {319continue;320};321322let Ok(view) = camera_views.get(default_camera_view.0) else {323continue;324};325326let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)327else {328continue;329};330331let pipeline = pipelines.specialize(332&pipeline_cache,333&box_shadow_pipeline,334BoxShadowPipelineKey {335hdr: view.hdr,336samples: shadow_samples.copied().unwrap_or_default().0,337},338);339340transparent_phase.add(TransparentUi {341draw_function,342pipeline,343entity: (entity, extracted_shadow.main_entity),344sort_key: FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW),345346batch_range: 0..0,347extra_index: PhaseItemExtraIndex::None,348index,349indexed: true,350});351}352}353354pub fn prepare_shadows(355mut commands: Commands,356render_device: Res<RenderDevice>,357render_queue: Res<RenderQueue>,358pipeline_cache: Res<PipelineCache>,359mut ui_meta: ResMut<BoxShadowMeta>,360mut extracted_shadows: ResMut<ExtractedBoxShadows>,361view_uniforms: Res<ViewUniforms>,362box_shadow_pipeline: Res<BoxShadowPipeline>,363mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,364mut previous_len: Local<usize>,365) {366if let Some(view_binding) = view_uniforms.uniforms.binding() {367let mut batches: Vec<(Entity, UiShadowsBatch)> = Vec::with_capacity(*previous_len);368369ui_meta.vertices.clear();370ui_meta.indices.clear();371ui_meta.view_bind_group = Some(render_device.create_bind_group(372"box_shadow_view_bind_group",373&pipeline_cache.get_bind_group_layout(&box_shadow_pipeline.view_layout),374&BindGroupEntries::single(view_binding),375));376377// Buffer indexes378let mut vertices_index = 0;379let mut indices_index = 0;380381for ui_phase in phases.values_mut() {382for item_index in 0..ui_phase.items.len() {383let item = &mut ui_phase.items[item_index];384let Some(box_shadow) = extracted_shadows385.box_shadows386.get(item.index)387.filter(|n| item.entity() == n.render_entity)388else {389continue;390};391let rect_size = box_shadow.bounds;392393// Specify the corners of the node394let positions = QUAD_VERTEX_POSITIONS.map(|pos| {395box_shadow396.transform397.transform_point2(pos * rect_size)398.extend(0.)399});400401// Calculate the effect of clipping402// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)403let positions_diff = if let Some(clip) = box_shadow.clip {404[405Vec2::new(406f32::max(clip.min.x - positions[0].x, 0.),407f32::max(clip.min.y - positions[0].y, 0.),408),409Vec2::new(410f32::min(clip.max.x - positions[1].x, 0.),411f32::max(clip.min.y - positions[1].y, 0.),412),413Vec2::new(414f32::min(clip.max.x - positions[2].x, 0.),415f32::min(clip.max.y - positions[2].y, 0.),416),417Vec2::new(418f32::max(clip.min.x - positions[3].x, 0.),419f32::min(clip.max.y - positions[3].y, 0.),420),421]422} else {423[Vec2::ZERO; 4]424};425426let positions_clipped = [427positions[0] + positions_diff[0].extend(0.),428positions[1] + positions_diff[1].extend(0.),429positions[2] + positions_diff[2].extend(0.),430positions[3] + positions_diff[3].extend(0.),431];432433let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size);434435// Don't try to cull nodes that have a rotation436// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π437// In those two cases, the culling check can proceed normally as corners will be on438// horizontal / vertical lines439// For all other angles, bypass the culling check440// This does not properly handles all rotations on all axis441if box_shadow.transform.x_axis[1] == 0.0 {442// Cull nodes that are completely clipped443if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x444|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y445{446continue;447}448}449450let uvs = [451Vec2::new(positions_diff[0].x, positions_diff[0].y),452Vec2::new(453box_shadow.bounds.x + positions_diff[1].x,454positions_diff[1].y,455),456Vec2::new(457box_shadow.bounds.x + positions_diff[2].x,458box_shadow.bounds.y + positions_diff[2].y,459),460Vec2::new(461positions_diff[3].x,462box_shadow.bounds.y + positions_diff[3].y,463),464]465.map(|pos| pos / box_shadow.bounds);466467for i in 0..4 {468ui_meta.vertices.push(BoxShadowVertex {469position: positions_clipped[i].into(),470uvs: uvs[i].into(),471vertex_color: box_shadow.color.to_f32_array(),472size: box_shadow.size.into(),473radius: box_shadow.radius.into(),474blur: box_shadow.blur_radius,475bounds: rect_size.into(),476});477}478479for &i in &QUAD_INDICES {480ui_meta.indices.push(indices_index + i as u32);481}482483batches.push((484item.entity(),485UiShadowsBatch {486range: vertices_index..vertices_index + 6,487camera: box_shadow.extracted_camera_entity,488},489));490491vertices_index += 6;492indices_index += 4;493494// shadows are sent to the gpu non-batched495*ui_phase.items[item_index].batch_range_mut() =496item_index as u32..item_index as u32 + 1;497}498}499ui_meta.vertices.write_buffer(&render_device, &render_queue);500ui_meta.indices.write_buffer(&render_device, &render_queue);501*previous_len = batches.len();502commands.try_insert_batch(batches);503}504extracted_shadows.box_shadows.clear();505}506507pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow);508509pub struct SetBoxShadowViewBindGroup<const I: usize>;510impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetBoxShadowViewBindGroup<I> {511type Param = SRes<BoxShadowMeta>;512type ViewQuery = Read<ViewUniformOffset>;513type ItemQuery = ();514515fn render<'w>(516_item: &P,517view_uniform: &'w ViewUniformOffset,518_entity: Option<()>,519ui_meta: SystemParamItem<'w, '_, Self::Param>,520pass: &mut TrackedRenderPass<'w>,521) -> RenderCommandResult {522let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {523return RenderCommandResult::Failure("view_bind_group not available");524};525pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);526RenderCommandResult::Success527}528}529530pub struct DrawBoxShadow;531impl<P: PhaseItem> RenderCommand<P> for DrawBoxShadow {532type Param = SRes<BoxShadowMeta>;533type ViewQuery = ();534type ItemQuery = Read<UiShadowsBatch>;535536#[inline]537fn render<'w>(538_item: &P,539_view: (),540batch: Option<&'w UiShadowsBatch>,541ui_meta: SystemParamItem<'w, '_, Self::Param>,542pass: &mut TrackedRenderPass<'w>,543) -> RenderCommandResult {544let Some(batch) = batch else {545return RenderCommandResult::Skip;546};547let ui_meta = ui_meta.into_inner();548let Some(vertices) = ui_meta.vertices.buffer() else {549return RenderCommandResult::Failure("missing vertices to draw ui");550};551let Some(indices) = ui_meta.indices.buffer() else {552return RenderCommandResult::Failure("missing indices to draw ui");553};554555// Store the vertices556pass.set_vertex_buffer(0, vertices.slice(..));557// Define how to "connect" the vertices558pass.set_index_buffer(indices.slice(..), IndexFormat::Uint32);559// Draw the vertices560pass.draw_indexed(batch.range.clone(), 0, 0..1);561RenderCommandResult::Success562}563}564565566