Boids/Go

From Rosetta Code
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.

package main

/*
#cgo LDFLAGS: -lGLU
#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()
}