Boids/Go
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
/*
- 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()
}</lang>