Boids/Go

From Rosetta Code
Revision as of 21:24, 28 January 2020 by PureFox (talk | contribs) (Removed Go header to prevent task being double-counted.)
Translation of: C
Library: go.gl
Library: glut


Although it's (hopefully) a faithful translation, not as smooth as the original with the boids getting quite spread out at times.

As I couldn't find a Go wrapper for the old 'glu' library, I've had to use cgo to call glu.LookAt directly. Only tested on Ubuntu 18.04. <lang go>package main

/*

  1. cgo LDFLAGS: -lGLU
  2. include <GL/glu.h>
  • /

import "C" import (

   "fmt"
   "github.com/go-gl/gl/v2.1/gl"
   "github.com/vbsw/glut"
   "log"
   "math"
   "math/rand"
   "os"
   "time"

)

const (

   minMountainRadius = 3.0
   maxMountainRadius = 5.0
   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

)

var updateTime time.Time

// 3D vector stuff type vec struct{ x [3]float32 }

func vscale(a *vec, r float32) {

   a.x[0] *= r
   a.x[1] *= r
   a.x[2] *= r

}

func vmuladdTo(a, b *vec, r float32) {

   a.x[0] += r * b.x[0]
   a.x[1] += r * b.x[1]
   a.x[2] += r * b.x[2]

}

func vaddTo(a, b *vec) {

   a.x[0] += b.x[0]
   a.x[1] += b.x[1]
   a.x[2] += b.x[2]

}

func vadd(a, b vec) vec {

   return vec{[3]float32{a.x[0] + b.x[0], a.x[1] + b.x[1], a.x[2] + b.x[2]}}

}

func vsub(a, b vec) vec {

   return vec{[3]float32{a.x[0] - b.x[0], a.x[1] - b.x[1], a.x[2] - b.x[2]}}

}

func vlen2(a vec) float32 {

   return a.x[0]*a.x[0] + a.x[1]*a.x[1] + a.x[2]*a.x[2]

}

func vdist2(a, b vec) float32 {

   return vlen2(vsub(a, b))

}

func vcross(a, b vec) vec {

   return vec{[3]float32{
       a.x[1]*b.x[2] - a.x[2]*b.x[1],
       a.x[2]*b.x[0] - a.x[0]*b.x[2],
       a.x[0]*b.x[1] - a.x[1]*b.x[0],
   }}

}

func vnormalize(a *vec) {

   r := float32(math.Sqrt(float64(vlen2(*a))))
   if r == 0 {
       return
   }
   a.x[0] /= r
   a.x[1] /= r
   a.x[2] /= r

}

type boid struct {

   position   vec
   heading    vec
   newheading vec
   speed      float32

}

var boids [n]boid

type mountain struct {

   x, y, h int
   r       float64
   next    *mountain

}

type worldType struct {

   x, y         [2]int // min/max coords of world
   ground       []float32
   groundNormal []vec
   hills        *mountain

}

var world worldType

type cameraType struct {

   pitch, yaw, distance float64
   target               vec

}

var camera = cameraType{-math.Pi / 4, 0, 100, vec{}}

func hashXY(x, y int) uint {

   ror := func(a uint, d int) uint {
       return (a << d) | (a >> (32 - d))
   }
   var h, tmp uint = 0x12345678, uint(x)
   h += ror(h, 15) ^ ror(tmp, 5)
   tmp = uint(y)
   h += ror(h, 15) ^ ror(tmp, 5)
   h ^= ror(h, 7)
   h += ror(h, 23)
   h ^= ror(h, 19)
   h += ror(h, 11)
   return h

}

func hillHeight(m *mountain, x, y float64) float64 {

   x -= float64(m.x)
   y -= float64(m.y)
   return float64(m.h) * math.Exp(-float64(x*x+y*y)/(m.r*m.r))

}

func hillHight(m *mountain, x, y float32) float32 {

   xx, yy := x-float32(m.x), y-float32(m.y)
   return float32(m.h) * float32(math.Exp(-float64(xx*xx+yy*yy)/(m.r*m.r)))

}

func groundHeight(x, y float32) float32 {

   p := world.hills
   h := float32(0)
   for p != nil {
       h += hillHight(p, x, y)
       p = p.next
   }
   return h

}

func calcNormal(x, y float32) vec {

   v := vec{}
   p := world.hills
   for p != nil {
       h := float32(hillHeight(p, float64(x), float64(y)))
       t := 2 / float32(p.r*p.r)
       v.x[0] += (x - float32(p.x)) * t * h
       v.x[1] += (y - float32(p.y)) * t * h
       p = p.next
   }
   v.x[2] = 1
   vnormalize(&v)
   return v

}

func makeTerrain(cx, cy int) {

   if cx*2 == world.x[0]+world.x[1] &&
       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
   var x, y int
   nx := world.x[1] - world.x[0] + 1
   ny := world.y[1] - world.y[0] + 1
   for world.hills != nil {
       world.hills = world.hills.next
   }
   for x = world.x[0]; x <= world.x[1]; x++ {
       for y = world.y[0]; y <= world.y[1]; y++ {
           h := hashXY(x, y) % mountainRatio
           if h != 0 {
               continue
           }
           m := &mountain{}
           m.x, m.y = x, y
           m.r = minMountainRadius + float64(hashXY(y, x)%100)/100.0*
               (maxMountainRadius-minMountainRadius)
           m.h = int(hashXY((y+x)/2, (y-x)/2) % maxMountainHeight)
           m.next = world.hills
           world.hills = m
       }
   }
   if world.ground == nil {
       world.ground = make([]float32, nx*ny)
   }
   if world.groundNormal == nil {
       world.groundNormal = make([]vec, nx*ny)
   }
   for x = 0; x < nx; x++ {
       xx := x + world.x[0]
       for y = 0; y < ny; y++ {
           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))
       }
   }

}

func boidThink(b *boid) {

   g := groundHeight(b.position.x[0], b.position.x[1])
   migrationDrive := vec{[3]float32{0, 0.5, 0}}
   heightDrive, crowdingDrive, groupingDrive := vec{}, vec{}, vec{}
   heightDrive.x[2] = (idealHeight + g - b.position.x[2]) * 0.3
   // follow the ground surface normal
   terrainDrive := calcNormal(b.position.x[0], b.position.x[1])
   totalWeight := float32(0)
   for i := 0; i < n; i++ {
       other := &boids[i]
       if other == b {
           continue
       }
       diff := vsub(other.position, b.position)
       d2 := vlen2(diff)
       weight := 1 / (d2 * d2)
       vnormalize(&diff)
       if d2 > idealDistance*idealDistance {
           vmuladdTo(&crowdingDrive, &diff, weight)
       } else {
           vmuladdTo(&crowdingDrive, &diff, -weight)
       }
       vmuladdTo(&groupingDrive, &other.heading, weight)
       totalWeight += weight
   }
   vscale(&groupingDrive, 1/totalWeight)
   b.newheading = migrationDrive
   vaddTo(&b.newheading, &heightDrive)
   vaddTo(&b.newheading, &terrainDrive)
   vaddTo(&b.newheading, &crowdingDrive)
   vaddTo(&b.newheading, &groupingDrive)
   vscale(&b.newheading, 0.2)
   vnormalize(&b.newheading)
   cx := float32(world.x[0]+world.x[1]) / 2.0
   cy := float32(world.y[0]+world.y[1]) / 2.0
   b.newheading.x[0] += (cx - b.position.x[0]) / 400
   b.newheading.x[1] += (cy - b.position.x[1]) / 400

}

func runBoids(msec int) {

   for i := 0; i < n; i++ {
       vmuladdTo(&boids[i].position, &boids[i].heading, float32(msec)*boids[i].speed)
   }
   average := vec{}
   for i := 0; i < n; i++ {
       vaddTo(&average, &boids[i].position)
   }
   vscale(&average, 1.0/n)
   camera.target = average
   makeTerrain(int(average.x[0]), int(average.x[1]))
   for i := 0; i < n; i++ {
       boidThink(&boids[i])
   }
   for i := 0; i < n; i++ {
       boids[i].heading = boids[i].newheading
   }

}

// windowing stuff var gwin, winWidth, winHeight int

func resize(w, h int) {

   winWidth, winHeight = w, h
   gl.Viewport(0, 0, int32(w), int32(h))

}

func 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)
   }
   gl.MatrixMode(gl.PROJECTION)
   gl.LoadIdentity()
   gl.Frustum(-hor, hor, -ver, ver, 0.1, 1000)

}

func clamp(x *float64, min, max float64) {

   if *x < min {
       *x = min
   } else if *x > max {
       *x = max
   }

}

func drawTerrain() {

   var x, y int
   nx := world.x[1] - world.x[0] + 1
   ny := world.y[1] - world.y[0] + 1
   gl.Color3f(0.1, 0.25, 0.35)
   for x = 0; x < nx-1; x++ {
       xx := x + world.x[0]
       gl.Begin(gl.QUAD_STRIP)
       for y = 0; y < ny; y++ {
           yy := y + world.y[0]
           gl.Normal3fv(&world.groundNormal[x*ny+y].x[0])
           gl.Vertex3f(float32(xx), float32(yy), world.ground[x*ny+y])
           gl.Normal3fv(&world.groundNormal[(1+x)*ny+y].x[0])
           gl.Vertex3f(float32(xx+1), float32(yy), world.ground[(1+x)*ny+y])
       }
       gl.End()
   }

}

func drawBoid(b *boid) {

   gl.Color3f(0.6, 0.3, 0.3)
   gl.PushMatrix()
   gl.Translatef(b.position.x[0], b.position.x[1], b.position.x[2])
   x := b.heading.x
   yaw := float32(math.Atan2(float64(x[1]), float64(x[0]))/math.Pi*180 - 90)
   gl.Rotatef(yaw, 0, 0, 1)
   rxy := float32(math.Sqrt(float64(x[0]*x[0] + x[1]*x[1])))
   pitch := float32(math.Atan2(float64(x[2]), float64(rxy)) / math.Pi * 180)
   gl.Rotatef(pitch, 1, 0, 0)
   gl.Begin(gl.TRIANGLES)
   gl.Normal3f(-0.8, 0, 0.6)
   gl.Vertex3f(0, 0.5, 0)
   gl.Vertex3f(-0.5, -0.5, 0)
   gl.Vertex3f(0, 0, 0.1)
   gl.Normal3f(0.8, 0, 0.6)
   gl.Vertex3f(0, 0.5, 0)
   gl.Vertex3f(0.5, -0.5, 0)
   gl.Vertex3f(0, 0, 0.1)
   gl.Normal3f(-0.8, 0, -0.6)
   gl.Vertex3f(0, 0.5, 0)
   gl.Vertex3f(-0.5, -0.5, 0)
   gl.Vertex3f(0, 0, -0.1)
   gl.Normal3f(0.8, 0, -0.6)
   gl.Vertex3f(0, 0.5, 0)
   gl.Vertex3f(0.5, -0.5, 0)
   gl.Vertex3f(0, 0, -0.1)
   gl.Normal3f(1, -1, 0)
   gl.Vertex3f(-0.5, -0.5, 0)
   gl.Vertex3f(0, 0, 0.1)
   gl.Vertex3f(0, 0, -0.1)
   gl.Normal3f(-1, -1, 0)
   gl.Vertex3f(0.5, -0.5, 0)
   gl.Vertex3f(0, 0, 0.1)
   gl.Vertex3f(0, 0, -0.1)
   gl.End()
   gl.PopMatrix()

}

func setLighting() {

   lightAmbient := [4]float32{0.3, 0.3, 0.3, 1}
   lightDiffuse := [4]float32{1, 1, 1, 1}
   lightPosition := [4]float32{0, 1, 2, 1}
   gl.Enable(gl.LIGHTING)
   gl.Lightfv(gl.LIGHT1, gl.AMBIENT, &lightAmbient[0])
   gl.Lightfv(gl.LIGHT1, gl.DIFFUSE, &lightDiffuse[0])
   gl.Lightfv(gl.LIGHT1, gl.POSITION, &lightPosition[0])
   gl.Enable(gl.LIGHT1)
   gl.ShadeModel(gl.FLAT)
   gl.Enable(gl.COLOR_MATERIAL)

}

type dbl = C.GLdouble

func render() {

   now := time.Now()
   msec := now.Sub(updateTime).Milliseconds()
   if msec < 16 {
       time.Sleep(time.Duration(16-msec) * time.Millisecond)
       return
   }
   runBoids(int(msec))
   updateTime = now
   gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
   gl.Enable(gl.DEPTH_TEST)
   setProjection(winWidth, winHeight)
   clamp(&camera.distance, 1, 1000)
   clamp(&camera.pitch, -math.Pi/2.1, math.Pi/2.1)
   rz := camera.distance * math.Sin(camera.pitch)
   rxy := camera.distance * math.Cos(camera.pitch)
   gl.MatrixMode(gl.MODELVIEW)
   gl.LoadIdentity()
   setLighting()
   fmt.Printf("%.5f %.5f\r", camera.target.x[0], camera.target.x[1])
   C.gluLookAt(dbl(float64(camera.target.x[0])-rxy*math.Cos(camera.yaw)),
       dbl(float64(camera.target.x[1])-rxy*math.Sin(camera.yaw)),
       dbl(float64(camera.target.x[2])-rz),
       dbl(camera.target.x[0]),
       dbl(camera.target.x[1]),
       dbl(camera.target.x[2]),
       0, 0, 1)
   drawTerrain()
   for i := 0; i < n; i++ {
       drawBoid(&boids[i])
   }
   gl.Flush()
   glut.SwapBuffers()

}

func keydown(key byte, x, y int) {

   fmt.Printf("key down: %c (%d %d)\n", key, x, y)
   if key == 'q' {
       os.Exit(0)
   }

}

func keyup(key byte, x, y int) {

   fmt.Printf("key up: %c (%d %d)\n", key, x, y)

}

// camera movement stuff var cursorX, cursorY int

func mousebutton(button, state, x, y int) {

   if state == glut.UP {
       return
   }
   if button == 3 {
       camera.distance /= 2
   } else if button == 4 {
       camera.distance *= 2
   }
   cursorX, cursorY = x, y

}

func mousemove(x, y int) {

   ext := winWidth
   if ext < winHeight {
       ext = winHeight
   }
   ext /= 4
   camera.yaw -= float64(x-cursorX) / float64(ext)
   camera.pitch -= float64(y-cursorY) / float64(ext)
   cursorY, cursorX = y, x

}

func initGL() {

   if err := gl.Init(); err != nil {
       log.Fatal(err)
   }
   updateTime = time.Now()
   glut.Init()
   glut.InitDisplayMode(glut.RGB | glut.DOUBLE)
   glut.InitWindowSize(600, 400)
   gwin = glut.CreateWindow("Boids")
   glut.IgnoreKeyRepeat(1)
   glut.KeyboardFunc(keydown)
   glut.KeyboardUpFunc(keyup)
   glut.ReshapeFunc(resize)
   glut.IdleFunc(render)
   glut.MouseFunc(mousebutton)
   glut.MotionFunc(mousemove)
   setLighting()

}

func main() {

   rand.Seed(time.Now().UnixNano())
   makeTerrain(0, 1)
   for i := 0; i < n; i++ {
       x := float32(rand.Intn(10) - 5)
       y := float32(rand.Intn(10) - 5)
       z := (rand.Float32()+0.5)*idealHeight + groundHeight(x, y)
       boids[i].position = vec{[3]float32{x, y, z}}
       boids[i].speed = (0.98 + 0.04*rand.Float32()) * moveSpeed
   }
   initGL()
   glut.MainLoop()

}</lang>