Path: blob/main/crates/bevy_ui_render/src/ui_material_pipeline.rs
6596 views
use crate::ui_material::{MaterialNode, UiMaterial, UiMaterialKey};1use crate::*;2use bevy_asset::*;3use bevy_ecs::{4prelude::{Component, With},5query::ROQueryItem,6system::{7lifetimeless::{Read, SRes},8*,9},10};11use bevy_image::BevyDefault as _;12use bevy_math::{Affine2, FloatOrd, Rect, Vec2};13use bevy_mesh::VertexBufferLayout;14use bevy_render::{15globals::{GlobalsBuffer, GlobalsUniform},16render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},17render_phase::*,18render_resource::{binding_types::uniform_buffer, *},19renderer::{RenderDevice, RenderQueue},20sync_world::{MainEntity, TemporaryRenderEntity},21view::*,22Extract, ExtractSchedule, Render, RenderSystems,23};24use bevy_render::{RenderApp, RenderStartup};25use bevy_shader::{load_shader_library, Shader, ShaderRef};26use bevy_sprite::BorderRect;27use bevy_utils::default;28use bytemuck::{Pod, Zeroable};29use core::{hash::Hash, marker::PhantomData, ops::Range};3031/// Adds the necessary ECS resources and render logic to enable rendering entities using the given32/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types).33pub struct UiMaterialPlugin<M: UiMaterial>(PhantomData<M>);3435impl<M: UiMaterial> Default for UiMaterialPlugin<M> {36fn default() -> Self {37Self(Default::default())38}39}4041impl<M: UiMaterial> Plugin for UiMaterialPlugin<M>42where43M::Data: PartialEq + Eq + Hash + Clone,44{45fn build(&self, app: &mut App) {46load_shader_library!(app, "ui_vertex_output.wgsl");4748embedded_asset!(app, "ui_material.wgsl");4950app.init_asset::<M>()51.register_type::<MaterialNode<M>>()52.add_plugins(RenderAssetPlugin::<PreparedUiMaterial<M>>::default());5354if let Some(render_app) = app.get_sub_app_mut(RenderApp) {55render_app56.add_render_command::<TransparentUi, DrawUiMaterial<M>>()57.init_resource::<ExtractedUiMaterialNodes<M>>()58.init_resource::<UiMaterialMeta<M>>()59.init_resource::<SpecializedRenderPipelines<UiMaterialPipeline<M>>>()60.add_systems(RenderStartup, init_ui_material_pipeline::<M>)61.add_systems(62ExtractSchedule,63extract_ui_material_nodes::<M>.in_set(RenderUiSystems::ExtractBackgrounds),64)65.add_systems(66Render,67(68queue_ui_material_nodes::<M>.in_set(RenderSystems::Queue),69prepare_uimaterial_nodes::<M>.in_set(RenderSystems::PrepareBindGroups),70),71);72}73}74}7576#[derive(Resource)]77pub struct UiMaterialMeta<M: UiMaterial> {78vertices: RawBufferVec<UiMaterialVertex>,79view_bind_group: Option<BindGroup>,80marker: PhantomData<M>,81}8283impl<M: UiMaterial> Default for UiMaterialMeta<M> {84fn default() -> Self {85Self {86vertices: RawBufferVec::new(BufferUsages::VERTEX),87view_bind_group: Default::default(),88marker: PhantomData,89}90}91}9293#[repr(C)]94#[derive(Copy, Clone, Pod, Zeroable)]95pub struct UiMaterialVertex {96pub position: [f32; 3],97pub uv: [f32; 2],98pub size: [f32; 2],99pub border: [f32; 4],100pub radius: [f32; 4],101}102103// in this [`UiMaterialPipeline`] there is (currently) no batching going on.104// Therefore the [`UiMaterialBatch`] is more akin to a draw call.105#[derive(Component)]106pub struct UiMaterialBatch<M: UiMaterial> {107/// The range of vertices inside the [`UiMaterialMeta`]108pub range: Range<u32>,109pub material: AssetId<M>,110}111112/// Render pipeline data for a given [`UiMaterial`]113#[derive(Resource)]114pub struct UiMaterialPipeline<M: UiMaterial> {115pub ui_layout: BindGroupLayout,116pub view_layout: BindGroupLayout,117pub vertex_shader: Handle<Shader>,118pub fragment_shader: Handle<Shader>,119marker: PhantomData<M>,120}121122impl<M: UiMaterial> SpecializedRenderPipeline for UiMaterialPipeline<M>123where124M::Data: PartialEq + Eq + Hash + Clone,125{126type Key = UiMaterialKey<M>;127128fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {129let vertex_layout = VertexBufferLayout::from_vertex_formats(130VertexStepMode::Vertex,131vec![132// position133VertexFormat::Float32x3,134// uv135VertexFormat::Float32x2,136// size137VertexFormat::Float32x2,138// border widths139VertexFormat::Float32x4,140// border radius141VertexFormat::Float32x4,142],143);144let shader_defs = Vec::new();145146let mut descriptor = RenderPipelineDescriptor {147vertex: VertexState {148shader: self.vertex_shader.clone(),149shader_defs: shader_defs.clone(),150buffers: vec![vertex_layout],151..default()152},153fragment: Some(FragmentState {154shader: self.fragment_shader.clone(),155shader_defs,156targets: vec![Some(ColorTargetState {157format: if key.hdr {158ViewTarget::TEXTURE_FORMAT_HDR159} else {160TextureFormat::bevy_default()161},162blend: Some(BlendState::ALPHA_BLENDING),163write_mask: ColorWrites::ALL,164})],165..default()166}),167label: Some("ui_material_pipeline".into()),168..default()169};170171descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()];172173M::specialize(&mut descriptor, key);174175descriptor176}177}178179pub fn init_ui_material_pipeline<M: UiMaterial>(180mut commands: Commands,181render_device: Res<RenderDevice>,182asset_server: Res<AssetServer>,183) {184let ui_layout = M::bind_group_layout(&render_device);185186let view_layout = render_device.create_bind_group_layout(187"ui_view_layout",188&BindGroupLayoutEntries::sequential(189ShaderStages::VERTEX_FRAGMENT,190(191uniform_buffer::<ViewUniform>(true),192uniform_buffer::<GlobalsUniform>(false),193),194),195);196197let load_default = || load_embedded_asset!(asset_server.as_ref(), "ui_material.wgsl");198199commands.insert_resource(UiMaterialPipeline::<M> {200ui_layout,201view_layout,202vertex_shader: match M::vertex_shader() {203ShaderRef::Default => load_default(),204ShaderRef::Handle(handle) => handle,205ShaderRef::Path(path) => asset_server.load(path),206},207fragment_shader: match M::fragment_shader() {208ShaderRef::Default => load_default(),209ShaderRef::Handle(handle) => handle,210ShaderRef::Path(path) => asset_server.load(path),211},212marker: PhantomData,213});214}215216pub type DrawUiMaterial<M> = (217SetItemPipeline,218SetMatUiViewBindGroup<M, 0>,219SetUiMaterialBindGroup<M, 1>,220DrawUiMaterialNode<M>,221);222223pub struct SetMatUiViewBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);224impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P> for SetMatUiViewBindGroup<M, I> {225type Param = SRes<UiMaterialMeta<M>>;226type ViewQuery = Read<ViewUniformOffset>;227type ItemQuery = ();228229fn render<'w>(230_item: &P,231view_uniform: &'w ViewUniformOffset,232_entity: Option<()>,233ui_meta: SystemParamItem<'w, '_, Self::Param>,234pass: &mut TrackedRenderPass<'w>,235) -> RenderCommandResult {236pass.set_bind_group(237I,238ui_meta.into_inner().view_bind_group.as_ref().unwrap(),239&[view_uniform.offset],240);241RenderCommandResult::Success242}243}244245pub struct SetUiMaterialBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);246impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P>247for SetUiMaterialBindGroup<M, I>248{249type Param = SRes<RenderAssets<PreparedUiMaterial<M>>>;250type ViewQuery = ();251type ItemQuery = Read<UiMaterialBatch<M>>;252253fn render<'w>(254_item: &P,255_view: (),256material_handle: Option<ROQueryItem<'_, '_, Self::ItemQuery>>,257materials: SystemParamItem<'w, '_, Self::Param>,258pass: &mut TrackedRenderPass<'w>,259) -> RenderCommandResult {260let Some(material_handle) = material_handle else {261return RenderCommandResult::Skip;262};263let Some(material) = materials.into_inner().get(material_handle.material) else {264return RenderCommandResult::Skip;265};266pass.set_bind_group(I, &material.bind_group, &[]);267RenderCommandResult::Success268}269}270271pub struct DrawUiMaterialNode<M>(PhantomData<M>);272impl<P: PhaseItem, M: UiMaterial> RenderCommand<P> for DrawUiMaterialNode<M> {273type Param = SRes<UiMaterialMeta<M>>;274type ViewQuery = ();275type ItemQuery = Read<UiMaterialBatch<M>>;276277#[inline]278fn render<'w>(279_item: &P,280_view: (),281batch: Option<&'w UiMaterialBatch<M>>,282ui_meta: SystemParamItem<'w, '_, Self::Param>,283pass: &mut TrackedRenderPass<'w>,284) -> RenderCommandResult {285let Some(batch) = batch else {286return RenderCommandResult::Skip;287};288289pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));290pass.draw(batch.range.clone(), 0..1);291RenderCommandResult::Success292}293}294295pub struct ExtractedUiMaterialNode<M: UiMaterial> {296pub stack_index: u32,297pub transform: Affine2,298pub rect: Rect,299pub border: BorderRect,300pub border_radius: [f32; 4],301pub material: AssetId<M>,302pub clip: Option<Rect>,303// Camera to render this UI node to. By the time it is extracted,304// it is defaulted to a single camera if only one exists.305// Nodes with ambiguous camera will be ignored.306pub extracted_camera_entity: Entity,307pub main_entity: MainEntity,308pub render_entity: Entity,309}310311#[derive(Resource)]312pub struct ExtractedUiMaterialNodes<M: UiMaterial> {313pub uinodes: Vec<ExtractedUiMaterialNode<M>>,314}315316impl<M: UiMaterial> Default for ExtractedUiMaterialNodes<M> {317fn default() -> Self {318Self {319uinodes: Default::default(),320}321}322}323324pub fn extract_ui_material_nodes<M: UiMaterial>(325mut commands: Commands,326mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,327materials: Extract<Res<Assets<M>>>,328uinode_query: Extract<329Query<(330Entity,331&ComputedNode,332&UiGlobalTransform,333&MaterialNode<M>,334&InheritedVisibility,335Option<&CalculatedClip>,336&ComputedUiTargetCamera,337)>,338>,339camera_map: Extract<UiCameraMap>,340) {341let mut camera_mapper = camera_map.get_mapper();342343for (entity, computed_node, transform, handle, inherited_visibility, clip, camera) in344uinode_query.iter()345{346// skip invisible nodes347if !inherited_visibility.get() || computed_node.is_empty() {348continue;349}350351// Skip loading materials352if !materials.contains(handle) {353continue;354}355356let Some(extracted_camera_entity) = camera_mapper.map(camera) else {357continue;358};359360extracted_uinodes.uinodes.push(ExtractedUiMaterialNode {361render_entity: commands.spawn(TemporaryRenderEntity).id(),362stack_index: computed_node.stack_index,363transform: transform.into(),364material: handle.id(),365rect: Rect {366min: Vec2::ZERO,367max: computed_node.size(),368},369border: computed_node.border(),370border_radius: computed_node.border_radius().into(),371clip: clip.map(|clip| clip.clip),372extracted_camera_entity,373main_entity: entity.into(),374});375}376}377378pub fn prepare_uimaterial_nodes<M: UiMaterial>(379mut commands: Commands,380render_device: Res<RenderDevice>,381render_queue: Res<RenderQueue>,382mut ui_meta: ResMut<UiMaterialMeta<M>>,383mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,384view_uniforms: Res<ViewUniforms>,385globals_buffer: Res<GlobalsBuffer>,386ui_material_pipeline: Res<UiMaterialPipeline<M>>,387mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,388mut previous_len: Local<usize>,389) {390if let (Some(view_binding), Some(globals_binding)) = (391view_uniforms.uniforms.binding(),392globals_buffer.buffer.binding(),393) {394let mut batches: Vec<(Entity, UiMaterialBatch<M>)> = Vec::with_capacity(*previous_len);395396ui_meta.vertices.clear();397ui_meta.view_bind_group = Some(render_device.create_bind_group(398"ui_material_view_bind_group",399&ui_material_pipeline.view_layout,400&BindGroupEntries::sequential((view_binding, globals_binding)),401));402let mut index = 0;403404for ui_phase in phases.values_mut() {405let mut batch_item_index = 0;406let mut batch_shader_handle = AssetId::invalid();407408for item_index in 0..ui_phase.items.len() {409let item = &mut ui_phase.items[item_index];410if let Some(extracted_uinode) = extracted_uinodes411.uinodes412.get(item.index)413.filter(|n| item.entity() == n.render_entity)414{415let mut existing_batch = batches416.last_mut()417.filter(|_| batch_shader_handle == extracted_uinode.material);418419if existing_batch.is_none() {420batch_item_index = item_index;421batch_shader_handle = extracted_uinode.material;422423let new_batch = UiMaterialBatch {424range: index..index,425material: extracted_uinode.material,426};427428batches.push((item.entity(), new_batch));429430existing_batch = batches.last_mut();431}432433let uinode_rect = extracted_uinode.rect;434435let rect_size = uinode_rect.size();436437let positions = QUAD_VERTEX_POSITIONS.map(|pos| {438extracted_uinode439.transform440.transform_point2(pos * rect_size)441.extend(1.0)442});443444let positions_diff = if let Some(clip) = extracted_uinode.clip {445[446Vec2::new(447f32::max(clip.min.x - positions[0].x, 0.),448f32::max(clip.min.y - positions[0].y, 0.),449),450Vec2::new(451f32::min(clip.max.x - positions[1].x, 0.),452f32::max(clip.min.y - positions[1].y, 0.),453),454Vec2::new(455f32::min(clip.max.x - positions[2].x, 0.),456f32::min(clip.max.y - positions[2].y, 0.),457),458Vec2::new(459f32::max(clip.min.x - positions[3].x, 0.),460f32::min(clip.max.y - positions[3].y, 0.),461),462]463} else {464[Vec2::ZERO; 4]465};466467let positions_clipped = [468positions[0] + positions_diff[0].extend(0.),469positions[1] + positions_diff[1].extend(0.),470positions[2] + positions_diff[2].extend(0.),471positions[3] + positions_diff[3].extend(0.),472];473474let transformed_rect_size =475extracted_uinode.transform.transform_vector2(rect_size);476477// Don't try to cull nodes that have a rotation478// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π479// In those two cases, the culling check can proceed normally as corners will be on480// horizontal / vertical lines481// For all other angles, bypass the culling check482// This does not properly handles all rotations on all axis483if extracted_uinode.transform.x_axis[1] == 0.0 {484// Cull nodes that are completely clipped485if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x486|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y487{488continue;489}490}491let uvs = [492Vec2::new(493uinode_rect.min.x + positions_diff[0].x,494uinode_rect.min.y + positions_diff[0].y,495),496Vec2::new(497uinode_rect.max.x + positions_diff[1].x,498uinode_rect.min.y + positions_diff[1].y,499),500Vec2::new(501uinode_rect.max.x + positions_diff[2].x,502uinode_rect.max.y + positions_diff[2].y,503),504Vec2::new(505uinode_rect.min.x + positions_diff[3].x,506uinode_rect.max.y + positions_diff[3].y,507),508]509.map(|pos| pos / uinode_rect.max);510511for i in QUAD_INDICES {512ui_meta.vertices.push(UiMaterialVertex {513position: positions_clipped[i].into(),514uv: uvs[i].into(),515size: extracted_uinode.rect.size().into(),516radius: extracted_uinode.border_radius,517border: [518extracted_uinode.border.left,519extracted_uinode.border.top,520extracted_uinode.border.right,521extracted_uinode.border.bottom,522],523});524}525526index += QUAD_INDICES.len() as u32;527existing_batch.unwrap().1.range.end = index;528ui_phase.items[batch_item_index].batch_range_mut().end += 1;529} else {530batch_shader_handle = AssetId::invalid();531}532}533}534ui_meta.vertices.write_buffer(&render_device, &render_queue);535*previous_len = batches.len();536commands.try_insert_batch(batches);537}538extracted_uinodes.uinodes.clear();539}540541pub struct PreparedUiMaterial<T: UiMaterial> {542pub bindings: BindingResources,543pub bind_group: BindGroup,544pub key: T::Data,545}546547impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {548type SourceAsset = M;549550type Param = (SRes<RenderDevice>, SRes<UiMaterialPipeline<M>>, M::Param);551552fn prepare_asset(553material: Self::SourceAsset,554_: AssetId<Self::SourceAsset>,555(render_device, pipeline, material_param): &mut SystemParamItem<Self::Param>,556_: Option<&Self>,557) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {558let bind_group_data = material.bind_group_data();559match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) {560Ok(prepared) => Ok(PreparedUiMaterial {561bindings: prepared.bindings,562bind_group: prepared.bind_group,563key: bind_group_data,564}),565Err(AsBindGroupError::RetryNextUpdate) => {566Err(PrepareAssetError::RetryNextUpdate(material))567}568Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),569}570}571}572573pub fn queue_ui_material_nodes<M: UiMaterial>(574extracted_uinodes: Res<ExtractedUiMaterialNodes<M>>,575draw_functions: Res<DrawFunctions<TransparentUi>>,576ui_material_pipeline: Res<UiMaterialPipeline<M>>,577mut pipelines: ResMut<SpecializedRenderPipelines<UiMaterialPipeline<M>>>,578pipeline_cache: Res<PipelineCache>,579render_materials: Res<RenderAssets<PreparedUiMaterial<M>>>,580mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,581mut render_views: Query<&UiCameraView, With<ExtractedView>>,582camera_views: Query<&ExtractedView>,583) where584M::Data: PartialEq + Eq + Hash + Clone,585{586let draw_function = draw_functions.read().id::<DrawUiMaterial<M>>();587588for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() {589let Some(material) = render_materials.get(extracted_uinode.material) else {590continue;591};592593let Ok(default_camera_view) =594render_views.get_mut(extracted_uinode.extracted_camera_entity)595else {596continue;597};598599let Ok(view) = camera_views.get(default_camera_view.0) else {600continue;601};602603let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)604else {605continue;606};607608let pipeline = pipelines.specialize(609&pipeline_cache,610&ui_material_pipeline,611UiMaterialKey {612hdr: view.hdr,613bind_group_data: material.key.clone(),614},615);616if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() {617transparent_phase.items.reserve_exact(618extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(),619);620}621transparent_phase.add(TransparentUi {622draw_function,623pipeline,624entity: (extracted_uinode.render_entity, extracted_uinode.main_entity),625sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL),626batch_range: 0..0,627extra_index: PhaseItemExtraIndex::None,628index,629indexed: false,630});631}632}633634635