Refactor game window and shader management; add fog shader implementation and update configuration settings

This commit is contained in:
2026-03-29 04:44:55 +00:00
parent 3dbeadf976
commit 2064232226
6 changed files with 296 additions and 171 deletions
+92
View File
@@ -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);
}
+5
View File
@@ -0,0 +1,5 @@
#version 120
void main() {
gl_Position = gl_Vertex;
}
+3 -76
View File
@@ -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
+1 -91
View File
@@ -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
+13 -4
View File
@@ -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
+182
View File
@@ -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