From 206423222679640a26456a2e950230830284ebd4 Mon Sep 17 00:00:00 2001 From: Daanish Date: Sun, 29 Mar 2026 04:44:55 +0000 Subject: [PATCH] Refactor game window and shader management; add fog shader implementation and update configuration settings --- assets/shaders/fog.frag | 92 +++++++++++++++++++ assets/shaders/fog.vert | 5 ++ game/scene.rb | 79 +---------------- main.rb | 92 +------------------ settings/configuration.rb | 17 +++- window.rb | 182 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 296 insertions(+), 171 deletions(-) create mode 100644 assets/shaders/fog.frag create mode 100644 assets/shaders/fog.vert create mode 100644 window.rb diff --git a/assets/shaders/fog.frag b/assets/shaders/fog.frag new file mode 100644 index 0000000..ff88e4a --- /dev/null +++ b/assets/shaders/fog.frag @@ -0,0 +1,92 @@ +#version 120 + +uniform vec2 player_pos; +uniform vec2 resolution; // 720x480 +uniform vec2 offset; // padding in pixels +uniform float scale; // scale factor +uniform vec2 torch_lights[32]; +uniform int num_torches; +uniform float time_sec; + +// Simplex 2D noise +// Source: https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83 +vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } + +float snoise(vec2 v){ + const vec4 C = vec4(0.211324865405187, 0.366025403784439, + -0.577350269189626, 0.024390243902439); + vec2 i = floor(v + dot(v, C.yy) ); + vec2 x0 = v - i + dot(i, C.xx); + vec2 i1; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + i = mod(i, 289.0); + vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + + i.x + vec3(0.0, i1.x, 1.0 )); + vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), + dot(x12.zw,x12.zw)), 0.0); + m = m*m ; + m = m*m ; + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} + +void main() { + vec2 virtual_coord = (gl_FragCoord.xy - offset) / scale; + + if (virtual_coord.x < 0.0 || virtual_coord.x > resolution.x || + virtual_coord.y < 0.0 || virtual_coord.y > resolution.y) { + return; + } + + float p_i_radius = 100.0; + float p_o_radius = 340.0; + float t_i_radius = 10.0; + float t_o_radius = 160.0; + + // player is always at screen center in virtual coords + vec2 player_screen = vec2(resolution.x / 2.0, resolution.y / 2.0); + + float light_strength = 0.0; + + // Player light + float d = distance(virtual_coord, player_screen); + if (d < p_i_radius) { + light_strength = 1.0; + } else if (d < p_o_radius) { + float t = (d - p_i_radius) / (p_o_radius - p_i_radius); + light_strength = 1.0 - t; + } + + // Torch lights + for (int i = 0; i < num_torches; i++) { + float td = distance(virtual_coord, torch_lights[i]); + float strength = 0.0; + if (td < t_i_radius) { + strength = 1.0; + } else if (td < t_o_radius) { + float t = (td - t_i_radius) / (t_o_radius - t_i_radius); + strength = 1.0 - t; + } + light_strength += strength; + } + + light_strength = clamp(light_strength, 0.0, 1.0); + + // Noise + vec2 world_coord = (virtual_coord + player_pos - resolution / 2.0) * 0.005; + float noise = (snoise(world_coord + vec2(time_sec / 5.0, time_sec / 6.0)) + 1.0) / 2.0; + + float t = 1.0 - light_strength; + float alpha = clamp(t * (1.0 + noise), 0.0, 1.0); + + gl_FragColor = vec4(0.0, 0.0, 0.0, alpha); +} \ No newline at end of file diff --git a/assets/shaders/fog.vert b/assets/shaders/fog.vert new file mode 100644 index 0000000..7a7123e --- /dev/null +++ b/assets/shaders/fog.vert @@ -0,0 +1,5 @@ +#version 120 + +void main() { + gl_Position = gl_Vertex; +} \ No newline at end of file diff --git a/game/scene.rb b/game/scene.rb index cf94f09..6a79afc 100644 --- a/game/scene.rb +++ b/game/scene.rb @@ -37,8 +37,8 @@ class Game < Scene @character.draw @enemies.draw @props.draw - @hud.draw draw_fog! if $bus.get(:settings, :fog) + @hud.draw draw_debug! if $bus.get(:settings, :debug) end @@ -49,83 +49,10 @@ class Game < Scene end def draw_fog! - cam_x, cam_y = @camera - cell = 10 - p_i_radius = 80.0 - p_o_radius = 350.0 - t_i_radius = 10.0 - o_radius = 160.0 + $bus.emit(:shade) + return - # Player screen position - px = SCREEN_SIZE[0] / 2.0 - py = SCREEN_SIZE[1] / 2.0 - # Collect torch positions relative to camera - torch_lights = [] - # Only calculate torch lights if the setting is enabled, to save performance - if $bus.get(:settings, :torches_lightup) - torches = $bus.get(:nearby_torches, - [cam_x - SCREEN_SIZE[0] / 2, cam_y - SCREEN_SIZE[1] / 2, SCREEN_SIZE[0] * 2, SCREEN_SIZE[1] * 2] - ) || [] - torch_lights = torches.map { |t| [t[0] - cam_x, t[1] - cam_y] } - end - - tiles_x = (SCREEN_SIZE[0] / cell) + 2 - tiles_y = (SCREEN_SIZE[1] / cell) + 2 - - # This part is a shader but due to time constraints, I'm doing it on the CPU. - # If done on the GPU, we could have much better performance. - # and also make it more fine-grained and smoother gradients. - # doing it on the GPU would require better knowledge of the ruby openGL bindings and shader programming, - # which I don't have right now. - - tiles_y.times do |j| - tiles_x.times do |i| - sx = i * cell - sy = j * cell - - light_strength = 0.0 - - d = Math.sqrt((px - sx)**2 + (py - sy)**2) - if d < p_i_radius - light_strength = 1.0 - elsif d < p_o_radius - t = (d - p_i_radius) / (p_o_radius - p_i_radius) # 0..1 - light_strength = 1.0 - t - end - - torch_lights.each do |lx, ly| - d = Math.sqrt((sx - lx)**2 + (sy - ly)**2) - - if d < t_i_radius - strength = 1.0 - elsif d < o_radius - t = (d - t_i_radius) / (o_radius - t_i_radius) # 0..1 - strength = 1.0 - t - else - strength = 0.0 - end - - light_strength += strength - end - - light_strength = light_strength.clamp(0.0, 1.0) - - t = 1.0 - light_strength - - world_x = (sx + cam_x) * 0.005 - world_y = (sy + cam_y) * 0.005 - - noise = (@noise[ - world_x + Gosu.milliseconds / 5000.0, - world_y + Gosu.milliseconds / 6000.0 - ] + 1.0) / 2.0 - - alpha = (t * (1.0 + noise) * 255).to_i.clamp(0, 255) - - Gosu.draw_rect(sx, sy, cell, cell, Gosu::Color.new(alpha, 0, 0, 0), 10000 + sy) - end - end end def draw_floor diff --git a/main.rb b/main.rb index d6b5a7f..fe7d47b 100644 --- a/main.rb +++ b/main.rb @@ -1,97 +1,7 @@ -require 'gosu' - require_relative 'utils/bus' $bus = EventBus.new -require_relative 'settings/configuration' -require_relative 'utils/utils' -require_relative 'utils/scene' -require_relative 'settings/scene' -require_relative 'menu/scene' -require_relative 'game/scene' +require_relative 'window' # Main window -class Window < Gosu::Window - def initialize - super *SCREEN_SIZE, resizable: true - self.caption = "Mist" - - @scene = Menu.new - @last_time = Gosu.milliseconds - - $bus.on(:quit_game) { close! } - - $bus.on(:mouse_pos) do - next mouse_relative(mouse_x, mouse_y) - end - - $bus.on(:change_scene) do |new_scene| - @scene.close - @scene = new_scene - end - end - - def button_down(id) - @scene.button_down(id, mouse_relative(mouse_x, mouse_y)) - end - - def update - now = Gosu.milliseconds - dt = (now - @last_time) / 1000.0 - @last_time = now - - @scene.update(dt) - end - - def compute_transform - scale_x = width.to_f / SCREEN_SIZE[0] - scale_y = height.to_f / SCREEN_SIZE[1] - - scale = [scale_x, scale_y].min - - if scale_x < scale_y - offset_x = 0 - offset_y = (height - SCREEN_SIZE[1] * scale) / 2 - else - offset_x = (width - SCREEN_SIZE[0] * scale) / 2 - offset_y = 0 - end - - [scale, offset_x, offset_y] - end - - def mouse_relative(sx, sy) - scale, offset_x, offset_y = compute_transform - - virtual_x = (sx - offset_x) / scale - virtual_y = (sy - offset_y) / scale - - return nil if virtual_x < 0 || virtual_x > SCREEN_SIZE[0] || - virtual_y < 0 || virtual_y > SCREEN_SIZE[1] - - return [virtual_x, virtual_y] - end - - def draw - scale, offset_x, offset_y = compute_transform - - Gosu.translate(offset_x, offset_y) do - Gosu.scale(scale) do - @scene.draw - end - end - - padding_color = Gosu::Color::BLACK - if offset_y > 0 - # Letterbox: top and bottom - Gosu.draw_rect(0, 0, width, offset_y, padding_color, Float::INFINITY) - Gosu.draw_rect(0, height - offset_y, width, offset_y, padding_color, Float::INFINITY) - else - # Pillarbox: left and right - Gosu.draw_rect(0, 0, offset_x, height, padding_color, Float::INFINITY) - Gosu.draw_rect(width - offset_x, 0, offset_x, height, padding_color, Float::INFINITY) - end - end -end - Window.new.show \ No newline at end of file diff --git a/settings/configuration.rb b/settings/configuration.rb index e5fbfd3..b1d8839 100644 --- a/settings/configuration.rb +++ b/settings/configuration.rb @@ -8,16 +8,25 @@ module Config @config = { debug: ARGV.include?("--debug"), fog: true, - torches_lightup: false + torches_lightup: true, } - - $bus.on(:settings) do |k| - next @config[k] + + if File.exist?("config.json") + data = JSON.parse(File.read("config.json")) + @config.merge!(data.transform_keys(&:to_sym)) end + $bus.on(:settings) { |k| @config[k] } + module_function def toggle(key) @config[key] = !@config[key] end + + def persist! + File.open("config.json", "w") do |f| + f.write(JSON.pretty_generate(@config)) + end + end end diff --git a/window.rb b/window.rb new file mode 100644 index 0000000..6dbb870 --- /dev/null +++ b/window.rb @@ -0,0 +1,182 @@ +require 'opengl' +require 'glu' +require 'gosu' + +require_relative 'settings/configuration' +require_relative 'utils/utils' +require_relative 'utils/scene' +require_relative 'settings/scene' +require_relative 'menu/scene' +require_relative 'game/scene' + +class Window < Gosu::Window + def initialize + super *SCREEN_SIZE, resizable: true + + GL.load_lib() + GLU.load_lib() + + @shader = create_shader + + self.caption = "Mist" + + @scene = Menu.new + @last_time = Gosu.milliseconds + + $bus.on(:quit_game) { close! } + + $bus.on(:mouse_pos) do + next mouse_relative(mouse_x, mouse_y) + end + + $bus.on(:change_scene) do |new_scene| + @scene.close + @scene = new_scene + end + + $bus.on(:shade) do + shade! + end + end + + def create_shader + vertex_src = File.read("assets/shaders/fog.vert") + fragment_src = File.read("assets/shaders/fog.frag") + + vs = GL.CreateShader(GL::VERTEX_SHADER) + GL.ShaderSource(vs, 1, [vertex_src].pack("p"), [vertex_src.size].pack("L")) + GL.CompileShader(vs) + + fs = GL.CreateShader(GL::FRAGMENT_SHADER) + GL.ShaderSource(fs, 1, [fragment_src].pack("p"), [fragment_src.size].pack("L")) + GL.CompileShader(fs) + + program = GL.CreateProgram() + GL.AttachShader(program, vs) + GL.AttachShader(program, fs) + GL.LinkProgram(program) + + program + end + + def shade! + scale, offset_x, offset_y = compute_transform + + # As the gl object only works in the Window class we need to use the bus to bring all the data here. + # This should otherwise be a shader object on (and controlled by) the game scene. + + cam_x, cam_y = $bus.get(:camera_pos) || [0, 0] + torch_lights = [] + # Only calculate torch lights if the setting is enabled, to save performance + if $bus.get(:settings, :torches_lightup) + torches = $bus.get(:nearby_torches, + [cam_x - SCREEN_SIZE[0] / 2, cam_y - SCREEN_SIZE[1] / 2, SCREEN_SIZE[0] * 2, SCREEN_SIZE[1] * 2] + ) || [] + torch_lights = torches.map { |t| [t[0] - cam_x, SCREEN_SIZE[1] - (t[1] - cam_y)] }.flatten + end + + torch_lights = torch_lights.first(32) + torch_count = torch_lights.size / 2 + torch_lights += [0.0] * (32 - torch_lights.size) + + gl do + GL.UseProgram(@shader) + GL.Enable(GL::BLEND) + GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA) + + # Pass virtual resolution + res_loc = GL.GetUniformLocation(@shader, "resolution") + GL.Uniform2f(res_loc, SCREEN_SIZE[0], SCREEN_SIZE[1]) + + # Pass padding offset and scale + offset_loc = GL.GetUniformLocation(@shader, "offset") + GL.Uniform2f(offset_loc, offset_x, offset_y) + + scale_loc = GL.GetUniformLocation(@shader, "scale") + GL.Uniform1f(scale_loc, scale) + + player_loc = GL.GetUniformLocation(@shader, "player_pos") + GL.Uniform2f(player_loc, SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2) + + torches_loc = GL.GetUniformLocation(@shader, "torch_lights") + GL.Uniform2fv(torches_loc, torch_lights.size / 2, torch_lights.pack("f*")) + + num_torches_loc = GL.GetUniformLocation(@shader, "num_torches") + GL.Uniform1i(num_torches_loc, torch_count) + + time_loc = GL.GetUniformLocation(@shader, "time_sec") + GL.Uniform1f(time_loc, Gosu.milliseconds / 1000.0) + + GL.Begin(GL::QUADS) + GL.Vertex2f(-1, -1) + GL.Vertex2f( 1, -1) + GL.Vertex2f( 1, 1) + GL.Vertex2f(-1, 1) + GL.End + + GL.UseProgram(0) + end + end + + def button_down(id) + @scene.button_down(id, mouse_relative(mouse_x, mouse_y)) + end + + def update + now = Gosu.milliseconds + dt = (now - @last_time) / 1000.0 + @last_time = now + + @scene.update(dt) + end + + def compute_transform + scale_x = width.to_f / SCREEN_SIZE[0] + scale_y = height.to_f / SCREEN_SIZE[1] + + scale = [scale_x, scale_y].min + + if scale_x < scale_y + offset_x = 0 + offset_y = (height - SCREEN_SIZE[1] * scale) / 2 + else + offset_x = (width - SCREEN_SIZE[0] * scale) / 2 + offset_y = 0 + end + + [scale, offset_x, offset_y] + end + + def mouse_relative(sx, sy) + scale, offset_x, offset_y = compute_transform + + virtual_x = (sx - offset_x) / scale + virtual_y = (sy - offset_y) / scale + + return nil if virtual_x < 0 || virtual_x > SCREEN_SIZE[0] || + virtual_y < 0 || virtual_y > SCREEN_SIZE[1] + + return [virtual_x, virtual_y] + end + + def draw + scale, offset_x, offset_y = compute_transform + + Gosu.translate(offset_x, offset_y) do + Gosu.scale(scale) do + @scene.draw + end + end + + padding_color = Gosu::Color::BLACK + if offset_y > 0 + # Letterbox: top and bottom + Gosu.draw_rect(0, 0, width, offset_y, padding_color, Float::INFINITY) + Gosu.draw_rect(0, height - offset_y, width, offset_y, padding_color, Float::INFINITY) + else + # Pillarbox: left and right + Gosu.draw_rect(0, 0, offset_x, height, padding_color, Float::INFINITY) + Gosu.draw_rect(width - offset_x, 0, offset_x, height, padding_color, Float::INFINITY) + end + end +end \ No newline at end of file