Hi all! I've found a video with something interesting for me! Let's make similar simulatios together!
https://www.youtube.com/watch?v=qlfh_rv6khY
Snake, lizard, fish, robot (train) procedural animation
Snake, lizard, fish, robot (train) procedural animation
Last edited by darkfrei on Wed Apr 02, 2025 4:18 pm, edited 2 times in total.
Re: Snake, lizard, fish, robot (train) procedural animation
I am also interested in making these kind of animations with love2d.
I tried ton convert the source code from java (I have no skill in this langage) : (https://github.com/argonautcode/animal-proc-anim) to lua/love2d, with chat Gpt and (https://app.codeconvert.ai/home) but it's kind of a mess (I am a beginner). The fish animation was working but the final rendering was quite far from the one on the video.
Instead of doing that, I am heading to start from skratch, using my brain.
I tried ton convert the source code from java (I have no skill in this langage) : (https://github.com/argonautcode/animal-proc-anim) to lua/love2d, with chat Gpt and (https://app.codeconvert.ai/home) but it's kind of a mess (I am a beginner). The fish animation was working but the final rendering was quite far from the one on the video.
Instead of doing that, I am heading to start from skratch, using my brain.
Re: Snake, lizard, fish, robot (train) procedural animation
First is the simplest working simulation:
Or with Lissajous figure:
Code: Select all
local chain = {}
local segmentLength = 20
local chainLength = 10
function love.load()
-- create the chain with evenly spaced nodes
for i = 1, chainLength do
local x = 100 + (chainLength - i) * segmentLength
local y = 100
chain[i] = { x = x, y = y }
end
end
function love.update(dt)
-- move the first node to follow the mouse
local mx, my = love.mouse.getPosition()
local prev = chain[1]
prev.x, prev.y = mx, my
-- update the rest of the chain
for i = 2, #chain do
local node = chain[i]
-- calculate the vector between nodes
local dx, dy = node.x - prev.x, node.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 0 then
local move = segmentLength-dist
if dist > 0 then
-- adjust the position to maintain segment length
node.x = node.x + (dx / dist) * move
node.y = node.y + (dy / dist) * move
end
end
prev = node
end
end
function love.draw()
-- draw nodes and connect them with lines
love.graphics.setColor (1,1,1)
for i = 1, #chain do
love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
if i > 1 then
love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
end
end
end
Code: Select all
local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0
function love.load()
-- create the chain with evenly spaced nodes
for i = 1, chainLength do
local x = 100 + (chainLength - i) * segmentLength
local y = 100
chain[i] = { x = x, y = y }
end
end
function love.update(dt)
-- move the first node to follow the mouse
-- local mx, my = love.mouse.getPosition()
-- move the first node along a lissajous curve (figure-eight)
local a, b = 150, 150 -- amplitude
local omegaX, omegaY = 1, 2 -- frequency
-- local phase = math.pi / 2 -- phase shift
local phase =0 -- phase shift
time = time + dt -- control speed of movement
-- lissajous figure parameters
local delta = math.pi / 2
local prev = chain[1]
-- move the first node along a lissajous figure
prev.x = 400 + a * math.sin(omegaX * time)
prev.y = 300 + b * math.sin(omegaY * time + phase)
-- update the rest of the chain
for i = 2, #chain do
local node = chain[i]
-- calculate the vector between nodes
local dx, dy = node.x - prev.x, node.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 0 then
local move = segmentLength-dist
if dist > 0 then
-- adjust the position to maintain segment length
node.x = node.x + (dx / dist) * move
node.y = node.y + (dy / dist) * move
end
end
prev = node
end
end
function love.draw()
-- draw nodes and connect them with lines
love.graphics.setColor (1,1,1)
for i = 1, #chain do
love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
if i > 1 then
love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
end
end
end
- Attachments
-
chain-movement-01.love
- (664 Bytes) Downloaded 52 times
Last edited by darkfrei on Sat Feb 22, 2025 10:51 am, edited 1 time in total.
Re: Snake, lizard, fish, robot (train) procedural animation
Added hard angle limit for the chain links:
With soft limits:
Code: Select all
local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0
local maxAngle = math.rad(20)
-- lissajous figure parameters
local a, b = 150, 150 -- amplitude
local omegaX, omegaY = 1, 2 -- frequency
local phase =0 -- phase shift
local delta = math.pi / 2
function love.load()
-- create the chain with evenly spaced nodes
for i = 1, chainLength do
local x = 100 + (chainLength - i) * segmentLength
local y = 100
chain[i] = { x = x, y = y }
end
end
function math.sign(v)
if v > 0 then
return 1
elseif v < 0 then
return -1
else
return 0
end
end
-- function to return node position if the angle between segments exceeds maxAngle
local function adjustNodePosition(x1, y1, x2, y2, x3, y3)
-- calculate the vectors between the points
local dx1, dy1 = x2 - x1, y2 - y1
local dx2, dy2 = x3 - x2, y3 - y2
-- calculate the angles of the vectors
local angle1 = math.atan2(dy1, dx1) -- angle of the vector from (x1, y1) to (x2, y2)
local angle2 = math.atan2(dy2, dx2) -- angle of the vector from (x2, y2) to (x3, y3)
-- calculate the difference between the two angles
local angleDiff = angle2 - angle1
-- normalize the angle difference to the range [-pi, pi]
if angleDiff > math.pi then
angleDiff = angleDiff - 2 * math.pi
elseif angleDiff < -math.pi then
angleDiff = angleDiff + 2 * math.pi
end
-- if the angle difference exceeds the max allowed angle, adjust the node position
if math.abs(angleDiff) > maxAngle then
-- limit the angle difference to the max angle
angleDiff = math.sign(angleDiff) * maxAngle
-- calculate the new angle for the second vector
local newAngle = angle1 + angleDiff
-- calculate the new position for the third node using the new angle
local length = math.sqrt(dx2 * dx2 + dy2 * dy2)
local newDx = math.cos(newAngle) * length
local newDy = math.sin(newAngle) * length
-- set the new position for the third node
x3 = x2 + newDx
y3 = y2 + newDy
end
return x3, y3
end
function love.update(dt)
time = time + dt -- control speed of movement
local prev = chain[1]
-- move the first node along a lissajous figure
prev.x = 400 + a * math.sin(omegaX * time)
prev.y = 300 + b * math.sin(omegaY * time + phase)
-- update the rest of the chain
for i = 2, #chain do
local node = chain[i]
-- calculate the vector between nodes
local dx, dy = node.x - prev.x, node.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 0 then
local move = segmentLength-dist
if dist > 0 then
-- adjust the position to maintain segment length
node.x = node.x + (dx / dist) * move
node.y = node.y + (dy / dist) * move
end
end
-- check the angle between segments and adjust if necessary
if i > 1 then
-- use the adjustNodePosition function to modify node[3] if needed
local nextNode = chain[i+1]
if nextNode then
local x3, y3 = adjustNodePosition(
prev.x, prev.y,
node.x, node.y,
nextNode.x, nextNode.y)
nextNode.x = x3
nextNode.y = y3
end
end
prev = node
end
end
function love.draw()
-- draw nodes and connect them with lines
love.graphics.setColor (1,1,1)
for i = 1, #chain do
love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
if i > 1 then
love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
end
end
end
Code: Select all
local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0
local maxAngle = math.rad(20)
local maxOmega = math.rad(180*14)
print (maxOmega)
-- lissajous figure parameters
local a, b = 150, 150 -- amplitude
local omegaX, omegaY = 1, 2 -- frequency
local phase =0 -- phase shift
local delta = math.pi / 2
function love.load()
-- create the chain with evenly spaced nodes
for i = 1, chainLength do
local x = 100 + (chainLength - i) * segmentLength
local y = 100
chain[i] = { x = x, y = y }
end
end
function math.sign(v)
if v > 0 then
return 1
elseif v < 0 then
return -1
else
return 0
end
end
-- function to return node position with soft influence if the angle between segments exceeds maxAngle
local function adjustNodePosition(x1, y1, x2, y2, x3, y3, dt)
-- calculate the vectors between the points
local dx1, dy1 = x2 - x1, y2 - y1
local dx2, dy2 = x3 - x2, y3 - y2
-- calculate the angles of the vectors
local angle1 = math.atan2(dy1, dx1) -- angle of the vector from (x1, y1) to (x2, y2)
local angle2 = math.atan2(dy2, dx2) -- angle of the vector from (x2, y2) to (x3, y3)
-- calculate the difference between the two angles
local angleDiff = angle2 - angle1
-- normalize the angle difference to the range [-pi, pi]
if angleDiff > math.pi then
angleDiff = angleDiff - 2 * math.pi
elseif angleDiff < -math.pi then
angleDiff = angleDiff + 2 * math.pi
end
-- if the angle difference exceeds the max allowed angle, adjust the node position
if math.abs(angleDiff) > maxAngle then
-- limit the angle change to maxOmega * dt (soft adjustment)
local maxDelta = maxOmega * dt
local newMaxAngle = math.max (maxAngle, maxAngle + (math.abs(angleDiff)-maxAngle)*maxDelta)
-- print (maxDelta, maxAngle, math.abs(maxAngle)-maxDelta)
angleDiff = math.sign(angleDiff) * newMaxAngle
-- angleDiff = math.sign(angleDiff) * math.min(math.abs(angleDiff), maxDelta)
-- calculate the new angle for the second vector
local newAngle = angle1 + angleDiff
-- calculate the new position for the third node using the new angle
local length = math.sqrt(dx2 * dx2 + dy2 * dy2)
local newDx = math.cos(newAngle) * length
local newDy = math.sin(newAngle) * length
-- set the new position for the third node
x3 = x2 + newDx
y3 = y2 + newDy
end
return x3, y3
end
function love.update(dt)
time = time + dt -- control speed of movement
local prev = chain[1]
-- move the first node along a lissajous figure
prev.x = 400 + a * math.sin(omegaX * time)
prev.y = 300 + b * math.sin(omegaY * time + phase)
-- prev.x, prev.y = love.mouse.getPosition()
-- update the rest of the chain
for i = 2, #chain do
local node = chain[i]
-- calculate the vector between nodes
local dx, dy = node.x - prev.x, node.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 0 then
local move = segmentLength-dist
if dist > 0 then
-- adjust the position to maintain segment length
node.x = node.x + (dx / dist) * move
node.y = node.y + (dy / dist) * move
end
end
-- check the angle between segments and adjust if necessary
if i > 1 then
-- use the adjustNodePosition function to modify node[3] if needed
local nextNode = chain[i+1]
if nextNode then
local x3, y3 = adjustNodePosition(
prev.x, prev.y,
node.x, node.y,
nextNode.x, nextNode.y, dt)
nextNode.x = x3
nextNode.y = y3
end
end
prev = node
end
end
function love.draw()
-- draw nodes and connect them with lines
love.graphics.setColor (1,1,1)
for i = 1, #chain do
love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
if i > 1 then
love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
end
end
end
- Attachments
-
chain-movement-02.love
- (1.45 KiB) Downloaded 48 times
Re: Snake, lizard, fish, robot (train) procedural animation
The train has other movement: the hard track without corner shortening. Also no limits for angles for the links, just do what the rail do.

Code: Select all
local chain = {} -- array of wagons (locomotive + cars)
local trail = {} -- array of locomotive position records (trail)
local trailMaxLength = 370 -- max number of positions in the trail
local segmentLength = 20 -- distance between wagons
local chainLength = 10 -- number of nodes (locomotive + wagons)
local a, b = 150, 150 -- amplitudes for the trajectory (Lissajous)
local omegaX, omegaY = 1, 2 -- frequencies for the trajectory
local phase = 0 -- phase shift
local time = 0
function love.load()
-- initialize the chain nodes. the first node is the locomotive, the rest are wagons
for i = 1, chainLength do
chain[i] = { x = 400, y = 300 }
end
end
-- function returns the position along the trail corresponding to the given distance from the start
local function getPositionAtDistance(distance)
local accumulated = 0
for j = 1, #trail - 1 do
local p1 = trail[j]
local p2 = trail[j + 1]
local dx = p1.x - p2.x
local dy = p1.y - p2.y
local d = math.sqrt(dx * dx + dy * dy)
if accumulated + d >= distance then
local ratio = (distance - accumulated) / d
local x = p1.x + (p2.x - p1.x) * ratio
local y = p1.y + (p2.y - p1.y) * ratio
return { x = x, y = y }, j
end
accumulated = accumulated + d
end
end
-- function to delete trail elements after a specified index
local function deleteTrailAfterIndex(endIndex)
for i = #trail, endIndex + 3, -1 do -- small tail overlap
table.remove(trail, i)
end
end
function love.update(dt)
time = time + dt
-- update the position of the locomotive (first node) along the Lissajous trajectory
local head = chain[1]
head.x = 400 + a * math.sin(omegaX * time)
head.y = 300 + b * math.sin(omegaY * time + phase)
-- the first wagon creates the path by recording its position
table.insert(trail, 1, { x = head.x, y = head.y })
if #trail > trailMaxLength then
table.remove(trail)
end
-- for each wagon, calculate its position along the trail
local pos, lastTrailIndex
for i = 2, chainLength do
local desiredDistance = (i - 1) * segmentLength
pos, lastTrailIndex = getPositionAtDistance(desiredDistance)
if pos then
chain[i].x = pos.x
chain[i].y = pos.y
end
end
-- delete the trail elements after the last wagon
if lastTrailIndex then
-- print ('delete lastTrailIndex', lastTrailIndex)
-- deleteTrailAfterIndex (lastTrailIndex)
end
end
function love.draw()
-- optionally: draw the locomotive trail for visibility
love.graphics.setColor(0, 1, 0)
for i = 1, #trail - 1 do
-- love.graphics.circle('line', trail[i].x, trail[i].y, 2)
love.graphics.line(trail[i].x, trail[i].y, trail[i + 1].x, trail[i + 1].y)
end
-- draw nodes and connect them with lines
love.graphics.setColor (1,1,1)
for i = 1, #chain do
love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
-- if i > 1 then
-- love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
-- end
end
end
- Attachments
-
train-movement-01.love
- (1.23 KiB) Downloaded 44 times
Re: Snake, lizard, fish, robot (train) procedural animation
Cool thread. Reminds me of the "Pulled String" mode from Lazy Nezumi, a pen stabilizer utility for artists using graphics tablets. Notice how the blue gizmo bends when the mouse isn't stretching it.
(I appreciate that some art software like Krita, Paint Tool SAI and Clip Studio Paint have these pen stabilization features built-in.)
(I appreciate that some art software like Krita, Paint Tool SAI and Clip Studio Paint have these pen stabilization features built-in.)
Re: Snake, lizard, fish, robot (train) procedural animation
The inverse kinematics in the Love2D:
Update!
The hard angle limiting links in the IK chains!

Code: Select all
-- chain parameters
local totalSum = 400
local numSegments = 8
local segmentLength = totalSum / numSegments
-- list of polyline points
local points = {}
-- fixed start point
local startPoint = {x = 400, y = 580}
-- target point (where we want to reach)
local targetPoint = {x = 600, y = 300}
-- end of the chain (may not reach target)
local endPoint = {x = 600, y = 300}
--Lissajous
local a, b = 150, 150 -- amplitudes for the trajectory
local omegaX, omegaY = 1, 2 -- frequencies for the trajectory
local phase = 0 -- phase shift
local time = 0
-- initialize points
for i = 0, numSegments do
table.insert(points, {x = startPoint.x + i * segmentLength, y = startPoint.y})
end
-- fabrik inverse kinematics function
-- function to calculate distance between two points
local function distance(x1, y1, x2, y2)
return math.sqrt((x2 - x1)^2 + (y2 - y1)^2)
end
-- function to calculate angle between two points
local function angleBetween(x1, y1, x2, y2)
return math.atan2(y2 - y1, x2 - x1)
end
local function solveIK_FABRIK()
-- calculate distance to target
local distToTarget = distance(startPoint.x, startPoint.y, targetPoint.x, targetPoint.y)
-- if target is too far, move endPoint to the maximum possible distance
if distToTarget > totalSum then
local angle = angleBetween(startPoint.x, startPoint.y, targetPoint.x, targetPoint.y)
endPoint.x = startPoint.x + math.cos(angle) * totalSum
endPoint.y = startPoint.y + math.sin(angle) * totalSum
else
endPoint.x = targetPoint.x
endPoint.y = targetPoint.y
end
-- forward pass (pull end point toward target)
points[#points].x = endPoint.x
points[#points].y = endPoint.y
for i = #points - 1, 1, -1 do
local d = distance(points[i].x, points[i].y, points[i+1].x, points[i+1].y)
local angle = angleBetween(points[i+1].x, points[i+1].y, points[i].x, points[i].y)
local ratio = segmentLength / d
points[i].x = points[i+1].x + (points[i].x - points[i+1].x) * ratio
points[i].y = points[i+1].y + (points[i].y - points[i+1].y) * ratio
end
-- backward pass (fix start point)
points[1].x = startPoint.x
points[1].y = startPoint.y
for i = 2, #points do
local d = distance(points[i].x, points[i].y, points[i-1].x, points[i-1].y)
local angle = angleBetween(points[i-1].x, points[i-1].y, points[i].x, points[i].y)
local ratio = segmentLength / d
points[i].x = points[i-1].x + math.cos(angle) * segmentLength
points[i].y = points[i-1].y + math.sin(angle) * segmentLength
end
end
function love.update(dt)
time = time + dt
targetPoint.x = 400 + a * math.sin(omegaX * time)
targetPoint.y = 300 + b * math.sin(omegaY * time + phase)
solveIK_FABRIK()
end
-- rendering function
function love.draw()
-- solve ik
solveIK_FABRIK()
-- draw target point (mouse)
love.graphics.setColor(0, 1, 0) -- green
love.graphics.circle("fill", targetPoint.x, targetPoint.y, 5)
-- draw lines and points
for i = 1, #points - 1 do
love.graphics.setColor(1, 1, 1) -- white
love.graphics.line(points[i].x, points[i].y, points[i+1].x, points[i+1].y)
love.graphics.circle("fill", points[i].x, points[i].y, 5)
end
-- draw end point
love.graphics.circle("fill", endPoint.x, endPoint.y, 5)
end
-- mouse movement handler
--function love.mousemoved(x, y, dx, dy, istouch)
-- targetPoint.x = x
-- targetPoint.y = y
-- solveIK_FABRIK()
--end

Update!
The hard angle limiting links in the IK chains!

Code: Select all
-- chain parameters
local totalSum = 400
local numSegments = 16
local segmentLength = totalSum / numSegments
local maxAngle = math.rad(20) -- maximum allowed rotation in radians
-- list of polyline points
local points = {}
-- fixed start point
local startPoint = {x = 400, y = 580}
-- target point (where we want to reach)
local targetPoint = {x = 600, y = 300}
-- end of the chain (may not reach target)
local endPoint = {x = 600, y = 300}
-- initialize points
for i = 0, numSegments do
table.insert(points, {x = startPoint.x + i * segmentLength, y = startPoint.y})
end
-- function to calculate distance between two points
local function distanceP (p1, p2)
return math.sqrt((p2.x - p1.x)^2 + (p2.y - p1.y)^2)
end
-- function to calculate angle between two points
local function angleBetweenP (p1, p2)
return math.atan2(p2.y - p1.y, p2.x - p1.x)
end
function math.sign(v)
if v > 0 then
return 1
elseif v < 0 then
return -1
else
return 0
end
end
-- function to clamp joint angle within limit
local function clampJointAngle (baseAngle, angleDiff)
angleDiff = ((angleDiff-baseAngle) + math.pi) % (2 * math.pi) - math.pi
local absAngle = math.min (maxAngle, math.abs (angleDiff))
local resultAngle = baseAngle + math.sign(angleDiff) * absAngle
return resultAngle
end
-- function to solve inverse kinematics using the fabrik algorithm
local function solveIK_FABRIK()
-- forward pass (pull end point toward target)
points[#points].x = targetPoint.x
points[#points].y = targetPoint.y
for i = #points - 1, 1, -1 do
local p1, p2, p3 = points[i], points[i+1], points[i+2]
local d = distanceP(p1, p2)
local ratio = segmentLength / d
-- adjust the position of the next segment
p1.x = p2.x + (p1.x - p2.x) * ratio
p1.y = p2.y + (p1.y - p2.y) * ratio
if p3 then
local a1 = angleBetweenP(p1, p2)
local a2 = angleBetweenP(p2, p3)
local a3 = clampJointAngle (a2, a1)
p1.x = p2.x - segmentLength*math.cos (a3)
p1.y = p2.y - segmentLength*math.sin (a3)
end
end
-- backward pass (fix start point)
points[1].x = startPoint.x
points[1].y = startPoint.y
-- propagate adjustments forward from the start point
for i = 1, #points-1 do
local p1, p2, p3 = points[i], points[i+1], points[i+2]
local d = distanceP(p1, p2)
local ratio = segmentLength / d
-- adjust the position of the next segment
p2.x = p1.x + (p2.x - p1.x) * ratio
p2.y = p1.y + (p2.y - p1.y) * ratio
if p3 then
local a1 = angleBetweenP(p1, p2)
local a2 = angleBetweenP(p2, p3)
local a3 = clampJointAngle (a1, a2)
p3.x = p2.x + math.cos(a3) * segmentLength
p3.y = p2.y + math.sin(a3) * segmentLength
end
end
-- update end point position
endPoint.x = points[#points].x
endPoint.y = points[#points].y
end
-- parameters for a lissajous curve trajectory
local a, b = 150, 150 -- amplitudes for the trajectory
local omegaX, omegaY = 1, 2 -- frequencies for the trajectory
local phase = 0 -- phase shift
local time = 0
-- update function, moving the target along a lissajous curve
function love.update(dt)
time = time + dt
targetPoint.x = 400 + a * math.sin(omegaX * time)
targetPoint.y = 300 + b * math.sin(omegaY * time + phase)
solveIK_FABRIK()
end
-- rendering function
function love.draw()
-- solve ik
solveIK_FABRIK()
-- draw target point (mouse)
love.graphics.setColor(0, 1, 0) -- green
love.graphics.circle("fill", targetPoint.x, targetPoint.y, 5)
-- draw lines and points
for i = 1, #points - 1 do
love.graphics.setColor(1, 1, 1) -- white
love.graphics.line(points[i].x, points[i].y, points[i+1].x, points[i+1].y)
love.graphics.circle("fill", points[i].x, points[i].y, 5)
end
-- draw end point
love.graphics.circle("fill", endPoint.x, endPoint.y, 5)
end
-- mouse movement handler
--function love.mousemoved(x, y, dx, dy, istouch)
-- targetPoint.x = x
-- targetPoint.y = y
-- solveIK_FABRIK()
--end
- Attachments
-
inverse-kinematics-02.love
- (1.43 KiB) Downloaded 41 times
Re: Snake, lizard, fish, robot (train) procedural animation
Something similar:
https://www.youtube.com/watch?v=T73lvhhw_rA
See Also: https://zalo.github.io/blog/constraints/
Update. Added new video:
https://www.youtube.com/watch?v=wFqSKHLb0lo
https://www.youtube.com/watch?v=T73lvhhw_rA



See Also: https://zalo.github.io/blog/constraints/

https://www.youtube.com/watch?v=wFqSKHLb0lo
Who is online
Users browsing this forum: Ahrefs [Bot], Bing [Bot] and 8 guests