Path: blob/main/crates/bevy_ui_render/src/ui_material_pipeline.rs
9390 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: BindGroupLayoutDescriptor,116pub view_layout: BindGroupLayoutDescriptor,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,181asset_server: Res<AssetServer>,182render_device: Res<RenderDevice>,183) {184let ui_layout = M::bind_group_layout_descriptor(&render_device);185186let view_layout = BindGroupLayoutDescriptor::new(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>,382pipeline_cache: Res<PipelineCache>,383mut ui_meta: ResMut<UiMaterialMeta<M>>,384mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,385view_uniforms: Res<ViewUniforms>,386globals_buffer: Res<GlobalsBuffer>,387ui_material_pipeline: Res<UiMaterialPipeline<M>>,388mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,389mut previous_len: Local<usize>,390) {391if let (Some(view_binding), Some(globals_binding)) = (392view_uniforms.uniforms.binding(),393globals_buffer.buffer.binding(),394) {395let mut batches: Vec<(Entity, UiMaterialBatch<M>)> = Vec::with_capacity(*previous_len);396397ui_meta.vertices.clear();398ui_meta.view_bind_group = Some(render_device.create_bind_group(399"ui_material_view_bind_group",400&pipeline_cache.get_bind_group_layout(&ui_material_pipeline.view_layout),401&BindGroupEntries::sequential((view_binding, globals_binding)),402));403let mut index = 0;404405for ui_phase in phases.values_mut() {406let mut batch_item_index = 0;407let mut batch_shader_handle = AssetId::invalid();408409for item_index in 0..ui_phase.items.len() {410let item = &mut ui_phase.items[item_index];411if let Some(extracted_uinode) = extracted_uinodes412.uinodes413.get(item.index)414.filter(|n| item.entity() == n.render_entity)415{416let mut existing_batch = batches417.last_mut()418.filter(|_| batch_shader_handle == extracted_uinode.material);419420if existing_batch.is_none() {421batch_item_index = item_index;422batch_shader_handle = extracted_uinode.material;423424let new_batch = UiMaterialBatch {425range: index..index,426material: extracted_uinode.material,427};428429batches.push((item.entity(), new_batch));430431existing_batch = batches.last_mut();432}433434let uinode_rect = extracted_uinode.rect;435436let rect_size = uinode_rect.size();437438let positions = QUAD_VERTEX_POSITIONS.map(|pos| {439extracted_uinode440.transform441.transform_point2(pos * rect_size)442.extend(1.0)443});444445let positions_diff = if let Some(clip) = extracted_uinode.clip {446[447Vec2::new(448f32::max(clip.min.x - positions[0].x, 0.),449f32::max(clip.min.y - positions[0].y, 0.),450),451Vec2::new(452f32::min(clip.max.x - positions[1].x, 0.),453f32::max(clip.min.y - positions[1].y, 0.),454),455Vec2::new(456f32::min(clip.max.x - positions[2].x, 0.),457f32::min(clip.max.y - positions[2].y, 0.),458),459Vec2::new(460f32::max(clip.min.x - positions[3].x, 0.),461f32::min(clip.max.y - positions[3].y, 0.),462),463]464} else {465[Vec2::ZERO; 4]466};467468let positions_clipped = [469positions[0] + positions_diff[0].extend(0.),470positions[1] + positions_diff[1].extend(0.),471positions[2] + positions_diff[2].extend(0.),472positions[3] + positions_diff[3].extend(0.),473];474475let transformed_rect_size =476extracted_uinode.transform.transform_vector2(rect_size);477478// Don't try to cull nodes that have a rotation479// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π480// In those two cases, the culling check can proceed normally as corners will be on481// horizontal / vertical lines482// For all other angles, bypass the culling check483// This does not properly handles all rotations on all axis484if extracted_uinode.transform.x_axis[1] == 0.0 {485// Cull nodes that are completely clipped486if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x487|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y488{489continue;490}491}492let uvs = [493Vec2::new(494uinode_rect.min.x + positions_diff[0].x,495uinode_rect.min.y + positions_diff[0].y,496),497Vec2::new(498uinode_rect.max.x + positions_diff[1].x,499uinode_rect.min.y + positions_diff[1].y,500),501Vec2::new(502uinode_rect.max.x + positions_diff[2].x,503uinode_rect.max.y + positions_diff[2].y,504),505Vec2::new(506uinode_rect.min.x + positions_diff[3].x,507uinode_rect.max.y + positions_diff[3].y,508),509]510.map(|pos| pos / uinode_rect.max);511512for i in QUAD_INDICES {513ui_meta.vertices.push(UiMaterialVertex {514position: positions_clipped[i].into(),515uv: uvs[i].into(),516size: extracted_uinode.rect.size().into(),517radius: extracted_uinode.border_radius,518border: [519extracted_uinode.border.min_inset.x,520extracted_uinode.border.min_inset.y,521extracted_uinode.border.max_inset.x,522extracted_uinode.border.max_inset.y,523],524});525}526527index += QUAD_INDICES.len() as u32;528existing_batch.unwrap().1.range.end = index;529ui_phase.items[batch_item_index].batch_range_mut().end += 1;530} else {531batch_shader_handle = AssetId::invalid();532}533}534}535ui_meta.vertices.write_buffer(&render_device, &render_queue);536*previous_len = batches.len();537commands.try_insert_batch(batches);538}539extracted_uinodes.uinodes.clear();540}541542pub struct PreparedUiMaterial<T: UiMaterial> {543pub bindings: BindingResources,544pub bind_group: BindGroup,545pub key: T::Data,546}547548impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {549type SourceAsset = M;550551type Param = (552SRes<RenderDevice>,553SRes<PipelineCache>,554SRes<UiMaterialPipeline<M>>,555M::Param,556);557558fn prepare_asset(559material: Self::SourceAsset,560_: AssetId<Self::SourceAsset>,561(render_device, pipeline_cache, pipeline, material_param): &mut SystemParamItem<562Self::Param,563>,564_: Option<&Self>,565) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {566let bind_group_data = material.bind_group_data();567match material.as_bind_group(568&pipeline.ui_layout.clone(),569render_device,570pipeline_cache,571material_param,572) {573Ok(prepared) => Ok(PreparedUiMaterial {574bindings: prepared.bindings,575bind_group: prepared.bind_group,576key: bind_group_data,577}),578Err(AsBindGroupError::RetryNextUpdate) => {579Err(PrepareAssetError::RetryNextUpdate(material))580}581Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),582}583}584}585586pub fn queue_ui_material_nodes<M: UiMaterial>(587extracted_uinodes: Res<ExtractedUiMaterialNodes<M>>,588draw_functions: Res<DrawFunctions<TransparentUi>>,589ui_material_pipeline: Res<UiMaterialPipeline<M>>,590mut pipelines: ResMut<SpecializedRenderPipelines<UiMaterialPipeline<M>>>,591pipeline_cache: Res<PipelineCache>,592render_materials: Res<RenderAssets<PreparedUiMaterial<M>>>,593mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,594mut render_views: Query<&UiCameraView, With<ExtractedView>>,595camera_views: Query<&ExtractedView>,596) where597M::Data: PartialEq + Eq + Hash + Clone,598{599let draw_function = draw_functions.read().id::<DrawUiMaterial<M>>();600601for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() {602let Some(material) = render_materials.get(extracted_uinode.material) else {603continue;604};605606let Ok(default_camera_view) =607render_views.get_mut(extracted_uinode.extracted_camera_entity)608else {609continue;610};611612let Ok(view) = camera_views.get(default_camera_view.0) else {613continue;614};615616let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)617else {618continue;619};620621let pipeline = pipelines.specialize(622&pipeline_cache,623&ui_material_pipeline,624UiMaterialKey {625hdr: view.hdr,626bind_group_data: material.key.clone(),627},628);629if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() {630transparent_phase.items.reserve_exact(631extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(),632);633}634transparent_phase.add(TransparentUi {635draw_function,636pipeline,637entity: (extracted_uinode.render_entity, extracted_uinode.main_entity),638sort_key: FloatOrd(extracted_uinode.stack_index as f32 + M::stack_z_offset()),639batch_range: 0..0,640extra_index: PhaseItemExtraIndex::None,641index,642indexed: false,643});644}645}646647648