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