use crate::ecs::camera::queries::query_active_camera_matrices;
use crate::ecs::sprite::components::{SpriteBlendMode, SpriteGradient, SpriteStencilMode};
use crate::ecs::world::World;
use nalgebra_glm::{Vec2, Vec4};
use super::{GlobalUniforms, SpriteDrawBatch, SpriteInstance, SpritePass};
impl SpritePass {
pub(super) fn update_texture_bind_group(&mut self, device: &wgpu::Device) {
self.texture_bind_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.atlas.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas.sampler),
},
],
label: Some("Sprite Texture Atlas Bind Group"),
}));
}
pub(super) fn update_background_bind_group(&mut self, device: &wgpu::Device) {
self.background_bind_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.background_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.background_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.background_sampler),
},
],
label: Some("Sprite Background Bind Group"),
}));
}
pub(super) fn ensure_stencil_texture(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
) {
if self.stencil_texture_size.0 == width && self.stencil_texture_size.1 == height {
return;
}
self.stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Sprite Stencil Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
self.stencil_view = self
.stencil_texture
.create_view(&wgpu::TextureViewDescriptor::default());
self.stencil_texture_size = (width, height);
}
pub(super) fn ensure_background_texture(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
) {
if self.background_texture_size.0 == width && self.background_texture_size.1 == height {
return;
}
self.background_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Sprite Background Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
self.background_view = self
.background_texture
.create_view(&wgpu::TextureViewDescriptor::default());
self.background_texture_size = (width, height);
self.background_bind_group = None;
}
fn gradient_instance_fields(gradient: &SpriteGradient) -> (u32, f32, f32) {
match gradient {
SpriteGradient::None => (0, 0.0, 0.0),
SpriteGradient::Linear { angle } => (1, angle.sin(), angle.cos()),
SpriteGradient::Radial { center } => (2, center.x, center.y),
}
}
fn emit_sprite_instance(&mut self, sprite: &crate::ecs::sprite::components::Sprite) {
let (sin_r, cos_r) = if sprite.rotation != 0.0 {
(sprite.rotation.sin(), sprite.rotation.cos())
} else {
(0.0, 1.0)
};
let scale_x = sprite.scale.x * if sprite.flip_x { -1.0 } else { 1.0 };
let scale_y = sprite.scale.y * if sprite.flip_y { -1.0 } else { 1.0 };
let (gradient_type, gradient_param_a, gradient_param_b) =
Self::gradient_instance_fields(&sprite.gradient);
let advanced_blend_mode = sprite.blend_mode.advanced_blend_index();
if let Some(ref nine_slice) = sprite.nine_slice {
self.emit_nine_slice_instances(
sprite,
nine_slice,
cos_r,
sin_r,
scale_x,
scale_y,
gradient_type,
gradient_param_a,
gradient_param_b,
advanced_blend_mode,
);
} else {
self.instance_data.push(SpriteInstance {
position: sprite.position,
size: sprite.size,
uv_min: sprite.uv_min,
uv_max: sprite.uv_max,
color: Vec4::from_column_slice(&sprite.color),
rotation_scale: Vec4::new(cos_r, sin_r, scale_x, scale_y),
anchor: sprite.anchor,
depth: sprite.depth,
texture_slot: sprite.texture_index,
texture_slot2: sprite.texture_index2,
blend_factor: sprite.blend_factor,
gradient_type,
gradient_param_a,
gradient_param_b,
advanced_blend_mode,
_padding: [0.0; 2],
});
self.blend_modes.push(sprite.blend_mode);
self.stencil_modes.push(sprite.stencil_mode);
self.stencil_references.push(sprite.stencil_reference);
self.clip_rects.push(sprite.clip_rect);
}
}
#[allow(clippy::too_many_arguments)]
fn emit_nine_slice_instances(
&mut self,
sprite: &crate::ecs::sprite::components::Sprite,
nine_slice: &crate::ecs::sprite::components::NineSlice,
cos_r: f32,
sin_r: f32,
scale_x: f32,
scale_y: f32,
gradient_type: u32,
gradient_param_a: f32,
gradient_param_b: f32,
advanced_blend_mode: u32,
) {
let total_width = sprite.size.x;
let total_height = sprite.size.y;
let center_width = (total_width - nine_slice.left - nine_slice.right).max(0.0);
let center_height = (total_height - nine_slice.top - nine_slice.bottom).max(0.0);
let uv_range = sprite.uv_max - sprite.uv_min;
let left_uv_frac = if total_width > 0.0 {
nine_slice.left / total_width
} else {
0.0
};
let right_uv_frac = if total_width > 0.0 {
nine_slice.right / total_width
} else {
0.0
};
let bottom_uv_frac = if total_height > 0.0 {
nine_slice.bottom / total_height
} else {
0.0
};
let top_uv_frac = if total_height > 0.0 {
nine_slice.top / total_height
} else {
0.0
};
let uv_cols = [
sprite.uv_min.x,
sprite.uv_min.x + uv_range.x * left_uv_frac,
sprite.uv_max.x - uv_range.x * right_uv_frac,
sprite.uv_max.x,
];
let uv_rows = [
sprite.uv_min.y,
sprite.uv_min.y + uv_range.y * bottom_uv_frac,
sprite.uv_max.y - uv_range.y * top_uv_frac,
sprite.uv_max.y,
];
let widths = [nine_slice.left, center_width, nine_slice.right];
let heights = [nine_slice.bottom, center_height, nine_slice.top];
let anchor_offset_x = sprite.anchor.x * total_width * scale_x;
let anchor_offset_y = sprite.anchor.y * total_height * scale_y;
let base_x = -total_width * 0.5;
let base_y = -total_height * 0.5;
let color = Vec4::from_column_slice(&sprite.color);
let mut accumulated_y = 0.0f32;
for row in 0..3 {
let piece_height = heights[row];
if piece_height <= 0.0 {
continue;
}
let mut accumulated_x = 0.0f32;
for col in 0..3 {
let piece_width = widths[col];
if piece_width <= 0.0 {
continue;
}
let local_center_x = base_x + accumulated_x + piece_width * 0.5;
let local_center_y = base_y + accumulated_y + piece_height * 0.5;
let scaled_cx = local_center_x * scale_x - anchor_offset_x;
let scaled_cy = local_center_y * scale_y - anchor_offset_y;
let rotated_x = scaled_cx * cos_r - scaled_cy * sin_r;
let rotated_y = scaled_cx * sin_r + scaled_cy * cos_r;
let position =
Vec2::new(sprite.position.x + rotated_x, sprite.position.y + rotated_y);
let size = Vec2::new(piece_width, piece_height);
let piece_uv_min = Vec2::new(uv_cols[col], uv_rows[row]);
let piece_uv_max = Vec2::new(uv_cols[col + 1], uv_rows[row + 1]);
self.instance_data.push(SpriteInstance {
position,
size,
uv_min: piece_uv_min,
uv_max: piece_uv_max,
color,
rotation_scale: Vec4::new(cos_r, sin_r, scale_x, scale_y),
anchor: Vec2::new(0.0, 0.0),
depth: sprite.depth,
texture_slot: sprite.texture_index,
texture_slot2: sprite.texture_index2,
blend_factor: sprite.blend_factor,
gradient_type,
gradient_param_a,
gradient_param_b,
advanced_blend_mode,
_padding: [0.0; 2],
});
self.blend_modes.push(sprite.blend_mode);
self.stencil_modes.push(sprite.stencil_mode);
self.stencil_references.push(sprite.stencil_reference);
self.clip_rects.push(sprite.clip_rect);
accumulated_x += piece_width;
}
accumulated_y += piece_height;
}
}
pub(super) fn prepare_sprites(
&mut self,
world: &World,
surface_width: u32,
surface_height: u32,
) {
self.instance_data.clear();
self.blend_modes.clear();
self.stencil_modes.clear();
self.stencil_references.clear();
self.clip_rects.clear();
self.draw_batches.clear();
self.cached_view_projection = None;
self.cached_surface_width = surface_width;
self.cached_surface_height = surface_height;
let width = surface_width as f32;
let height = surface_height as f32;
let camera_matrices = query_active_camera_matrices(world);
let view_projection = if let Some(ref matrices) = camera_matrices {
let vp = matrices.projection * matrices.view;
self.cached_view_projection = Some(vp);
vp
} else {
let vp = nalgebra_glm::ortho_rh_zo(0.0, width, 0.0, height, -1.0, 1.0);
self.cached_view_projection = Some(vp);
vp
};
let cull_bounds = camera_matrices.as_ref().map(|matrices| {
let camera_pos = matrices.camera_position;
let half_extent = world
.resources
.active_camera
.and_then(|entity| world.core.get_camera(entity))
.map(|camera| match &camera.projection {
crate::ecs::camera::components::Projection::Orthographic(ortho) => {
Vec2::new(ortho.x_mag, ortho.y_mag)
}
crate::ecs::camera::components::Projection::Perspective(persp) => {
let distance = camera_pos.z.abs();
let half_height = distance * (persp.y_fov_rad / 2.0).tan();
let aspect = persp.aspect_ratio.unwrap_or(width / height);
let half_width = half_height * aspect;
Vec2::new(half_width, half_height)
}
})
.unwrap_or_else(|| Vec2::new(width * 0.5, height * 0.5));
let min_x = camera_pos.x - half_extent.x;
let max_x = camera_pos.x + half_extent.x;
let min_y = camera_pos.y - half_extent.y;
let max_y = camera_pos.y + half_extent.y;
(min_x, max_x, min_y, max_y)
});
let sprite_entities: Vec<_> = world.sprite2d.query_entities(crate::ecs::SPRITE).collect();
for entity in sprite_entities {
let Some(sprite) = world.sprite2d.get_sprite(entity) else {
continue;
};
let Some(visibility) = world.core.get_visibility(entity) else {
continue;
};
if !visibility.visible {
continue;
}
let render_layer = world
.core
.get_render_layer(entity)
.map(|layer| layer.0)
.unwrap_or(crate::ecs::render_layer::components::RenderLayer::WORLD);
let should_render = match render_layer {
crate::ecs::render_layer::components::RenderLayer::WORLD => {
world.resources.graphics.render_layer_world_enabled
}
crate::ecs::render_layer::components::RenderLayer::OVERLAY => {
world.resources.graphics.render_layer_overlay_enabled
}
_ => true,
};
if !should_render {
continue;
}
if let Some((min_x, max_x, min_y, max_y)) = cull_bounds {
let scaled_size = sprite.size.component_mul(&sprite.scale);
let half_size = scaled_size * 0.5;
if sprite.position.x + half_size.x < min_x
|| sprite.position.x - half_size.x > max_x
|| sprite.position.y + half_size.y < min_y
|| sprite.position.y - half_size.y > max_y
{
continue;
}
}
if self.instance_data.len() >= self.max_instances {
break;
}
self.emit_sprite_instance(sprite);
}
self.prepare_tilemaps(world, cull_bounds);
self.sort_and_build_batches();
let _ = view_projection;
}
fn prepare_tilemaps(&mut self, world: &World, cull_bounds: Option<(f32, f32, f32, f32)>) {
let tilemap_entities: Vec<_> = world.sprite2d.query_entities(crate::ecs::TILEMAP).collect();
for entity in tilemap_entities {
let Some(tilemap) = world.sprite2d.get_tilemap(entity) else {
continue;
};
let Some(visibility) = world.core.get_visibility(entity) else {
continue;
};
if !visibility.visible {
continue;
}
let render_layer = world
.core
.get_render_layer(entity)
.map(|layer| layer.0)
.unwrap_or(crate::ecs::render_layer::components::RenderLayer::WORLD);
let should_render = match render_layer {
crate::ecs::render_layer::components::RenderLayer::WORLD => {
world.resources.graphics.render_layer_world_enabled
}
crate::ecs::render_layer::components::RenderLayer::OVERLAY => {
world.resources.graphics.render_layer_overlay_enabled
}
_ => true,
};
if !should_render {
continue;
}
let (col_start, col_end, row_start, row_end) =
if let Some((min_x, max_x, min_y, max_y)) = cull_bounds {
let cs = ((min_x - tilemap.position.x) / tilemap.tile_size.x)
.floor()
.max(0.0) as u32;
let ce = ((max_x - tilemap.position.x) / tilemap.tile_size.x)
.ceil()
.min(tilemap.grid_width as f32) as u32;
let rs = ((min_y - tilemap.position.y) / tilemap.tile_size.y)
.floor()
.max(0.0) as u32;
let re = ((max_y - tilemap.position.y) / tilemap.tile_size.y)
.ceil()
.min(tilemap.grid_height as f32) as u32;
(cs, ce, rs, re)
} else {
(0, tilemap.grid_width, 0, tilemap.grid_height)
};
let sheet_tile_uv_width = if tilemap.sheet_columns > 0 {
tilemap.uv_max.x / tilemap.sheet_columns as f32
} else {
tilemap.uv_max.x
};
let sheet_tile_uv_height = if tilemap.sheet_rows > 0 {
tilemap.uv_max.y / tilemap.sheet_rows as f32
} else {
tilemap.uv_max.y
};
let color = Vec4::from_column_slice(&tilemap.color);
for row in row_start..row_end {
for col in col_start..col_end {
let tile_index = (row * tilemap.grid_width + col) as usize;
if tile_index >= tilemap.tiles.len() {
continue;
}
let Some(ref tile_data) = tilemap.tiles[tile_index] else {
continue;
};
if self.instance_data.len() >= self.max_instances {
return;
}
let sheet_col = tile_data.tile_id % tilemap.sheet_columns;
let sheet_row = tile_data.tile_id / tilemap.sheet_columns;
let half_texel_x = sheet_tile_uv_width * 0.001;
let half_texel_y = sheet_tile_uv_height * 0.001;
let uv_min = Vec2::new(
sheet_col as f32 * sheet_tile_uv_width + half_texel_x,
sheet_row as f32 * sheet_tile_uv_height + half_texel_y,
);
let uv_max = Vec2::new(
(sheet_col + 1) as f32 * sheet_tile_uv_width - half_texel_x,
(sheet_row + 1) as f32 * sheet_tile_uv_height - half_texel_y,
);
let position = Vec2::new(
tilemap.position.x
+ col as f32 * tilemap.tile_size.x
+ tilemap.tile_size.x * 0.5,
tilemap.position.y
+ row as f32 * tilemap.tile_size.y
+ tilemap.tile_size.y * 0.5,
);
let flip_scale_x = if tile_data.flip_x { -1.0 } else { 1.0 };
let flip_scale_y = if tile_data.flip_y { -1.0 } else { 1.0 };
self.instance_data.push(SpriteInstance {
position,
size: tilemap.tile_size,
uv_min,
uv_max,
color,
rotation_scale: Vec4::new(1.0, 0.0, flip_scale_x, flip_scale_y),
anchor: Vec2::new(0.0, 0.0),
depth: tilemap.depth,
texture_slot: tilemap.texture_index,
texture_slot2: tilemap.texture_index,
blend_factor: 0.0,
gradient_type: 0,
gradient_param_a: 0.0,
gradient_param_b: 0.0,
advanced_blend_mode: 0,
_padding: [0.0; 2],
});
self.blend_modes.push(SpriteBlendMode::Alpha);
self.stencil_modes.push(SpriteStencilMode::None);
self.stencil_references.push(1);
self.clip_rects.push(None);
}
}
}
}
fn stencil_sort_priority(mode: SpriteStencilMode) -> u8 {
match mode {
SpriteStencilMode::Write => 0,
SpriteStencilMode::Test => 1,
SpriteStencilMode::None => 1,
}
}
fn sort_and_build_batches(&mut self) {
if self.instance_data.is_empty() {
return;
}
self.sort_indices.clear();
self.sort_indices.extend(0..self.instance_data.len());
self.sort_indices.sort_unstable_by(|&a, &b| {
let stencil_a = Self::stencil_sort_priority(self.stencil_modes[a]);
let stencil_b = Self::stencil_sort_priority(self.stencil_modes[b]);
stencil_a
.cmp(&stencil_b)
.then_with(|| self.stencil_references[a].cmp(&self.stencil_references[b]))
.then_with(|| {
self.instance_data[a]
.depth
.total_cmp(&self.instance_data[b].depth)
})
});
self.sorted_instances_scratch.clear();
self.sorted_blend_modes_scratch.clear();
self.sorted_stencil_modes_scratch.clear();
self.sorted_stencil_references_scratch.clear();
self.sorted_clip_rects_scratch.clear();
for &index in &self.sort_indices {
self.sorted_instances_scratch
.push(self.instance_data[index]);
self.sorted_blend_modes_scratch
.push(self.blend_modes[index]);
self.sorted_stencil_modes_scratch
.push(self.stencil_modes[index]);
self.sorted_stencil_references_scratch
.push(self.stencil_references[index]);
self.sorted_clip_rects_scratch.push(self.clip_rects[index]);
}
std::mem::swap(&mut self.instance_data, &mut self.sorted_instances_scratch);
std::mem::swap(&mut self.blend_modes, &mut self.sorted_blend_modes_scratch);
std::mem::swap(
&mut self.stencil_modes,
&mut self.sorted_stencil_modes_scratch,
);
std::mem::swap(
&mut self.stencil_references,
&mut self.sorted_stencil_references_scratch,
);
std::mem::swap(&mut self.clip_rects, &mut self.sorted_clip_rects_scratch);
let mut current_blend = self.blend_modes[0];
let mut current_stencil = self.stencil_modes[0];
let mut current_stencil_ref = self.stencil_references[0];
let mut current_clip = self.clip_rects[0];
let mut batch_start = 0u32;
for index in 1..self.blend_modes.len() {
if self.blend_modes[index] != current_blend
|| self.stencil_modes[index] != current_stencil
|| self.stencil_references[index] != current_stencil_ref
|| self.clip_rects[index] != current_clip
{
self.draw_batches.push(SpriteDrawBatch {
blend_mode: current_blend,
stencil_mode: current_stencil,
stencil_reference: current_stencil_ref as u32,
clip_rect: current_clip,
instance_start: batch_start,
instance_count: index as u32 - batch_start,
});
current_blend = self.blend_modes[index];
current_stencil = self.stencil_modes[index];
current_stencil_ref = self.stencil_references[index];
current_clip = self.clip_rects[index];
batch_start = index as u32;
}
}
self.draw_batches.push(SpriteDrawBatch {
blend_mode: current_blend,
stencil_mode: current_stencil,
stencil_reference: current_stencil_ref as u32,
clip_rect: current_clip,
instance_start: batch_start,
instance_count: self.blend_modes.len() as u32 - batch_start,
});
}
pub(super) fn write_buffers(
&mut self,
queue: &wgpu::Queue,
surface_width: u32,
surface_height: u32,
) {
if self.instance_data.is_empty() {
return;
}
let view_projection = self.cached_view_projection.unwrap_or_else(|| {
nalgebra_glm::ortho_rh_zo(
0.0,
surface_width as f32,
0.0,
surface_height as f32,
-1.0,
1.0,
)
});
let globals = GlobalUniforms {
view_projection,
screen_size: Vec2::new(surface_width as f32, surface_height as f32),
atlas_slots_per_row: self.atlas.slots_per_row as f32,
atlas_slot_uv_size: 1.0 / self.atlas.slots_per_row as f32,
};
queue.write_buffer(
&self.global_uniform_buffer,
0,
bytemuck::cast_slice(&[globals]),
);
queue.write_buffer(
&self.instance_buffer,
0,
bytemuck::cast_slice(&self.instance_data),
);
}
}