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
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 3 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 3 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 3 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.)
Who is online
Users browsing this forum: Ahrefs [Bot] and 4 guests