Last active
February 26, 2025 22:21
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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