Skip to content

Instantly share code, notes, and snippets.

@antonhornquist
Created September 29, 2021 22:06
Show Gist options
  • Save antonhornquist/f5f6a30178b33141f876298f028b8e1f to your computer and use it in GitHub Desktop.
Save antonhornquist/f5f6a30178b33141f876298f028b8e1f to your computer and use it in GitHub Desktop.
-- flocking.
--
engine.name = 'PolyPerc'
local function to_hz(note)
local exp = (note - 21) / 12
return 27.5 * 2^exp
end
-- http://processingjs.org/learning/topic/flocking/
-- All Examples Written by Casey Reas and Ben Fry
-- unless otherwise stated.
local WIDTH = 128*2
local HEIGHT = 64*2
local Vector3D = {}
Vector3D.__index = Vector3D
function Vector3D.new(x, y, z)
local v = setmetatable({}, Vector3D)
v.x = x or 0
v.y = y or 0
v.z = z or 0
return v
end
function Vector3D:set_x(x)
self.x = x
end
function Vector3D:set_y(y)
self.y = y
end
function Vector3D:set_z(z)
self.z = z
end
function Vector3D:set_xy(x, y)
self.x = x
self.y = y
end
function Vector3D:set_xyz(x, y, z)
self.x = x
self.y = y
self.z = z
end
function Vector3D:magnitude()
return math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
end
function Vector3D:copy()
return Vector3D.new(self.x, self.y, self.z)
end
function Vector3D:add(v)
self.x = self.x + v.x
self.y = self.y + v.y
self.z = self.z + v.z
end
--[[
function Vector3D:sub(v)
self.x = self.x - v.x
self.y = self.y - v.y
self.z = self.z - v.z
end
]]
function Vector3D.sub(v1, v2)
return Vector3D.new(v1.x-v2.x, v1.y-v2.y, v1.z-v2.z)
end
function Vector3D:mult(n)
self.x = self.x * n
self.y = self.y * n
self.z = self.z * n
end
function Vector3D:div(n)
self.x = self.x / n
self.y = self.y / n
self.z = self.z / n
end
function Vector3D:normalize()
local m = self:magnitude()
if m > 0 then
self:div(m)
end
end
function Vector3D:limit(max)
if self:magnitude() > max then
self:normalize()
self:mult(max)
end
end
function Vector3D:heading2D()
local angle = math.atan2(-self.y, self.x)
return -1*angle
end
function Vector3D.distance(v1, v2)
local dx = v1.x - v2.x
local dy = v1.y - v2.y
local dz = v1.z - v2.z
return math.sqrt(dx*dx + dy*dy + dz*dz)
end
local Boid = {}
Boid.__index = Boid
function Boid.new(l, ms, mf)
local b = setmetatable({}, Boid)
b.acc = Vector3D.new(0, 0)
local velx = math.random()*2-1
local vely = math.random()*2-1
-- print("x:" .. velx .. "y:" .. vely)
b.vel = Vector3D.new(velx, vely)
b.loc = l:copy()
b.r = 2.0
b.maxspeed = ms -- Maximum speed
b.maxforce = mf -- Maximum steering force
b.left_edge = function (b) end
b.right_edge = function (b) end
b.top_edge = function (b) end
b.bottom_edge = function (b) end
return b
end
function Boid:run(boids)
self:flock(boids)
self:update()
self:borders()
end
-- We accumulate a new acceleration each time based on three rules
function Boid:flock(boids)
local sep = self:separate(boids)
local ali = self:align(boids)
local coh = self:cohesion(boids)
-- Arbitrarily weight these forces
--sep:mult(2)
--ali:mult(1)
--coh:mult(1)
sep:mult(params:get("sep"))
ali:mult(params:get("ali"))
coh:mult(params:get("coh"))
-- Add the force vectors to acceleration
self.acc:add(sep)
self.acc:add(ali)
self.acc:add(coh)
end
-- Method to update location
function Boid:update()
-- Update velocity
self.vel:add(self.acc)
-- Limit speed
self.vel:limit(self.maxspeed)
self.loc:add(self.vel)
-- Reset accelertion to 0 each cycle
self.acc:set_xyz(0, 0, 0)
end
function Boid:seek(target)
self.acc:add(self:steer(target, false))
end
function Boid:arrive(target)
self.acc:add(self:steer(target, true))
end
-- A method that calculates a steering vector towards a target
-- Takes a second argument, if true, it slows down as it approaches the target
function Boid:steer(target, slowdown)
local steer
local desired = Vector3D.sub(target, self.loc) -- A vector pointing from the location to the target
local d = desired:magnitude() -- Distance from the target is the magnitude of the vector
-- If the distance is greater than 0, calc steering (otherwise return zero vector)
if d > 0 then
-- Normalize desired
desired:normalize()
-- Two options for desired vector magnitude (1 -- based on distance, 2 -- maxspeed)
if slowdown and (d < 100) then
desired:mult(self.maxspeed*(d/100)) -- This damping is somewhat arbitrary
else
desired:mult(self.maxspeed)
end
-- Steering = Desired minus Velocity
steer = Vector3D.sub(desired, self.vel)
steer:limit(self.maxforce) -- Limit to maximum steering force
else
steer = Vector3D.new(0, 0)
end
return steer
end
-- Wraparound
function Boid:borders()
local loc = self.loc
local r = self.r
if loc.x < -r then
loc.x = WIDTH+r
self.left_edge(self)
end
if loc.y < -r then
loc.y = HEIGHT+r
self.top_edge(self)
end
if loc.x > WIDTH+r then
loc.x = -r
self.right_edge(self)
end
if loc.y > HEIGHT+r then
loc.y = -r
self.bottom_edge(self)
end
end
-- Separation
-- Method checks for nearby boids and steers away
function Boid:separate(boids)
local desiredseparation = 25.0
local sum = Vector3D.new(0, 0, 0)
local count = 0
-- For every boid in the system, check if it's too close
for i, b in ipairs(boids) do
local other = b
local d = Vector3D.distance(self.loc, other.loc)
-- If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
if d > 0 and d < desiredseparation then
-- Calculate vector pointing away from neighbor
local diff = Vector3D.sub(self.loc, other.loc)
diff:normalize()
diff:div(d) --Weight by distance
sum:add(diff)
count = count + 1 -- Keep track of how many
end
end
-- Average -- divide by how many
if count > 0 then
sum:div(count)
end
return sum
end
-- Alignment
-- For every nearby boid in the system, calculate the average velocity
function Boid:align(boids)
local neighbordist = 50.0
local sum = Vector3D.new(0, 0, 0)
local count = 0
for i, b in ipairs(boids) do
local other = b
local d = Vector3D.distance(self.loc, other.loc)
if d > 0 and d < neighbordist then
sum:add(other.vel)
count = count + 1
end
end
if count > 0 then
sum:div(count)
sum:limit(self.maxforce)
end
return sum
end
-- Cohesion
-- For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
function Boid:cohesion(boids)
local neighbordist = 50.0
local sum = Vector3D.new(0, 0, 0) -- Start with empty vector to accumulate all locations
local count = 0
for i, b in ipairs(boids) do
local other = b
local d = Vector3D.distance(self.loc, other.loc)
if d > 0 and d < neighbordist then
sum:add(other.loc) -- Add location
count = count + 1
end
end
if count > 0 then
sum:div(count)
return self:steer(sum, false) -- Steer towards the location
end
return sum
end
local Flock = {}
Flock.__index = Flock
function Flock.new()
local f = setmetatable({}, Flock)
f.boids = {} -- Initialize the arraylist
return f
end
function Flock:run()
for i, b in ipairs(self.boids) do
b:run(self.boids) -- Passing the entire list of boids to each boid individually
end
end
function Flock:add_boid(b)
table.insert(self.boids, b)
end
local flock
local function respawn()
flock = Flock.new()
-- Add an initial set of boids into the system
for i=1,params:get("boids") do
local boid = Boid.new(Vector3D.new(WIDTH/2, HEIGHT/2), 1.0, 0.05)
local notes = {60, 62, 63, 65, 67, 69}
local rand = math.random(1, 6)
boid.left_edge = function(b)
engine.hz(to_hz(notes[rand]))
end
boid.right_edge = function(b)
engine.hz(to_hz(notes[rand]+12))
end
boid.bottom_edge = function(b)
engine.hz(to_hz(notes[rand]-12))
end
boid.top_edge = function(b)
engine.hz(to_hz(notes[rand]+24))
end
flock:add_boid(boid)
end
end
function init()
timer = metro.init()
timer.event = function ()
flock:run()
redraw()
end
params:add_number("boids", "boids", 5, 25, 15)
params:add_number("sep", "sep", 1, 10, 2)
params:add_number("ali", "ali", 1, 10, 1)
params:add_number("coh", "coh", 1, 10, 1)
params:add_option("run", "run", {"yes", "no"})
params:set_action("run", function (value)
if value == 1 then
timer:start()
else
timer:stop()
end
end)
params:add_number("fps", "fps", 1, 120, 60)
params:set_action("fps", function (value)
timer.time = 1/value
end)
params:bang()
respawn()
-- params:read("jah/flocking.pset")
screen.line_width(1.0)
end
function redraw()
screen.clear()
screen.aa(1)
screen.font_size(8)
screen.level(15)
screen.move(0, 8)
--screen.text("flocking")
for i, b in ipairs(flock.boids) do
local x = b.loc.x/2
local y = b.loc.y/2
screen.rect(x, y, 1, 1)
screen.fill()
screen.move(x, y)
screen.line_rel(b.vel.x*1.25, b.vel.y*1.25)
screen.stroke()
end
screen.update()
end
function key(n, z)
if n == 3 and z == 1 then
respawn()
end
end
function enc(n, delta)
if n == 1 then
mix:delta("output", delta)
elseif n == 2 then
params:delta("sep", delta)
redraw()
elseif n == 3 then
params:delta("coh", delta)
redraw()
end
end
function cleanup()
-- params:write("jah/flocking.pset")
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment