use crate::clipping::clip_polygon;
use crate::ui_material::{MaterialNode, UiMaterial, UiMaterialKey};
use crate::*;
use bevy_asset::*;
use bevy_ecs::{
prelude::{Component, With},
query::ROQueryItem,
system::{
lifetimeless::{Read, SRes},
*,
},
};
use bevy_math::{Affine2, FloatOrd, Rect, Vec2};
use bevy_mesh::VertexBufferLayout;
use bevy_render::material_bind_groups::FallbackBuffer;
use bevy_render::storage::GpuShaderBuffer;
use bevy_render::{
globals::{GlobalsBuffer, GlobalsUniform},
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::MainEntity,
view::*,
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_render::{GpuResourceAppExt, RenderApp, RenderStartup};
use bevy_shader::{load_shader_library, Shader, ShaderRef};
use bevy_sprite::BorderRect;
use bevy_ui::ComputedStackIndex;
use bevy_utils::default;
use bytemuck::{Pod, Zeroable};
use core::{hash::Hash, marker::PhantomData, ops::Range};
pub struct UiMaterialPlugin<M: UiMaterial>(PhantomData<M>);
impl<M: UiMaterial> Default for UiMaterialPlugin<M> {
fn default() -> Self {
Self(Default::default())
}
}
impl<M: UiMaterial> Plugin for UiMaterialPlugin<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) {
load_shader_library!(app, "ui_vertex_output.wesl");
embedded_asset!(app, "ui_material.wesl");
app.init_asset::<M>()
.register_type::<MaterialNode<M>>()
.add_plugins(RenderAssetPlugin::<
PreparedUiMaterial<M>,
(GpuImage, GpuShaderBuffer),
>::default());
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<TransparentUi, DrawUiMaterial<M>>()
.init_resource::<ExtractedUiMaterialNodes<M>>()
.init_gpu_resource::<UiMaterialMeta<M>>()
.init_gpu_resource::<SpecializedRenderPipelines<UiMaterialPipeline<M>>>()
.add_systems(RenderStartup, init_ui_material_pipeline::<M>)
.add_systems(
ExtractSchedule,
extract_ui_material_nodes::<M>.in_set(RenderUiSystems::ExtractBackgrounds),
)
.add_systems(
Render,
(
queue_ui_material_nodes::<M>.in_set(RenderSystems::Queue),
prepare_uimaterial_nodes::<M>.in_set(RenderSystems::PrepareBindGroups),
),
);
}
}
}
#[derive(Resource)]
pub struct UiMaterialMeta<M: UiMaterial> {
vertices: RawBufferVec<UiMaterialVertex>,
view_bind_group: Option<BindGroup>,
marker: PhantomData<M>,
}
impl<M: UiMaterial> Default for UiMaterialMeta<M> {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
view_bind_group: Default::default(),
marker: PhantomData,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct UiMaterialVertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub size: [f32; 2],
pub border: [f32; 4],
pub radius: [[f32; 4]; 2],
}
#[derive(Component)]
pub struct UiMaterialBatch<M: UiMaterial> {
pub range: Range<u32>,
pub material: AssetId<M>,
}
#[derive(Resource)]
pub struct UiMaterialPipeline<M: UiMaterial> {
pub ui_layout: BindGroupLayoutDescriptor,
pub view_layout: BindGroupLayoutDescriptor,
pub vertex_shader: Handle<Shader>,
pub fragment_shader: Handle<Shader>,
marker: PhantomData<M>,
}
impl<M: UiMaterial> SpecializedRenderPipeline for UiMaterialPipeline<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
type Key = UiMaterialKey<M>;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
VertexFormat::Float32x3,
VertexFormat::Float32x2,
VertexFormat::Float32x2,
VertexFormat::Float32x4,
VertexFormat::Float32x4,
VertexFormat::Float32x4,
],
);
let shader_defs = Vec::new();
let mut descriptor = RenderPipelineDescriptor {
vertex: VertexState {
shader: self.vertex_shader.clone(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
..default()
},
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
targets: vec![Some(ColorTargetState {
format: key.target_format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
..default()
}),
label: Some("ui_material_pipeline".into()),
..default()
};
descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()];
M::specialize(&mut descriptor, key);
descriptor
}
}
pub fn init_ui_material_pipeline<M: UiMaterial>(
mut commands: Commands,
asset_server: Res<AssetServer>,
render_device: Res<RenderDevice>,
) {
let ui_layout = M::bind_group_layout_descriptor(&render_device);
let view_layout = BindGroupLayoutDescriptor::new(
"ui_view_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX_FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<GlobalsUniform>(false),
),
),
);
let load_default = || load_embedded_asset!(asset_server.as_ref(), "ui_material.wesl");
commands.insert_resource(UiMaterialPipeline::<M> {
ui_layout,
view_layout,
vertex_shader: match M::vertex_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
fragment_shader: match M::fragment_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
marker: PhantomData,
});
}
pub type DrawUiMaterial<M> = (
SetItemPipeline,
SetMatUiViewBindGroup<M, 0>,
SetUiMaterialBindGroup<M, 1>,
DrawUiMaterialNode<M>,
);
pub struct SetMatUiViewBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P> for SetMatUiViewBindGroup<M, I> {
type Param = SRes<UiMaterialMeta<M>>;
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 {
pass.set_bind_group(
I,
ui_meta.into_inner().view_bind_group.as_ref().unwrap(),
&[view_uniform.offset],
);
RenderCommandResult::Success
}
}
pub struct SetUiMaterialBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P>
for SetUiMaterialBindGroup<M, I>
{
type Param = SRes<RenderAssets<PreparedUiMaterial<M>>>;
type ViewQuery = ();
type ItemQuery = Read<UiMaterialBatch<M>>;
fn render<'w>(
_item: &P,
_view: (),
material_handle: Option<ROQueryItem<'_, '_, Self::ItemQuery>>,
materials: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(material_handle) = material_handle else {
return RenderCommandResult::Skip;
};
let Some(material) = materials.into_inner().get(material_handle.material) else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, &material.bind_group, &[]);
RenderCommandResult::Success
}
}
pub struct DrawUiMaterialNode<M>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial> RenderCommand<P> for DrawUiMaterialNode<M> {
type Param = SRes<UiMaterialMeta<M>>;
type ViewQuery = ();
type ItemQuery = Read<UiMaterialBatch<M>>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiMaterialBatch<M>>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
pass.draw(batch.range.clone(), 0..1);
RenderCommandResult::Success
}
}
pub struct ExtractedUiMaterialNode<M: UiMaterial> {
pub stack_index: u32,
pub transform: Affine2,
pub rect: Rect,
pub border: BorderRect,
pub border_radius: [[f32; 4]; 2],
pub material: AssetId<M>,
pub clip: Option<CalculatedClip>,
}
#[derive(Resource)]
pub struct ExtractedUiMaterialNodes<M: UiMaterial> {
pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap<ExtractedUiMaterialNode<M>>)>,
}
impl<M: UiMaterial> Default for ExtractedUiMaterialNodes<M> {
fn default() -> Self {
Self {
uinodes: Default::default(),
}
}
}
pub fn extract_ui_material_nodes<M: UiMaterial>(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,
materials: Extract<Res<Assets<M>>>,
uinode_query: Extract<
Query<
(
Entity,
&ComputedNode,
&ComputedStackIndex,
&UiGlobalTransform,
&MaterialNode<M>,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedUiTargetCamera,
),
Or<(
Changed<ComputedNode>,
Changed<ComputedStackIndex>,
Changed<UiGlobalTransform>,
Changed<MaterialNode<M>>,
Changed<InheritedVisibility>,
Changed<CalculatedClip>,
Changed<ComputedUiTargetCamera>,
)>,
>,
>,
unfiltered_uinode_query: Extract<
Query<(
Entity,
&ComputedNode,
&ComputedStackIndex,
&UiGlobalTransform,
&MaterialNode<M>,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedUiTargetCamera,
)>,
>,
camera_map: Extract<UiCameraMap>,
(
mut removed_computed_node_query,
mut removed_computed_stack_index_query,
mut removed_ui_global_transform_query,
mut removed_material_node_query,
mut removed_inherited_visibility_query,
mut removed_calculated_clip_query,
mut removed_computed_ui_target_camera_query,
): (
Extract<RemovedComponents<ComputedNode>>,
Extract<RemovedComponents<ComputedStackIndex>>,
Extract<RemovedComponents<UiGlobalTransform>>,
Extract<RemovedComponents<MaterialNode<M>>>,
Extract<RemovedComponents<InheritedVisibility>>,
Extract<RemovedComponents<CalculatedClip>>,
Extract<RemovedComponents<ComputedUiTargetCamera>>,
),
mut nodes_to_reextract_next_frame: Local<MainEntityHashSet>,
mut nodes_processed_this_frame: Local<MainEntityHashSet>,
) {
nodes_processed_this_frame.clear();
let mut camera_mapper = camera_map.get_mapper();
let nodes_to_reextract = mem::take(&mut *nodes_to_reextract_next_frame);
for (
entity,
computed_node,
stack_index,
transform,
handle,
inherited_visibility,
clip,
camera,
) in uinode_query.iter().chain(
nodes_to_reextract
.into_iter()
.map(|main_entity| main_entity.entity())
.chain(removed_calculated_clip_query.read())
.filter_map(|entity| unfiltered_uinode_query.get(entity).ok()),
) {
let main_entity = MainEntity::from(entity);
if nodes_processed_this_frame.contains(&main_entity) {
continue;
}
for (render_entity, _) in extracted_uinodes
.uinodes
.get_mut(&main_entity)
.iter_mut()
.flat_map(|(_, nodes)| nodes.drain(..))
{
commands.entity(render_entity).despawn();
}
if !inherited_visibility.get() || computed_node.is_empty() {
continue;
}
if !materials.contains(handle) {
nodes_to_reextract_next_frame.insert(main_entity);
continue;
}
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
continue;
};
if let Some((camera_entity, _)) = extracted_uinodes.uinodes.get_mut(&main_entity) {
*camera_entity = extracted_camera_entity;
}
nodes_processed_this_frame.insert(main_entity);
extracted_uinodes
.uinodes
.entry(main_entity)
.or_insert_with(|| (extracted_camera_entity, Default::default()))
.1
.insert(
commands.spawn_empty().id(),
ExtractedUiMaterialNode {
stack_index: stack_index.0,
transform: transform.into(),
material: handle.id(),
rect: Rect {
min: Vec2::ZERO,
max: computed_node.size(),
},
border: computed_node.border(),
border_radius: computed_node.border_radius().into(),
clip: clip.cloned(),
},
);
}
for main_entity in removed_computed_node_query
.read()
.chain(removed_computed_stack_index_query.read())
.chain(removed_ui_global_transform_query.read())
.chain(removed_material_node_query.read())
.chain(removed_inherited_visibility_query.read())
.chain(removed_computed_ui_target_camera_query.read())
{
let main_entity = MainEntity::from(main_entity);
if nodes_processed_this_frame.contains(&main_entity) {
continue;
}
let Some((_, mut extracted_nodes)) = extracted_uinodes.uinodes.remove(&main_entity) else {
continue;
};
for (render_entity, _) in extracted_nodes.drain(..) {
commands.entity(render_entity).despawn();
}
}
}
pub fn prepare_uimaterial_nodes<M: UiMaterial>(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
pipeline_cache: Res<PipelineCache>,
mut ui_meta: ResMut<UiMaterialMeta<M>>,
extracted_uinodes: Res<ExtractedUiMaterialNodes<M>>,
view_uniforms: Res<ViewUniforms>,
globals_buffer: Res<GlobalsBuffer>,
ui_material_pipeline: Res<UiMaterialPipeline<M>>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut previous_len: Local<usize>,
) {
if let (Some(view_binding), Some(globals_binding)) = (
view_uniforms.uniforms.binding(),
globals_buffer.buffer.binding(),
) {
let mut batches: Vec<(Entity, UiMaterialBatch<M>)> = Vec::with_capacity(*previous_len);
ui_meta.vertices.clear();
ui_meta.view_bind_group = Some(render_device.create_bind_group(
"ui_material_view_bind_group",
&pipeline_cache.get_bind_group_layout(&ui_material_pipeline.view_layout),
&BindGroupEntries::sequential((view_binding, globals_binding)),
));
let mut index = 0;
for ui_phase in phases.values_mut() {
let mut batch_item_index = 0;
let mut batch_shader_handle = None;
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(extracted_uinode) = extracted_uinodes
.uinodes
.get(&item.main_entity())
.and_then(|(_, subnodes)| subnodes.get(&item.entity()))
{
item.batch_range = (item_index as u32)..(item_index as u32);
let mut existing_batch = batches
.last_mut()
.filter(|_| batch_shader_handle == Some(extracted_uinode.material));
if existing_batch.is_none() {
batch_item_index = item_index;
batch_shader_handle = Some(extracted_uinode.material);
let new_batch = UiMaterialBatch {
range: index..index,
material: extracted_uinode.material,
};
batches.push((item.entity(), new_batch));
existing_batch = batches.last_mut();
}
let uinode_rect = extracted_uinode.rect;
let rect_size = uinode_rect.size();
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| extracted_uinode.transform.transform_point2(pos * rect_size));
let uvs = [
Vec2::new(uinode_rect.min.x, uinode_rect.min.y),
Vec2::new(uinode_rect.max.x, uinode_rect.min.y),
Vec2::new(uinode_rect.max.x, uinode_rect.max.y),
Vec2::new(uinode_rect.min.x, uinode_rect.max.y),
]
.map(|pos| pos / uinode_rect.max);
let polygon = [
(positions[0], uvs[0]),
(positions[1], uvs[1]),
(positions[2], uvs[2]),
(positions[3], uvs[3]),
];
let clipped_polygon =
clip_polygon(extracted_uinode.clip.as_ref(), &polygon, Vec2::lerp);
if clipped_polygon.is_empty() {
continue;
}
for i in 1..clipped_polygon.len() - 1 {
for vertex in [
clipped_polygon[0],
clipped_polygon[i],
clipped_polygon[i + 1],
] {
ui_meta.vertices.push(UiMaterialVertex {
position: vertex.0.extend(1.0).into(),
uv: vertex.1.into(),
size: extracted_uinode.rect.size().into(),
radius: extracted_uinode.border_radius,
border: [
extracted_uinode.border.min_inset.x,
extracted_uinode.border.min_inset.y,
extracted_uinode.border.max_inset.x,
extracted_uinode.border.max_inset.y,
],
});
}
}
index += 3 * (clipped_polygon.len() as u32 - 2);
existing_batch.unwrap().1.range.end = index;
ui_phase.items[batch_item_index].batch_range_mut().end += 1;
} else {
batch_shader_handle = None;
}
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
*previous_len = batches.len();
commands.try_insert_batch(batches);
}
}
pub struct PreparedUiMaterial<T: UiMaterial> {
pub bindings: BindingResources,
pub bind_group: BindGroup,
pub key: T::Data,
}
impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {
type SourceAsset = M;
type Param = (
SRes<RenderDevice>,
SRes<PipelineCache>,
SRes<FallbackBuffer>,
SRes<RenderAssets<GpuShaderBuffer>>,
SRes<UiMaterialPipeline<M>>,
M::Param,
);
fn prepare_asset(
material: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(
render_device,
pipeline_cache,
fallback_buffer,
shader_buffer_assets,
pipeline,
material_param,
): &mut SystemParamItem<Self::Param>,
_: Option<&Self>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let bind_group_data = material.bind_group_data();
match material.as_bind_group(
&pipeline.ui_layout.clone(),
render_device,
pipeline_cache,
fallback_buffer,
shader_buffer_assets,
material_param,
) {
Ok(prepared) => Ok(PreparedUiMaterial {
bindings: prepared.bindings,
bind_group: prepared.bind_group,
key: bind_group_data,
}),
Err(AsBindGroupError::RetryNextUpdate) => {
Err(PrepareAssetError::RetryNextUpdate(material))
}
Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),
}
}
}
pub fn queue_ui_material_nodes<M: UiMaterial>(
extracted_uinodes: Res<ExtractedUiMaterialNodes<M>>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
ui_material_pipeline: Res<UiMaterialPipeline<M>>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiMaterialPipeline<M>>>,
pipeline_cache: Res<PipelineCache>,
render_materials: Res<RenderAssets<PreparedUiMaterial<M>>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
render_views: Query<&UiCameraView, With<ExtractedView>>,
camera_views: Query<&ExtractedView>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
{
let draw_function = draw_functions.read().id::<DrawUiMaterial<M>>();
let mut current_camera_entity = Entity::PLACEHOLDER;
let mut current_phase = None;
for (main_entity, (extracted_camera_entity, extracted_sub_uinodes)) in
extracted_uinodes.uinodes.iter()
{
if current_camera_entity != *extracted_camera_entity {
current_phase =
render_views
.get(*extracted_camera_entity)
.ok()
.and_then(|default_camera_view| {
camera_views
.get(default_camera_view.0)
.ok()
.and_then(|view| {
transparent_render_phases
.get_mut(&view.retained_view_entity)
.map(|transparent_phase| {
(view.target_format, transparent_phase)
})
})
});
current_camera_entity = *extracted_camera_entity;
}
let Some((target_format, transparent_phase)) = current_phase.as_mut() else {
continue;
};
for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() {
let Some(material) = render_materials.get(extracted_uinode.material) else {
continue;
};
let pipeline = pipelines.specialize(
&pipeline_cache,
&ui_material_pipeline,
UiMaterialKey {
target_format: *target_format,
bind_group_data: material.key.clone(),
},
);
if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() {
transparent_phase.items.reserve_exact(
extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(),
);
}
transparent_phase.add_transient(TransparentUi {
draw_function,
pipeline,
entity: (*render_entity, *main_entity),
sort_key: FloatOrd(extracted_uinode.stack_index as f32 + M::stack_z_offset()),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
indexed: false,
});
}
}
}