diff --git a/crates/comet_renderer/Cargo.toml b/crates/comet_renderer/Cargo.toml index 69a46be..ed5463e 100644 --- a/crates/comet_renderer/Cargo.toml +++ b/crates/comet_renderer/Cargo.toml @@ -16,4 +16,4 @@ wgpu = { version = "22.0", features = ["glsl", "wgsl", "naga-ir"]} winit = { version = "0.29", features = ["rwh_05"] } chrono = "0.4.40" pollster = "0.3" -image = "0.24" +image = "0.24" \ No newline at end of file diff --git a/crates/comet_renderer/src/renderer2d.rs b/crates/comet_renderer/src/renderer2d.rs index 3cc06d0..f498023 100644 --- a/crates/comet_renderer/src/renderer2d.rs +++ b/crates/comet_renderer/src/renderer2d.rs @@ -1,12 +1,12 @@ +use crate::renderer::Renderer; use crate::{ camera::CameraManager, render_context::RenderContext, render_pass::{universal_clear_execute, universal_load_execute, RenderPass}, - renderer::Renderer, }; use comet_colors::Color; use comet_ecs::{Component, Render, Render2D, Transform2D}; -use comet_log::*; +use comet_log::{debug, error, info}; use comet_resources::{ font::Font, graphic_resource_manager::GraphicResourceManager, texture_atlas::*, Texture, Vertex, }; @@ -541,28 +541,13 @@ impl<'a> Renderer2D<'a> { } fn get_glyph_region(&self, glyph: char, font: String) -> &TextureRegion { - let key = format!("{}::{}", font, glyph); - - match self.resource_manager.font_atlas().textures().get(&key) { - Some(region) => region, - None => { - warn!( - "Missing glyph for character '{}' in font '{}', using fallback.", - glyph, font - ); - let fallback_key = format!("{}:: ", font); - self.resource_manager - .font_atlas() - .textures() - .get(&fallback_key) - .unwrap_or_else(|| { - panic!( - "No fallback glyph available (space also missing) for font '{}'", - font - ) - }) - } - } + let font_atlas = self + .resource_manager + .fonts() + .iter() + .find(|f| f.name() == font) + .unwrap(); + font_atlas.get_glyph(glyph).unwrap() } pub fn add_text_to_buffers( diff --git a/crates/comet_resources/Cargo.toml b/crates/comet_resources/Cargo.toml index b2ec333..0980ad6 100644 --- a/crates/comet_resources/Cargo.toml +++ b/crates/comet_resources/Cargo.toml @@ -5,12 +5,11 @@ edition = "2021" [dependencies] comet_log = { path = "../comet_log" } -wgpu = { version = "22.0", features = ["glsl", "wgsl", "naga-ir"] } +wgpu = { version = "22.0", features = ["spirv"] } anyhow = "1.0" bytemuck = { version = "1.16", features = [ "derive" ] } ab_glyph = "0.2.29" chrono = "0.4.40" -rect_packer = "0.2.1" [dependencies.image] version = "0.24" diff --git a/crates/comet_resources/src/graphic_resource_manager.rs b/crates/comet_resources/src/graphic_resource_manager.rs index a82c0bb..f092e0c 100644 --- a/crates/comet_resources/src/graphic_resource_manager.rs +++ b/crates/comet_resources/src/graphic_resource_manager.rs @@ -6,7 +6,10 @@ use crate::{ Texture, }; use comet_log::info; -use wgpu::{naga::ShaderStage, Device, Queue, ShaderModule}; +use wgpu::{ + naga::{self, ShaderStage}, + Device, Queue, ShaderModule, +}; pub struct GraphicResourceManager { texture_atlas: TextureAtlas, @@ -124,7 +127,7 @@ impl GraphicResourceManager { source: wgpu::ShaderSource::Glsl { shader: shader_source.into(), stage, - defines: Default::default(), + defines: naga::FastHashMap::default(), }, }) } else { diff --git a/crates/comet_resources/src/texture_atlas.rs b/crates/comet_resources/src/texture_atlas.rs index 95bfac7..0e558fc 100644 --- a/crates/comet_resources/src/texture_atlas.rs +++ b/crates/comet_resources/src/texture_atlas.rs @@ -1,7 +1,6 @@ use crate::font::*; use comet_log::*; -use image::{DynamicImage, GenericImage, GenericImageView, RgbaImage}; -use rect_packer::{Config, Packer, Rect}; +use image::{DynamicImage, GenericImage, GenericImageView}; use std::collections::HashMap; use std::path::Path; @@ -82,332 +81,335 @@ pub struct TextureAtlas { impl TextureAtlas { pub fn empty() -> Self { Self { - atlas: DynamicImage::new_rgba8(1, 1), + atlas: DynamicImage::new(1, 1, image::ColorType::Rgb8), textures: HashMap::new(), } } pub fn texture_paths(&self) -> Vec { - self.textures.keys().cloned().collect() + self.textures.keys().map(|k| k.to_string()).collect() } - #[inline(always)] - fn next_power_of_two(mut x: u32) -> u32 { - if x == 0 { - return 1; + fn calculate_atlas_width(textures: &Vec) -> u32 { + let mut last_height: u32 = textures.get(0).unwrap().height(); + let mut widths: Vec = Vec::new(); + let mut current_width: u32 = 0; + + for texture in textures { + if last_height != texture.height() { + widths.push(current_width); + current_width = 0; + last_height = texture.height(); + } + current_width += texture.width(); } - x -= 1; - x |= x >> 1; - x |= x >> 2; - x |= x >> 4; - x |= x >> 8; - x |= x >> 16; - x + 1 + + widths.push(current_width); + + *widths.iter().max().unwrap() } - fn pack_textures( - textures: &[(&String, &DynamicImage)], - padding: u32, - ) -> (u32, u32, HashMap) { - let mut atlas_size = 512; - let max_size = 8192; + fn calculate_atlas_height(textures: &Vec) -> u32 { + let last_height: u32 = textures.get(0).unwrap().height(); + let mut height: u32 = 0; + height += last_height; - let valid_textures: Vec<(String, DynamicImage)> = textures - .iter() - .map(|(name, tex)| { - let (w, h) = (tex.width(), tex.height()); - if w == 0 || h == 0 { - warn!( - "Texture '{}' has invalid size {}x{}, replacing with 1x1 transparent dummy.", - name, w, h - ); - let mut img = RgbaImage::new(1, 1); - img.put_pixel(0, 0, image::Rgba([0, 0, 0, 0])); - (name.to_string(), DynamicImage::ImageRgba8(img)) - } else { - ((*name).clone(), (*tex).clone()) - } - }) - .collect(); - - if valid_textures.is_empty() { - error!("No valid textures to pack!"); - return (0, 0, HashMap::new()); - } - - loop { - let config = Config { - width: atlas_size as i32, - height: atlas_size as i32, - border_padding: padding as i32, - rectangle_padding: padding as i32, - }; - - let mut packer = Packer::new(config); - let mut placements = HashMap::new(); - let mut max_x = 0i32; - let mut max_y = 0i32; - let mut failed = false; - - for (name, tex) in &valid_textures { - let width = tex.width() as i32; - let height = tex.height() as i32; - - if width > atlas_size as i32 || height > atlas_size as i32 { - error!( - "Texture '{}' is too large ({width}x{height}) for current atlas size {atlas_size}x{atlas_size}", - name - ); - failed = true; - break; - } - - if let Some(rect) = packer.pack(width, height, false) { - max_x = max_x.max(rect.x + rect.width); - max_y = max_y.max(rect.y + rect.height); - placements.insert(name.clone(), rect); - } else { - failed = true; - break; - } + for texture in textures { + if last_height == texture.height() { + continue; } - if failed { - if atlas_size >= max_size { - error!( - "Failed to pack all textures even at max atlas size ({}x{}).", - max_size, max_size - ); - return (max_x as u32, max_y as u32, placements); - } - - info!( - "Atlas size {}x{} too small, doubling to {}x{}...", - atlas_size, - atlas_size, - atlas_size * 2, - atlas_size * 2 - ); - atlas_size *= 2; - } else { - info!( - "Created texture atlas ({}x{}) with {} textures.", - atlas_size, - atlas_size, - placements.len() - ); - return (max_x as u32, max_y as u32, placements); - } + height += texture.height(); } + + height } - fn build_atlas( - textures: &[(&String, &DynamicImage)], - placements: &HashMap, - atlas_width: u32, - atlas_height: u32, - ) -> (RgbaImage, HashMap) { - let mut base = RgbaImage::new(atlas_width, atlas_height); - let mut regions = HashMap::new(); - - for (name, tex) in textures { - if let Some(rect) = placements.get(*name) { - base.copy_from(&tex.to_rgba8(), rect.x as u32, rect.y as u32) - .unwrap_or_else(|_| { - panic!( - "Failed to blit texture '{}' into atlas at ({}, {})", - name, rect.x, rect.y - ) - }); - - let u0 = rect.x as f32 / atlas_width as f32; - let v0 = rect.y as f32 / atlas_height as f32; - let u1 = (rect.x + rect.width) as f32 / atlas_width as f32; - let v1 = (rect.y + rect.height) as f32 / atlas_height as f32; - - regions.insert( - (*name).clone(), - TextureRegion::new( - u0, - v0, - u1, - v1, - (rect.width as u32, rect.height as u32), - 0.0, - 0.0, - 0.0, - ), - ); + fn insert_texture_at(base: &mut DynamicImage, texture: &DynamicImage, x_pos: u32, y_pos: u32) { + for y in 0..texture.height() { + for x in 0..texture.width() { + let pixel = texture.get_pixel(x, y); + base.put_pixel(x + x_pos, y + y_pos, pixel); } } - - (base, regions) } pub fn from_texture_paths(paths: Vec) -> Self { - let mut textures = Vec::new(); + let mut textures: Vec = Vec::new(); + let mut regions: HashMap = HashMap::new(); info!("Loading textures..."); + for path in &paths { - let img = image::open(Path::new(path)).expect("Failed to load texture"); - textures.push((path, img)); + textures.push(image::open(&Path::new(path.as_str())).expect("Failed to load texture")); } - info!("Packing textures..."); - let tex_refs: Vec<(&String, &DynamicImage)> = - textures.iter().map(|(p, i)| (*p, i)).collect(); + info!("Textures loaded!"); + info!("Sorting textures by height..."); - let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2); + let mut texture_path_pairs: Vec<(&DynamicImage, &String)> = + textures.iter().zip(paths.iter()).collect(); + texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height())); + let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) = + texture_path_pairs.into_iter().unzip(); + let sorted_textures: Vec = + sorted_textures.into_iter().map(|t| t.clone()).collect(); + let sorted_paths: Vec = sorted_paths.into_iter().map(|s| s.to_string()).collect(); - let atlas_w = Self::next_power_of_two(atlas_w); - let atlas_h = Self::next_power_of_two(atlas_h); - - let (base, regions) = Self::build_atlas(&tex_refs, &placements, atlas_w, atlas_h); - - info!( - "Created texture atlas ({}x{}) with {} textures.", - atlas_w, - atlas_h, - regions.len() + let (height, width) = ( + Self::calculate_atlas_height(&sorted_textures), + Self::calculate_atlas_width(&sorted_textures), ); + let mut base = DynamicImage::new_rgba8(width, height); + + let mut previous = sorted_textures.get(0).unwrap().height(); + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; + + info!("Creating texture atlas..."); + + for (texture, path) in sorted_textures.iter().zip(sorted_paths.iter()) { + if texture.height() != previous { + y_offset += previous; + x_offset = 0; + previous = texture.height(); + } + + Self::insert_texture_at(&mut base, &texture, x_offset, y_offset); + let texel_w = 0.5 / width as f32; + let texel_h = 0.5 / height as f32; + + let u0 = (x_offset as f32 + texel_w) / width as f32; + let v0 = (y_offset as f32 + texel_h) / height as f32; + let u1 = ((x_offset + texture.width()) as f32 - texel_w) / width as f32; + let v1 = ((y_offset + texture.height()) as f32 - texel_h) / height as f32; + + regions.insert( + path.to_string(), + TextureRegion::new(u0, v0, u1, v1, texture.dimensions(), 0.0, 0.0, 0.0), + ); + x_offset += texture.width(); + } + + info!("Texture atlas created!"); TextureAtlas { - atlas: DynamicImage::ImageRgba8(base), + atlas: base, textures: regions, } } pub fn from_textures(names: Vec, textures: Vec) -> Self { - assert_eq!( - names.len(), - textures.len(), - "Names and textures must have the same length." + let mut regions: HashMap = HashMap::new(); + + info!("Sorting textures by height..."); + + let mut texture_path_pairs: Vec<(&DynamicImage, &String)> = + textures.iter().zip(names.iter()).collect(); + texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height())); + let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) = + texture_path_pairs.into_iter().unzip(); + let sorted_textures: Vec = + sorted_textures.into_iter().map(|t| t.clone()).collect(); + let sorted_paths: Vec = sorted_paths.into_iter().map(|s| s.to_string()).collect(); + + let (height, width) = ( + Self::calculate_atlas_height(&sorted_textures), + Self::calculate_atlas_width(&sorted_textures), + ); + let mut base = DynamicImage::new_rgba8(width, height); + + let mut previous = sorted_textures.get(0).unwrap().height(); + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; + + info!("Creating texture atlas..."); + + for (texture, name) in sorted_textures.iter().zip(sorted_paths.iter()) { + if texture.height() != previous { + y_offset += previous; + x_offset = 0; + previous = texture.height(); + } + + Self::insert_texture_at(&mut base, &texture, x_offset, y_offset); + regions.insert( + name.to_string(), + TextureRegion::new( + x_offset as f32 / width as f32, + y_offset as f32 / height as f32, + (x_offset + texture.width()) as f32 / width as f32, + (y_offset + texture.height()) as f32 / height as f32, + texture.dimensions(), + 0.0, + 0.0, + 0.0, + ), + ); + x_offset += texture.width(); + } + + info!("Texture atlas created!"); + + TextureAtlas { + atlas: base, + textures: regions, + } + } + + pub fn from_glyphs(mut glyphs: Vec) -> Self { + glyphs.sort_by(|a, b| b.render.height().cmp(&a.render.height())); + + let height = Self::calculate_atlas_height( + &glyphs.iter().map(|g| g.render.clone()).collect::>(), + ); + let width = Self::calculate_atlas_width( + &glyphs.iter().map(|g| g.render.clone()).collect::>(), ); - let tex_refs: Vec<(&String, &DynamicImage)> = names.iter().zip(textures.iter()).collect(); + let padding = (glyphs.len() * 3) as u32; - let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2); - let atlas_w = Self::next_power_of_two(atlas_w); - let atlas_h = Self::next_power_of_two(atlas_h); - - let (base, regions) = Self::build_atlas(&tex_refs, &placements, atlas_w, atlas_h); - - TextureAtlas { - atlas: DynamicImage::ImageRgba8(base), - textures: regions, - } - } - - pub fn from_glyphs(glyphs: Vec) -> Self { - let textures: Vec<(String, DynamicImage)> = glyphs - .iter() - .map(|g| (g.name.clone(), g.render.clone())) - .collect(); - - let tex_refs: Vec<(&String, &DynamicImage)> = - textures.iter().map(|(n, i)| (n, i)).collect(); - - let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2); - let atlas_w = Self::next_power_of_two(atlas_w); - let atlas_h = Self::next_power_of_two(atlas_h); - - let mut base = RgbaImage::new(atlas_w, atlas_h); + let mut base = DynamicImage::new_rgba8(width + padding, height); let mut regions = HashMap::new(); + let mut current_row_height = glyphs[0].render.height(); + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; for g in glyphs.iter() { - if let Some(rect) = placements.get(&g.name) { - base.copy_from(&g.render.to_rgba8(), rect.x as u32, rect.y as u32) - .unwrap(); + let glyph_w = g.render.width(); + let glyph_h = g.render.height(); - let u0 = rect.x as f32 / atlas_w as f32; - let v0 = rect.y as f32 / atlas_h as f32; - let u1 = (rect.x + rect.width) as f32 / atlas_w as f32; - let v1 = (rect.y + rect.height) as f32 / atlas_h as f32; - - let region = TextureRegion::new( - u0, - v0, - u1, - v1, - (rect.width as u32, rect.height as u32), - g.advance, - g.offset_x, - g.offset_y, - ); - - regions.insert(g.name.clone(), region); + if glyph_h != current_row_height { + y_offset += current_row_height + 3; + x_offset = 0; + current_row_height = glyph_h; } + + Self::insert_texture_at(&mut base, &g.render, x_offset, y_offset); + + let u0 = x_offset as f32 / (width + padding) as f32; + let v0 = y_offset as f32 / height as f32; + let u1 = (x_offset + glyph_w) as f32 / (width + padding) as f32; + let v1 = (y_offset + glyph_h) as f32 / height as f32; + + let region = TextureRegion::new( + u0, + v0, + u1, + v1, + (glyph_w, glyph_h), + g.advance, + g.offset_x, + g.offset_y, + ); + + regions.insert(g.name.clone(), region); + + x_offset += glyph_w + 3; } TextureAtlas { - atlas: DynamicImage::ImageRgba8(base), + atlas: base, textures: regions, } } - pub fn from_fonts(fonts: &[Font]) -> Self { + pub fn from_fonts(fonts: &Vec) -> Self { if fonts.is_empty() { return Self::empty(); } - let mut all_glyphs = Vec::new(); + let mut all_glyphs: Vec<(String, DynamicImage, TextureRegion)> = Vec::new(); - for font in fonts { + let mut font_indices: Vec = (0..fonts.len()).collect(); + font_indices.sort_by(|&a, &b| fonts[a].name().cmp(&fonts[b].name())); + + for fi in font_indices { + let font = &fonts[fi]; let font_name = font.name(); - let src_atlas = font.glyphs().atlas(); - let atlas_width = src_atlas.width(); - let atlas_height = src_atlas.height(); - for (glyph_name, region) in font.glyphs().textures() { - let src_x = (region.u0() * atlas_width as f32) as u32; - let src_y = (region.v0() * atlas_height as f32) as u32; - let width = region.dimensions().0; - let height = region.dimensions().1; + let mut glyph_names: Vec = font.glyphs().textures().keys().cloned().collect(); + glyph_names.sort(); - let glyph_img = src_atlas.view(src_x, src_y, width, height).to_image(); + for glyph_name in glyph_names { + let region = font.glyphs().textures().get(&glyph_name).unwrap(); + + let (u0, v0) = (region.u0(), region.v0()); + let (width, height) = region.dimensions(); + + let src_x = (u0 * font.glyphs().atlas().width() as f32) as u32; + let src_y = (v0 * font.glyphs().atlas().height() as f32) as u32; + + let glyph_img = DynamicImage::ImageRgba8( + font.glyphs() + .atlas() + .view(src_x, src_y, width, height) + .to_image(), + ); let key = format!("{}::{}", font_name, glyph_name); - all_glyphs.push((key, DynamicImage::ImageRgba8(glyph_img), region.clone())); + + all_glyphs.push((key, glyph_img, region.clone())); } } - let tex_refs: Vec<(&String, &DynamicImage)> = - all_glyphs.iter().map(|(n, i, _)| (n, i)).collect(); - let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2); - let atlas_w = Self::next_power_of_two(atlas_w); - let atlas_h = Self::next_power_of_two(atlas_h); + all_glyphs.sort_by(|a, b| { + let ha = a.1.height(); + let hb = b.1.height(); + match hb.cmp(&ha) { + std::cmp::Ordering::Equal => a.0.cmp(&b.0), + other => other, + } + }); - let mut base = RgbaImage::new(atlas_w, atlas_h); + let textures: Vec = + all_glyphs.iter().map(|(_, img, _)| img.clone()).collect(); + let atlas_height = Self::calculate_atlas_height(&textures); + let atlas_width = Self::calculate_atlas_width(&textures); + + let padding = (all_glyphs.len() * 3) as u32; + let mut base = DynamicImage::new_rgba8(atlas_width + padding, atlas_height); let mut regions = HashMap::new(); + let mut current_row_height = textures[0].height(); + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; + for (key, img, original_region) in all_glyphs { - if let Some(rect) = placements.get(&key) { - base.copy_from(&img.to_rgba8(), rect.x as u32, rect.y as u32) - .unwrap(); + let w = img.width(); + let h = img.height(); - let u0 = rect.x as f32 / atlas_w as f32; - let v0 = rect.y as f32 / atlas_h as f32; - let u1 = (rect.x + rect.width) as f32 / atlas_w as f32; - let v1 = (rect.y + rect.height) as f32 / atlas_h as f32; - - regions.insert( - key, - TextureRegion::new( - u0, - v0, - u1, - v1, - (rect.width as u32, rect.height as u32), - original_region.advance(), - original_region.offset_x(), - original_region.offset_y(), - ), - ); + if h != current_row_height { + y_offset += current_row_height + 3; + x_offset = 0; + current_row_height = h; } + + Self::insert_texture_at(&mut base, &img, x_offset, y_offset); + + let u0 = x_offset as f32 / (atlas_width + padding) as f32; + let v0 = y_offset as f32 / atlas_height as f32; + let u1 = (x_offset + w) as f32 / (atlas_width + padding) as f32; + let v1 = (y_offset + h) as f32 / atlas_height as f32; + + let region = TextureRegion::new( + u0, + v0, + u1, + v1, + (w, h), + original_region.advance(), + original_region.offset_x(), + original_region.offset_y(), + ); + + regions.insert(key, region); + + x_offset += w + 3; } TextureAtlas { - atlas: DynamicImage::ImageRgba8(base), + atlas: base, textures: regions, } } diff --git a/res/shaders/base2d.wgsl b/res/shaders/base2d.wgsl new file mode 100644 index 0000000..79a3aa0 --- /dev/null +++ b/res/shaders/base2d.wgsl @@ -0,0 +1,42 @@ +// Vertex shader +struct CameraUniform { + view_proj: mat4x4, +}; +@group(1) @binding(0) // 1. +var camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) tex_coords: vec2, + @location(2) color: vec4, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.color = model.color; + out.clip_position = camera.view_proj * vec4(model.position, 1.0); + return out; +} + +// Fragment shader + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let sample_color = textureSample(t_diffuse, s_diffuse, in.tex_coords); + return sample_color * in.color; +} \ No newline at end of file diff --git a/res/shaders/blacknwhite.wgsl b/res/shaders/blacknwhite.wgsl new file mode 100644 index 0000000..dac0caf --- /dev/null +++ b/res/shaders/blacknwhite.wgsl @@ -0,0 +1,39 @@ +struct CameraUniform { + view_proj: mat4x4, +}; +@group(1) @binding(0) +var camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) tex_coords: vec2, + @location(2) color: vec4, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.color = model.color; + out.clip_position = camera.view_proj * vec4(model.position, 1.0); + return out; +} + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let color = textureSample(t_diffuse, s_diffuse, in.tex_coords); + return vec4((color.r + color.g + color.b) / 3.0, (color.r + color.g + color.b) / 3.0, (color.r + color.g + color.b) / 3.0, color.a); +} \ No newline at end of file diff --git a/res/shaders/crt.wgsl b/res/shaders/crt.wgsl new file mode 100644 index 0000000..cd20a11 --- /dev/null +++ b/res/shaders/crt.wgsl @@ -0,0 +1,68 @@ +// Vertex shader +struct CameraUniform { + view_proj: mat4x4, +}; +@group(1) @binding(0) // 1. +var camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) tex_coords: vec2, + @location(2) color: vec4, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.color = model.color; + out.clip_position = camera.view_proj * vec4(model.position, 1.0); + return out; +} + +@group(0) @binding(0) +var t_diffuse: texture_2d; // Diffuse texture +@group(0) @binding(1) +var s_diffuse: sampler; // Sampler for the texture + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + // Sample the texture using the input texture coordinates + var texColor: vec4 = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + // Apply CRT curvature effect (distortion) + let center = vec2(0.5, 0.5); // center of the screen + let dist = distance(in.tex_coords, center); // Distance from the center + let curvature = 0.1; // Adjust for more or less curvature + var distorted_uv = in.tex_coords + (in.tex_coords - center) * curvature * dist; + + // Apply chromatic aberration by slightly offsetting the UV coordinates + let redOffset = 0.005; + let greenOffset = 0.0; + let blueOffset = -0.005; + + let redColor = textureSample(t_diffuse, s_diffuse, distorted_uv + vec2(redOffset, 0.0)); + let greenColor = textureSample(t_diffuse, s_diffuse, distorted_uv + vec2(greenOffset, 0.0)); + let blueColor = textureSample(t_diffuse, s_diffuse, distorted_uv + vec2(blueOffset, 0.0)); + + // Combine chromatic aberration with the original color + texColor.r = redColor.r; + texColor.g = greenColor.g; + texColor.b = blueColor.b; + + // Apply scanline effect (darken even rows for scanlines) + let scanlineEffect = 0.1 * (sin(in.clip_position.y * 0.3) + 1.0); // Horizontal scanlines + texColor.r *= scanlineEffect; // Apply scanline effect to red channel + texColor.g *= scanlineEffect; // Apply scanline effect to green channel + texColor.b *= scanlineEffect; // Apply scanline effect to blue channel + + return texColor; +} diff --git a/res/shaders/glitch.wgsl b/res/shaders/glitch.wgsl new file mode 100644 index 0000000..d4c2534 --- /dev/null +++ b/res/shaders/glitch.wgsl @@ -0,0 +1,89 @@ +// Vertex shader +struct CameraUniform { + view_proj: mat4x4, +}; +@group(1) @binding(0) // 1. +var camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) tex_coords: vec2, + @location(2) color: vec4, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) color: vec4, +} + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.color = model.color; + out.clip_position = camera.view_proj * vec4(model.position, 1.0); + return out; +} + +@group(0) @binding(0) +var t_diffuse: texture_2d; // Diffuse texture +@group(0) @binding(1) +var s_diffuse: sampler; // Sampler for the texture + +// A simple pseudo-random number generator (using fragment coordinates) +fn rand2D(p: vec2) -> f32 { + let s = sin(dot(p, vec2(12.9898, 78.233))); // Pseudorandom calculation + return fract(s * 43758.5453); // Return value between 0 and 1 +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + // Sample the texture using the input texture coordinates + var texColor: vec4 = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + // Random horizontal displacement for glitching effect + let glitchStrength = 0.03; // How far the texture will "glitch" + let glitchAmount = rand2D(in.tex_coords * 100.0); // Use a different scale for more variation + var displacedUV = in.tex_coords + vec2(glitchAmount * glitchStrength, 0.0); + + // Sample the texture at the displaced position + texColor = textureSample(t_diffuse, s_diffuse, displacedUV); + + // Apply random color flickering + let colorFlickerAmount = rand2D(in.tex_coords * 50.0); // More frequency for faster flickering + texColor.r *= mix(0.7, 1.3, colorFlickerAmount); // Randomly adjust red channel brightness + texColor.g *= mix(0.7, 1.3, colorFlickerAmount); // Randomly adjust green channel brightness + texColor.b *= mix(0.7, 1.3, colorFlickerAmount); // Randomly adjust blue channel brightness + + // Occasionally "flicker" the texture to simulate complete signal loss + let flickerChance = rand2D(in.tex_coords * 200.0); // Different scale for randomness + if (flickerChance < 0.05) { // 5% chance to completely "flicker" out + texColor.r = 0.0; // Turn red channel to black + texColor.g = 0.0; // Turn green channel to black + texColor.b = 0.0; // Turn blue channel to black + } + + // Apply random horizontal offset to simulate screen tearing + let tearEffect = rand2D(in.tex_coords * 25.0); // Vary this value for different effects + if (tearEffect < 0.15) { + texColor.r = 1.0; // Simulate red "tear" + texColor.g = 0.0; // No green in the tear + texColor.b = 0.0; // No blue in the tear + } else if (tearEffect < 0.3) { + texColor.r = 0.0; // No red in the tear + texColor.g = 1.0; // Simulate green "tear" + texColor.b = 0.0; // No blue in the tear + } + + // Optionally, you can add a "flickering noise" layer for additional effect + let noiseAmount = rand2D(in.tex_coords * 500.0); // Highly random noise for flickering + texColor.r += noiseAmount * 0.1; // Add small amount of random noise to red channel + texColor.g += noiseAmount * 0.1; // Add small amount of random noise to green channel + texColor.b += noiseAmount * 0.1; // Add small amount of random noise to blue channel + + // Return the altered texture color + return texColor; +}