Boids/Nim

From Rosetta Code
Revision as of 14:52, 29 August 2022 by PureFox (talk | contribs) (Fixed syntax highlighting.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Translation of: C
Library: OpenGL
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()