Re: Code Doodles!
Posted: Wed Mar 06, 2024 7:00 am
Another quick doodle. This time soft-body physics with Verlet Integration. The gif didn't come out great but it's quite fun to play with.
You can grab and drag any particle with the mouse. F1 resets the cloth, F2 toggles a fan that blows from the side and F3 toggles colour.
I'm pretty sure I could have done this faster and easier with love.physics but I wanted to try my hand at implementing it from scratch.
You can grab and drag any particle with the mouse. F1 resets the cloth, F2 toggles a fan that blows from the side and F3 toggles colour.
I'm pretty sure I could have done this faster and easier with love.physics but I wanted to try my hand at implementing it from scratch.
Code: Select all
-- Disable output buffer so debug messages print in real time
io.stdout:setvbuf("no")
function love.load()
love.window.setTitle("Verlet Physics")
particles = {}
sticks = {}
screenWidth = love.graphics.getWidth()
screenHeight = love.graphics.getHeight()
gravity = 900
drag = 0.03
heldParticle = -1
fan = 0
colourMode = true
createGrid()
end
-- simple hsv to rgb converter
function hsv2rgb(h, s, v)
h = h / 60
local c = s * v
local x = c * (1 - math.abs(h % 2 - 1))
if h < 1 then
return {c, x, 0}
elseif h < 2 then
return {x, c, 0}
elseif h < 3 then
return {0, c, x}
elseif h < 4 then
return {0, x, c}
elseif h < 5 then
return {x, 0, c}
else
return {c, 0, x}
end
end
-- function to create the initial sheet of particles and springs
function createGrid()
-- clear any existing particles
particles = {}
sticks = {}
local fixed, col
local dx = 0
for x = 100, 700, 10 do
dx = 0
-- set colour just based on x
col = hsv2rgb(((x - 100) / 600) * 360, 0.8, 0.8)
for y = 50, 90 do
fixed = false
-- fix a few particles in the top row
if y == 50 and x % 50 == 0 then
fixed = true
end
addParticle(x + dx, y, fixed, col)
-- small dx so the particles fall slightly offest
dx = dx + 1
-- add spring above
if y > 50 then
addStick(particles[#particles], particles[#particles - 1], 10, true, col)
end
-- add spring to left
if x > 100 then
addStick(particles[#particles], particles[#particles - 41], 10, true, col)
end
end
end
end
-- keypressed (f1 resets the cloth, f2 toggles the "fan", f3 toggles colour)
function love.keypressed(key)
if key == "f1" then
createGrid()
end
if key == "f2" then
if fan ~= 0 then
fan = 0
else
fan = math.random(50, 200)
end
end
if key == "f3" then
colourMode = not colourMode
end
end
-- mousepressed, get a close particle and grab it
function love.mousepressed(x, y, button)
if button == 1 then
local dx, dy
for i, p in ipairs(particles) do
dx = math.abs(x - p.x)
dy = math.abs(y - p.y)
if dx < 6 and dy < 6 then
p.fixed = true
heldParticle = i
break
end
end
end
end
-- mouse released, drop a particle if we have one
function love.mousereleased(x, y, button)
if button == 1 and heldParticle ~= -1 then
particles[heldParticle].fixed = false
heldParticle = -1
end
end
-- mouse moved, set pos and old pos of held particle
function love.mousemoved(x, y)
if heldParticle ~= -1 then
particles[heldParticle].x = x
particles[heldParticle].y = y
particles[heldParticle].ox = x
particles[heldParticle].oy = y
end
end
function addStick(p1, p2, length, spring, col)
local s = {p1 = p1, p2 = p2, length = length, spring = spring, col = col}
table.insert(sticks, s)
return s
end
function addParticle(x, y, fixed, col)
local p = {x = x, y = y, ox = x, oy = y, fixed = fixed, col = col}
table.insert(particles, p)
return p
end
function updateParticle(p, dt)
-- don't move fixed points
if p.fixed then return end
-- apply force from fan and gravity
ax = fan
ay = gravity
-- calculate velocity based on last position and acceleration
local vx = (p.x - p.ox) * (1 - drag) + ax * dt * dt
local vy = (p.y - p.oy) * (1 - drag) + ay * dt * dt
-- move
p.ox, p.x = p.x, p.x + vx
p.oy, p.y = p.y, p.y + vy
end
function particleMovePos(p, x, y)
if p.fixed then return end
p.x = p.x + x
p.y = p.y + y
end
function checkBounds(p)
-- check bounds
if p.x > screenWidth then
p.x = screenWidth
elseif p.x < 0 then
p.x = 0
end
if p.y > screenHeight then
p.y = screenHeight
elseif p.y < 0 then
p.y = 0
end
end
function updateStick(s)
-- check distance between particles
local dx = s.p1.x - s.p2.x
local dy = s.p1.y - s.p2.y
local dist = math.sqrt(dx * dx + dy * dy)
-- work out difference and correction amount
local diff = (s.length - dist) / dist * 0.5
-- springs only do anything if dist > length
if s.spring and diff > 0 then return end
-- apply to particles
particleMovePos(s.p1, dx * diff, dy * diff)
particleMovePos(s.p2, -dx * diff, -dy * diff)
end
function drawParticle(p)
if colourMode then love.graphics.setColor(p.col) end
love.graphics.circle("fill", p.x, p.y, 3)
end
function drawStick (s)
if colourMode then love.graphics.setColor(s.col) end
love.graphics.line(s.p1.x, s.p1.y, s.p2.x, s.p2.y)
end
function love.update(dt)
-- if the fan is on, randomize it a bit
if fan ~=0 then
fan = fan + math.random(100) - 50
end
-- update particles
for _, p in ipairs(particles) do
updateParticle(p, dt)
end
-- update sticks (do multiple times to make smoother)
for i = 1, 4 do
for _, s in ipairs(sticks) do
updateStick(s)
end
end
-- check bounds
for _, p in ipairs(particles) do
checkBounds(p)
end
end
function love.draw()
love.graphics.setColor(1, 1, 1)
-- draw particles
for _, p in ipairs(particles) do
drawParticle(p)
end
-- draw sticks
for _, s in ipairs(sticks) do
drawStick(s)
end
end