pub mod prelude;
use int_math::{URect, UVec2, Vec2, Vec3};
use log::info;
use std::rc::Rc;
use std::sync::Arc;
use swamp_wgpu_math::Matrix4;
use swamp_wgpu_math::Vec4;
use swamp_wgpu_sprites::{SpriteInfo, SpriteInstanceUniform};
use wgpu::{BindGroup, BindGroupLayout, Buffer, RenderPass, RenderPipeline};
pub struct Color {
r: u8,
g: u8,
b: u8,
a: u8,
}
impl Color {
pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
Self::from_octet(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
}
pub const fn from_octet(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub fn to_f64(&self) -> (f64, f64, f64, f64) {
(
self.r as f64 / 255.0,
self.g as f64 / 255.0,
self.b as f64 / 255.0,
self.a as f64 / 255.0,
)
}
}
fn to_wgpu_color(c: Color) -> wgpu::Color {
let f = c.to_f64();
wgpu::Color {
r: f.0,
g: f.1,
b: f.2,
a: f.3,
}
}
#[derive(Debug)]
pub struct Render {
index_buffer: Buffer,
vertex_buffer: Buffer,
sampler: wgpu::Sampler,
pipeline: RenderPipelineRef,
virtual_surface_size: UVec2,
camera_bind_group: BindGroup,
#[allow(unused)]
camera_buffer: Buffer,
texture_sampler_bind_group_layout: BindGroupLayout,
sprite_instance_buffer: Buffer,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>, sprites: Vec<Sprite>,
materials: Vec<SpriteMaterialRef>,
batch_offsets: Vec<(SpriteMaterialRef, u32, u32)>,
viewport: URect,
clear_color: wgpu::Color,
}
const fn sources() -> (&'static str, &'static str) {
let vertex_shader_source = "
// Bind Group 0: Uniforms (view-projection matrix)
struct Uniforms {
view_proj: mat4x4<f32>,
};
@group(0) @binding(0)
var<uniform> camera_uniforms: Uniforms;
// Bind Group 1: Texture and Sampler (Unused in Vertex Shader but needed for consistency)
@group(1) @binding(0)
var diffuse_texture: texture_2d<f32>;
@group(1) @binding(1)
var sampler_diffuse: sampler;
// Vertex input structure
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tex_coords: vec2<f32>,
@builtin(instance_index) instance_idx: u32,
};
// Vertex output structure to fragment shader
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
};
// Vertex shader entry point
@vertex
fn vs_main(
input: VertexInput,
// Instance attributes
@location(2) model_matrix0: vec4<f32>,
@location(3) model_matrix1: vec4<f32>,
@location(4) model_matrix2: vec4<f32>,
@location(5) model_matrix3: vec4<f32>,
@location(6) tex_multiplier: vec4<f32>,
) -> VertexOutput {
var output: VertexOutput;
// Reconstruct the model matrix from the instance data
let model_matrix = mat4x4<f32>(
model_matrix0,
model_matrix1,
model_matrix2,
model_matrix3,
);
// Compute world position
let world_position = model_matrix * vec4<f32>(input.position, 1.0);
// Apply view-projection matrix
output.position = camera_uniforms.view_proj * world_position;
// Modify texture coordinates
output.tex_coords = input.tex_coords * tex_multiplier.xy + tex_multiplier.zw;
return output;
}
";
let fragment_shader_source = "
// Bind Group 1: Texture and Sampler
@group(1) @binding(0)
var diffuse_texture: texture_2d<f32>;
@group(1) @binding(1)
var sampler_diffuse: sampler;
// Fragment input structure from vertex shader
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
};
// Fragment shader entry point
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// Sample the texture using the texture coordinates
let color = textureSample(diffuse_texture, sampler_diffuse, input.tex_coords);
return color;
}
";
(vertex_shader_source, fragment_shader_source)
}
impl Render {
pub fn new(
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>, surface_texture_format: wgpu::TextureFormat,
physical_size: (u16, u16),
virtual_size: (u16, u16),
) -> Self {
let (vertex_shader_source, fragment_shader_source) = sources();
let sprite_info = SpriteInfo::new(
&device,
surface_texture_format,
virtual_size,
vertex_shader_source,
fragment_shader_source,
);
Self {
device,
queue,
sprites: Vec::new(),
materials: Vec::new(),
sampler: sprite_info.sampler,
pipeline: Rc::new(sprite_info.sprite_pipeline),
texture_sampler_bind_group_layout: sprite_info.sprite_texture_sampler_bind_group_layout,
index_buffer: sprite_info.index_buffer,
vertex_buffer: sprite_info.vertex_buffer,
sprite_instance_buffer: sprite_info.sprite_instance_buffer,
camera_bind_group: sprite_info.camera_bind_group,
batch_offsets: Vec::new(),
virtual_surface_size: UVec2::new(virtual_size.0, virtual_size.1),
camera_buffer: sprite_info.camera_uniform_buffer,
viewport: Self::viewport_from_integer_scale(
UVec2::new(physical_size.0, physical_size.1),
UVec2::new(virtual_size.0, virtual_size.1),
),
clear_color: to_wgpu_color(Color::from_f32(0.01, 0.02, 0.01, 1.0)),
}
}
pub fn viewport_from_integer_scale(physical_size: UVec2, virtual_size: UVec2) -> URect {
let window_aspect = physical_size.x as f32 / physical_size.y as f32;
let virtual_aspect = virtual_size.x as f32 / virtual_size.y as f32;
if physical_size.x < virtual_size.x || physical_size.y < virtual_size.y {
return URect::new(0, 0, physical_size.x, physical_size.y);
}
let mut integer_scale = if window_aspect > virtual_aspect {
physical_size.y / virtual_size.y
} else {
physical_size.x / virtual_size.x
};
if integer_scale < 1 {
integer_scale = 1;
}
let viewport_actual_size = UVec2::new(
virtual_size.x * integer_scale,
virtual_size.y * integer_scale,
);
let border_size = physical_size - viewport_actual_size;
let offset = border_size / 2;
URect::new(
offset.x,
offset.y,
viewport_actual_size.x,
viewport_actual_size.y,
)
}
pub fn resize(&mut self, physical_size: (u16, u16)) {
self.viewport = Self::viewport_from_integer_scale(
UVec2::new(physical_size.0, physical_size.1),
self.virtual_surface_size,
);
}
pub fn render_sprite(
&mut self,
position: Vec3,
material: &SpriteMaterialRef,
params: SpriteParams,
) {
let atlas_rect = URect::new(0, 0, material.texture_size.x, material.texture_size.y);
self.sprites.push(Sprite {
position,
atlas_rect,
material: Rc::clone(material),
params,
})
}
pub fn sprite_atlas(
&mut self,
position: Vec3,
atlas_rect: URect,
material: &SpriteMaterialRef,
) {
self.sprites.push(Sprite {
position,
atlas_rect,
material: Rc::clone(material),
params: Default::default(),
})
}
pub fn sprite_atlas_frame(&mut self, position: Vec3, frame: u16, atlas: &impl AtlasLookup) {
let (material_ref, atlas_rect) = atlas.lookup(frame);
self.sprites.push(Sprite {
position,
atlas_rect,
material: Rc::clone(&material_ref),
params: Default::default(),
})
}
pub fn render_sprite_2d(
&mut self,
position: Vec2,
atlas_rect: URect,
material: &SpriteMaterialRef,
params: SpriteParams,
) {
self.sprites.push(Sprite {
position: position.into(),
atlas_rect,
material: Rc::clone(material),
params,
})
}
pub fn set_clear_color(&mut self, color: Color) {
self.clear_color = to_wgpu_color(color);
}
pub const fn clear_color(&self) -> wgpu::Color {
self.clear_color
}
fn calculate_texture_coords_mul_add(atlas_rect: URect, texture_size: UVec2) -> Vec4 {
let x = atlas_rect.position.x as f32 / texture_size.x as f32;
let y = atlas_rect.position.y as f32 / texture_size.y as f32;
let width = atlas_rect.size.x as f32 / texture_size.x as f32;
let height = atlas_rect.size.y as f32 / texture_size.y as f32;
Vec4([width, height, x, y])
}
pub fn order_sprites_in_batches(&self) -> Vec<Vec<&Sprite>> {
let mut material_batches: Vec<Vec<&Sprite>> = Vec::new();
let mut current_batch: Vec<&Sprite> = Vec::new();
let mut current_material: Option<&SpriteMaterialRef> = None;
for sprite in &self.sprites {
if Some(&sprite.material) != current_material {
if !current_batch.is_empty() {
material_batches.push(current_batch.clone());
current_batch.clear();
}
current_material = Some(&sprite.material);
}
current_batch.push(sprite);
}
if !current_batch.is_empty() {
material_batches.push(current_batch);
}
material_batches
}
pub fn prepare_render(&mut self) {
sort_sprites_by_z_then_y(&mut self.sprites);
let batches = self.order_sprites_in_batches();
let mut all_instances: Vec<SpriteInstanceUniform> = Vec::new();
let mut batch_offsets: Vec<(SpriteMaterialRef, u32, u32)> = Vec::new();
for sprites in &batches {
let start = all_instances.len() as u32;
let count = sprites.len() as u32;
let material_ref = &sprites.first().unwrap().material;
let current_texture_size = material_ref.texture_size;
for sprite in sprites {
let size_x = sprite.atlas_rect.size.x as f32;
let size_y = sprite.atlas_rect.size.y as f32;
let model_matrix = Matrix4::from_translation(
sprite.position.x as f32,
sprite.position.y as f32,
0.0,
) * Matrix4::from_scale(size_x, size_y, 1.0);
let atlas = sprite.atlas_rect;
let tex_coords_mul_add =
Self::calculate_texture_coords_mul_add(atlas, current_texture_size);
let sprite_instance = SpriteInstanceUniform::new(model_matrix, tex_coords_mul_add);
all_instances.push(sprite_instance);
}
batch_offsets.push((Rc::clone(material_ref), start, count));
}
self.queue.write_buffer(
&self.sprite_instance_buffer,
0,
bytemuck::cast_slice(&all_instances),
);
self.batch_offsets = batch_offsets;
}
pub fn render(&mut self, render_pass: &mut RenderPass) {
self.prepare_render();
let instance_size = size_of::<SpriteInstanceUniform>() as wgpu::BufferAddress;
render_pass.set_viewport(
self.viewport.position.x as f32,
self.viewport.position.y as f32,
self.viewport.size.x as f32,
self.viewport.size.y as f32,
0.0,
1.0,
);
render_pass.set_pipeline(&self.pipeline);
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.sprite_instance_buffer.slice(..));
render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
let num_indices = swamp_wgpu_sprites::INDICES.len() as u32;
for (material_ref, start, count) in &self.batch_offsets {
render_pass.set_bind_group(1, &material_ref.bind_group, &[]);
let octet_offset = *start as u64 * instance_size;
render_pass.set_vertex_buffer(1, self.sprite_instance_buffer.slice(octet_offset..));
render_pass.draw_indexed(0..num_indices, 0, 0..*count);
}
self.sprites.clear();
}
pub fn create_material_png(&mut self, png: &[u8], label: &str) -> SpriteMaterialRef {
let texture =
swamp_wgpu_sprites::load_texture_from_memory(&self.device, &self.queue, png, label);
info!("loaded texture!");
let size = &texture.size();
let bind_group = swamp_wgpu_sprites::create_sprite_texture_and_sampler_bind_group(
&self.device,
&self.texture_sampler_bind_group_layout,
texture,
&self.sampler,
label,
);
let texture_size = UVec2::new(size.width as u16, size.height as u16);
let material = Rc::new(SpriteMaterial {
bind_group,
render_pipeline: Rc::clone(&self.pipeline),
texture_size,
});
self.materials.push(Rc::clone(&material));
material
}
}
fn sort_sprites_by_z_then_y(sprites: &mut [Sprite]) {
sprites.sort_by_key(|sprite| (sprite.position.z, sprite.position.y));
}
#[derive(Default, Debug)]
pub struct SpriteParams {
pub dest_size: Option<UVec2>,
pub source: Option<URect>,
pub rotation: u16,
pub flip_x: bool,
pub flip_y: bool,
pub pivot: Option<Vec2>,
}
pub type SpriteMaterialRef = Rc<SpriteMaterial>;
#[derive(Debug)]
pub struct Sprite {
pub position: Vec3,
pub atlas_rect: URect,
pub material: SpriteMaterialRef,
pub params: SpriteParams,
}
pub type RenderPipelineRef = Rc<RenderPipeline>;
#[derive(Debug, PartialEq, Eq)]
pub struct SpriteMaterial {
pub bind_group: BindGroup,
pub render_pipeline: RenderPipelineRef,
pub texture_size: UVec2,
}
pub trait AtlasLookup {
fn lookup(&self, frame: u16) -> (SpriteMaterialRef, URect);
}
#[derive(Debug, PartialEq, Eq)]
pub struct FixedAtlas {
material: SpriteMaterialRef,
grid_size: UVec2,
cell_size: UVec2,
}
impl FixedAtlas {
pub fn new(grid_size: UVec2, material_ref: SpriteMaterialRef) -> Self {
let cell_size = UVec2::new(
material_ref.texture_size.x / grid_size.x,
material_ref.texture_size.y / grid_size.y,
);
Self {
material: material_ref,
grid_size,
cell_size,
}
}
}
impl AtlasLookup for FixedAtlas {
fn lookup(&self, frame: u16) -> (SpriteMaterialRef, URect) {
let x = frame % self.cell_size.x;
let y = frame / self.cell_size.x;
(
self.material.clone(),
URect::new(
x * self.grid_size.x,
y * self.grid_size.y,
self.grid_size.x,
self.grid_size.y,
),
)
}
}