Boids/Nim
< Boids
import math, os, random, strformat, times
import opengl, opengl/glut, opengl/glu
const
MinMountainRadius = 3
MaxMountainRadius = 5
MaxMountainHeight = 25
MountainRatio = 500
IdealHeight = 1 # how high a boid prefers to stay above ground.
IdealDistance = 5 # how far boids prefer to stay away from each other.
MoveSpeed = 1e-2
N = 100
WorldSize = 40
####################################################################################################
# Vector.
type Vec = tuple[x, y, z: float32]
func `*=`(a: var Vec; r: float32) =
a.x *= r
a.y *= r
a.z *= r
func muladd(a: var Vec; b: Vec; r: float32) =
a.x += r * b.x
a.y += r * b.y
a.z += r * b.z
func `+=`(a: var Vec; b: Vec) =
a.x += b.x
a.y += b.y
a.z += b.z
func `+`(a, b: Vec): Vec =
(a.x + b.x, a.y + b.y, a.z + b.z)
func `-`(a, b: Vec): Vec =
(a.x - b.x, a.y - b.y, a.z - b.z)
func len2(a: Vec): float32 =
a.x * a.x + a.y * a.y + a.z * a.z
func normalize(a: var Vec) =
let r = sqrt(a.len2)
if r == 0: return
a.x /= r
a.y /= r
a.z /= r
####################################################################################################
type
Boid = ref object
position: Vec
heading: Vec
newHeading: Vec
speed: float32
Mountain = ref object
x, y, h: int
r: float64
next: Mountain
World = object
x, y: array[2, int] # mgroupingDrivein/max coords of world.
ground: seq[float32]
groundNormal: seq[Vec]
hills: Mountain
Camera = object
pitch: float
yaw: float
distance: float64
target: Vec
var
world: World
boids: array[N, Boid]
winWidth, winHeight: int
camera = Camera(pitch: -PI / 4, yaw: 0, distance: 100, target: (0f32, 0f32, 0f32))
cursorX, cursorY: int
updateTime: int
func hashXY(x, y: int): uint32 =
func ror(a: uint32; d: int): uint32 = a shl d or a shr (32 - d)
var h = 0x12345678u32
h += ror(h, 15) xor ror(x.uint32, 5)
h += ror(h, 15) xor ror(y.uint32, 5)
h = h xor ror(h, 7)
h += ror(h, 23)
h = h xor ror(h, 19)
h += ror(h, 11)
func hillHeight[T: SomeFloat](m: Mountain; x, y: T): T =
let x = x - T(m.x)
let y = y - T(m.y)
result = T(m.h) * exp(-(x * x + y * y) / (m.r * m.r))
proc groundHeight(x, y: float32): float32 =
var p = world.hills
while not p.isNil:
result += hillHeight(p, x, y)
p = p.next
proc calcNormal(x, y: float32): Vec =
var p = world.hills
while not p.isNil:
let h = hillHeight(p, float64(x), float64(y))
let t: float32 = 2 / (p.r * p.r)
result.x += (x - float32(p.x)) * t * h
result.y += (y - float32(p.y)) * t * h
p = p.next
result.z = 1
result.normalize()
proc resize(w, h: cint) {.cdecl.} =
(winWidth, winHeight) = (w, h)
glViewport(0, 0, w, h)
proc setProjection(w, h: int) =
var hor, ver: float64
if w > h:
hor = 0.05
ver = hor * float64(h) / float64(w)
else:
ver = 0.05
hor = ver * float64(w) / float64(h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glFrustum(-hor, hor, -ver, ver, 0.1, 1000)
func clamp(x: var float64; min, max: float64) =
if x < min: x = min
elif x > max: x = max
proc makeTerrain(cx, cy: int) =
if cx * 2 == world.x[0] + world.x[1] and cy * 2 == world.y[0] + world.y[1]:
return
world.x[0] = cx - WorldSize
world.x[1] = cx + WorldSize
world.y[0] = cy - WorldSize
world.y[1] = cy + WorldSize
let nx = world.x[1] - world.x[0] + 1
let ny = world.y[1] - world.y[0] + 1
world.hills = nil
for x in world.x[0]..world.x[1]:
for y in world.y[0]..world.y[1]:
let h = hashXY(x, y) mod MountainRatio
if h != 0: continue
let m = Mountain(x: x,
y: y,
r: MinMountainRadius + float64(hashXY(y, x) mod 100) / 100 *
(MaxMountainRadius - MinMountainRadius),
h: int(hashXY((y + x) div 2, (y - x) div 2) mod MaxMountainHeight),
next: world.hills)
world.hills = m
if world.ground.len == 0:
world.ground = newSeq[float32](nx * ny)
if world.groundNormal.len == 0:
world.groundNormal = newSeq[Vec](nx * ny)
for x in 0..<nx:
let xx = x + world.x[0]
for y in 0..<ny:
let yy = y + world.y[0]
world.ground[x * ny + y] = groundHeight(float32(xx), float32(yy))
world.groundNormal[x * ny + y] = calcNormal(float32(xx), float32(yy))
proc setLighting() =
var
lightAmbient = [float32 0.3, 0.3, 0.3, 1]
lightDiffuse = [float32 1, 1, 1, 1]
lightPosition = [float32 0, 1, 2, 1]
glEnable(GL_LIGHTING)
glLightfv(GL_LIGHT1, GL_AMBIENT, addr(lightAmbient[0]))
glLightfv(GL_LIGHT1, GL_DIFFUSE, addr(lightDiffuse[0]))
glLightfv(GL_LIGHT1, GL_POSITION, addr(lightPosition[0]))
glEnable(GL_LIGHT1)
glShadeModel(GL_FLAT)
glEnable(GL_COLOR_MATERIAL)
proc boidThink(boid: Boid) =
let g = groundHeight(boid.position.x, boid.position.y)
let migrationDrive: Vec = (0f32, 0.5f32, 0f32)
var heightDrive, crowdingDrive, groupingDrive: Vec
heightDrive.z = (IdealHeight + g - boid.position.z) * 0.3
# Follow the ground surface normal.
let terrainDrive = calcNormal(boid.position.x, boid.position.y)
var totalWeight = 0f32
for other in boids:
if other == boid: continue
var diff = other.position - boid.position
let d2 = diff.len2
let weight = 1 / (d2 * d2)
diff.normalize()
crowdingDrive.muladd(diff, if d2 > IdealDistance * IdealDistance: weight else: -weight)
groupingDrive.muladd(other.heading, weight)
totalWeight += weight
groupingDrive *= 1 / totalWeight
boid.newheading = migrationDrive + heightDrive + terrainDrive + crowdingDrive + groupingDrive
boid.newHeading *= 0.2
boid.newheading.normalize()
let cx = float32(world.x[0] + world.x[1]) / 2
let cy = float32(world.y[0] + world.y[1]) / 2
boid.newheading.x += (cx - boid.position.x) / 400
boid.newheading.y += (cy - boid.position.y) / 400
proc runBoids(msec: int) =
for boid in boids:
boid.position.muladd(boid.heading, float32(msec) * boid.speed)
var average: Vec
for boid in boids:
average += boid.position
average *= 1 / N
camera.target = average
makeTerrain(average.x.toInt, average.y.toInt)
for boid in boids:
boidThink(boid)
for boid in boids:
boid.heading = boid.newheading
proc drawTerrain() =
let nx = world.x[1] - world.x[0] + 1
let ny = world.y[1] - world.y[0] + 1
glColor3f(0.1, 0.25, 0.35)
for x in 0..<(nx-1):
let xx = x + world.x[0]
glBegin(GL_QUAD_STRIP)
for y in 0..<ny:
let yy = y + world.y[0]
glNormal3fv(addr(world.groundNormal[x*ny+y].x))
glVertex3f(float32(xx), float32(yy), world.ground[x*ny+y])
glNormal3fv(addr(world.groundNormal[(1+x)*ny+y].x))
glVertex3f(float32(xx+1), float32(yy), world.ground[(1+x)*ny+y])
glEnd()
proc draw(boid: Boid) =
glColor3f(0.6, 0.3, 0.3)
glPushMatrix()
glTranslatef(boid.position.x, boid.position.y, boid.position.z)
let (x, y, z) = boid.heading
let yaw: float32 = arctan2(float64(y), float64(x)) / PI * 180 - 90
glRotatef(yaw, 0, 0, 1)
let rxy = sqrt(float64(x * x + y * y))
let pitch: float32 = arctan2(float64(z), rxy) / PI * 180
glRotatef(pitch, 1, 0, 0)
glBegin(GL_TRIANGLES)
glNormal3f(-0.8, 0, 0.6)
glVertex3f(0, 0.5, 0)
glVertex3f(-0.5, -0.5, 0)
glVertex3f(0, 0, 0.1)
glNormal3f(0.8, 0, 0.6)
glVertex3f(0, 0.5, 0)
glVertex3f(0.5, -0.5, 0)
glVertex3f(0, 0, 0.1)
glNormal3f(-0.8, 0, -0.6)
glVertex3f(0, 0.5, 0)
glVertex3f(-0.5, -0.5, 0)
glVertex3f(0, 0, -0.1)
glNormal3f(0.8, 0, -0.6)
glVertex3f(0, 0.5, 0)
glVertex3f(0.5, -0.5, 0)
glVertex3f(0, 0, -0.1)
glNormal3f(1, -1, 0)
glVertex3f(-0.5, -0.5, 0)
glVertex3f(0, 0, 0.1)
glVertex3f(0, 0, -0.1)
glNormal3f(-1, -1, 0)
glVertex3f(0.5, -0.5, 0)
glVertex3f(0, 0, 0.1)
glVertex3f(0, 0, -0.1)
glEnd()
glPopMatrix()
proc render() {.cdecl.} =
let msec = (epochTime() * 1000).toInt
if msec < updateTime + 16:
sleep updateTime + 16 - msec
runBoids(msec - updateTime)
updateTime = msec
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
glEnable(GL_DEPTH_TEST)
setProjection(winWidth, winHeight)
clamp(camera.distance, 1, 1000)
clamp(camera.pitch, -PI / 2.1, PI / 2.1)
let rz = camera.distance * sin(camera.pitch)
let rxy = camera.distance * cos(camera.pitch)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
setLighting()
stdout.write &"{camera.target.x:.5f} {camera.target.y:.5f}\r"
stdout.flushFile()
gluLookAt(camera.target.x - rxy * cos(camera.yaw),
camera.target.y - rxy * sin(camera.yaw),
camera.target.z - rz,
camera.target.x,
camera.target.y,
camera.target.z,
0, 0, 1)
drawTerrain()
for boid in boids:
boid.draw()
glFlush()
glutSwapBuffers()
proc keyDown(key: int8; x, y: cint) {.cdecl.} =
echo &"key down: {chr(key)} ({x} {y})"
if key == ord('q'):
quit QuitSuccess
proc mouseButton(button, state, x, y: cint) {.cdecl.} =
if state == GLUT_UP: return
if button == 3:
camera.distance /= 1.2
elif button == 4:
camera.distance *= 1.2
cursorX = x
cursorY = y
proc mouseMove(x, y: cint) {.cdecl.} =
let ext = max(winWidth, winHeight) div 4
camera.yaw -= (x - cursorX) / ext
camera.pitch -= (y - cursorY) / ext
cursorX = x
cursorY = y
proc initGL() =
updateTime = (epochTime() * 1000).toInt
glutInit()
glutInitDisplayMode(GLUT_RGB or GLUT_DOUBLE)
glutInitWindowSize(600, 400)
discard glutCreateWindow("Boids")
glutIgnoreKeyRepeat(1)
glutKeyboardFunc(keyDown)
glutReshapeFunc(resize)
glutIdleFunc(render)
glutDisplayFunc(render)
glutMouseFunc(mouseButton)
glutMotionFunc(mouseMove)
loadExtensions()
setLighting()
randomize()
makeTerrain(0, 1)
for i in 0..<N:
new(boids[i])
let x, y: float32 = rand(-5.0..5.0)
let z: float32 = rand(0.5..1.5) * IdealHeight + groundHeight(x, y)
boids[i].position = (x, y, z)
boids[i].speed = rand(0.98..1.02) * MoveSpeed
initGL()
glutMainLoop()