Categories
blog

Fulubula

Today was my last official day on my Fugu project. I made a Fulubula thing. Its code/DNA is after the jump.

module(...,package.seeall)

local n, m, spike, new_spike, next_vert, simulate_to_age, make_base_mesh
local sim_time = 0

function setup()
m = make_base_mesh()
n = meshnode(m)
fgu:add(n)

vertices = {}
local all_vertices = vertexlist(m)
each(all_vertices, function(v)
v:set_uv(0,0)
end)

while #all_vertices>0 do
vertices[#vertices+1] = next_vert(all_vertices)
end
spike = nil

sim_time = 0
end

function update(dt)
sim_time = sim_time + 0.01
local age = exp(sim_time) - .99
print(age)
simulate_to_age(age)
end

make_base_mesh = function()
local m = icosahedron()
m:smooth_subdivide(2)
return m
end

-- simulate the growth of the thing until age
simulate_to_age = function(age)
m = make_base_mesh()

vertices = {}
local all_vertices = vertexlist(m)
each(all_vertices, function(v)
v:set_uv(0,0)
end)

while #all_vertices>0 do
vertices[#vertices+1] = next_vert(all_vertices)
end
spike = nil

local keep_going = true
local dt = .03
while (keep_going) do
if (spike~=nil) then
local continue = spike:update(dt)
if (not continue) then
spike=nil
end
end
if (spike==nil and #vertices>0) then
local v = vertices[#vertices]
remove(vertices,#vertices)
spike = new_spike(m,v,age)
elseif (#vertices==0) then
keep_going = false
end
end

n:set_mesh(m)
end

-- get the next random vertex from vertices,
-- and remove its immediate neighbours
next_vert = function(vertices)
if #vertices==0 then return nil end
local v = vertices[1] -- choose(vertices)
local neighbours = loopv(v)
insert(neighbours,v)
each(neighbours,
function(u)
i,_ = find(vertices, function(x) return u==x end)
if i then remove(vertices, i) end
end)
return v
end

-- create a new spike program at the specified vertex
-- this creates a new object with an update(dt) function
-- the update function returns false once the growth is complete
-- the internal logic is a simple state machine
new_spike = function(the_mesh,the_vertex,age)
local states = {
move = 1,
inset = 2,
done = 3
}

local dy = (the_vertex.p.y+1.2)/2.4

local obj = {
m=the_mesh,
v=the_vertex,
age = 0,
max_age=age,

normal=the_vertex.n,
axis_of_rotation=cross(the_vertex.n,vec3(0,1,0)),
seg = 1,

SPEED = sqr((1.5-dy) * 3), -- 4,
SEG_LENGTH = (.02 + sqr(1-dy)*0.5)*clamp(age*2,.1,2),
NUM_SEGS = 6, -- 7,
RADII = {.9,.7,.7,1.2,2,.8},
ROTATION_RAD = 5*(.1 + (1-dy)*.05)* clamp(age*2,0,1),
RADII_MULT = clamp(age*2,0,1),
MEANDER = .2 * clamp(age*2,0,1),

v_coord = 0,

state=states.move,
next_state=nil,
--state_change=.01
}

-- don't extrude things pointing directly up or down
if (length(obj.axis_of_rotation)<.01) then return nil end

local actions = {}
actions[states.move] = function(self,dt)
if self.distance==nil then
self.distance = 0
end
local dist = self.SPEED*dt
local q = quat(self.axis_of_rotation,self.ROTATION_RAD + random(-self.MEANDER,self.MEANDER))
local dir = q*self.v.n
local u = (self.seg - 1 + self.distance/self.SEG_LENGTH)/self.NUM_SEGS

self.v.p = self.v.p + dir*dist
self.v:set_uv(u,self.v_coord)
if (self.cap) then
-- adjust the outer loop on the cap
-- so it grows in the normal,
-- reaches the right scale,
-- becomes circular
-- and then rotates to orient in the growth dir
local t = self.distance/self.SEG_LENGTH -- amount done
local outer = capov(self.cap)
local center = vec3(0,0,0)
for i,ov in ipairs(outer) do
-- move in growing direction
ov:set_uv(u,self.v_coord)
ov.p = ov.p + dir*dist
center = center + ov.p
end
center = center/#outer
local er = self.cap_avg_radius * self.RADII[self.seg]*self.RADII_MULT
for i,ov in ipairs(outer) do
-- make a bit more circular
-- current, start, end radii
local cr = distance(ov.p,center)
local sr = self.cap_radii[i]
local tr = max(1,self.SPEED*dt + (cr-sr)/(er-sr))
local d = normalise(ov.p-center)
ov.p = center + d*lerp(sr,er,tr)
end
-- finally flatten in the growth dir
flattenvl(self.m,outer,self.v.p,dir)
end
self.distance = self.distance + dist
if (self.distance > self.SEG_LENGTH) then
self.seg = self.seg + 1
if (self.seg < self.NUM_SEGS) then
self.state = states.inset
else
self.state = states.done
end
end
end

actions[states.inset] = function(self,dt)
-- inset a tiny bit and store the cap for subsequent pulls
self.cap = inset(self.m,self.v,.99)
-- compute the avg radius of this cap so we can circulise it
-- also store the current radius of each outer vert
self.cap_radii = {}
local outer = capov(self.cap)
local center = vec3(0,0,0)
for _,ov in ipairs(outer) do
center = center + ov.p
end
center = center/#outer
local avg_radius = 0
for _,ov in ipairs(outer) do
local r = distance(ov.p,center)
self.cap_radii[#self.cap_radii+1] = r
avg_radius = avg_radius + r
end
avg_radius = avg_radius/#outer
self.cap_avg_radius = avg_radius
self.state = states.move
self.distance = nil
end

obj.update = function(self,dt)
self.age = self.age + dt
if (self.age > self.max_age) then obj.state=states.done end

if obj.state==states.done then
return false
else
actions[obj.state](obj,dt)
return true
end
end

return obj
end