Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Last active February 26, 2025 22:21
Show Gist options
  • Save amirrajan/e6737284dc727a954af373f4e596a1c5 to your computer and use it in GitHub Desktop.
Save amirrajan/e6737284dc727a954af373f4e596a1c5 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Shader Tech Demo https://www.youtube.com/watch?v=LZgvvU91yyI
class Game
attr_gtk
def tick
defaults
render
input
calc
end
def defaults
state.keeper.max_speed = 3
state.keeper.acceleration = 0.05
state.keeper.x ||= 659
state.keeper.y ||= 260
state.keeper.w ||= 191
state.keeper.h ||= 156
state.keeper.speed ||= 0
state.keeper.angle ||= 0
state.keeper.action ||= :idle
state.keeper.sprite_data ||= load_keeper_sprite_data
state.queues.ripples ||= []
state.lilies ||= 30.map { random_lily_pads }
end
def render
outputs.shader_path = "shaders/shader.glsl"
outputs.shader_tex1 = :water_displacement
outputs.shader_tex2 = :water_mask
outputs.shader_tex3 = :water
args.audio[:bg] ||= {
name: :bg,
input: "sounds/10L.ogg",
gain: 0.0,
looping: true
}
args.audio.each do |k, v|
if v[:gain] < 1.0
v[:gain] += 0.01
end
end
outputs.background_color = [0, 0, 0]
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/background.png" }
render_moving_clouds
render_land
render_willow_tree
render_ripples
render_lilies
render_keeper
render_horus
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/fog.png" }
render_shader_uniforms
end
def render_shader_uniforms
outputs[:water].background_color = [255, 255, 255, 0]
outputs[:water].w = 1280
outputs[:water].h = 720
outputs[:water].transient!
outputs[:water].sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/background.png" }
outputs[:water].sprites << { x: state.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" }
outputs[:water].sprites << { x: -1280 + state.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" }
outputs[:water].sprites << state.lilies.flatten.map { |l| l.merge(x: l.x, y: l.y, a: 45) }
keeper_sprite = state.keeper.sprite_data[state.keeper.angle.round]
boat_sprite_path = "sprites/boat/#{keeper_sprite.boat_sprite_path}"
outputs[:water].sprites << { x: state.keeper.x,
y: state.keeper.y,
w: state.keeper.w,
h: state.keeper.h,
path: boat_sprite_path,
flip_horizontally: keeper_sprite.boat_sprite_flip,
angle: keeper_sprite.boat_delta_angle,
angle_anchor_x: 0.5,
angle_anchor_y: 0.5, r: 0, g: 0, b: 0, blendmode_enum: 1 }
outputs[:water_mask].background_color = [255, 255, 255, 0]
outputs[:water_mask].w = 1280
outputs[:water_mask].h = 720
outputs[:water_mask].transient!
outputs[:water_mask].primitives << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/water-mask.png" }
index = 0.frame_index(count: 15, hold_for: 10, repeat: true)
outputs[:water_mask].primitives << { x: 325, y: 150, w: 134 / 2, h: 163 / 2, path: "sprites/horus/#{index}.png", r: 0, g: 0, b: 0 }
keeper_animation_frame = (state.keeper.move_at || 0).frame_index count: 30, hold_for: 8, repeat: true
boat_sprite_path = "sprites/boat/#{keeper_sprite.boat_sprite_path}"
outputs[:water_mask].primitives << { x: state.keeper.x,
y: state.keeper.y,
w: state.keeper.w,
h: state.keeper.h,
path: boat_sprite_path,
flip_horizontally: keeper_sprite.boat_sprite_flip,
angle: keeper_sprite.boat_delta_angle,
angle_anchor_x: 0.5,
angle_anchor_y: 0.5, r: 0, g: 0, b: 0, blendmode_enum: 1 }
outputs[:water_mask].primitives << { x: state.keeper.x,
y: state.keeper.y,
w: 191,
h: 156,
path: "sprites/keeper/#{state.keeper.action}/#{keeper_sprite.keeper_sprite_angle}-#{keeper_animation_frame}.png",
flip_horizontally: keeper_sprite.keeper_sprite_flip,
angle_anchor_x: 0.5,
angle_anchor_y: 0.5, r: 0, g: 0, b: 0, blendmode_enum: 1 }
outputs[:water_mask].primitives << state.lilies.flatten.map { |l| l.merge(r: 0, g: 0, b: 0) }
szw = 1000
szh = 1000
args.outputs[:scrolling_water].background_color = [255, 255, 255, 255]
args.outputs[:scrolling_water].w = 2 * szw
args.outputs[:scrolling_water].h = 2 * szh
args.outputs[:scrolling_water].transient!
easing_perc = (args.state.tick_count % 6000) / 6000
sprite_props = { w: szw, h: szh, path: "sprites/water-displacement.png", a: 255 }
tile_count = (((szw * 2).idiv szw) * 3)
args.outputs[:scrolling_water].sprites << tile_count.flat_map do |i|
tile_count.map do |j|
[
{ x: szw * (j - tile_count.idiv(2) - 1) + szw * easing_perc + szw * (i - 1),
y: szh * easing_perc + szh * (i - tile_count.idiv(2)),
**sprite_props, a: 128 },
{ x: (2 * szw) - (szw * (j - tile_count.idiv(2) - 1) + szw * easing_perc + szw * (i - 1)),
y: szh * easing_perc + szh * (i - tile_count.idiv(2)),
**sprite_props, a: 128 },
]
end
end
outputs[:water_displacement].background_color = [255, 255, 255, 0]
outputs[:water_displacement].w = 1280
outputs[:water_displacement].h = 720
outputs[:water_displacement].transient!
outputs[:water_displacement].primitives << { x: 640, y: 380, w: 1440, h: 1440, path: :scrolling_water, anchor_x: 0.5, anchor_y: 0.5, a: 128 }
outputs[:water_displacement].primitives << state.queues.ripples.map { |r| r.merge(path: "sprites/ripple-displacement.png") }
end
def render_moving_clouds
outputs.sprites << { x: state.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" }
outputs.sprites << { x: -1280 + state.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" }
end
def render_land
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/land.png" }
render_frame sprite_directory: "sprites/dock-shadow", frame_count: 11
render_frame sprite_directory: "sprites/grass-land", frame_count: 15
render_frame sprite_directory: "sprites/grass-cliff", frame_count: 16
render_frame sprite_directory: "sprites/dock-water", frame_count: 6
render_frame sprite_directory: "sprites/cliff-water", frame_count: 11
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/house.png" }
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "sprites/vignette.png" }
end
def render_willow_tree
index = 0.frame_index(count: 24, hold_for: 4, repeat: true)
outputs.sprites << { x: 75, y: 455, w: 250 / 1.5, h: 350 / 1.5, path: "sprites/tree_yellow.png" }
outputs.sprites << { x: 30, y: 450, w: 393 / 1.5, h: 409 / 1.5, path: "sprites/willowtree/#{index}.png" }
end
def render_ripples
outputs.sprites << state.queues.ripples
end
def render_lilies
outputs.sprites << state.lilies
end
def render_keeper
keeper_sprite = state.keeper.sprite_data[state.keeper.angle.round]
boat_sprite_path = "sprites/boat/#{keeper_sprite.boat_sprite_path}"
keeper_animation_frame = (state.keeper.move_at || 0).frame_index count: 30, hold_for: 8, repeat: true
outputs.sprites << { x: state.keeper.x,
y: state.keeper.y,
w: state.keeper.w,
h: state.keeper.h,
path: boat_sprite_path,
flip_horizontally: keeper_sprite.boat_sprite_flip,
angle: keeper_sprite.boat_delta_angle,
angle_anchor_x: 0.5,
angle_anchor_y: 0.5 }
outputs.sprites << { x: state.keeper.x,
y: state.keeper.y,
w: 191,
h: 156,
path: "sprites/keeper/#{state.keeper.action}/#{keeper_sprite.keeper_sprite_angle}-#{keeper_animation_frame}.png",
flip_horizontally: keeper_sprite.keeper_sprite_flip,
angle_anchor_x: 0.5,
angle_anchor_y: 0.5 }
end
def render_horus
index = 0.frame_index(count: 15, hold_for: 10, repeat: true)
outputs.sprites << { x: 325, y: 150, w: 134 / 2, h: 163 / 2, path: "sprites/horus/#{index}.png" }
end
def input
if inputs.keyboard.key_down.r
$gtk.reset_next_tick
end
state.keeper.angle -= inputs.left_right
state.keeper.speed *= 0.99
if inputs.up
state.keeper.action = :moving
state.keeper.move_at ||= state.tick_count
state.keeper.speed += state.keeper.acceleration
else
state.keeper.action = :idle
state.keeper.move_at = nil
end
end
def render_frame sprite_directory:, frame_count:;
index = 0.frame_index(count: frame_count, hold_for: 6, repeat: true)
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: "#{sprite_directory}/#{index}.png", a: 255 }
end
def calc
calc_keeper
calc_lilies
calc_ripples
end
def calc_keeper
state.keeper.angle = state.keeper.angle % 360
state.keeper.angle = 360 - state.keeper.angle if state.keeper.angle < 0
state.keeper.speed = state.keeper.speed.clamp 0, state.keeper.max_speed
state.keeper.x += state.keeper.angle.vector_x state.keeper.speed
state.keeper.y += state.keeper.angle.vector_y state.keeper.speed
top_edge = 720
bottom_edge = 0 - state.keeper.w
left_edge = 550
right_edge = 1280 - state.keeper.w
if state.keeper.x > right_edge
state.keeper.x = right_edge
elsif state.keeper.x < left_edge
state.keeper.x = left_edge
end
if state.keeper.y > top_edge
state.keeper.y = bottom_edge
elsif state.keeper.y < bottom_edge
state.keeper.y = top_edge
end
end
def calc_lilies
state.lilies.each do |groups|
groups.each do |l|
g = 100
m1 = 10
m2 = 5
selected_point = { x: state.keeper.x + 96, y: state.keeper.y + 78 }
angle = geometry.angle_to selected_point, l
boat_vector = angle.to_vector
d = geometry.distance selected_point, l
origin_d = geometry.distance l[:origin], l
origin_angle = geometry.angle_from l[:origin], l
k = 0.03
if d != 0
f = (g * m1 * m2).fdiv(d**2)
if f > 10
f = 5
end
s = origin_d * k
dx = (boat_vector.x * f) + (origin_angle.vector_x * s)
dy = (boat_vector.y * f) + (origin_angle.vector_y * s)
l.x += dx
l.y += dy
magnitude = (dx + dy).abs
if magnitude >= 0.5
front_ripple_rect = {
x: l.x + l.w / 2,
y: l.y + l.h / 2,
w: 5,
h: 5
}
zm = if magnitude > 3
5
elsif magnitude > 2
10
elsif magnitude > 1
15
else
30
end
if state.tick_count.zmod?(zm)
state.queues.ripples << front_ripple_rect.merge(path: "sprites/ripples/ripple-0-front.png",
angle_anchor_x: 0.5,
angle_anchor_y: 0.5,
anchor_x: 0.5,
anchor_y: 0.5,
angle: -180,
dw: 0.5 * magnitude,
dh: 0.5 * magnitude,
da: 1,
dx: 0,
dy: 0)
end
end
end
end
end
end
def calc_ripples
if state.keeper.speed.round > 0 && state.tick_count.zmod?(20)
queue_ripples
end
state.queues.ripples.each do |r|
r.dw ||= 2
r.dh ||= 2
r.da ||= 2
r.a ||= 255
if r.w > 100
r.a -= r.da.abs
end
r.w += r.dw
r.h += r.dh
if !r.anchor_x
r.x -= r.dw / 2
end
if !r.anchor_y
r.y -= r.dh / 2
end
r.x += r.dx
r.y += r.dy
r.dx *= 0.95
r.dy *= 0.95
end
state.queues.ripples.reject! { |r| r.a < 0 }
end
def load_keeper_sprite_data
contents = gtk.read_file "app/keeper-sprites.txt"
sprite_data = {}
contents.each_line do |l|
tokens = l.split ","
angle = tokens[0].strip.to_i
sprite_data[angle] = {
angle: tokens[0].strip.to_i,
keeper_sprite_angle: tokens[1].strip.to_i,
keeper_sprite_flip: tokens[2].strip == "true",
boat_sprite_path: tokens[3].strip,
boat_sprite_flip: tokens[4].strip == "true",
boat_delta_angle: tokens[5].strip.to_i
}
end
sprite_data
end
def random_lily_pads
big_lily = random_big_lily
small_lily_1 = random_small_lily big_lily
small_lily_2 = random_small_lily big_lily
lotus = random_lotus big_lily
[big_lily, small_lily_1, small_lily_2, lotus].reject_nil
end
def random_big_lily
x = 600 * rand + 600
y = 600 * rand + 30
big_lily_1 = { path: 'sprites/lilies/lilypad1.png', w: 30, h: 25 }
big_lily_2 = { path: 'sprites/lilies/lilypad3.png', w: 42, h: 26 }
[big_lily_1, big_lily_2].sample.merge(x: x,
y: y,
origin: [x, y])
end
def random_small_lily big_lily
small_lily_1 = { path: 'sprites/lilies/lilypad2.png', w: 22, h: 17 }
small_lily_2 = { path: 'sprites/lilies/lilypad4.png', w: 26, h: 20 }
small_lily = [small_lily_1, small_lily_2].sample
place_lily_nearby big_lily, small_lily
end
def random_lotus big_lily
if rand > 0.65
flower = { path: 'sprites/lilies/lotus.png', w: 37, h: 33 }
place_lily_nearby big_lily, flower
elsif rand > 0.5
lotuspod1 = { path: 'sprites/lilies/lotuspod1.png', w: 18, h: 63 }
lotuspod2 = { path: 'sprites/lilies/lotuspod2.png', w: 16, h: 32 }
place_lily_nearby big_lily, [lotuspod1, lotuspod2].sample
end
end
def place_lily_nearby big_lily, small_lily
small_lily_distance = -5
coin_flip = rand
small_lily_origin = if coin_flip > 0.75
{ x: big_lily.right + small_lily_distance,
y: big_lily.top + small_lily_distance }
elsif coin_flip > 0.5
{ x: big_lily.right + small_lily_distance,
y: big_lily.bottom - small_lily_distance - small_lily.h }
elsif coin_flip > 0.25
{ x: big_lily.left - small_lily_distance - small_lily.w,
y: big_lily.top }
else
{ x: big_lily.left - small_lily_distance - small_lily.w,
y: big_lily.bottom - small_lily_distance - small_lily.h }
end
small_lily.merge(x: small_lily_origin.x,
y: small_lily_origin.y,
origin: { x: small_lily_origin.x, y: small_lily_origin.y })
end
def queue_ripples
front_ripple_rect = {
x: state.keeper.x,
y: state.keeper.y,
w: 20,
h: 17
}.center_inside_rect(state.keeper)
front_ripple_rect.x += state.keeper.angle.vector_x * (state.keeper.w / 4)
front_ripple_rect.y += state.keeper.angle.vector_y * (state.keeper.h / 4)
state.queues.ripples << front_ripple_rect.merge(path: "sprites/ripples/ripple-0-front.png",
angle_anchor_x: 0.5,
angle_anchor_y: 0.5,
angle: state.keeper.angle - 180,
dx: state.keeper.angle.vector_x * state.keeper.speed,
dy: state.keeper.angle.vector_y * state.keeper.speed)
end
end
def tick args
$game ||= Game.new
$game.args = args
$game.tick
end
uniform sampler2D tex0;
uniform sampler2D tex1; // water displacement
uniform sampler2D tex2; // water mask
uniform sampler2D tex3; // water
uniform float mouse_coord_y;
uniform int tick_count;
varying vec2 v_texCoord;
void noop() {
gl_FragColor = texture2D(tex0, v_texCoord);
}
void water() {
vec4 displacement = texture2D(tex1, v_texCoord);
float displacment_magnitude = texture2D(tex2, v_texCoord).r;
float displacement_threshold = 0.2;
if (displacment_magnitude <= displacement_threshold) {
displacment_magnitude = 0.0;
}
vec2 distortedCoords = vec2(v_texCoord.x + ((1.0 - displacement.r) * 0.1),
v_texCoord.y + ((1.0 - displacement.r) * 0.1));
if (displacment_magnitude <= displacement_threshold) {
gl_FragColor = texture2D(tex0, v_texCoord);
} else {
gl_FragColor = texture2D(tex3, distortedCoords);
if (1.0 - displacement.r > 0.9) {
gl_FragColor.r = gl_FragColor.r + 0.2;
gl_FragColor.g = gl_FragColor.g + 0.2;
gl_FragColor.b = gl_FragColor.b + 0.5;
}
}
}
void main() {
water();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment