Boids/Phix

From Rosetta Code

Only uses about 2.5% cpu with 60 boids at 25 FPS. Split into two source files, for no particular reason.

Library: pGUI
--
-- demo\pGUI\boids3d.exw
--
-- Originally by by Matt Lewis
-- Ported from arwen to pGUI, Pete Lomax June 2017
--
include pGUI.e
include boids3d.e
 
constant TITLE = "Boids 3D on pGUI"
 
Ihandle canvas, dialog,
restart, speed_label, speed_txt, dist_label, dist_txt,
radius_label, radius_txt, boids_label, boids_txt, shadow_check
cdCanvas cddbuffer, cdcanvas
 
integer ox,oy,oz
procedure set_observer()
{ox,oy,oz} = {floor(X_MAX/2),floor(Y_MAX/2),Z_MAX*2}
end procedure
 
sequence colors = {}
 
sequence verts = {}
 
integer draw_shadows = 1
 
integer dx = 0, dy = 0, dw = 0, dh = 0
 
function project_point(sequence pt)
atom d
atom px,py,pz
 
{px,py,pz} = pt
if pz=oz then
d = 0.0001
else
d = 1-pz/(pz-oz)
end if
 
px = floor(ox+(ox-px)/d)
py = dh - floor(oy+(oy-py)/d)
 
return {px,py}
end function
 
procedure draw_lines(sequence coords)
--
-- Draw zero or more lines.
-- The lines are drawn between the sets of coordinates in coords.
-- This sequence can contain Colors, Lines, or Points:
-- A Color is a single atom that is a 24-bit color value. Subsequent lines use this color.
-- A Line is a 4-element sequence {X1,Y1,X2,Y2} that specifies the X,Y position of a lines
-- starting point and the X,Y position of its end point. The line is drawn from X1,Y1
-- to X2,Y2.
-- A Point is a 2-element sequence {X,Y} that gives the X,Y position of the end-point
-- of a line. The line is drawn to this position from the last end-point supplied.
-- There must have been a preceding Line (or Point), else x1 unassigned error.
--
-- If no color parameters are supplied, the current pen color for the control is used.
--
-- Example:
--
-- -- draw a shape in TheWindow
-- draw_lines({White,{40,0,0,80},{80,80},{40,0},
-- Blue,{40,5,0,85},{80,85},{40,5}})
--
-- draws white lines {40,0}..{0,80}, {0,80}..{80,80}, {80,80}..{40,0}
-- and blue lines {40,5}..{0,85}, {0,85}..{80,85}, {80,85}..{40,5}.
--
atom x1, y1, x2, y2
object ci
 
for i = 1 to length(coords) do
ci = coords[i]
if atom(ci) then
cdCanvasSetForeground(cddbuffer, ci)
elsif length(ci) = 4 then
{x1,y1,x2,y2} = ci
cdCanvasLine(cddbuffer,x1,y1,x2,y2)
{x1,y1} = {x2,y2}
elsif length(ci) = 2 then
{x2,y2} = ci
cdCanvasLine(cddbuffer,x1,y1,x2,y2)
{x1,y1} = {x2,y2}
else
 ?9/0
end if
end for
end procedure
 
procedure draw_polygon(sequence points)
integer {{x1,y1},{x2,y2},{x3,y3}} = points
cdCanvasBegin(cddbuffer,CD_FILL)
cdCanvasVertex(cddbuffer,x1,y1)
cdCanvasVertex(cddbuffer,x2,y2)
cdCanvasVertex(cddbuffer,x3,y3)
cdCanvasEnd(cddbuffer)
end procedure
 
function redraw_cb(Ihandle /*ih*/, integer /*posx*/, integer /*posy*/)
integer N, x, y, z
atom x1, y1, z1,
x2, y2, z2
{dw,dh} = IupGetIntInt(canvas, "DRAWSIZE")
{dx,dy} = {floor(dw/4)+1,floor(dh/4)+1}
{X_MAX,Y_MAX,Z_MAX} = {dw,dh,floor((dw+dh)/2)}
set_observer()
cdCanvasActivate(cddbuffer)
cdCanvasClear(cddbuffer)
cdCanvasSetForeground(cddbuffer, #909090)
 
{x1,y1,z1} = {X_MIN, 0.0,Z_MIN}
{x2,y2,z2} = {X_MAX,Y_MAX,Z_MAX}
 
-- draw the verticals on the sides and horizontals on the floor and ceiling
N = floor((z2-z1)/100)
if N<3 then N = 3 end if
for i=1 to N-1 do
z = z1+floor((i/N)*(z2-z1))
draw_lines({#808080,
project_point({x2, y2, z}) & project_point({x2, y1, z}),
project_point({x1, y1, z}),
project_point({x1, y2, z}),
#FFFFFF,
project_point({x2, y2, z})})
end for
 
-- draw the horizontals on the back and the not-quite horizontals on the sides
N = floor((y2-y1)/100)
if N<3 then N = 3 end if
for i=1 to N-1 do
y = y1+floor((i/N)*(y2-y1))
draw_lines({#808080,
project_point({x1,y,z1}) & project_point({x1,y,z2}),
project_point({x2,y,z2}),
project_point({x2,y,z1})})
end for
 
-- draw the verticals on the back and the not-quite-verticals on floor/ceiling
N = floor((x2-x1)/100)
if N<3 then N = 3 end if
for i=1 to N-1 do
x = x1+floor((i/N)*(x2-x1))
draw_lines({#808080,
project_point({x,y2,z2}) & project_point({x,y1,z2}),
project_point({x,y1,z1}),
#FFFFFF,
project_point({x,y2,z1}) & project_point({x,y2,z2})})
end for
 
-- draw the minimal room outline
draw_lines({CD_BLACK,
project_point({x1, y1, z2}) & project_point({x2, y1, z2}),
project_point({x2, y2, z2}),
project_point({x1, y2, z2}),
project_point({x1, y1, z2}),
project_point({x1, y1, z1}) & project_point({x1, y1, z2}),
project_point({x2, y1, z1}) & project_point({x2, y1, z2}),
project_point({x2, y2, z1}) & project_point({x2, y2, z2}),
project_point({x1, y2, z1}) & project_point({x1, y2, z2})})
 
if draw_shadows then
cdCanvasSetForeground(cddbuffer, #A0A0A0)
for i=1 to BOIDS do
draw_polygon(verts[i][6..8])
end for
end if
 
-- draw boids as polygons, starting with farthest from the POV
for i=BOIDS to 1 by -1 do
sequence v = verts[i]
cdCanvasSetForeground(cddbuffer, colors[v[5]])
draw_polygon(v[2..4])
end for
 
cdCanvasFlush(cddbuffer)
return IUP_DEFAULT
end function
 
constant null_u = {0,0}
function get_ortho(sequence v)
sequence y, yhat, z, u
 
u = v[1..2]
if equal(u, null_u) then
return {1,1,0}
end if
y = {1,1}
yhat = sq_mul(sq_div(dot(u,y),dot(u,u)),u)
z = sq_sub(y,yhat)
if equal(z, null_u) then
return {1,1,0}
end if
return z & 0
end function
 
function timer_cb(Ihandle /*ih*/)
sequence in,boid, pt, v, v1, v2, v3, sv
 
move_boids()
 
in = boidsnp1
 
if length(colors)=0 then
colors = repeat(0, BOIDS)
for i=1 to BOIDS do
colors[i] = rand(#FFFFFF)
end for
end if
 
-- convert boid location and velocities into triangles in 3D
-- and project them onto the screen
verts = repeat("", BOIDS)
for i=1 to BOIDS do
 
boid = in[i]
 
pt = boid[B_X..B_Z]
v = boid[B_XV..B_ZV]
 
if equal(v,{0,0,0}) then
v = {1,0,1}
end if
 
-- the 'nose' of the boid
v1 = sq_add(pt,make_length(v, 20))
 
-- make the base of the triangle
sv = make_length(get_ortho(v), 4)
v2 = sq_add(pt,sv)
v3 = sq_sub(pt,sv)
 
verts[i] = {distance3(pt, {ox,oy,oz}), v1, v2, v3, i, v1, v2, v3}
 
-- project the 3D points onto a 2D viewing surface
for j=2 to 4 do
verts[i][j] = project_point(verts[i][j])
end for
if draw_shadows then
for j=6 to 8 do
pt = verts[i][j]
pt[2] = 0.0
verts[i][j] = project_point(pt)
end for
end if
 
end for
 
-- sort by distance, so nearer boids clip those that are more distant
verts = sort(verts)
 
IupUpdate(canvas)
return IUP_IGNORE
end function
 
function add_boids(integer num)
sequence boid = repeat(0, B_ELEMENTS)
if num>BOIDS then
for i=BOIDS+1 to num do
boid[B_X] = rand(X_MAX)
boid[B_Y] = rand(Y_MAX)
boid[B_Z] = rand(Z_MAX)
 
boid[B_XV] = floor(V_MAX/2)-rand(V_MAX)
boid[B_YV] = floor(V_MAX/2)-rand(V_MAX)
boid[B_ZV] = floor(V_MAX/2)-rand(V_MAX)
boidsnp1 = append(boidsnp1, boid)
colors &= rand(#FFFFFF)
end for
boidsn = boidsnp1
end if
-- BOIDS = num
return num
end function
 
function valuechanged_cb(Ihandle ih)
integer i = IupGetInt(ih, "VALUE")
switch ih do
case speed_txt: V_MAX = max(i,1)
case dist_txt: DIST = max(i,10)
case radius_txt: N_DIST = max(i,1)
case boids_txt: BOIDS = add_boids(i)
end switch
return IUP_DEFAULT
end function
 
function shadow_cb(Ihandle /*ih*/, integer state)
draw_shadows = state
return IUP_DEFAULT
end function
 
function restart_cb(Ihandle /*ih*/)
setup()
return IUP_DEFAULT
end function
 
function map_cb(Ihandle ih)
cdcanvas = cdCreateCanvas(CD_IUP, ih)
cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
cdCanvasSetBackground(cddbuffer, CD_GREY)
return IUP_DEFAULT
end function
 
function esc_close(Ihandle /*ih*/, atom c)
if c=K_ESC then return IUP_CLOSE end if
return IUP_CONTINUE
end function
 
procedure main()
Ihandle hbox
 
IupOpen()
 
canvas = IupCanvas("RASTERSIZE=625x690")
IupSetCallback(canvas, "MAP_CB", Icallback("map_cb"))
IupSetCallback(canvas, "ACTION", Icallback("redraw_cb"))
 
speed_label = IupLabel("Max Speed","PADDING=0x4")
speed_txt = IupText("VALUECHANGED_CB", Icallback("valuechanged_cb"),
"SPIN=Yes, SPINMIN=1, RASTERSIZE=48x")
IupSetInt(speed_txt,"VALUE",V_MAX)
 
dist_label = IupLabel("Separation","PADDING=0x4")
dist_txt = IupText("VALUECHANGED_CB", Icallback("valuechanged_cb"),
"SPIN=Yes, SPINMIN=10, SPINMAX=600, RASTERSIZE=48x")
IupSetInt(dist_txt,"VALUE",DIST)
 
radius_label = IupLabel("Neighbor Radius","PADDING=0x4")
radius_txt = IupText("VALUECHANGED_CB", Icallback("valuechanged_cb"),
"SPIN=Yes, SPINMIN=1, SPINMAX=200, RASTERSIZE=48x")
IupSetInt(radius_txt,"VALUE",N_DIST)
 
boids_label = IupLabel("Boids","PADDING=0x4")
boids_txt = IupText("VALUECHANGED_CB", Icallback("valuechanged_cb"),
"SPIN=Yes, SPINMIN=1, SPINMAX=200, RASTERSIZE=48x")
IupSetInt(boids_txt,"VALUE",BOIDS)
 
shadow_check = IupToggle("Draw Shadows","ACTION",Icallback("shadow_cb"),"VALUE=ON")
restart = IupButton("Restart","ACTION",Icallback("restart_cb"),"GAP=0x14")
 
hbox = IupHbox({IupVbox({speed_label,speed_txt},"NORMALIZESIZE=HORIZONTAL"),
IupVbox({dist_label,dist_txt},"NORMALIZESIZE=HORIZONTAL"),
IupVbox({radius_label,radius_txt},"NORMALIZESIZE=HORIZONTAL"),
IupVbox({boids_label,boids_txt},"NORMALIZESIZE=HORIZONTAL"),
IupVbox({shadow_check,restart},"NORMALIZESIZE=HORIZONTAL")})
IupDestroy(IupNormalizer({speed_label,shadow_check},"NORMALIZE=VERTICAL"))
dialog = IupDialog(IupVbox({canvas,hbox}, "MARGIN=5x5, GAP=5"),"MINSIZE=455x170")
IupSetAttribute(dialog,"TITLE",TITLE);
IupSetCallback(dialog, "K_ANY", Icallback("esc_close"));
 
Ihandle hTimer = IupTimer(Icallback("timer_cb"), 40)
setup()
set_observer()
{} = timer_cb(hTimer)
IupShow(dialog)
IupSetAttribute(canvas, "RASTERSIZE", NULL)
 
IupMainLoop()
IupClose()
end procedure
main()
--
-- demo\pGUI\boids3d.e
-- ===================
--
global integer BOIDS = 60
-- OBSTACLES = 3
global atom N_DIST = 75.0,
DIST = 30.0,
V_MIN = 4.0,
V_MAX = 10.0,
X_MIN = 0.0,
Y_MIN = 0.0,
Z_MIN = 0.0,
X_MAX = 600.0,
Y_MAX = 600.0,
Z_MAX = 600.0
 
constant DIST_FACTOR = 1.0/100.0
 
global enum B_X, B_Y, B_Z, B_XV, B_YV, B_ZV, B_ELEMENTS = B_ZV
 
global sequence boidsn, boidsnp1
--, obstacles
 
function magnitude3(sequence v)
-- return the scalar magnitude of a 3D vector
return sqrt(v[1]*v[1]+v[2]*v[2]+v[3]*v[3])
end function
 
global function make_length(sequence v, atom l)
-- change the scalar magnitude of a 3D vector
return sq_mul(v,l/magnitude3(v))
end function
 
global function dot(sequence u, sequence v)
-- return the dot product of 2 2D vectors
return u[1]*v[1]+u[2]*v[2]
end function
 
global function distance3(sequence v1, sequence v2)
-- return the distance between two 3D vectors
return magnitude3(sq_sub(v1[1..3],v2[1..3]))
end function
 
function boids_dist(integer b1, integer b2)
-- return the distance between two boids, identified by their position in the boidsn sequence
return distance3(boidsn[b1], boidsn[b2])
end function
 
sequence n_id, n_dist
 
function neighbors(integer bid, atom distance)
-- return a list of boids within the N_DIST radius of a specified boid
atom dist
integer ix
sequence n, nid, ndist
 
if bid=1 then
n_id = repeat("", BOIDS)
n_dist = n_id
end if
 
nid = n_id[bid]
ndist = n_dist[bid]
n = repeat({}, BOIDS)
ix = 0
for i=1 to length(n_id[bid]) do
ix += 1
n[ix] = {nid[i], ndist[i]}
end for
 
for i=bid+1 to BOIDS do
dist = boids_dist(bid, i)
if dist<=distance then
ix += 1
n[ix] = {i,dist}
n_id[i] &= bid
n_dist[i] &= dist
end if
end for
 
return n[1..ix]
end function
 
procedure maintain_distance(integer bid, sequence n)
-- alter a boids velocity to try to stay at least DIST away from other boids
atom dx, dy, dz
sequence this, other
 
dx = 0.0
dy = 0.0
dz = 0.0
 
this = boidsn[bid]
 
for i=1 to length(n) do
if n[i][2]<DIST then
 
other = boidsn[n[i][1]]
 
dx -= (other[B_X]-this[B_X])*2
dy -= (other[B_Y]-this[B_Y])*2
dz -= (other[B_Z]-this[B_Z])*2
 
end if
 
end for
 
dx *= DIST_FACTOR
dy *= DIST_FACTOR
dz *= DIST_FACTOR
 
boidsnp1[bid][B_XV] += dx
boidsnp1[bid][B_YV] += dy
boidsnp1[bid][B_ZV] += dz
end procedure
 
procedure avoid_walls(integer bid)
-- avoid the boundaries of MAX and MIN for each dimension (X, Y, Z)
sequence this
atom dx, dy, dz, t
dx = 0.0
dy = 0.0
dz = 0.0
this = boidsn[bid]
t = this[B_X]
if t<DIST+X_MIN then
dx += 1
elsif t>X_MAX-DIST then
dx -= 1
end if
 
t = this[B_Y]
if t<DIST+Y_MIN then
dy += 1
elsif t>Y_MAX-DIST then
dy -= 1
end if
 
t = this[B_Z]
if t<DIST+Z_MIN then
dz += 1
elsif t>Z_MAX-DIST then
dz -= 1
end if
 
boidsnp1[bid][B_XV] += dx
boidsnp1[bid][B_YV] += dy
boidsnp1[bid][B_ZV] += dz
end procedure
 
procedure match_velocity(integer bid, sequence n)
-- try to match the velocity of a boid to its neighbors
atom dx, dy, dz
sequence this, other
 
if length(n) then
dx = 0.0
dy = 0.0
dz = 0.0
this = boidsn[bid]
 
for i=1 to length(n) do
other = boidsn[n[i][1]]
 
dx += other[B_XV]
dy += other[B_YV]
dz += other[B_ZV]
end for
 
dx /= length(n)
dy /= length(n)
dz /= length(n)
 
dx -= this[B_XV]
dy -= this[B_YV]
dz -= this[B_ZV]
 
dx *= DIST_FACTOR
dy *= DIST_FACTOR
dz *= DIST_FACTOR
 
boidsnp1[bid][B_XV] += dx
boidsnp1[bid][B_YV] += dy
boidsnp1[bid][B_ZV] += dz
end if
end procedure
 
procedure move_to_center(integer bid, sequence n)
-- try to move a boid toward the center of its neighbors
atom x, y, z
sequence other
if length(n) then
x = 0.0
y = 0.0
z = 0.0
for i=1 to length(n) do
other = boidsn[n[i][1]]
x += other[B_X]
y += other[B_Y]
z += other[B_Z]
end for
 
-- compute the center
x /= length(n)
y /= length(n)
z /= length(n)
 
-- figure out the direction...
other = boidsn[bid]
x -= other[B_X]
y -= other[B_Y]
z -= other[B_Z]
 
x *= DIST_FACTOR
y *= DIST_FACTOR
z *= DIST_FACTOR
 
boidsnp1[bid][B_XV] += x
boidsnp1[bid][B_YV] += y
boidsnp1[bid][B_ZV] += z
end if
end procedure
 
procedure constrain(integer bid)
-- don't let them go too fast or too slow
atom mag = magnitude3(boidsnp1[bid][B_XV..B_ZV])
if mag>V_MAX then
boidsnp1[bid][B_XV..B_ZV] = sq_div(boidsnp1[bid][B_XV..B_ZV],mag/V_MAX)
elsif mag<V_MIN then
if mag then
boidsnp1[bid][B_XV..B_ZV] = sq_mul(boidsnp1[bid][B_XV..B_ZV],V_MIN/mag)
else
boidsnp1[bid][B_XV] = V_MIN*rand(100)/100
boidsnp1[bid][B_YV] = V_MIN*rand(100)/100
boidsnp1[bid][B_ZV] = V_MIN*rand(100)/100
end if
end if
end procedure
 
procedure move(integer bid)
boidsnp1[bid][B_X..B_Z] = sq_add(boidsnp1[bid][B_X..B_Z],boidsnp1[bid][B_XV..B_ZV])
end procedure
 
global procedure setup()
atom mag
boidsn = repeat(repeat(0.0, B_ELEMENTS), BOIDS)
boidsnp1 = boidsn
 
-- place them randomly
for boid=1 to BOIDS do
boidsnp1[boid][B_X] = rand(X_MAX)
boidsnp1[boid][B_Y] = rand(Y_MAX)
boidsnp1[boid][B_Z] = rand(Z_MAX)
 
boidsnp1[boid][B_XV] = V_MAX-rand(2*V_MAX)
boidsnp1[boid][B_YV] = V_MAX-rand(2*V_MAX)
boidsnp1[boid][B_ZV] = V_MAX-rand(2*V_MAX)
 
mag = magnitude3(boidsnp1[boid])/V_MAX
 
if mag>1.0 then
boidsnp1[boid][B_XV..B_ZV] = sq_div(boidsnp1[boid][B_XV..B_ZV],mag)
end if
end for
 
-- obstacles = repeat({},OBSTACLES)
-- for o=1 to OBSTACLES do
-- obstacles[o] = {rand(X_MAX), rand(Y_MAX), rand(Z_MAX), 30}
-- end for
 
boidsn = boidsnp1
 
end procedure
 
global procedure move_boids()
sequence n
for boid=1 to BOIDS do
n = neighbors(boid, N_DIST)
maintain_distance(boid, n)
match_velocity(boid, n)
move_to_center(boid, n)
avoid_walls(boid)
constrain(boid)
move(boid)
end for
boidsn = boidsnp1
end procedure