Boids/Nim

From Rosetta Code
Revision as of 21:42, 27 July 2021 by rosettacode>Lscrd (Creation of Nim page.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Translation of: C
Library: OpenGL

<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


  1. 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>