Boids/Nim
<lang Nim>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()</lang>