use crate::ecs::camera::queries::query_active_camera_matrices;
use crate::ecs::sprite::components::{SpriteBlendMode, SpriteGradient, SpriteStencilMode};
use crate::ecs::sprite_text::components::SpriteTextAlignment;
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use crate::render::wgpu::sprite_font_atlas::{SpriteFontAtlas, rasterize_sprite_font};
use crate::render::wgpu::sprite_texture_atlas::SpriteTextureAtlas;
use nalgebra_glm::{Mat4, Vec2, Vec4};
use std::collections::HashMap;
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SpriteInstance {
pub position: Vec2,
pub size: Vec2,
pub uv_min: Vec2,
pub uv_max: Vec2,
pub color: Vec4,
pub rotation_scale: Vec4,
pub anchor: Vec2,
pub depth: f32,
pub texture_slot: u32,
pub texture_slot2: u32,
pub blend_factor: f32,
pub gradient_type: u32,
pub gradient_param_a: f32,
pub gradient_param_b: f32,
pub advanced_blend_mode: u32,
pub _padding: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SpriteVertex {
pub offset: Vec2,
pub uv: Vec2,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlobalUniforms {
pub view_projection: Mat4,
pub screen_size: Vec2,
pub atlas_slots_per_row: f32,
pub atlas_slot_uv_size: f32,
}
struct SpriteDrawBatch {
blend_mode: SpriteBlendMode,
stencil_mode: SpriteStencilMode,
stencil_reference: u32,
clip_rect: Option<[f32; 4]>,
instance_start: u32,
instance_count: u32,
}
pub struct SpritePass {
pipelines_normal: [wgpu::RenderPipeline; 4],
pipelines_stencil_write: [wgpu::RenderPipeline; 4],
pipelines_stencil_test: [wgpu::RenderPipeline; 4],
pipeline_advanced_normal: wgpu::RenderPipeline,
pipeline_advanced_stencil_test: wgpu::RenderPipeline,
global_uniform_buffer: wgpu::Buffer,
instance_buffer: wgpu::Buffer,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
texture_bind_group: Option<wgpu::BindGroup>,
background_bind_group_layout: wgpu::BindGroupLayout,
background_texture: wgpu::Texture,
background_view: wgpu::TextureView,
background_sampler: wgpu::Sampler,
background_bind_group: Option<wgpu::BindGroup>,
background_texture_size: (u32, u32),
max_instances: usize,
instance_data: Vec<SpriteInstance>,
blend_modes: Vec<SpriteBlendMode>,
stencil_modes: Vec<SpriteStencilMode>,
stencil_references: Vec<u8>,
clip_rects: Vec<Option<[f32; 4]>>,
draw_batches: Vec<SpriteDrawBatch>,
atlas: SpriteTextureAtlas,
stencil_texture: wgpu::Texture,
stencil_view: wgpu::TextureView,
stencil_texture_size: (u32, u32),
cached_view_projection: Option<Mat4>,
cached_surface_width: u32,
cached_surface_height: u32,
sort_indices: Vec<usize>,
sorted_instances_scratch: Vec<SpriteInstance>,
sorted_blend_modes_scratch: Vec<SpriteBlendMode>,
sorted_stencil_modes_scratch: Vec<SpriteStencilMode>,
sorted_stencil_references_scratch: Vec<u8>,
sorted_clip_rects_scratch: Vec<Option<[f32; 4]>>,
sprite_font_atlas: SpriteFontAtlas,
sprite_text_cache: HashMap<freecs::Entity, CachedSpriteText>,
}
struct CachedSpriteText {
instances: Vec<SpriteInstance>,
text: String,
position: Vec2,
depth: f32,
font_size: f32,
color: [f32; 4],
alignment: SpriteTextAlignment,
}
struct SpritePipelineConfig<'a> {
layout: &'a wgpu::PipelineLayout,
shader: &'a wgpu::ShaderModule,
color_format: wgpu::TextureFormat,
blend_state: wgpu::BlendState,
depth_stencil: Option<wgpu::DepthStencilState>,
color_write_mask: wgpu::ColorWrites,
label: &'a str,
fragment_entry: &'a str,
}
fn create_sprite_pipeline(
device: &wgpu::Device,
config: &SpritePipelineConfig,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(config.label),
layout: Some(config.layout),
vertex: wgpu::VertexState {
module: config.shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<SpriteVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<Vec2>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
],
}],
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: config.depth_stencil.clone(),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: config.shader,
entry_point: Some(config.fragment_entry),
targets: &[Some(wgpu::ColorTargetState {
format: config.color_format,
blend: Some(config.blend_state),
write_mask: config.color_write_mask,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
})
}
fn stencil_depth_state_normal() -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
read_mask: 0,
write_mask: 0,
},
bias: wgpu::DepthBiasState::default(),
}
}
fn stencil_depth_state_write() -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Replace,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Replace,
},
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
}
}
fn stencil_depth_state_test() -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
read_mask: 0xFF,
write_mask: 0,
},
bias: wgpu::DepthBiasState::default(),
}
}
impl SpritePass {
pub fn new(
device: &wgpu::Device,
color_format: wgpu::TextureFormat,
queue: &wgpu::Queue,
) -> Self {
let max_instances = 1_000_000;
let mut atlas = SpriteTextureAtlas::new(
device,
crate::render::wgpu::sprite_texture_atlas::SPRITE_ATLAS_TOTAL_SLOTS,
crate::render::wgpu::sprite_texture_atlas::SPRITE_ATLAS_SLOT_SIZE,
);
let total_pixels = (atlas.atlas_size.0 * atlas.atlas_size.1 * 4) as usize;
let white_texture = vec![255u8; total_pixels];
queue.write_texture(
atlas.texture.as_image_copy(),
&white_texture,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(atlas.atlas_size.0 * 4),
rows_per_image: Some(atlas.atlas_size.1),
},
wgpu::Extent3d {
width: atlas.atlas_size.0,
height: atlas.atlas_size.1,
depth_or_array_layers: 1,
},
);
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Sprite 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: wgpu::BufferSize::new(std::mem::size_of::<
GlobalUniforms,
>()
as u64),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(std::mem::size_of::<
SpriteInstance,
>()
as u64),
},
count: None,
},
],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Sprite 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,
},
],
});
let background_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Sprite Background 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,
},
],
});
let standard_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Sprite Pipeline Layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
push_constant_ranges: &[],
});
let advanced_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Sprite Advanced Pipeline Layout"),
bind_group_layouts: &[
&uniform_bind_group_layout,
&texture_bind_group_layout,
&background_bind_group_layout,
],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Sprite Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/sprite.wgsl").into()),
});
let vertices = [
SpriteVertex {
offset: Vec2::new(-0.5, -0.5),
uv: Vec2::new(0.0, 1.0),
},
SpriteVertex {
offset: Vec2::new(0.5, -0.5),
uv: Vec2::new(1.0, 1.0),
},
SpriteVertex {
offset: Vec2::new(0.5, 0.5),
uv: Vec2::new(1.0, 0.0),
},
SpriteVertex {
offset: Vec2::new(-0.5, 0.5),
uv: Vec2::new(0.0, 0.0),
},
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Sprite Vertex Buffer"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Sprite Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let additive_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
let multiply_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
};
let screen_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
};
let blend_states = [
wgpu::BlendState::ALPHA_BLENDING,
additive_blend,
multiply_blend,
screen_blend,
];
let blend_labels = ["Alpha", "Additive", "Multiply", "Screen"];
let pipelines_normal = std::array::from_fn(|blend_index| {
create_sprite_pipeline(
device,
&SpritePipelineConfig {
layout: &standard_pipeline_layout,
shader: &shader,
color_format,
blend_state: blend_states[blend_index],
depth_stencil: Some(stencil_depth_state_normal()),
color_write_mask: wgpu::ColorWrites::ALL,
label: &format!("Sprite Pipeline Normal ({})", blend_labels[blend_index]),
fragment_entry: "fs_main",
},
)
});
let pipelines_stencil_write = std::array::from_fn(|blend_index| {
create_sprite_pipeline(
device,
&SpritePipelineConfig {
layout: &standard_pipeline_layout,
shader: &shader,
color_format,
blend_state: blend_states[blend_index],
depth_stencil: Some(stencil_depth_state_write()),
color_write_mask: wgpu::ColorWrites::empty(),
label: &format!(
"Sprite Pipeline Stencil Write ({})",
blend_labels[blend_index]
),
fragment_entry: "fs_main",
},
)
});
let pipelines_stencil_test = std::array::from_fn(|blend_index| {
create_sprite_pipeline(
device,
&SpritePipelineConfig {
layout: &standard_pipeline_layout,
shader: &shader,
color_format,
blend_state: blend_states[blend_index],
depth_stencil: Some(stencil_depth_state_test()),
color_write_mask: wgpu::ColorWrites::ALL,
label: &format!(
"Sprite Pipeline Stencil Test ({})",
blend_labels[blend_index]
),
fragment_entry: "fs_main",
},
)
});
let pipeline_advanced_normal = create_sprite_pipeline(
device,
&SpritePipelineConfig {
layout: &advanced_pipeline_layout,
shader: &shader,
color_format,
blend_state: wgpu::BlendState::REPLACE,
depth_stencil: Some(stencil_depth_state_normal()),
color_write_mask: wgpu::ColorWrites::ALL,
label: "Sprite Pipeline Advanced Normal",
fragment_entry: "fs_main_advanced",
},
);
let pipeline_advanced_stencil_test = create_sprite_pipeline(
device,
&SpritePipelineConfig {
layout: &advanced_pipeline_layout,
shader: &shader,
color_format,
blend_state: wgpu::BlendState::REPLACE,
depth_stencil: Some(stencil_depth_state_test()),
color_write_mask: wgpu::ColorWrites::ALL,
label: "Sprite Pipeline Advanced Stencil Test",
fragment_entry: "fs_main_advanced",
},
);
let global_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Sprite Global Uniform Buffer"),
size: std::mem::size_of::<GlobalUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Sprite Instance Buffer"),
size: (std::mem::size_of::<SpriteInstance>() * max_instances) 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("Sprite Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: global_uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: instance_buffer.as_entire_binding(),
},
],
});
let initial_stencil_size = (1u32, 1u32);
let stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Sprite Stencil Texture"),
size: wgpu::Extent3d {
width: initial_stencil_size.0,
height: initial_stencil_size.1,
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: &[],
});
let stencil_view = stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
let initial_bg_size = (1u32, 1u32);
let background_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Sprite Background Texture"),
size: wgpu::Extent3d {
width: initial_bg_size.0,
height: initial_bg_size.1,
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: &[],
});
let background_view =
background_texture.create_view(&wgpu::TextureViewDescriptor::default());
let background_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Sprite Background Sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let sprite_font_atlas = rasterize_sprite_font(queue, &mut atlas);
crate::render::wgpu::sprite_shapes::upload_builtin_shapes(queue, &mut atlas);
let mut sprite_pass = Self {
pipelines_normal,
pipelines_stencil_write,
pipelines_stencil_test,
pipeline_advanced_normal,
pipeline_advanced_stencil_test,
global_uniform_buffer,
instance_buffer,
vertex_buffer,
index_buffer,
uniform_bind_group,
texture_bind_group_layout,
texture_bind_group: None,
background_bind_group_layout,
background_texture,
background_view,
background_sampler,
background_bind_group: None,
background_texture_size: initial_bg_size,
max_instances,
instance_data: Vec::new(),
blend_modes: Vec::new(),
stencil_modes: Vec::new(),
stencil_references: Vec::new(),
clip_rects: Vec::new(),
draw_batches: Vec::new(),
atlas,
stencil_texture,
stencil_view,
stencil_texture_size: initial_stencil_size,
cached_view_projection: None,
cached_surface_width: 0,
cached_surface_height: 0,
sort_indices: Vec::new(),
sorted_instances_scratch: Vec::new(),
sorted_blend_modes_scratch: Vec::new(),
sorted_stencil_modes_scratch: Vec::new(),
sorted_stencil_references_scratch: Vec::new(),
sorted_clip_rects_scratch: Vec::new(),
sprite_font_atlas,
sprite_text_cache: HashMap::new(),
};
sprite_pass.update_texture_bind_group(device);
sprite_pass
}
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"),
}));
}
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 fn atlas_view(&self) -> &wgpu::TextureView {
&self.atlas.view
}
pub fn atlas_sampler(&self) -> &wgpu::Sampler {
&self.atlas.sampler
}
pub fn atlas_slots_per_row(&self) -> u32 {
self.atlas.slots_per_row
}
pub fn upload_texture_to_atlas(
&mut self,
queue: &wgpu::Queue,
slot: u32,
rgba_data: &[u8],
width: u32,
height: u32,
) {
self.atlas
.upload_texture(queue, slot, rgba_data, width, height);
}
fn pipeline_for_batch(
&self,
blend_mode: SpriteBlendMode,
stencil_mode: SpriteStencilMode,
) -> &wgpu::RenderPipeline {
if blend_mode.is_advanced() {
return match stencil_mode {
SpriteStencilMode::Test => &self.pipeline_advanced_stencil_test,
_ => &self.pipeline_advanced_normal,
};
}
let blend_index = match blend_mode {
SpriteBlendMode::Alpha => 0,
SpriteBlendMode::Additive => 1,
SpriteBlendMode::Multiply => 2,
SpriteBlendMode::Screen => 3,
_ => 0,
};
match stencil_mode {
SpriteStencilMode::None => &self.pipelines_normal[blend_index],
SpriteStencilMode::Write => &self.pipelines_stencil_write[blend_index],
SpriteStencilMode::Test => &self.pipelines_stencil_test[blend_index],
}
}
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;
}
}
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.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
.query_entities(crate::ecs::SPRITE | crate::ecs::VISIBILITY)
.collect();
for entity in sprite_entities {
let Some(sprite) = world.get_sprite(entity) else {
continue;
};
let Some(visibility) = world.get_visibility(entity) else {
continue;
};
if !visibility.visible {
continue;
}
let render_layer = world
.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.prepare_sprite_text(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
.query_entities(crate::ecs::TILEMAP | crate::ecs::VISIBILITY)
.collect();
for entity in tilemap_entities {
let Some(tilemap) = world.get_tilemap(entity) else {
continue;
};
let Some(visibility) = world.get_visibility(entity) else {
continue;
};
if !visibility.visible {
continue;
}
let render_layer = world
.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 prepare_sprite_text(&mut self, world: &World, cull_bounds: Option<(f32, f32, f32, f32)>) {
let sprite_text_entities: Vec<_> = world
.query_entities(crate::ecs::SPRITE_TEXT | crate::ecs::VISIBILITY)
.collect();
let mut active_entities = std::collections::HashSet::new();
for entity in &sprite_text_entities {
active_entities.insert(*entity);
let Some(sprite_text) = world.get_sprite_text(*entity) else {
continue;
};
let Some(visibility) = world.get_visibility(*entity) else {
continue;
};
if !visibility.visible {
continue;
}
let render_layer = world
.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 needs_rebuild = match self.sprite_text_cache.get(entity) {
None => true,
Some(cached) => {
cached.text != sprite_text.text
|| cached.position != sprite_text.position
|| cached.depth != sprite_text.depth
|| cached.font_size != sprite_text.font_size
|| cached.color != sprite_text.color
|| cached.alignment != sprite_text.alignment
}
};
if needs_rebuild {
let instances = self.layout_sprite_text(sprite_text);
self.sprite_text_cache.insert(
*entity,
CachedSpriteText {
instances,
text: sprite_text.text.clone(),
position: sprite_text.position,
depth: sprite_text.depth,
font_size: sprite_text.font_size,
color: sprite_text.color,
alignment: sprite_text.alignment,
},
);
}
if let Some(cached) = self.sprite_text_cache.get(entity) {
if cached.instances.is_empty() {
continue;
}
if let Some((min_x, max_x, min_y, max_y)) = cull_bounds {
let scale = sprite_text.font_size / self.sprite_font_atlas.rasterization_scale;
let line_height = self.sprite_font_atlas.line_height * scale;
let total_width = self.compute_text_width(sprite_text);
let half_width = total_width * 0.5;
let half_height = line_height * 0.5;
let center_x = match sprite_text.alignment {
SpriteTextAlignment::Left => sprite_text.position.x + half_width,
SpriteTextAlignment::Center => sprite_text.position.x,
SpriteTextAlignment::Right => sprite_text.position.x - half_width,
};
let center_y = sprite_text.position.y;
if center_x + half_width < min_x
|| center_x - half_width > max_x
|| center_y + half_height < min_y
|| center_y - half_height > max_y
{
continue;
}
}
for instance in &cached.instances {
if self.instance_data.len() >= self.max_instances {
return;
}
self.instance_data.push(*instance);
self.blend_modes.push(SpriteBlendMode::Alpha);
self.stencil_modes.push(SpriteStencilMode::None);
self.stencil_references.push(1);
self.clip_rects.push(None);
}
}
}
self.sprite_text_cache
.retain(|entity, _| active_entities.contains(entity));
}
fn compute_text_width(
&self,
sprite_text: &crate::ecs::sprite_text::components::SpriteText,
) -> f32 {
let scale = sprite_text.font_size / self.sprite_font_atlas.rasterization_scale;
let mut width = 0.0f32;
for character in sprite_text.text.chars() {
if let Some(glyph) = self.sprite_font_atlas.glyphs.get(&character) {
width += glyph.advance * scale;
}
}
width
}
fn layout_sprite_text(
&self,
sprite_text: &crate::ecs::sprite_text::components::SpriteText,
) -> Vec<SpriteInstance> {
let mut instances = Vec::new();
let scale = sprite_text.font_size / self.sprite_font_atlas.rasterization_scale;
let total_width = self.compute_text_width(sprite_text);
let start_x = match sprite_text.alignment {
SpriteTextAlignment::Left => sprite_text.position.x,
SpriteTextAlignment::Center => sprite_text.position.x - total_width * 0.5,
SpriteTextAlignment::Right => sprite_text.position.x - total_width,
};
let color = Vec4::from_column_slice(&sprite_text.color);
let mut cursor_x = start_x;
for character in sprite_text.text.chars() {
let Some(glyph) = self.sprite_font_atlas.glyphs.get(&character) else {
continue;
};
if glyph.size.x > 0.0 && glyph.size.y > 0.0 {
let glyph_width = glyph.size.x * scale;
let glyph_height = glyph.size.y * scale;
let glyph_x = cursor_x + glyph.bearing.x * scale + glyph_width * 0.5;
let glyph_y = sprite_text.position.y + glyph.bearing.y * scale + glyph_height * 0.5;
instances.push(SpriteInstance {
position: Vec2::new(glyph_x, glyph_y),
size: Vec2::new(glyph_width, glyph_height),
uv_min: glyph.uv_min,
uv_max: glyph.uv_max,
color,
rotation_scale: Vec4::new(1.0, 0.0, 1.0, 1.0),
anchor: Vec2::new(0.0, 0.0),
depth: sprite_text.depth,
texture_slot: self.sprite_font_atlas.atlas_slot,
texture_slot2: self.sprite_font_atlas.atlas_slot,
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],
});
}
cursor_x += glyph.advance * scale;
}
instances
}
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,
});
}
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),
);
}
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);
}
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 has_advanced_blend_batches(&self) -> bool {
self.draw_batches
.iter()
.any(|batch| batch.blend_mode.is_advanced())
}
fn render_sprites(
&mut self,
encoder: &mut wgpu::CommandEncoder,
color_view: &wgpu::TextureView,
color_texture: Option<&wgpu::Texture>,
device: &wgpu::Device,
surface_width: u32,
surface_height: u32,
) {
if self.instance_data.is_empty() {
return;
}
let has_advanced = self.has_advanced_blend_batches();
if has_advanced {
self.ensure_background_texture(device, surface_width, surface_height);
if self.background_bind_group.is_none() {
self.update_background_bind_group(device);
}
}
let has_stencil_sprites = self
.stencil_modes
.iter()
.any(|mode| *mode != SpriteStencilMode::None);
let depth_stencil_attachment = if has_stencil_sprites {
Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.stencil_view,
depth_ops: None,
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Store,
}),
})
} else {
Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.stencil_view,
depth_ops: None,
stencil_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Discard,
}),
})
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Sprite Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: depth_stencil_attachment.clone(),
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
if let Some(ref bind_group) = self.texture_bind_group {
render_pass.set_bind_group(1, bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
for batch_index in 0..self.draw_batches.len() {
let batch = &self.draw_batches[batch_index];
if batch.blend_mode.is_advanced() {
drop(render_pass);
if let Some(source_texture) = color_texture {
encoder.copy_texture_to_texture(
source_texture.as_image_copy(),
self.background_texture.as_image_copy(),
wgpu::Extent3d {
width: surface_width,
height: surface_height,
depth_or_array_layers: 1,
},
);
}
render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Sprite Render Pass (Advanced)"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: depth_stencil_attachment.clone(),
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
if let Some(ref tex_bind_group) = self.texture_bind_group {
render_pass.set_bind_group(1, tex_bind_group, &[]);
}
if let Some(ref bg_bind_group) = self.background_bind_group {
render_pass.set_bind_group(2, bg_bind_group, &[]);
}
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass
.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
}
let batch = &self.draw_batches[batch_index];
if let Some(clip) = batch.clip_rect {
let x = (clip[0] as u32).min(surface_width);
let y = (clip[1] as u32).min(surface_height);
let max_x = (clip[2] as u32).min(surface_width);
let max_y = (clip[3] as u32).min(surface_height);
let width = max_x.saturating_sub(x);
let height = max_y.saturating_sub(y);
if width == 0 || height == 0 {
continue;
}
render_pass.set_scissor_rect(x, y, width, height);
} else {
render_pass.set_scissor_rect(0, 0, surface_width, surface_height);
}
let pipeline = self.pipeline_for_batch(batch.blend_mode, batch.stencil_mode);
render_pass.set_pipeline(pipeline);
if batch.stencil_mode == SpriteStencilMode::Write
|| batch.stencil_mode == SpriteStencilMode::Test
{
render_pass.set_stencil_reference(batch.stencil_reference);
}
render_pass.draw_indexed(
0..6,
0,
batch.instance_start..batch.instance_start + batch.instance_count,
);
}
}
}
}
impl PassNode<World> for SpritePass {
fn name(&self) -> &str {
"sprite_pass"
}
fn reads(&self) -> Vec<&str> {
vec![]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color"]
}
fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, configs: &World) {
if let Some((width, height)) = configs.resources.window.cached_viewport_size {
self.ensure_stencil_texture(device, width, height);
self.prepare_sprites(configs, width, height);
self.write_buffers(queue, width, height);
}
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
crate::render::wgpu::rendergraph::RenderGraphError,
> {
let color_view = context.get_texture_view("color")?;
let color_texture = context.get_texture("color").ok();
self.render_sprites(
context.encoder,
color_view,
color_texture,
context.device,
self.cached_surface_width,
self.cached_surface_height,
);
Ok(vec![])
}
}