use crate::ecs::text::components::{HudAnchor, TextAlignment, TextVertex};
use crate::ecs::ui::types::Rect;
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec2, Vec3, Vec4};
pub struct HudPass {
pub render_pipeline: wgpu::RenderPipeline,
pub texture_bind_group_layout: wgpu::BindGroupLayout,
pub uniform_bind_group_layout: wgpu::BindGroupLayout,
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub uniform_buffer: wgpu::Buffer,
pub character_color_buffer: wgpu::Buffer,
pub uniform_bind_group: wgpu::BindGroup,
pub instance_uniform_bind_groups: Vec<wgpu::BindGroup>,
pub texture_bind_groups: Vec<wgpu::BindGroup>,
pub hud_instances: Vec<HudInstance>,
vertex_buffer_capacity: usize,
index_buffer_capacity: usize,
character_color_buffer_capacity: usize,
screen_width: f32,
screen_height: f32,
cached_font_texture_views: Vec<wgpu::TextureView>,
background_pipeline: wgpu::RenderPipeline,
background_vertex_buffer: wgpu::Buffer,
background_index_buffer: wgpu::Buffer,
background_uniform_buffer: wgpu::Buffer,
background_uniform_bind_group: wgpu::BindGroup,
background_vertex_count: u32,
background_index_count: u32,
background_vertex_buffer_capacity: usize,
background_index_buffer_capacity: usize,
}
use crate::render::wgpu::passes::geometry::UiLayer;
#[derive(Clone)]
pub struct HudInstance {
pub vertex_offset: u32,
pub index_offset: u32,
pub index_count: u32,
pub color: Vec4,
pub outline_color: Vec4,
pub outline_width: f32,
pub font_index: usize,
pub clip_rect: Option<Rect>,
pub layer: UiLayer,
pub z_index: i32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HudUniforms {
pub projection: Mat4,
pub color: Vec4,
pub outline_color: Vec4,
pub outline_width: f32,
pub depth: f32,
pub _padding: Vec2,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BackgroundVertex {
pub position: Vec3,
pub _pad: f32,
pub color: Vec4,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HudBackgroundUniforms {
pub projection: Mat4,
pub depth: f32,
pub _pad0: f32,
pub _pad1: f32,
pub _pad2: f32,
}
const INITIAL_CHARACTER_COLOR_CAPACITY: usize = 10000;
const INITIAL_BACKGROUND_VERTEX_CAPACITY: usize = 20000;
const INITIAL_BACKGROUND_INDEX_CAPACITY: usize = 30000;
fn calculate_screen_position(
position: Vec2,
bounds: Vec2,
anchor: HudAnchor,
text_alignment: TextAlignment,
has_anchor_character: bool,
screen_width: f32,
screen_height: f32,
) -> Vec2 {
const SCALE: f32 = 100.0;
let text_width = bounds.x * SCALE;
let text_height = bounds.y * SCALE;
let base_position = if has_anchor_character {
match anchor {
HudAnchor::TopLeft => nalgebra_glm::vec2(0.0, 0.0),
HudAnchor::TopCenter => nalgebra_glm::vec2(screen_width * 0.5, 0.0),
HudAnchor::TopRight => nalgebra_glm::vec2(screen_width, 0.0),
HudAnchor::CenterLeft => {
nalgebra_glm::vec2(0.0, screen_height * 0.5 - text_height * 0.5)
}
HudAnchor::Center => {
nalgebra_glm::vec2(screen_width * 0.5, screen_height * 0.5 - text_height * 0.5)
}
HudAnchor::CenterRight => {
nalgebra_glm::vec2(screen_width, screen_height * 0.5 - text_height * 0.5)
}
HudAnchor::BottomLeft => nalgebra_glm::vec2(0.0, screen_height - text_height),
HudAnchor::BottomCenter => {
nalgebra_glm::vec2(screen_width * 0.5, screen_height - text_height)
}
HudAnchor::BottomRight => nalgebra_glm::vec2(screen_width, screen_height - text_height),
}
} else {
match (anchor, text_alignment) {
(HudAnchor::TopLeft, _) => nalgebra_glm::vec2(0.0, 0.0),
(HudAnchor::TopCenter, TextAlignment::Center) => {
nalgebra_glm::vec2(screen_width * 0.5, 0.0)
}
(HudAnchor::TopCenter, TextAlignment::Left) => {
nalgebra_glm::vec2(screen_width * 0.5 - text_width * 0.5, 0.0)
}
(HudAnchor::TopCenter, TextAlignment::Right) => {
nalgebra_glm::vec2(screen_width * 0.5 + text_width * 0.5, 0.0)
}
(HudAnchor::TopRight, _) => nalgebra_glm::vec2(screen_width - text_width, 0.0),
(HudAnchor::CenterLeft, _) => {
nalgebra_glm::vec2(0.0, screen_height * 0.5 - text_height * 0.5)
}
(HudAnchor::Center, TextAlignment::Center) => {
nalgebra_glm::vec2(screen_width * 0.5, screen_height * 0.5 - text_height * 0.5)
}
(HudAnchor::Center, TextAlignment::Left) => nalgebra_glm::vec2(
screen_width * 0.5 - text_width * 0.5,
screen_height * 0.5 - text_height * 0.5,
),
(HudAnchor::Center, TextAlignment::Right) => nalgebra_glm::vec2(
screen_width * 0.5 + text_width * 0.5,
screen_height * 0.5 - text_height * 0.5,
),
(HudAnchor::CenterRight, _) => nalgebra_glm::vec2(
screen_width - text_width,
screen_height * 0.5 - text_height * 0.5,
),
(HudAnchor::BottomLeft, _) => nalgebra_glm::vec2(0.0, screen_height - text_height),
(HudAnchor::BottomCenter, TextAlignment::Center) => {
nalgebra_glm::vec2(screen_width * 0.5, screen_height - text_height)
}
(HudAnchor::BottomCenter, TextAlignment::Left) => nalgebra_glm::vec2(
screen_width * 0.5 - text_width * 0.5,
screen_height - text_height,
),
(HudAnchor::BottomCenter, TextAlignment::Right) => nalgebra_glm::vec2(
screen_width * 0.5 + text_width * 0.5,
screen_height - text_height,
),
(HudAnchor::BottomRight, _) => {
nalgebra_glm::vec2(screen_width - text_width, screen_height - text_height)
}
}
};
base_position + position
}
impl HudPass {
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HUD Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HUD Texture Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HUD Pipeline Layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("HUD Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/hud.wgsl").into()),
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HUD Render Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<TextVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32,
},
],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::GreaterEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Vertex Buffer"),
size: (std::mem::size_of::<TextVertex>() * 10000) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Index Buffer"),
size: (std::mem::size_of::<u32>() * 30000) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Uniform Buffer"),
size: std::mem::size_of::<HudUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let character_color_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Character Color Buffer"),
size: (std::mem::size_of::<[f32; 4]>() * INITIAL_CHARACTER_COLOR_CAPACITY) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HUD Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let background_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("HUD Background Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/hud_background.wgsl").into(),
),
});
let background_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("HUD Background Pipeline Layout"),
bind_group_layouts: &[&uniform_bind_group_layout],
push_constant_ranges: &[],
});
let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("HUD Background Render Pipeline"),
layout: Some(&background_pipeline_layout),
vertex: wgpu::VertexState {
module: &background_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<BackgroundVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::GreaterEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &background_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
let background_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Background Vertex Buffer"),
size: (std::mem::size_of::<BackgroundVertex>() * INITIAL_BACKGROUND_VERTEX_CAPACITY)
as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let background_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Background Index Buffer"),
size: (std::mem::size_of::<u32>() * INITIAL_BACKGROUND_INDEX_CAPACITY) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let background_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Background Uniform Buffer"),
size: std::mem::size_of::<HudBackgroundUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let background_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HUD Background Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: background_uniform_buffer.as_entire_binding(),
}],
});
Self {
render_pipeline,
texture_bind_group_layout,
uniform_bind_group_layout,
vertex_buffer,
index_buffer,
uniform_buffer,
character_color_buffer,
uniform_bind_group,
instance_uniform_bind_groups: Vec::new(),
texture_bind_groups: Vec::new(),
hud_instances: Vec::new(),
vertex_buffer_capacity: 10000,
index_buffer_capacity: 30000,
character_color_buffer_capacity: INITIAL_CHARACTER_COLOR_CAPACITY,
screen_width: 800.0,
screen_height: 600.0,
cached_font_texture_views: Vec::new(),
background_pipeline,
background_vertex_buffer,
background_index_buffer,
background_uniform_buffer,
background_uniform_bind_group,
background_vertex_count: 0,
background_index_count: 0,
background_vertex_buffer_capacity: INITIAL_BACKGROUND_VERTEX_CAPACITY,
background_index_buffer_capacity: INITIAL_BACKGROUND_INDEX_CAPACITY,
}
}
fn ensure_vertex_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.vertex_buffer_capacity {
let new_capacity = (required * 2).max(1000);
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Vertex Buffer (Resized)"),
size: (std::mem::size_of::<TextVertex>() * new_capacity) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.vertex_buffer_capacity = new_capacity;
}
}
fn ensure_index_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.index_buffer_capacity {
let new_capacity = (required * 2).max(3000);
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Index Buffer (Resized)"),
size: (std::mem::size_of::<u32>() * new_capacity) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.index_buffer_capacity = new_capacity;
}
}
fn ensure_background_vertex_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.background_vertex_buffer_capacity {
let new_capacity = (required * 2).max(1000);
self.background_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Background Vertex Buffer (Resized)"),
size: (std::mem::size_of::<BackgroundVertex>() * new_capacity) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.background_vertex_buffer_capacity = new_capacity;
}
}
fn ensure_background_index_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.background_index_buffer_capacity {
let new_capacity = (required * 2).max(3000);
self.background_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Background Index Buffer (Resized)"),
size: (std::mem::size_of::<u32>() * new_capacity) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.background_index_buffer_capacity = new_capacity;
}
}
fn ensure_character_color_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.character_color_buffer_capacity {
let new_capacity = (required * 2).max(1000);
self.character_color_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Character Color Buffer (Resized)"),
size: (std::mem::size_of::<[f32; 4]>() * new_capacity) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.character_color_buffer_capacity = new_capacity;
self.rebuild_texture_bind_groups(device);
}
}
pub fn update_texture_bind_groups(
&mut self,
device: &wgpu::Device,
font_textures: &[(wgpu::Texture, wgpu::TextureView)],
) {
self.cached_font_texture_views.clear();
for (_texture, texture_view) in font_textures {
self.cached_font_texture_views.push(texture_view.clone());
}
self.rebuild_texture_bind_groups(device);
}
fn rebuild_texture_bind_groups(&mut self, device: &wgpu::Device) {
self.texture_bind_groups.clear();
for texture_view in &self.cached_font_texture_views {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HUD Texture Bind Group"),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.character_color_buffer.as_entire_binding(),
},
],
});
self.texture_bind_groups.push(bind_group);
}
}
}
impl PassNode<World> for HudPass {
fn name(&self) -> &'static str {
"hud_pass"
}
fn reads(&self) -> Vec<&str> {
vec!["depth"]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color"]
}
fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
self.hud_instances.clear();
self.instance_uniform_bind_groups.clear();
let mut all_vertices = Vec::new();
let mut all_indices = Vec::new();
let mut all_character_colors: Vec<[f32; 4]> = Vec::new();
let mut character_color_offset = 0u32;
if let Some((width, height)) = world.resources.window.cached_viewport_size {
self.screen_width = width as f32;
self.screen_height = height as f32;
}
let text_entities = world.query_entities(crate::ecs::world::HUD_TEXT);
for entity in text_entities {
let hud_text = world.get_hud_text(entity);
let character_colors = world.get_text_character_colors(entity);
if let Some(hud_text) = hud_text
&& let Some(mesh) = &hud_text.cached_mesh
{
const SCALE: f32 = 100.0;
let screen_position = calculate_screen_position(
hud_text.position,
mesh.bounds,
hud_text.anchor,
hud_text.properties.alignment,
hud_text.properties.anchor_character.is_some(),
self.screen_width,
self.screen_height,
);
let vertex_offset = all_vertices.len() as u32;
let index_offset = all_indices.len() as u32;
let text_content = world
.resources
.text_cache
.get_text(hud_text.text_index)
.map(|s| s.to_string())
.unwrap_or_default();
let char_count = text_content.chars().filter(|c| *c != '\n').count();
for vertex in &mesh.vertices {
all_vertices.push(TextVertex {
position: nalgebra_glm::vec3(
vertex.position.x * SCALE + screen_position.x,
-vertex.position.y * SCALE + screen_position.y,
0.0,
),
tex_coords: vertex.tex_coords,
character_index: vertex.character_index + character_color_offset,
_padding: 0,
});
}
for character_index in 0..char_count {
let color = if let Some(colors) = character_colors {
colors
.colors
.get(character_index)
.and_then(|c| *c)
.map(|c| [c.x, c.y, c.z, c.w])
.unwrap_or([0.0, 0.0, 0.0, 0.0])
} else {
[0.0, 0.0, 0.0, 0.0]
};
all_character_colors.push(color);
}
character_color_offset += char_count as u32;
for &index in &mesh.indices {
all_indices.push(index + vertex_offset);
}
self.hud_instances.push(HudInstance {
vertex_offset,
index_offset,
index_count: mesh.indices.len() as u32,
color: hud_text.properties.color,
outline_color: hud_text.properties.outline_color,
outline_width: hud_text.properties.outline_width,
font_index: hud_text.font_index,
clip_rect: None,
layer: UiLayer::Background,
z_index: 0,
});
}
}
let mut sorted_text: Vec<_> = world
.resources
.retained_ui
.frame_text_meshes
.iter()
.collect();
sorted_text.sort_by(|a, b| match a.layer.cmp(&b.layer) {
std::cmp::Ordering::Equal => a.z_index.cmp(&b.z_index),
other => other,
});
for text_instance in sorted_text {
const SCALE: f32 = 100.0;
let vertex_offset = all_vertices.len() as u32;
let index_offset = all_indices.len() as u32;
let char_count = text_instance.mesh.vertices.len() / 4;
for vertex in &text_instance.mesh.vertices {
all_vertices.push(TextVertex {
position: nalgebra_glm::vec3(
vertex.position.x * SCALE + text_instance.position.x,
-vertex.position.y * SCALE + text_instance.position.y,
0.0,
),
tex_coords: vertex.tex_coords,
character_index: vertex.character_index + character_color_offset,
_padding: 0,
});
}
for _ in 0..char_count {
all_character_colors.push([0.0, 0.0, 0.0, 0.0]);
}
character_color_offset += char_count as u32;
for &index in &text_instance.mesh.indices {
all_indices.push(index + vertex_offset);
}
self.hud_instances.push(HudInstance {
vertex_offset,
index_offset,
index_count: text_instance.mesh.indices.len() as u32,
color: text_instance.color,
outline_color: text_instance.outline_color,
outline_width: text_instance.outline_width,
font_index: text_instance.font_index,
clip_rect: text_instance.clip_rect,
layer: text_instance.layer,
z_index: text_instance.z_index,
});
}
let mut bg_vertices: Vec<BackgroundVertex> = Vec::new();
let mut bg_indices: Vec<u32> = Vec::new();
let bg_text_entities = world.query_entities(crate::ecs::world::HUD_TEXT);
for entity in bg_text_entities {
let bg_colors = world.get_text_character_background_colors(entity);
let hud_text = world.get_hud_text(entity);
if let Some(bg_colors) = bg_colors
&& let Some(hud_text) = hud_text
&& let Some(monospace_width) = hud_text.properties.monospace_width
&& let Some(mesh) = &hud_text.cached_mesh
{
let screen_position = calculate_screen_position(
hud_text.position,
mesh.bounds,
hud_text.anchor,
hud_text.properties.alignment,
hud_text.properties.anchor_character.is_some(),
self.screen_width,
self.screen_height,
);
let cell_width = monospace_width;
let cell_height = hud_text.properties.font_size * hud_text.properties.line_height;
let text_content = world
.resources
.text_cache
.get_text(hud_text.text_index)
.map(|s| s.to_string())
.unwrap_or_default();
let mut char_index = 0usize;
let mut line_y = screen_position.y;
for line in text_content.split('\n') {
let mut char_x = screen_position.x;
for _character in line.chars() {
if let Some(Some(bg_color)) = bg_colors.colors.get(char_index)
&& bg_color.w > 0.0
{
let vertex_base = bg_vertices.len() as u32;
let left = char_x;
let right = char_x + cell_width;
let top = line_y;
let bottom = line_y + cell_height;
let color = *bg_color;
bg_vertices.push(BackgroundVertex {
position: nalgebra_glm::vec3(left, top, 0.0),
_pad: 0.0,
color,
});
bg_vertices.push(BackgroundVertex {
position: nalgebra_glm::vec3(right, top, 0.0),
_pad: 0.0,
color,
});
bg_vertices.push(BackgroundVertex {
position: nalgebra_glm::vec3(right, bottom, 0.0),
_pad: 0.0,
color,
});
bg_vertices.push(BackgroundVertex {
position: nalgebra_glm::vec3(left, bottom, 0.0),
_pad: 0.0,
color,
});
bg_indices.push(vertex_base);
bg_indices.push(vertex_base + 1);
bg_indices.push(vertex_base + 2);
bg_indices.push(vertex_base);
bg_indices.push(vertex_base + 2);
bg_indices.push(vertex_base + 3);
}
char_x += cell_width;
char_index += 1;
}
line_y += cell_height;
}
}
}
self.background_vertex_count = bg_vertices.len() as u32;
self.background_index_count = bg_indices.len() as u32;
if !bg_vertices.is_empty() {
self.ensure_background_vertex_buffer_capacity(device, bg_vertices.len());
queue.write_buffer(
&self.background_vertex_buffer,
0,
bytemuck::cast_slice(&bg_vertices),
);
self.ensure_background_index_buffer_capacity(device, bg_indices.len());
queue.write_buffer(
&self.background_index_buffer,
0,
bytemuck::cast_slice(&bg_indices),
);
let projection =
nalgebra_glm::ortho(0.0, self.screen_width, self.screen_height, 0.0, -1.0, 1.0);
let bg_uniforms = HudBackgroundUniforms {
projection,
depth: 0.05,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
};
queue.write_buffer(
&self.background_uniform_buffer,
0,
bytemuck::cast_slice(&[bg_uniforms]),
);
}
if all_character_colors.is_empty() {
all_character_colors.push([0.0, 0.0, 0.0, 0.0]);
}
self.ensure_character_color_buffer_capacity(device, all_character_colors.len());
queue.write_buffer(
&self.character_color_buffer,
0,
bytemuck::cast_slice(&all_character_colors),
);
if !all_vertices.is_empty() {
self.ensure_vertex_buffer_capacity(device, all_vertices.len());
queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&all_vertices));
}
if !all_indices.is_empty() {
self.ensure_index_buffer_capacity(device, all_indices.len());
queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&all_indices));
}
let projection =
nalgebra_glm::ortho(0.0, self.screen_width, self.screen_height, 0.0, -1.0, 1.0);
for instance in &self.hud_instances {
let combined_z = (instance.layer as i32) * 100000 + instance.z_index;
let depth = 0.1 + (combined_z as f32 / 10000000.0) * 0.8;
let uniforms = HudUniforms {
projection,
color: instance.color,
outline_color: instance.outline_color,
outline_width: instance.outline_width,
depth,
_padding: Vec2::new(0.0, 0.0),
};
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("HUD Instance Uniform Buffer"),
size: std::mem::size_of::<HudUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&instance_buffer, 0, bytemuck::cast_slice(&[uniforms]));
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("HUD Instance Uniform Bind Group"),
layout: &self.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: instance_buffer.as_entire_binding(),
}],
});
self.instance_uniform_bind_groups.push(bind_group);
}
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
crate::render::wgpu::rendergraph::RenderGraphError,
> {
if !context.is_pass_enabled() {
return Ok(context.into_sub_graph_commands());
}
let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
let (depth_view, depth_load, depth_store) = context.get_depth_attachment("depth")?;
let (render_target_width, render_target_height) = context.get_texture_size("color")?;
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HUD Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: color_load,
store: color_store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: depth_store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if self.background_index_count > 0 {
render_pass.set_pipeline(&self.background_pipeline);
render_pass.set_vertex_buffer(0, self.background_vertex_buffer.slice(..));
render_pass.set_index_buffer(
self.background_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.set_scissor_rect(0, 0, render_target_width, render_target_height);
render_pass.set_bind_group(0, &self.background_uniform_bind_group, &[]);
render_pass.draw_indexed(0..self.background_index_count, 0, 0..1);
}
if !self.hud_instances.is_empty() {
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
for (instance_index, instance) in self.hud_instances.iter().enumerate() {
if instance.font_index >= self.texture_bind_groups.len() {
tracing::warn!(
"Skipping HUD text instance with invalid font index: {}",
instance.font_index
);
continue;
}
if instance_index >= self.instance_uniform_bind_groups.len() {
tracing::warn!(
"Skipping HUD instance with missing uniform bind group: {}",
instance_index
);
continue;
}
let index_end = instance.index_offset.saturating_add(instance.index_count);
if instance.vertex_offset >= self.vertex_buffer_capacity as u32
|| index_end > self.index_buffer_capacity as u32
{
tracing::warn!("Skipping HUD instance with out-of-bounds indices");
continue;
}
let screen_w = render_target_width;
let screen_h = render_target_height;
if let Some(clip_rect) = instance.clip_rect {
let x = (clip_rect.min.x.max(0.0) as u32).min(screen_w);
let y = (clip_rect.min.y.max(0.0) as u32).min(screen_h);
let max_width = screen_w.saturating_sub(x);
let max_height = screen_h.saturating_sub(y);
let width = (clip_rect.width().max(0.0) as u32).min(max_width);
let height = (clip_rect.height().max(0.0) as u32).min(max_height);
if width > 0 && height > 0 {
render_pass.set_scissor_rect(x, y, width, height);
} else {
continue;
}
} else {
render_pass.set_scissor_rect(0, 0, screen_w, screen_h);
}
render_pass.set_bind_group(
0,
&self.instance_uniform_bind_groups[instance_index],
&[],
);
render_pass.set_bind_group(1, &self.texture_bind_groups[instance.font_index], &[]);
render_pass.draw_indexed(instance.index_offset..index_end, 0, 0..1);
}
}
drop(render_pass);
Ok(context.into_sub_graph_commands())
}
}