Snake, lizard, fish, robot (train) procedural animation

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
darkfrei
Party member
Posts: 1235
Joined: Sat Feb 08, 2020 11:09 pm

Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

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
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
SETTORB
Prole
Posts: 1
Joined: Thu Feb 20, 2025 11:44 am

Re: Snake, lizard, fish, robot (train) procedural animation

Post by SETTORB »

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.
User avatar
darkfrei
Party member
Posts: 1235
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

First is the simplest working simulation:
Animation (99).gif
Animation (99).gif (294.96 KiB) Viewed 178 times

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
Or with Lissajous figure:
Animation (100).gif
Animation (100).gif (310.08 KiB) Viewed 155 times

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.
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1235
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

Added hard angle limit for the chain links:
Animation (101).gif
Animation (101).gif (303.36 KiB) Viewed 160 times

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
With soft limits:
Animation (102).gif
Animation (102).gif (255.62 KiB) Viewed 151 times

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
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1235
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

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.

:awesome:
Animation (104).gif
Animation (104).gif (144.97 KiB) Viewed 117 times

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
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
RNavega
Party member
Posts: 438
Joined: Sun Aug 16, 2020 1:28 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by RNavega »

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.
lnpPulledString.gif
lnpPulledString.gif (15.72 KiB) Viewed 8 times
(I appreciate that some art software like Krita, Paint Tool SAI and Clip Studio Paint have these pen stabilization features built-in.)
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 4 guests