Animated Plasma in Love2D — Tutorial with Calculation Explanations
Screen recording of the actual program:
-- main.lua (plasma using ImageData)
local img, texture, w, h
local t = 0
function love.load()
w, h = 400, 300
img = love.image.newImageData(w, h)
texture = love.graphics.newImage(img)
end
local function plasma(x, y, time)
local nx = (x / w) - 0.5
local ny = (y / h) - 0.5
local v = 0
v = v + math.sin((nx * 10) + time)
v = v + math.sin((ny * 10) - time * 0.8)
v = v + math.sin((nx * 5 + ny * 5) + time * 0.6)
v = v + math.sin(math.sqrt(nx*nx + ny*ny) * 20 - time)
v = v / 4
return v
end
function love.update(dt)
t = t + dt * 1.2
img:mapPixel(function(x, y, r, g, b, a)
local v = plasma(x - 1, y - 1, t) -- ImageData coords start at 1
local cr = 0.5 + 0.5 * math.cos(3 + v * math.pi + 0.0)
local cg = 0.5 + 0.5 * math.cos(1 + v * math.pi + 2.0)
local cb = 0.5 + 0.5 * math.cos(2 + v * math.pi + 4.0)
return cr, cg, cb, 1
end)
texture:replacePixels(img)
end
function love.draw()
local ww, wh = love.graphics.getDimensions()
love.graphics.draw(texture, 0, 0, 0, ww / w, wh / h)
love.graphics.setColor(1,1,1)
love.graphics.print("plasma - size "..w.."x"..h.." FPS: "..love.timer.getFPS(), 10, 10)
end
Pixel to normalized coordinates nx ny
What — nx and ny map integer pixel indices into a -0.5..+0.5 range so center is near zero.
Why — Using a centered coordinate system simplifies using radial functions and creates symmetric patterns around the screen center.
Scaling factors in sin arguments
What — multipliers like 10, 5, and 20 control spatial frequency (how many bands or ripples fit across the screen).
Interpretation — Larger multiplier => more cycles over the same area => finer detail; smaller multiplier => broader, smoother blobs.
Combining and averaging
What — We sum several sin terms and divide by 4 to keep the result roughly in -1..1.
Why — Averaging prevents values from going beyond the expected range and makes color mapping more stable.
Color mapping using cos with offsets
What — The scalar v is fed into cos(... + offset) for each channel with different offsets to separate R G B phases.
Why — Different phase offsets cause colors to shift independently as v changes, producing colorful gradients instead of grayscale waves.
Stepwise Summary of the Math
- Normalize coordinates — Convert pixel coords to a centered, scale‑invariant space so math behaves the same across resolutions.
- Apply multiple wave functions — Use sines on different linear combinations of coordinates and on radial distance to produce varied motifs.
- Animate with time — Add a time-dependent phase to make waves move and evolve.
- Combine and normalize — Sum layers and scale to keep the value in a predictable range.
- Map scalar to RGB — Use cosine-based palettes or palette lookups to turn scalar values into smooth, pleasing colors.
Experiment Ideas
- Make one layer respond to mouse position by offsetting p before computing a term.
- Use different time multipliers on each sine term for rich temporal variation.
- Create a palette texture in the shader and sample it using value to get exact color sets.
- Try mixing sin and cos with noise for a less periodic, more organic plasma.