//! A more sophisticated example of how to use shaders //! and canvas's to do 2D GPU shadows. //! //! Sadly, it doesn't work as intended yet. (see the original ggez example for comparison) //! If you know how to fix it, please speak up! Your help is very welcome. extern crate good_web_game as ggez; use ggez::event; use ggez::graphics::{ self, set_uniforms, BlendMode, Canvas, Color, DrawParam, Drawable, Shader, ShaderId, ShaderMeta, UniformBlockLayout, UniformDesc, UniformType, }; use ggez::miniquad; use ggez::timer; use ggez::{Context, GameResult}; use glam::{Vec2, Vec4}; // Define the input struct for our shader. fn shader_meta() -> ShaderMeta { ShaderMeta { images: vec!["t_Texture".to_string()], uniforms: UniformBlockLayout { uniforms: vec![ UniformDesc::new("u_LightColor", UniformType::Float4), UniformDesc::new("u_ShadowColor", UniformType::Float4), UniformDesc::new("u_Pos", UniformType::Float2), UniformDesc::new("u_ScreenSize", UniformType::Float2), UniformDesc::new("u_Glow", UniformType::Float1), UniformDesc::new("u_Strength", UniformType::Float1), UniformDesc::new("_padding", UniformType::Int2), // `Projection` always comes last, as it is appended by gwg internally UniformDesc::new("Projection", UniformType::Mat4), ], }, } } use bytemuck_derive::{Pod, Zeroable}; #[repr(C)] #[derive(Copy, Clone, Zeroable, Pod)] // The attributes above ensure that this struct derives bytemuck::Pod, // which is necessary for it to be passed to `graphics::set_uniforms`. // Luckily glam implemented Pod for these types so we can simply derive it. pub struct Light { pub u_light_color: Vec4, pub u_shadow_color: Vec4, pub u_pos: Vec2, pub u_screen_size: Vec2, pub u_glow: f32, pub u_strength: f32, pub _padding: u64, } /// Shader source for calculating a 1D shadow map that encodes half distances /// in the red channel. The idea is that we scan X rays (X is the horizontal /// size of the output) and calculate the distance to the nearest pixel at that /// angle that has transparency above a threshold. The distance gets halved /// and encoded in the red channel (it is halved because if the distance can be /// greater than 1.0 - think bottom left to top right corner, that sqrt(1) and /// will not get properly encoded). const OCCLUSIONS_SHADER_SOURCE: &[u8] = b"#version 150 core uniform sampler2D t_Texture; in vec2 v_Uv; out vec4 Target0; layout (std140) uniform Light { vec4 u_LightColor; vec4 u_ShadowColor; vec2 u_Pos; vec2 u_ScreenSize; float u_Glow; float u_Strength; vec2 _padding; }; void main() { float dist = 1.0; float theta = radians(v_Uv.x * 360.0); vec2 dir = vec2(cos(theta), sin(theta)); for(int i = 0; i < 1024; i++) { float fi = i; float r = fi / 1024.0; vec2 rel = r * dir; vec2 p = clamp(u_Pos+rel, 0.0, 1.0); if (texture(t_Texture, p).a > 0.8) { dist = distance(u_Pos, p) * 0.5; break; } } float others = dist == 1.0 ? 0.0 : dist; Target0 = vec4(dist, others, others, 1.0); } "; const VERTEX_SHADER_SOURCE: &[u8] = include_bytes!("../resources/basic_150.glslv"); /// Shader for drawing shadows based on a 1D shadow map. It takes current /// fragment coordinates and converts them to polar coordinates centered /// around the light source, using the angle to sample from the 1D shadow map. /// If the distance from the light source is greater than the distance of the /// closest reported shadow, then the output is the shadow color, else it calculates some /// shadow based on the distance from light source based on strength and glow /// uniform parameters. const SHADOWS_SHADER_SOURCE: &[u8] = b"#version 150 core uniform sampler2D t_Texture; in vec2 v_Uv; out vec4 Target0; layout (std140) uniform Light { vec4 u_LightColor; vec4 u_ShadowColor; vec2 u_Pos; vec2 u_ScreenSize; float u_Glow; float u_Strength; vec2 _padding; }; void main() { vec2 coord = gl_FragCoord.xy / u_ScreenSize; vec2 rel = coord - u_Pos; float theta = atan(rel.y, rel.x); float ox = degrees(theta) / 360.0; if (ox < 0) { ox += 1.0; } float r = length(rel); float occl = texture(t_Texture, vec2(ox, 0.5)).r * 2.0; float intensity = 1.0; if (r < occl) { vec2 g = u_ScreenSize / u_ScreenSize.y; float p = u_Strength + u_Glow; float d = distance(g * coord, g * u_Pos); intensity = 1.0 - clamp(p/(d*d), 0.0, 1.0); } Target0 = mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(u_ShadowColor.rgb, 1.0), intensity); } "; /// Shader for drawing lights based on a 1D shadow map. It takes current /// fragment coordinates and converts them to polar coordinates centered /// around the light source, using the angle to sample from the 1D shadow map. /// If the distance from the light source is greater than the distance of the /// closest reported shadow, then the output is black, else it calculates some /// light based on the distance from light source based on strength and glow /// uniform parameters. It is meant to be used additively for drawing multiple /// lights. const LIGHTS_SHADER_SOURCE: &[u8] = b"#version 150 core uniform sampler2D t_Texture; in vec2 v_Uv; out vec4 Target0; layout (std140) uniform Light { vec4 u_LightColor; vec4 u_ShadowColor; vec2 u_Pos; vec2 u_ScreenSize; float u_Glow; float u_Strength; vec2 _padding; }; void main() { vec2 coord = gl_FragCoord.xy / u_ScreenSize; vec2 rel = coord - u_Pos; float theta = atan(rel.y, rel.x); float ox = degrees(theta) / 360.0; if (ox < 0) { ox += 1.0; } float r = length(rel); float occl = texture(t_Texture, vec2(ox, 0.5)).r * 2.0; float intensity = 0.0; if (r < occl) { vec2 g = u_ScreenSize / u_ScreenSize.y; float p = u_Strength + u_Glow; float d = distance(g * coord, g * u_Pos); intensity = clamp(p/(d*d), 0.0, 0.6); } Target0 = mix(vec4(0.0, 0.0, 0.0, 1.0), vec4(u_LightColor.rgb, 1.0), intensity); } "; struct MainState { background: graphics::Image, tile: graphics::Image, text: graphics::Text, torch: Light, static_light: Light, foreground: Canvas, occlusions: Canvas, shadows: Canvas, lights: Canvas, occlusions_shader: ShaderId, shadows_shader: ShaderId, lights_shader: ShaderId, } /// The color cast things take when not illuminated const AMBIENT_COLOR: [f32; 4] = [0.25, 0.22, 0.34, 1.0]; /// The default color for the static light const STATIC_LIGHT_COLOR: [f32; 4] = [0.37, 0.69, 0.75, 1.0]; /// The default color for the mouse-controlled torch const TORCH_COLOR: [f32; 4] = [0.80, 0.73, 0.44, 1.0]; /// The number of rays to cast to. Increasing this number will result in better /// quality shadows. If you increase too much you might hit some GPU shader /// hardware limits. const LIGHT_RAY_COUNT: u16 = 1440; /// The strength of the light - how far it shines const LIGHT_STRENGTH: f32 = 0.0035; /// The factor at which the light glows - just for fun const LIGHT_GLOW_FACTOR: f32 = 0.0001; /// The rate at which the glow effect oscillates const LIGHT_GLOW_RATE: f32 = 50.0; impl MainState { fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { let background = graphics::Image::new(ctx, quad_ctx, "/bg_top.png")?; let tile = graphics::Image::new(ctx, quad_ctx, "/tile.png")?; let text = { let font = graphics::Font::new(ctx, "/LiberationMono-Regular.ttf")?; graphics::Text::new(("SHADOWS...", font, 48.0)) }; let screen_size = { let size = graphics::drawable_size(quad_ctx); [size.0 as f32, size.1 as f32] }; let torch = Light { u_light_color: Vec4::from(TORCH_COLOR), u_shadow_color: Vec4::from(AMBIENT_COLOR), u_pos: [0.0, 0.0].into(), u_screen_size: Vec2::from(screen_size), u_glow: 0.0, u_strength: LIGHT_STRENGTH, _padding: 0, }; let (w, h) = graphics::drawable_size(quad_ctx); let (x, y) = (100.0 / w as f32, 1.0 - 75.0 / h as f32); let static_light = Light { u_pos: [x, y].into(), u_light_color: Vec4::from(STATIC_LIGHT_COLOR), u_shadow_color: Vec4::from(AMBIENT_COLOR), u_screen_size: Vec2::from(screen_size), u_glow: 0.0, u_strength: LIGHT_STRENGTH, _padding: 0, }; let foreground = Canvas::with_window_size(ctx, quad_ctx)?; let occlusions = Canvas::new(ctx, quad_ctx, LIGHT_RAY_COUNT, 1)?; let mut shadows = Canvas::with_window_size(ctx, quad_ctx)?; // The shadow map will be drawn on top using the multiply blend mode shadows.set_blend_mode(Some(BlendMode::Multiply)); let mut lights = Canvas::with_window_size(ctx, quad_ctx)?; // The light map will be drawn on top using the add blend mode lights.set_blend_mode(Some(BlendMode::Add)); let occlusions_shader = Shader::from_u8( ctx, quad_ctx, VERTEX_SHADER_SOURCE, OCCLUSIONS_SHADER_SOURCE, shader_meta(), None, ) .unwrap(); let shadows_shader = Shader::from_u8( ctx, quad_ctx, VERTEX_SHADER_SOURCE, SHADOWS_SHADER_SOURCE, shader_meta(), None, ) .unwrap(); let lights_shader = Shader::from_u8( ctx, quad_ctx, VERTEX_SHADER_SOURCE, LIGHTS_SHADER_SOURCE, shader_meta(), Some(BlendMode::Add), ) .unwrap(); Ok(MainState { background, tile, text, torch, static_light, foreground, occlusions, shadows, lights, occlusions_shader, shadows_shader, lights_shader, }) } fn render_light( &mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext, light: Light, origin: DrawParam, canvas_origin: DrawParam, ) -> GameResult { let size = graphics::drawable_size(quad_ctx); // Now we want to run the occlusions shader to calculate our 1D shadow // distances into the `occlusions` canvas. graphics::set_canvas(ctx, Some(&self.occlusions)); { let _shader_lock = graphics::use_shader(ctx, self.occlusions_shader); set_uniforms(ctx, self.occlusions_shader, light); graphics::draw(ctx, quad_ctx, &self.foreground, canvas_origin)?; } // Now we render our shadow map and light map into their respective // canvases based on the occlusion map. These will then be drawn onto // the final render target using appropriate blending modes. graphics::set_canvas(ctx, Some(&self.shadows)); { let _shader_lock = graphics::use_shader(ctx, self.shadows_shader); let param = origin.scale(Vec2::new( (size.0 as f32) / (LIGHT_RAY_COUNT as f32), size.1 as f32, )); set_uniforms(ctx, self.shadows_shader, light); graphics::draw(ctx, quad_ctx, &self.occlusions, param)?; } graphics::set_canvas(ctx, Some(&self.lights)); { let _shader_lock = graphics::use_shader(ctx, self.lights_shader); let param = origin.scale(Vec2::new( (size.0 as f32) / (LIGHT_RAY_COUNT as f32), size.1 as f32, )); set_uniforms(ctx, self.lights_shader, light); graphics::draw(ctx, quad_ctx, &self.occlusions, param)?; } Ok(()) } } impl event::EventHandler for MainState { fn update( &mut self, ctx: &mut Context, _quad_ctx: &mut miniquad::GraphicsContext, ) -> GameResult { if timer::ticks(ctx) % 100 == 0 { println!("Average FPS: {}", timer::fps(ctx)); } self.torch.u_glow = LIGHT_GLOW_FACTOR * ((timer::ticks(ctx) as f32) / LIGHT_GLOW_RATE).cos(); self.static_light.u_glow = LIGHT_GLOW_FACTOR * ((timer::ticks(ctx) as f32) / LIGHT_GLOW_RATE * 0.75).sin(); Ok(()) } fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { let origin = DrawParam::new() .dest(Vec2::new(0.0, 0.0)) .scale(Vec2::new(0.5, 0.5)); let canvas_origin = DrawParam::new(); // First thing we want to do it to render all the foreground items (that // will have shadows) onto their own Canvas (off-screen render). We will // use this canvas to: // - run the occlusions shader to determine where the shadows are // - render to screen once all the shadows are calculated and rendered { graphics::set_canvas(ctx, Some(&self.foreground)); graphics::clear(ctx, quad_ctx, graphics::Color::new(0.0, 0.0, 0.0, 0.0)); graphics::draw( ctx, quad_ctx, &self.tile, DrawParam::new().dest(Vec2::new(598.0, 124.0)), )?; graphics::draw( ctx, quad_ctx, &self.tile, DrawParam::new().dest(Vec2::new(92.0, 350.0)), )?; graphics::draw( ctx, quad_ctx, &self.tile, DrawParam::new().dest(Vec2::new(442.0, 468.0)).rotation(0.5), )?; graphics::draw(ctx, quad_ctx, &self.text, (Vec2::new(50.0, 200.0),))?; } // Then we draw our light and shadow maps { let torch = self.torch; let light = self.static_light; graphics::set_canvas(ctx, Some(&self.lights)); graphics::clear(ctx, quad_ctx, graphics::Color::new(0.0, 0.0, 0.0, 1.0)); graphics::set_canvas(ctx, Some(&self.shadows)); graphics::clear(ctx, quad_ctx, graphics::Color::new(0.0, 0.0, 0.0, 1.0)); self.render_light(ctx, quad_ctx, torch, origin, canvas_origin)?; self.render_light(ctx, quad_ctx, light, origin, canvas_origin)?; } // Now lets finally render to screen starting out with background, then // the shadows and lights overtop and finally our foreground. graphics::set_canvas(ctx, None); graphics::clear(ctx, quad_ctx, Color::WHITE); graphics::draw(ctx, quad_ctx, &self.background, DrawParam::default())?; graphics::draw(ctx, quad_ctx, &self.shadows, DrawParam::default())?; graphics::draw(ctx, quad_ctx, &self.foreground, DrawParam::default())?; graphics::draw(ctx, quad_ctx, &self.lights, DrawParam::default())?; // Uncomment following line to visualize the 1D occlusions canvas, // red pixels represent angles at which no shadows were found, and then // the greyscale pixels are the half distances of the nearest shadows to // the mouse position (equally encoded in all color channels). // graphics::draw(ctx, &self.occlusions, DrawParam::default())?; graphics::present(ctx, quad_ctx)?; Ok(()) } fn mouse_motion_event( &mut self, _ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext, x: f32, y: f32, _xrel: f32, _yrel: f32, ) { let (w, h) = graphics::drawable_size(quad_ctx); let (x, y) = (x / w as f32, 1.0 - y / h as f32); self.torch.u_pos = Vec2::from([x, y]); } } pub fn main() -> GameResult { ggez::start( ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), ) }