Skip to content

Instantly share code, notes, and snippets.

@liquidev
Created January 7, 2020 15:57
Show Gist options
  • Save liquidev/63537227809e282e88acdb58ba21574a to your computer and use it in GitHub Desktop.
Save liquidev/63537227809e282e88acdb58ba21574a to your computer and use it in GitHub Desktop.
A lava lamp effect in OpenGL, using an alpha threshold shader.
import math
import os
import parseopt
import random
import strutils
import rapid/gfx
import rapid/res/textures
import rapid/res/images
import rapid/gfx/fxsurface
#--
# CLI
#--
const Help = """
lava_lamp
=========
usage: lava_lamp [options]
options:
-h --help this
-b --bg|background:<color> background color
-f --fg|foreground:<color> foreground color
--threshold:<float> alpha threshold
--smooth:<float> alpha smoothing
-c --spawnChance:<float> blob spawn chance per frame
-S --size:<range> blob size
-s --speed:<range> blob speed
syntax:
dec = {'0'..'9'}
hex = dec + {'a'..'f', 'A'..'F'}
float = dec '.' dec
color = hex{6}
range = float '..' float
"""
var
colBg = rgb(219, 84, 97)
colFg = rgb(229, 115, 105)
threshold = 0.4
smooth = 0.025
spawnChance = 0.02
size = 32.0..128.0
speed = 0.5..1.0
proc parseColor(input: string): RColor =
result = rgb(parseHexInt(input[0..1]),
parseHexInt(input[2..3]),
parseHexInt(input[4..5]))
proc parseRange(input: string): Slice[float] =
let elem = input.split("..")
result = elem[0].parseFloat..elem[1].parseFloat
for kind, key, val in getopt(commandLineParams()):
case kind
of cmdShortOption, cmdLongOption:
case key
of "h", "help": quit(Help, 0)
of "b", "bg", "background": colBg = parseColor(val)
of "f", "fg", "foreground": colFg = parseColor(val)
of "threshold": threshold = parseFloat(val)
of "smooth": smooth = parseFloat(val)
of "c", "spawnChance": spawnChance = parseFloat(val)
of "S", "size": size = parseRange(val)
of "s", "speed": speed = parseRange(val)
else: quit("invalid option kind")
#--
# definitions
#--
type
Blob* = object
pos, vel: Vec2[float]
radius: float
const
AlphaThreshold = """
uniform float threshold;
uniform float smooth;
vec4 rEffect(vec2 pos) {
vec4 pix = rPixel(pos);
float a = smoothstep(threshold - smooth, threshold + smooth, pix.r);
return vec4(a, a, a, a);
}
"""
#--
# the implementation
#--
# generate the blob texture
const BlobResolution = 128
var blobData: seq[uint8]
func gaussian2d(x, y, a, ox, oy, sx, sy: float): float =
## 2D gaussian curve function.
a * exp(-((x - ox) ^ 2 / (2 * sx ^ 2) + (y - oy) ^ 2 / (2 * sy ^ 2)))
for py in 0..<BlobResolution:
for px in 0..<BlobResolution:
let
x = px / BlobResolution * 2 - 1
y = py / BlobResolution * 2 - 1
alphaf = gaussian2d(x, y, a = 1.0, ox = 0.0, oy = 0.0, sx = 0.3, sy = 0.3)
alphai = uint8(alphaf * 255.0)
blobData.add([255'u8, 255, 255, alphai])
# allocate all the resources
var
win = initRWindow()
.size(800, 600)
.title("lava_lamp")
.open()
surface = win.openGfx()
fx = newRFxSurface(surface.canvas)
fxAlphaThreshold = fx.newREffect(AlphaThreshold)
texBlob = newRTexture(BlobResolution, BlobResolution, blobData[0].addr)
var
blobs: seq[Blob]
# start blobbin'
randomize()
surface.loop:
init ctx:
fxAlphaThreshold.param("threshold", threshold)
fxAlphaThreshold.param("smooth", smooth)
draw ctx, step:
ctx.clear(colBg)
fx.begin(ctx)
ctx.begin()
ctx.texture = texBlob
for blob in blobs:
ctx.rect(blob.pos.x - blob.radius / 2, blob.pos.y - blob.radius / 2,
blob.radius, blob.radius)
ctx.draw()
ctx.noTexture()
fx.effect(fxAlphaThreshold)
ctx.color = colFg
fx.finish()
update step:
if rand(1.0) < spawnChance:
let
radius = rand(size)
pos = vec2(rand(0.0..surface.width), surface.height + radius / 2)
vel = vec2(0.0, -rand(speed))
blob = Blob(pos: pos, vel: vel, radius: radius)
blobs.add(blob)
for blob in blobs.mitems:
blob.pos += blob.vel
var garbage: seq[int]
for i, blob in blobs:
if blob.pos.y + blob.radius < 0:
garbage.add(i)
for n in countdown(garbage.len - 1, 0):
blobs.delete(garbage[n])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment