pub mod anim;
pub mod api;
mod bm;
pub mod prelude;
use crate::api::{Assets, FixedAtlas, FontAndMaterial, FrameLookup, Gfx, Glyph};
use crate::bm::Font;
use crate::prelude::FontAndMaterialRef;
use int_math::{URect, UVec2, Vec2, Vec3};
use log::{debug, trace};
use monotonic_time_rs::{InstantMonotonicClock, Millis, MonotonicClock};
use std::cmp::Ordering;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::{fs, io};
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,
}
}
struct RenderItem {
position: Vec3,
material_ref: MaterialRef,
renderable: Renderable,
}
enum Renderable {
Sprite(Sprite),
TileMap(TileMap),
}
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>, items: Vec<RenderItem>,
materials: Vec<MaterialRef>,
fonts: Vec<FontAndMaterialRef>,
origo: Vec2,
batch_offsets: Vec<(MaterialRef, u32, u32)>,
viewport: URect,
clear_color: wgpu::Color,
asset_prefix: String,
clock: InstantMonotonicClock,
last_render_at: Millis,
}
impl Assets for Render {
fn material_png_raw(&mut self, png: &[u8], label: &str) -> MaterialRef {
let texture =
swamp_wgpu_sprites::load_texture_from_memory(&self.device, &self.queue, png, label);
trace!("load texture from memory with name: '{label}'");
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(Material {
bind_group,
render_pipeline: Rc::clone(&self.pipeline),
texture_size,
});
self.materials.push(Rc::clone(&material));
material
}
fn material_png(&mut self, name: &str) -> MaterialRef {
let data: Vec<u8> = self.read(name, "png").expect("failed to read image");
self.material_png_raw(&data, name)
}
fn set_prefix(&mut self, prefix: &str) {
self.set_asset_prefix(prefix);
}
fn frame_fixed_grid_material_png(&mut self, name: &str, grid_size: UVec2) -> FixedAtlas {
debug!("loading '{name}' frame_fixed_grid_material_png");
let tileset_material_ref = self.material_png(name);
FixedAtlas::new(grid_size, Rc::clone(&tileset_material_ref))
}
fn bm_font(&mut self, name: &str) -> FontAndMaterialRef {
let data: Vec<u8> = self.read(name, "fnt").expect("failed to read image");
let font = Font::from_octets(&data);
let material = self.material_png(name);
let font_and_material = FontAndMaterial {
font,
material_ref: material,
};
let font_and_material_ref = Rc::new(font_and_material);
self.fonts.push(font_and_material_ref.clone());
font_and_material_ref
}
fn now(&self) -> Millis {
self.last_render_at
}
}
impl Gfx for Render {
fn set_origo(&mut self, position: Vec2) {
self.origo = position;
}
fn now(&self) -> Millis {
self.last_render_at
}
fn text_draw(&mut self, position: Vec3, text: &str, font_ref: &FontAndMaterialRef) {
let glyphs = font_ref.font.draw(text);
for glyph in glyphs {
self.push_sprite(
position + Vec3::from(glyph.surface_position),
&font_ref.material_ref,
Sprite {
atlas_rect: glyph.texture_rect,
params: Default::default(),
},
);
}
}
fn sprite_atlas(&mut self, position: Vec3, atlas_rect: URect, material: &MaterialRef) {
self.sprite_atlas(position, atlas_rect, material);
}
fn sprite_atlas_frame(&mut self, position: Vec3, frame: u16, atlas: &impl FrameLookup) {
self.sprite_atlas_frame(position, frame, atlas);
}
fn text_glyphs(
&mut self,
position: Vec2,
text: &str,
font_ref: &FontAndMaterialRef,
) -> Vec<Glyph> {
let glyphs = font_ref.font.draw(text);
glyphs
.iter()
.map(|bmf_glyph| Glyph {
relative_position: position + bmf_glyph.surface_position,
texture_rectangle: bmf_glyph.texture_rect,
})
.collect()
}
fn tilemap(&mut self, position: Vec3, tiles: &[u16], width: u16, atlas_ref: &FixedAtlas) {
self.items.push(RenderItem {
position,
material_ref: atlas_ref.material.clone(),
renderable: Renderable::TileMap(TileMap {
tiles_data_grid_size: UVec2::new(width, tiles.len() as u16 / width),
cell_count_size: atlas_ref.cell_count_size,
one_cell_size: atlas_ref.one_cell_size,
tiles: Vec::from(tiles),
}),
});
}
}
impl Render {
pub fn new(
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>, surface_texture_format: wgpu::TextureFormat,
physical_size: UVec2,
virtual_surface_size: UVec2,
) -> Self {
let (vertex_shader_source, fragment_shader_source) = sources();
let sprite_info = SpriteInfo::new(
&device,
surface_texture_format,
(virtual_surface_size.x, virtual_surface_size.y),
vertex_shader_source,
fragment_shader_source,
);
let clock = InstantMonotonicClock::new();
let now = clock.now();
Self {
device,
queue,
items: Vec::new(),
materials: Vec::new(),
fonts: 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,
camera_buffer: sprite_info.camera_uniform_buffer,
viewport: Self::viewport_from_integer_scale(physical_size, virtual_surface_size),
clear_color: to_wgpu_color(Color::from_f32(0.008, 0.015, 0.008, 1.0)),
origo: Vec2::new(0, 0),
asset_prefix: "assets".into(),
clock,
last_render_at: now,
}
}
fn set_asset_prefix(&mut self, prefix: &str) {
self.asset_prefix = prefix.into();
}
#[inline(always)]
fn push_sprite(&mut self, position: Vec3, material: &MaterialRef, sprite: Sprite) {
self.items.push(RenderItem {
position,
material_ref: material.clone(),
renderable: Renderable::Sprite(sprite),
})
}
fn is_valid_asset_name(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn read(&mut self, name: &str, extension: &str) -> io::Result<Vec<u8>> {
if !Self::is_valid_asset_name(name) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid asset name. must be lowercase a-z, 0-9, '_' or '-'. this is to avoid problems on different filesystems and platforms"));
}
trace!("loading asset: '{name}' of type '{extension}'");
let name_with_extension: String = name.to_ascii_lowercase() + "." + &*extension.to_string();
let full_path = Path::new(&self.asset_prefix).join(name_with_extension);
fs::read(full_path)
}
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: UVec2) {
self.viewport = Self::viewport_from_integer_scale(physical_size, self.virtual_surface_size);
}
pub fn render_sprite(&mut self, position: Vec3, material: &MaterialRef, params: SpriteParams) {
let atlas_rect = URect::new(0, 0, material.texture_size.x, material.texture_size.y);
self.push_sprite(position, material, Sprite { atlas_rect, params })
}
pub fn sprite_atlas(&mut self, position: Vec3, atlas_rect: URect, material_ref: &MaterialRef) {
self.push_sprite(
position,
material_ref,
Sprite {
atlas_rect,
params: Default::default(),
},
)
}
fn sprite_atlas_frame(&mut self, position: Vec3, frame: u16, atlas: &impl FrameLookup) {
let (material_ref, atlas_rect) = atlas.lookup(frame);
self.push_sprite(
position,
&material_ref,
Sprite {
atlas_rect,
params: Default::default(),
},
)
}
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])
}
fn order_sprites_in_batches(&self) -> Vec<Vec<&RenderItem>> {
let mut material_batches: Vec<Vec<&RenderItem>> = Vec::new();
let mut current_batch: Vec<&RenderItem> = Vec::new();
let mut current_material: Option<&MaterialRef> = None;
for sprite in &self.items {
if Some(&sprite.material_ref) != current_material {
if !current_batch.is_empty() {
material_batches.push(current_batch.clone());
current_batch.clear();
}
current_material = Some(&sprite.material_ref);
}
current_batch.push(sprite);
}
if !current_batch.is_empty() {
material_batches.push(current_batch);
}
material_batches
}
pub fn prepare_render(&mut self) {
sort_render_items_by_z_and_material(&mut self.items);
let batches = self.order_sprites_in_batches();
let mut all_instances: Vec<SpriteInstanceUniform> = Vec::new();
let mut batch_offsets: Vec<(MaterialRef, u32, u32)> = Vec::new();
for render_items in &batches {
let start = all_instances.len() as u32;
let mut count = 0;
let material_ref = &render_items.first().unwrap().material_ref;
let current_texture_size = material_ref.texture_size;
for render_item in render_items {
match render_item.renderable {
Renderable::Sprite(ref sprite) => {
let size = sprite.atlas_rect.size;
let render_atlas = sprite.atlas_rect;
let model_matrix =
Matrix4::from_translation(
(render_item.position.x - self.origo.x) as f32,
(render_item.position.y - self.origo.y) as f32,
0.0,
) * Matrix4::from_scale(size.x as f32, size.y as f32, 1.0);
let tex_coords_mul_add = Self::calculate_texture_coords_mul_add(
render_atlas,
current_texture_size,
);
let sprite_instance =
SpriteInstanceUniform::new(model_matrix, tex_coords_mul_add);
all_instances.push(sprite_instance);
count += 1;
}
Renderable::TileMap(ref tile_map) => {
for (index, tile) in tile_map.tiles.iter().enumerate() {
let cell_pos_x = (index as u16 % tile_map.tiles_data_grid_size.x)
* tile_map.one_cell_size.x;
let cell_pos_y = (index as u16 / tile_map.tiles_data_grid_size.x)
* tile_map.one_cell_size.y;
let cell_x = *tile % tile_map.cell_count_size.x;
let cell_y = *tile / tile_map.cell_count_size.x;
let tex_x = cell_x * tile_map.one_cell_size.x;
let tex_y = cell_y * tile_map.one_cell_size.x;
let cell_texture_area = URect::new(
tex_x,
tex_y,
tile_map.one_cell_size.x,
tile_map.one_cell_size.y,
);
let cell_model_matrix = Matrix4::from_translation(
(render_item.position.x - self.origo.x + cell_pos_x as i16) as f32,
(render_item.position.y - self.origo.y + cell_pos_y as i16) as f32,
0.0,
) * Matrix4::from_scale(
tile_map.one_cell_size.x as f32,
tile_map.one_cell_size.y as f32,
1.0,
);
let cell_tex_coords_mul_add = Self::calculate_texture_coords_mul_add(
cell_texture_area,
current_texture_size,
);
let sprite_instance = SpriteInstanceUniform::new(
cell_model_matrix,
cell_tex_coords_mul_add,
);
all_instances.push(sprite_instance);
count += 1;
}
}
}
}
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) {
trace!("swamp_render: render()");
self.last_render_at = self.clock.now();
self.prepare_render();
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, &[]);
trace!("swamp_render: instances: {start}..{count}");
render_pass.draw_indexed(0..num_indices, 0, *start..(start + count));
}
self.items.clear();
}
}
fn sort_render_items_by_z_and_material(items: &mut [RenderItem]) {
items.sort_by_key(|item| (item.position.z, item.material_ref.clone()));
}
#[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>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Material {
pub bind_group: BindGroup,
pub render_pipeline: RenderPipelineRef,
pub texture_size: UVec2,
}
impl PartialOrd<Self> for Material {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.bind_group.cmp(&other.bind_group))
}
}
impl Ord for Material {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.bind_group.cmp(&other.bind_group)
}
}
pub type MaterialRef = Rc<Material>;
#[derive(Debug)]
pub struct Sprite {
pub atlas_rect: URect,
pub params: SpriteParams,
}
#[derive(Debug)]
pub struct TileMap {
pub tiles_data_grid_size: UVec2,
pub cell_count_size: UVec2,
pub one_cell_size: UVec2,
pub tiles: Vec<u16>,
}
pub type RenderPipelineRef = Rc<RenderPipeline>;
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)
}