ソースを参照

Initial commit.

simondevyoutube 5 年 前
コミット
995fd059f7

+ 44 - 0
base.css

@@ -0,0 +1,44 @@
+.header {
+  font-size: 3em;
+  color: white;
+  background: #404040;
+  text-align: center;
+  height: 2.5em;
+  text-shadow: 4px 4px 4px black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+#error {
+  font-size: 2em;
+  color: red;
+  height: 50px;
+  text-shadow: 2px 2px 2px black;
+  margin: 2em;
+  display: none;
+}
+
+.container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+
+.visible {
+  display: block;
+}
+
+#target {
+  width: 100% !important;
+  height: 100% !important;
+  position: absolute;
+}
+
+body {
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}

+ 164 - 0
blaster.js

@@ -0,0 +1,164 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+export const blaster = (function() {
+
+  const _VS = `
+varying vec2 v_UV;
+varying vec3 vColor;
+
+void main() {
+  vColor = color;
+  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
+  gl_Position = projectionMatrix * mvPosition;
+  v_UV = uv;
+}
+`;
+
+  const _PS = `
+uniform sampler2D texture;
+varying vec2 v_UV;
+varying vec3 vColor;
+
+void main() {
+  gl_FragColor = vec4(vColor, 1.0) * texture2D(texture, v_UV);
+}
+`;
+
+  return {
+      BlasterSystem: class {
+        constructor(game, params) {
+          this._Initialize(game, params);
+        }
+
+        _Initialize(game, params) {
+          const uniforms = {
+          texture: {
+                  value: new THREE.TextureLoader().load(params.texture)
+              }
+          };
+          this._material = new THREE.ShaderMaterial( {
+              uniforms: uniforms,
+              vertexShader: _VS,
+              fragmentShader: _PS,
+
+              blending: THREE.AdditiveBlending,
+              depthTest: true,
+              depthWrite: false,
+              transparent: true,
+              vertexColors: true
+          } );
+
+          this._geometry = new THREE.BufferGeometry();
+
+          this._particleSystem = new THREE.Mesh(
+              this._geometry, this._material);
+
+          game._graphics._scene.add(this._particleSystem);
+
+          this._game = game;
+
+          this._liveParticles = [];
+        }
+
+        CreateParticle() {
+          const p = {
+            Start: new THREE.Vector3(0, 0, 0),
+            End: new THREE.Vector3(0, 0, 0),
+            Colour: new THREE.Color(),
+            Size: 1,
+            Alive: true,
+          };
+          this._liveParticles.push(p);
+          return p;
+        }
+
+        Update(timeInSeconds) {
+          for (const p of this._liveParticles) {
+            p.Life -= timeInSeconds;
+            p.End.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
+
+            const segment = p.End.clone().sub(p.Start);
+            if (segment.length() > p.Length) {
+              const dir = p.Velocity.clone().normalize();
+              p.Start = p.End.clone().sub(dir.multiplyScalar(p.Length));
+            }
+          }
+
+          this._liveParticles = this._liveParticles.filter(p => {
+            return p.Alive;
+          });
+
+          this._GenerateBuffers();
+        }
+
+        _GenerateBuffers() {
+          const indices = [];
+          const positions = [];
+          const colors = [];
+          const uvs = [];
+
+          const square = [0, 1, 2, 2, 3, 0];
+          let indexBase = 0;
+
+          for (const p of this._liveParticles) {
+            indices.push(...square.map(i => i + indexBase));
+            indexBase += 4;
+
+            const v1 = p.End.clone().applyMatrix4(
+                this._particleSystem.modelViewMatrix);
+            const v2 = p.Start.clone().applyMatrix4(
+                this._particleSystem.modelViewMatrix);
+            const dir = new THREE.Vector3().subVectors(v1, v2);
+            dir.z = 0;
+            dir.normalize();
+
+            const up = new THREE.Vector3(-dir.y, dir.x, 0);
+
+            const dirWS = up.clone().transformDirection(
+                this._game._graphics._camera.matrixWorld);
+            dirWS.multiplyScalar(p.Width);
+
+            const p1 = new THREE.Vector3().copy(p.Start);
+            p1.add(dirWS);
+
+            const p2 = new THREE.Vector3().copy(p.Start);
+            p2.sub(dirWS);
+
+            const p3 = new THREE.Vector3().copy(p.End);
+            p3.sub(dirWS);
+
+            const p4 = new THREE.Vector3().copy(p.End);
+            p4.add(dirWS);
+
+            positions.push(p1.x, p1.y, p1.z);
+            positions.push(p2.x, p2.y, p2.z);
+            positions.push(p3.x, p3.y, p3.z);
+            positions.push(p4.x, p4.y, p4.z);
+
+            uvs.push(0.0, 0.0);
+            uvs.push(1.0, 0.0);
+            uvs.push(1.0, 1.0);
+            uvs.push(0.0, 1.0);
+
+            const c = p.Colours[0].lerp(
+                p.Colours[1], 1.0 - p.Life / p.TotalLife);
+            for (let i = 0; i < 4; i++) {
+              colors.push(c.r, c.g, c.b);
+            }
+          }
+
+          this._geometry.setAttribute(
+              'position', new THREE.Float32BufferAttribute(positions, 3));
+          this._geometry.setAttribute(
+              'uv', new THREE.Float32BufferAttribute(uvs, 2));
+          this._geometry.setAttribute(
+              'color', new THREE.Float32BufferAttribute(colors, 3));
+          this._geometry.setIndex(
+              new THREE.BufferAttribute(new Uint32Array(indices), 1));
+
+          this._geometry.attributes.position.needsUpdate = true;
+          this._geometry.attributes.uv.needsUpdate = true;
+        }
+      }
+  };
+})();

+ 432 - 0
debug.js

@@ -0,0 +1,432 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {GUI} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/dat.gui.module.js';
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {visibility} from './visibility.js';
+
+let _APP = null;
+
+const _NUM_BOIDS = 30;
+const _BOID_SPEED = 5;
+const _BOID_ACCELERATION = _BOID_SPEED / 5.0;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 10.0;
+const _BOID_FORCE_ALIGNMENT = 5;
+const _BOID_FORCE_SEPARATION = 8;
+const _BOID_FORCE_COHESION = 4;
+const _BOID_FORCE_WANDER = 5;
+
+
+class LineRenderer {
+  constructor(game) {
+    this._game = game;
+
+    this._materials = {};
+    this._group = new THREE.Group();
+
+    this._game._graphics.Scene.add(this._group);
+  }
+
+  Reset() {
+    this._lines = [];
+    this._group.remove(...this._group.children);
+  }
+
+  Add(pt1, pt2, hexColour) {
+    const geometry = new THREE.Geometry();
+    geometry.vertices.push(pt1.clone());
+    geometry.vertices.push(pt2.clone());
+
+    let material = this._materials[hexColour];
+    if (!material) {
+      this._materials[hexColour] = new THREE.LineBasicMaterial(
+          {color: hexColour});
+      material = this._materials[hexColour];
+    }
+
+    const line = new THREE.Line(geometry, material);
+    this._lines.push(line);
+    this._group.add(line);
+  }
+}
+
+
+class Boid {
+  constructor(game, params) {
+    this._mesh = new THREE.Mesh(
+        params.geometry,
+        new THREE.MeshStandardMaterial({color: params.colour}));
+    this._mesh.castShadow = true;
+    this._mesh.receiveShadow = false;
+
+    this._group = new THREE.Group();
+    this._group.add(this._mesh);
+    this._group.position.set(
+        math.rand_range(-50, 50),
+        0,
+        math.rand_range(-50, 50));
+    this._direction = new THREE.Vector3(
+        math.rand_range(-1, 1),
+        0,
+        math.rand_range(-1, 1));
+    this._velocity = this._direction.clone();
+
+    this._maxSteeringForce = params.maxSteeringForce;
+    this._maxSpeed  = params.speed;
+    this._acceleration = params.acceleration;
+
+    this._radius = 1.0;
+    this._mesh.rotateX(-Math.PI / 2);
+
+    this._game = game;
+    game._graphics.Scene.add(this._group);
+    this._visibilityIndex = game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this);
+
+    this._wanderAngle = 0;
+    this._params = params;
+  }
+
+  DisplayDebug() {
+    const geometry = new THREE.SphereGeometry(10, 64, 64);
+    const material = new THREE.MeshBasicMaterial({
+      color: 0xFF0000,
+      transparent: true,
+      opacity: 0.25,
+    });
+    const mesh = new THREE.Mesh(geometry, material);
+    this._group.add(mesh);
+
+    this._mesh.material.color.setHex(0xFF0000);
+    this._displayDebug = true;
+    this._lineRenderer = new LineRenderer(this._game);
+  }
+
+  _UpdateDebug(local) {
+    this._lineRenderer.Reset();
+    this._lineRenderer.Add(
+        this.Position, this.Position.clone().add(this._velocity),
+        0xFFFFFF);
+    for (const e of local) {
+      this._lineRenderer.Add(this.Position, e.Position, 0x00FF00);
+      this._lineRenderer.Add(
+          e.Position, e.Position.clone().add(e._velocity),
+          0xFFFFFF);
+    }
+  }
+
+  get Position() {
+    return this._group.position;
+  }
+
+  get Velocity() {
+    return this._velocity;
+  }
+
+  get Direction() {
+    return this._direction;
+  }
+
+  get Radius() {
+    return this._radius;
+  }
+
+  Step(timeInSeconds) {
+    if (this._displayDebug) {
+      let a = 0;
+    }
+
+    const local = this._game._visibilityGrid.GetLocalEntities(
+        this.Position, 15);
+
+    this._ApplySteering(timeInSeconds, local);
+
+    const frameVelocity = this._velocity.clone();
+    frameVelocity.multiplyScalar(timeInSeconds);
+    this._group.position.add(frameVelocity);
+
+    const direction = this.Direction;
+    const m = new THREE.Matrix4();
+    m.lookAt(
+        new THREE.Vector3(0, 0, 0),
+        direction,
+        new THREE.Vector3(0, 1, 0));
+    this._group.quaternion.setFromRotationMatrix(m);
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this, this._visibilityIndex);
+
+    if (this._displayDebug) {
+      this._UpdateDebug(local);
+    }
+  }
+
+  CheckBounds() {
+    const pos = this._group.position;
+    if (pos.x > 65) {
+      pos.x = -65;
+    } else if (pos.x < -65) {
+      pos.x = 65;
+    } else if (pos.z < -35) {
+      pos.z = 35;
+    } else if (pos.z > 35) {
+      pos.z = -35;
+    }
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this, this._visibilityIndex);
+  }
+
+  _ApplySteering(timeInSeconds, local) {
+    const forces = [
+      this._ApplyWander(),
+    ];
+
+    if (this._params.guiParams.separationEnabled) {
+      forces.push(this._ApplySeparation(local));
+    }
+
+    if (this._params.guiParams.alignmentEnabled) {
+      forces.push(this._ApplyAlignment(local));
+    }
+
+    if (this._params.guiParams.cohesionEnabled) {
+      forces.push(this._ApplyCohesion(local));
+    }
+
+    const steeringForce = new THREE.Vector3(0, 0, 0);
+    for (const f of forces) {
+      steeringForce.add(f);
+    }
+
+    steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+
+    // Lock to xz dimension
+    steeringForce.multiply(new THREE.Vector3(1, 0, 1));
+
+    // Clamp the force applied
+    steeringForce.normalize();
+    steeringForce.multiplyScalar(this._maxSteeringForce);
+
+    this._velocity.add(steeringForce);
+
+    // Lock velocity for debug mode
+    this._velocity.normalize();
+    this._velocity.multiplyScalar(this._maxSpeed);
+
+    this._direction = this._velocity.clone();
+    this._direction.normalize();
+  }
+
+  _ApplyWander() {
+    this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
+    const randomPointOnCircle = new THREE.Vector3(
+        Math.cos(this._wanderAngle),
+        0,
+        Math.sin(this._wanderAngle));
+    const pointAhead = this._direction.clone();
+    pointAhead.multiplyScalar(2);
+    pointAhead.add(randomPointOnCircle);
+    pointAhead.normalize();
+    return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
+  }
+
+
+
+
+
+
+  _CalculateSeparationForce() {
+    totalForce = 0;
+    for (every boid in the area) {
+      totalForce += (ourPosition - theirPosition) / distanceBetween;
+    }
+    return totalForce;
+  }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+  _CalculateSeparationForce(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      const distanceToEntity = Math.max(
+          e.Position.distanceTo(this.Position) - (this.Radius + e.Radius),
+          0.001);
+      const directionFromEntity = new THREE.Vector3().subVectors(
+          this.Position, e.Position);
+      directionFromEntity.normalize();
+
+      const multiplier = _BOID_FORCE_SEPARATION * (
+          (this.Radius + e.Radius) / distanceToEntity);
+
+      forceVector.add(
+          directionFromEntity.multiplyScalar(multiplier));
+    }
+    return forceVector;
+  }
+
+  _ApplyAlignment(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    for (let e of local) {
+      const entityDirection = e.Direction;
+      forceVector.add(entityDirection);
+    }
+
+    forceVector.normalize();
+    forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
+
+    return forceVector;
+  }
+
+  _ApplyCohesion(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    if (local.length == 0) {
+      return forceVector;
+    }
+
+    const averagePosition = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      averagePosition.add(e.Position);
+    }
+
+    averagePosition.multiplyScalar(1.0 / local.length);
+
+    const directionToAveragePosition = averagePosition.clone().sub(
+        this.Position);
+    directionToAveragePosition.normalize();
+    directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
+
+    return directionToAveragePosition;
+  }
+}
+
+
+class DebugDemo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = [];
+
+    this._guiParams = {
+      separationEnabled: false,
+      cohesionEnabled: false,
+      alignmentEnabled: false,
+    };
+    this._gui = new GUI();
+    this._gui.add(this._guiParams, "separationEnabled");
+    this._gui.add(this._guiParams, "cohesionEnabled");
+    this._gui.add(this._guiParams, "alignmentEnabled");
+    this._gui.close();
+
+    const geoLibrary = {
+      cone: new THREE.ConeGeometry(1, 2, 32)
+    };
+    this._CreateEntities();
+    this._CreateBoids(geoLibrary);
+  }
+
+  _CreateEntities() {
+    const plane = new THREE.Mesh(
+        new THREE.PlaneGeometry(400, 400, 32, 32),
+        new THREE.MeshStandardMaterial({
+            color: 0x808080,
+            transparent: false,
+        }));
+    plane.position.set(0, -2, 0);
+    plane.castShadow = false;
+    plane.receiveShadow = false;
+    plane.rotation.x = -Math.PI / 2;
+    this._graphics.Scene.add(plane);
+
+    this._visibilityGrid = new visibility.VisibilityGrid(
+        [new THREE.Vector3(-500, 0, -500), new THREE.Vector3(500, 0, 500)],
+        [100, 100]);
+    this._graphics._camera.position.set(0, 50, 0);
+    this._controls.target.set(0, 0, 0);
+    this._controls.update();
+  }
+
+  _CreateBoids(geoLibrary) {
+    let params = {
+      geometry: geoLibrary.cone,
+      speedMin: 1.0,
+      speedMax: 1.0,
+      speed: _BOID_SPEED,
+      maxSteeringForce: _BOID_FORCE_MAX,
+      acceleration: _BOID_ACCELERATION,
+      colour: 0x80FF80,
+      guiParams: this._guiParams
+    };
+    for (let i = 0; i < _NUM_BOIDS * 2; i++) {
+      const e = new Boid(this, params);
+      this._entities.push(e);
+    }
+    this._entities[0].DisplayDebug();
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    if (this._entities.length == 0) {
+      return;
+    }
+
+    for (let e of this._entities) {
+      e.Step(timeInSeconds);
+    }
+
+    for (let e of this._entities) {
+      // Teleport to other side if offscreen
+      e.CheckBounds();
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new DebugDemo();
+}
+
+_Main();

+ 460 - 0
fish.js

@@ -0,0 +1,460 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {game} from './game.js';
+import {math} from './math.js';
+import {visibility} from './visibility.js';
+import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/OBJLoader.js';
+
+let _APP = null;
+
+const _NUM_BOIDS = 350;
+const _BOID_SPEED = 2.5;
+const _BOID_ACCELERATION = _BOID_SPEED / 5.0;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 10.0;
+const _BOID_FORCE_ORIGIN = 8;
+const _BOID_FORCE_ALIGNMENT = 10;
+const _BOID_FORCE_SEPARATION = 20;
+const _BOID_FORCE_COHESION = 10;
+const _BOID_FORCE_WANDER = 3;
+
+
+class LineRenderer {
+  constructor(game) {
+    this._game = game;
+
+    this._materials = {};
+    this._group = new THREE.Group();
+
+    this._game._graphics.Scene.add(this._group);
+  }
+
+  Reset() {
+    this._lines = [];
+    this._group.remove(...this._group.children);
+  }
+
+  Add(pt1, pt2, hexColour) {
+    const geometry = new THREE.Geometry();
+    geometry.vertices.push(pt1);
+    geometry.vertices.push(pt2);
+
+    let material = this._materials[hexColour];
+    if (!material) {
+      this._materials[hexColour] = new THREE.LineBasicMaterial(
+          {color: hexColour});
+      material = this._materials[hexColour];
+    }
+
+    const line = new THREE.Line(geometry, material);
+    this._lines.push(line);
+    this._group.add(line);
+  }
+}
+
+
+class Boid {
+  constructor(game, params) {
+    this._mesh = new THREE.Mesh(
+        params.geometry,
+        new THREE.MeshStandardMaterial({color: params.colour}));
+    this._mesh.castShadow = true;
+    this._mesh.receiveShadow = false;
+
+    this._group = new THREE.Group();
+    this._group.add(this._mesh);
+    this._group.position.set(
+        math.rand_range(-50, 50),
+        math.rand_range(1, 25),
+        math.rand_range(-50, 50));
+    this._direction = new THREE.Vector3(
+        math.rand_range(-1, 1),
+        0,
+        math.rand_range(-1, 1));
+    this._velocity = this._direction.clone();
+
+    const speedMultiplier = math.rand_range(params.speedMin, params.speedMax);
+    this._maxSteeringForce = params.maxSteeringForce * speedMultiplier;
+    this._maxSpeed  = params.speed * speedMultiplier;
+    this._acceleration = params.acceleration * speedMultiplier;
+
+    const scale = 1.0 / speedMultiplier;
+    this._radius = scale;
+    this._mesh.scale.setScalar(scale);
+    this._mesh.rotateX(-Math.PI / 2);
+
+    this._game = game;
+    game._graphics.Scene.add(this._group);
+    this._visibilityIndex = game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this);
+
+    this._wanderAngle = 0;
+  }
+
+  DisplayDebug() {
+    const geometry = new THREE.SphereGeometry(10, 64, 64);
+    const material = new THREE.MeshBasicMaterial({
+      color: 0xFF0000,
+      transparent: true,
+      opacity: 0.25,
+    });
+    const mesh = new THREE.Mesh(geometry, material);
+    this._group.add(mesh);
+
+    this._mesh.material.color.setHex(0xFF0000);
+    this._displayDebug = true;
+    this._lineRenderer = new LineRenderer(this._game);
+  }
+
+  _UpdateDebug(local) {
+    this._lineRenderer.Reset();
+    this._lineRenderer.Add(
+        this.Position, this.Position.clone().add(this._velocity),
+        0xFFFFFF);
+    for (const e of local) {
+      this._lineRenderer.Add(this.Position, e.Position, 0x00FF00);
+    }
+  }
+
+  get Position() {
+    return this._group.position;
+  }
+
+  get Velocity() {
+    return this._velocity;
+  }
+
+  get Direction() {
+    return this._direction;
+  }
+
+  get Radius() {
+    return this._radius;
+  }
+
+  Step(timeInSeconds) {
+    if (this._displayDebug) {
+      let a = 0;
+    }
+
+    const local = this._game._visibilityGrid.GetLocalEntities(
+        this.Position, 15);
+
+    this._ApplySteering(timeInSeconds, local);
+
+    const frameVelocity = this._velocity.clone();
+    frameVelocity.multiplyScalar(timeInSeconds);
+    this._group.position.add(frameVelocity);
+
+    const direction = this.Direction;
+    const m = new THREE.Matrix4();
+    m.lookAt(
+        new THREE.Vector3(0, 0, 0),
+        direction,
+        new THREE.Vector3(0, 1, 0));
+    this._group.quaternion.setFromRotationMatrix(m);
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this, this._visibilityIndex);
+
+    if (this._displayDebug) {
+      this._UpdateDebug(local);
+    }
+  }
+
+  _ApplySteering(timeInSeconds, local) {
+    const forces = [
+      this._ApplySeek(new THREE.Vector3(0, 10, 0)),
+      this._ApplyWander(),
+      this._ApplyGroundAvoidance(),
+      this._ApplySeparation(local),
+    ];
+
+    if (this._radius < 5) {
+      // Only apply alignment and cohesion to similar sized fish.
+      local = local.filter((e) => {
+        const ratio = this.Radius / e.Radius;
+
+        return (ratio <= 1.35 && ratio >= 0.75);
+      });
+
+      forces.push(
+        this._ApplyAlignment(local),
+        this._ApplyCohesion(local),
+        this._ApplySeparation(local)
+      )
+    }
+
+    const steeringForce = new THREE.Vector3(0, 0, 0);
+    for (const f of forces) {
+      steeringForce.add(f);
+    }
+
+    steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+
+    // Preferentially move in x/z dimension
+    steeringForce.multiply(new THREE.Vector3(1, 0.25, 1));
+
+    // Clamp the force applied
+    if (steeringForce.length() > this._maxSteeringForce) {
+      steeringForce.normalize();
+      steeringForce.multiplyScalar(this._maxSteeringForce);
+    }
+
+    this._velocity.add(steeringForce);
+
+    // Clamp velocity
+    if (this._velocity.length() > this._maxSpeed) {
+      this._velocity.normalize();
+      this._velocity.multiplyScalar(this._maxSpeed);
+    }
+
+    this._direction = this._velocity.clone();
+    this._direction.normalize();
+  }
+
+  _ApplyGroundAvoidance() {
+    const p = this.Position;
+    let force = new THREE.Vector3(0, 0, 0);
+
+    if (p.y < 10) {
+      force = new THREE.Vector3(0, 10 - p.y, 0);
+    } else if (p.y > 30) {
+      force = new THREE.Vector3(0, p.y - 50, 0);
+    }
+    return force.multiplyScalar(_BOID_FORCE_SEPARATION);
+  }
+
+  _ApplyWander() {
+    this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
+    const randomPointOnCircle = new THREE.Vector3(
+        Math.cos(this._wanderAngle),
+        0,
+        Math.sin(this._wanderAngle));
+    const pointAhead = this._direction.clone();
+    pointAhead.multiplyScalar(2);
+    pointAhead.add(randomPointOnCircle);
+    pointAhead.normalize();
+    return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
+  }
+
+  _ApplySeparation(local) {
+    if (local.length == 0) {
+      return new THREE.Vector3(0, 0, 0);
+    }
+
+    const forceVector = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      const distanceToEntity = Math.max(
+          e.Position.distanceTo(this.Position) - 1.5 * (this.Radius + e.Radius),
+          0.001);
+      const directionFromEntity = new THREE.Vector3().subVectors(
+          this.Position, e.Position);
+      const multiplier = (
+          _BOID_FORCE_SEPARATION / distanceToEntity) * (this.Radius + e.Radius);
+      directionFromEntity.normalize();
+      forceVector.add(
+          directionFromEntity.multiplyScalar(multiplier));
+    }
+    return forceVector;
+  }
+
+  _ApplyAlignment(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    for (let e of local) {
+      const entityDirection = e.Direction;
+      forceVector.add(entityDirection);
+    }
+
+    forceVector.normalize();
+    forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
+
+    return forceVector;
+  }
+
+  _ApplyCohesion(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    if (local.length == 0) {
+      return forceVector;
+    }
+
+    const averagePosition = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      averagePosition.add(e.Position);
+    }
+
+    averagePosition.multiplyScalar(1.0 / local.length);
+
+    const directionToAveragePosition = averagePosition.clone().sub(
+        this.Position);
+    directionToAveragePosition.normalize();
+    directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
+
+    return directionToAveragePosition;
+  }
+
+  _ApplySeek(destination) {
+    const distance = Math.max(0,((
+        this.Position.distanceTo(destination) - 50) / 250)) ** 2;
+    const direction = destination.clone().sub(this.Position);
+    direction.normalize();
+
+    const forceVector = direction.multiplyScalar(
+        _BOID_FORCE_ORIGIN * distance);
+    return forceVector;
+  }
+}
+
+
+class FishDemo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = [];
+
+    this._graphics.Scene.fog = new THREE.FogExp2(
+        new THREE.Color(0x4d7dbe), 0.01);
+
+    this._LoadBackground();
+
+    const loader = new OBJLoader();
+    const geoLibrary = {};
+    loader.load("./resources/fish.obj", (result) => {
+      geoLibrary.fish = result.children[0].geometry;
+      loader.load("./resources/bigfish.obj", (result) => {
+        geoLibrary.bigFish = result.children[0].geometry;
+        this._CreateBoids(geoLibrary);
+      });
+    });
+    this._CreateEntities();
+  }
+
+  _LoadBackground() {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('./resources/underwater.jpg');
+    this._graphics._scene.background = texture;
+  }
+
+  _CreateEntities() {
+    const plane = new THREE.Mesh(
+        new THREE.PlaneGeometry(400, 400, 32, 32),
+        new THREE.MeshStandardMaterial({
+            color: 0x837860,
+            transparent: true,
+            opacity: 0.5,
+        }));
+    plane.position.set(0, -5, 0);
+    plane.castShadow = false;
+    plane.receiveShadow = true;
+    plane.rotation.x = -Math.PI / 2;
+    this._graphics.Scene.add(plane);
+
+    this._visibilityGrid = new visibility.VisibilityGrid(
+        [new THREE.Vector3(-500, 0, -500), new THREE.Vector3(500, 0, 500)],
+        [100, 100]);
+
+  }
+
+  _CreateBoids(geoLibrary) {
+    const NUM_SMALL = _NUM_BOIDS * 2;
+    const NUM_MEDIUM = _NUM_BOIDS / 2;
+    const NUM_LARGE = _NUM_BOIDS / 20;
+    const NUM_WHALES = 3;
+
+    let params = {
+      geometry: geoLibrary.fish,
+      speedMin: 3.0,
+      speedMax: 4.0,
+      speed: _BOID_SPEED,
+      maxSteeringForce: _BOID_FORCE_MAX,
+      acceleration: _BOID_ACCELERATION,
+      colour: 0x80FF80,
+    };
+    for (let i = 0; i < NUM_SMALL; i++) {
+      const e = new Boid(this, params);
+      this._entities.push(e);
+    }
+
+    params = {
+      geometry: geoLibrary.fish,
+      speedMin: 0.85,
+      speedMax: 1.1,
+      speed: _BOID_SPEED,
+      maxSteeringForce: _BOID_FORCE_MAX,
+      acceleration: _BOID_ACCELERATION,
+      colour: 0x8080FF,
+    };
+    for (let i = 0; i < NUM_MEDIUM; i++) {
+      const e = new Boid(this, params);
+      this._entities.push(e);
+    }
+
+    params = {
+      geometry: geoLibrary.fish,
+      speedMin: 0.4,
+      speedMax: 0.6,
+      speed: _BOID_SPEED,
+      maxSteeringForce: _BOID_FORCE_MAX / 4,
+      acceleration: _BOID_ACCELERATION,
+      colour: 0xFF8080,
+    };
+    for (let i = 0; i < NUM_LARGE; i++) {
+      const e = new Boid(this, params);
+      this._entities.push(e);
+    }
+
+    params = {
+      geometry: geoLibrary.bigFish,
+      speedMin: 0.1,
+      speedMax: 0.12,
+      speed: _BOID_SPEED,
+      maxSteeringForce: _BOID_FORCE_MAX / 20,
+      acceleration: _BOID_ACCELERATION,
+      colour: 0xFF8080,
+    };
+    for (let i = 0; i < NUM_WHALES; i++) {
+      const e = new Boid(this, params);
+      e._group.position.y = math.rand_range(23, 26);
+      this._entities.push(e);
+    }
+    //this._entities[0].DisplayDebug();
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    if (this._entities.length == 0) {
+      return;
+    }
+
+    // const eye = this._entities[0].Position.clone();
+    // const dir = this._entities[0].Direction.clone();
+    // dir.multiplyScalar(5);
+    // eye.sub(dir);
+    //
+    // const m = new THREE.Matrix4();
+    // m.lookAt(eye, this._entities[0].Position, new THREE.Vector3(0, 1, 0));
+    //
+    // const q = new THREE.Quaternion();
+    // q.setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0));
+    //
+    // const oldPosition = this._graphics._camera.position;
+    // this._graphics._camera.position.lerp(eye, 0.05);
+    // this._graphics._camera.quaternion.copy(this._entities[0]._group.quaternion);
+    // //this._graphics._camera.quaternion.multiply(q);
+    // this._controls.enabled = false;
+
+    for (let e of this._entities) {
+      e.Step(timeInSeconds);
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new FishDemo();
+}
+
+_Main();

+ 59 - 0
game.js

@@ -0,0 +1,59 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import { WEBGL } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+import {graphics} from './graphics.js';
+
+export const game = (function() {
+  return {
+    Game: class {
+      constructor() {
+        this._Initialize();
+      }
+
+      _Initialize() {
+        this._graphics = new graphics.Graphics(this);
+        if (!this._graphics.Initialize()) {
+          this._DisplayError('WebGL2 is not available.');
+          return;
+        }
+
+        this._controls = this._CreateControls();
+        this._previousRAF = null;
+
+        this._OnInitialize();
+        this._RAF();
+      }
+
+      _CreateControls() {
+        const controls = new OrbitControls(
+            this._graphics._camera, this._graphics._threejs.domElement);
+        controls.target.set(0, 0, 0);
+        controls.update();
+        return controls;
+      }
+
+      _DisplayError(errorText) {
+        const error = document.getElementById('error');
+        error.innerText = errorText;
+      }
+
+      _RAF() {
+        requestAnimationFrame((t) => {
+          if (this._previousRAF === null) {
+            this._previousRAF = t;
+          }
+          this._Render(t - this._previousRAF);
+          this._previousRAF = t;
+        });
+      }
+
+      _Render(timeInMS) {
+        const timeInSeconds = timeInMS * 0.001;
+        this._OnStep(timeInSeconds);
+        this._graphics.Render(timeInSeconds);
+
+        this._RAF();
+      }
+    }
+  };
+})();

+ 116 - 0
graphics.js

@@ -0,0 +1,116 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import Stats from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/stats.module.js';
+import {WEBGL} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/EffectComposer.js';
+import {RenderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/RenderPass.js';
+import {GlitchPass } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/GlitchPass.js';
+import {UnrealBloomPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/UnrealBloomPass.js';
+
+export const graphics = (function() {
+  return {
+    PostFX: {
+      UnrealBloomPass: UnrealBloomPass,
+      GlitchPass: GlitchPass,
+    },
+    Graphics: class {
+      constructor(game) {
+      }
+
+      Initialize() {
+        if (!WEBGL.isWebGL2Available()) {
+          return false;
+        }
+
+        this._threejs = new THREE.WebGLRenderer({
+            antialias: true,
+        });
+        this._threejs.shadowMap.enabled = true;
+        this._threejs.shadowMap.type = THREE.PCFSoftShadowMap;
+        this._threejs.setPixelRatio(window.devicePixelRatio);
+        this._threejs.setSize(window.innerWidth, window.innerHeight);
+
+        const target = document.getElementById('target');
+        target.appendChild(this._threejs.domElement);
+
+        this._stats = new Stats();
+				target.appendChild(this._stats.dom);
+
+        window.addEventListener('resize', () => {
+          this._OnWindowResize();
+        }, false);
+
+        const fov = 60;
+        const aspect = 1920 / 1080;
+        const near = 1.0;
+        const far = 1000.0;
+        this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+        this._camera.position.set(75, 20, 0);
+
+        this._scene = new THREE.Scene();
+
+        this._CreateLights();
+
+        const composer = new EffectComposer(this._threejs);
+        this._composer = composer;
+        this._composer.addPass(new RenderPass(this._scene, this._camera));
+
+        return true;
+      }
+
+      _CreateLights() {
+        let light = new THREE.DirectionalLight(0xFFFFFF, 1, 100);
+        light.position.set(100, 100, 100);
+        light.target.position.set(0, 0, 0);
+        light.castShadow = true;
+        light.shadowCameraVisible = true;
+        light.shadow.bias = -0.01;
+        light.shadow.mapSize.width = 2048;
+        light.shadow.mapSize.height = 2048;
+        light.shadow.camera.near = 1.0;
+        light.shadow.camera.far = 500;
+        light.shadow.camera.left = 200;
+        light.shadow.camera.right = -200;
+        light.shadow.camera.top = 200;
+        light.shadow.camera.bottom = -200;
+        this._scene.add(light);
+
+        light = new THREE.DirectionalLight(0x404040, 1, 100);
+        light.position.set(-100, 100, -100);
+        light.target.position.set(0, 0, 0);
+        light.castShadow = false;
+        this._scene.add(light);
+
+        light = new THREE.DirectionalLight(0x404040, 1, 100);
+        light.position.set(100, 100, -100);
+        light.target.position.set(0, 0, 0);
+        light.castShadow = false;
+        this._scene.add(light);
+      }
+
+      AddPostFX(passClass, params) {
+        const pass = new passClass();
+        for (const k in params) {
+          pass[k] = params[k];
+        }
+        this._composer.addPass(pass);
+        return pass;
+      }
+
+      _OnWindowResize() {
+        this._camera.aspect = window.innerWidth / window.innerHeight;
+        this._camera.updateProjectionMatrix();
+        this._threejs.setSize(window.innerWidth, window.innerHeight);
+        this._composer.setSize(window.innerWidth, window.innerHeight);
+      }
+
+      get Scene() {
+        return this._scene;
+      }
+
+      Render(timeInSeconds) {
+        this._composer.render();
+        this._stats.update();
+      }
+    }
+  };
+})();

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Javascript 3D Projects: Flocking Demo</title>
+  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+  <link rel="stylesheet" type="text/css" href="base.css">
+  <script src="https://github.com/mrdoob/three.js/blob/r112/build/three.module.js"></script>
+</head>
+<body>
+  <div id="target"></canvas>
+  <script src="space.js" type="module">
+  </script>
+</body>
+</html>

+ 24 - 0
math.js

@@ -0,0 +1,24 @@
+export const math = (function() {
+  return {
+    rand_range: function (a, b) {
+      return Math.random() * (b - a) + a;
+    },
+
+    rand_normalish: function () {
+      const r = Math.random() + Math.random() + Math.random() + Math.random();
+      return (r / 4.0) * 2.0 - 1;
+    },
+
+    lerp: function (x, a, b) {
+      return x * (b - a) + a;
+    },
+
+    clamp: function (x, a, b) {
+      return Math.min(Math.max(x, a), b);
+    },
+
+    sat: function (x) {
+      return Math.min(Math.max(x, 0.0), 1.0);
+    },
+  };
+})();

+ 101 - 0
particles.js

@@ -0,0 +1,101 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+export const particles = (function() {
+
+  const _VS = `
+attribute float size;
+varying vec3 vColor;
+
+void main() {
+  vColor = color;
+  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
+  gl_PointSize = size * ( 300.0 / -mvPosition.z );
+  gl_Position = projectionMatrix * mvPosition;
+}
+`;
+
+  const _PS = `
+uniform sampler2D pointTexture;
+varying vec3 vColor;
+
+void main() {
+  gl_FragColor = vec4(vColor * 4.0, 1.0);
+  gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord );
+}
+`;
+
+  return {
+      ParticleSystem: class {
+        constructor(game, params) {
+          this._Initialize(game, params);
+        }
+
+        _Initialize(game, params) {
+          const uniforms = {
+          pointTexture: {
+                  value: new THREE.TextureLoader().load(params.texture)
+              }
+          };
+          this._material = new THREE.ShaderMaterial( {
+              uniforms: uniforms,
+              vertexShader: _VS,
+              fragmentShader: _PS,
+
+              blending: THREE.AdditiveBlending,
+              depthTest: true,
+              depthWrite: false,
+              transparent: true,
+              vertexColors: true
+          } );
+
+          this._geometry = new THREE.BufferGeometry();
+
+          this._particleSystem = new THREE.Points(
+              this._geometry, this._material);
+
+          game._graphics._scene.add(this._particleSystem);
+
+          this._liveParticles = [];
+        }
+
+        CreateParticle() {
+          const p = {
+            Position: new THREE.Vector3(0, 0, 0),
+            Colour: new THREE.Color(),
+            Size: 1,
+            Alive: true,
+          };
+          this._liveParticles.push(p);
+          return p;
+        }
+
+        Update() {
+          this._liveParticles = this._liveParticles.filter(p => {
+            return p.Alive;
+          });
+
+          const positions = [];
+          const colors = [];
+          const sizes = [];
+
+          for (const p of this._liveParticles) {
+            positions.push(p.Position.x, p.Position.y, p.Position.z);
+            colors.push(p.Colour.r, p.Colour.g, p.Colour.b);
+            sizes.push(p.Size);
+          }
+
+          this._geometry.setAttribute(
+              'position', new THREE.Float32BufferAttribute(positions, 3));
+          this._geometry.setAttribute(
+              'color', new THREE.Float32BufferAttribute(colors, 3));
+          this._geometry.setAttribute(
+              'size', new THREE.Float32BufferAttribute(sizes, 1).setUsage(
+                  THREE.DynamicDrawUsage));
+
+          this._geometry.attributes.position.needsUpdate = true;
+          this._geometry.attributes.color.needsUpdate = true;
+          this._geometry.attributes.size.needsUpdate = true;
+        }
+      }
+  };
+})();

+ 308 - 0
resources/bigfish.obj

@@ -0,0 +1,308 @@
+# Blender v2.79 (sub 0) OBJ File: 'bigfish.blend'
+# www.blender.org
+mtllib bigfish.mtl
+o Fish_Mesh
+v -0.025934 -0.930735 0.247159
+v -0.047352 1.497133 0.116014
+v -0.025934 -0.930735 -0.265016
+v -0.047352 1.497133 -0.157841
+v 0.025934 -0.930735 0.247159
+v 0.047352 1.497133 0.116014
+v 0.025934 -0.930735 -0.265016
+v 0.047352 1.497133 -0.157841
+v -0.052007 -0.734213 0.173356
+v -0.052007 -0.376371 0.107881
+v -0.070747 -0.141090 0.235824
+v -0.133233 0.401762 0.337004
+v -0.164172 0.709300 0.390242
+v -0.164172 1.079936 0.390242
+v -0.123239 1.079936 -0.328899
+v -0.123239 0.709300 -0.328899
+v -0.092301 0.401762 -0.288978
+v -0.070747 -0.141090 -0.206026
+v -0.052007 -0.376371 -0.119786
+v -0.052007 -0.734213 -0.167404
+v 0.122440 1.079936 -0.328899
+v 0.122440 0.709300 -0.328899
+v 0.091502 0.401762 -0.288978
+v 0.070747 -0.141090 -0.206026
+v 0.052007 -0.376371 -0.119786
+v 0.052007 -0.734213 -0.167404
+v 0.164172 1.079936 0.390242
+v 0.164172 0.709300 0.390242
+v 0.133233 0.401762 0.337004
+v 0.070747 -0.141090 0.235824
+v 0.052007 -0.376371 0.107881
+v 0.052007 -0.734213 0.173356
+v -0.025934 -0.930735 0.000000
+v -0.047352 1.497133 0.000000
+v 0.025934 -0.930735 0.000000
+v 0.047352 1.497133 0.000000
+v 0.052007 -0.734213 0.000000
+v 0.052007 -0.376371 0.000000
+v 0.070747 -0.141090 0.000000
+v 0.425164 0.401762 0.000000
+v 0.456102 0.709300 0.000000
+v 0.456102 1.079936 0.000000
+v -0.052007 -0.734213 0.000000
+v -0.052007 -0.376371 0.000000
+v -0.070747 -0.141090 0.000000
+v -0.413461 0.401762 0.000000
+v -0.444400 0.709300 0.000000
+v -0.444400 1.079936 0.000000
+v -0.123117 1.322820 0.303283
+v -0.082185 1.322820 -0.255257
+v 0.081386 1.322820 -0.255257
+v 0.123117 1.322820 0.303283
+v 0.415047 1.322820 0.000000
+v -0.403345 1.322820 0.000000
+v -0.012844 0.536465 0.575955
+v -0.015826 0.614972 0.581087
+v -0.015826 0.730290 0.581087
+v 0.015826 0.730290 0.581087
+v 0.015826 0.614972 0.581087
+v 0.012844 0.536465 0.575955
+v -0.050121 0.983457 0.441395
+v 0.050121 0.983457 0.441395
+v 0.040676 0.565908 0.425142
+v -0.040676 0.565908 0.425142
+v -0.050121 0.713557 0.441395
+v 0.050121 0.713557 0.441395
+v -0.025934 -0.930735 0.123579
+v 0.047352 1.497133 0.058007
+v 0.052007 -0.734213 0.086678
+v 0.052007 -0.376371 0.053941
+v 0.070747 -0.141090 0.117912
+v 0.256589 0.401762 0.168502
+v 0.287528 0.709300 0.195121
+v 0.287528 1.079936 0.195121
+v -0.047352 1.497133 0.058007
+v 0.025934 -0.930735 0.123579
+v -0.052007 -0.734213 0.086678
+v -0.052007 -0.376371 0.053941
+v -0.070747 -0.141090 0.117912
+v -0.250738 0.401762 0.168502
+v -0.281677 0.709300 0.195121
+v -0.281677 1.079936 0.195121
+v -0.240622 1.322820 0.151642
+v 0.246473 1.322820 0.151642
+v -0.047352 1.497133 -0.078920
+v 0.025934 -0.930735 -0.132508
+v -0.052007 -0.734213 -0.083702
+v -0.052007 -0.376371 -0.059893
+v -0.070747 -0.141090 -0.103013
+v -0.320305 0.401762 -0.144489
+v -0.351243 0.709300 -0.164450
+v -0.351243 1.079936 -0.164450
+v -0.025934 -0.930735 -0.132508
+v 0.047352 1.497133 -0.078920
+v 0.052007 -0.734213 -0.083702
+v 0.052007 -0.376371 -0.059893
+v 0.070747 -0.141090 -0.103013
+v 0.326156 0.401762 -0.144489
+v 0.357094 0.709300 -0.164450
+v 0.357094 1.079936 -0.164450
+v 0.316040 1.322820 -0.127629
+v -0.310189 1.322820 -0.127629
+v 0.613221 0.711171 -0.171783
+v 0.613221 0.826436 -0.171783
+v 0.582430 0.711171 -0.222926
+v 0.582430 0.826436 -0.222926
+v -0.615150 0.704433 -0.174428
+v -0.615150 0.828613 -0.174428
+v -0.583938 0.704433 -0.229526
+v -0.583938 0.828613 -0.229526
+vn -0.5030 0.6623 -0.5553
+vn 0.0000 0.4878 -0.8729
+vn 0.5832 0.7363 0.3431
+vn 0.0000 0.7320 0.6813
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+vn 0.0000 0.3516 0.9362
+vn 0.0000 0.1800 0.9837
+vn 0.0000 -0.4777 0.8785
+vn 0.0000 -0.1832 0.9831
+vn 0.0000 -0.9815 -0.1916
+vn 0.0000 0.4831 0.8756
+vn 0.9913 -0.1315 0.0000
+vn 1.0000 -0.0000 0.0000
+vn 0.9968 -0.0794 0.0000
+vn 0.8877 -0.2565 0.3823
+vn 0.8176 -0.1543 0.5547
+vn 0.8453 0.0000 0.5344
+vn 0.0000 0.4448 -0.8956
+vn 0.0000 0.1319 -0.9913
+vn 0.0000 -0.3442 -0.9389
+vn 0.0000 -0.1511 -0.9885
+vn 0.0000 -0.1287 -0.9917
+vn 0.0000 0.0000 -1.0000
+vn -0.9913 -0.1315 0.0000
+vn -1.0000 -0.0000 0.0000
+vn -0.9968 -0.0794 0.0000
+vn -0.7116 -0.2528 -0.6555
+vn -0.5557 -0.1358 -0.8202
+vn -0.5850 0.0000 -0.8111
+vn -0.7680 0.0000 0.6405
+vn -0.7412 -0.1033 0.6633
+vn -0.7954 -0.4040 0.4519
+vn 0.0072 -1.0000 -0.0043
+vn 0.8375 -0.1017 -0.5368
+vn 0.8184 -0.4722 -0.3274
+vn 0.4559 0.8628 -0.2185
+vn -0.4572 0.8155 0.3548
+vn -0.7171 0.1814 0.6730
+vn 0.8143 0.1795 -0.5520
+vn 0.0000 0.3371 0.9415
+vn 0.7819 0.2815 0.5563
+vn 0.0000 0.2902 -0.9570
+vn -0.5190 0.2720 -0.8103
+vn 0.0000 -0.0652 0.9979
+vn 0.0000 0.0000 1.0000
+vn -0.9712 0.0000 0.2384
+vn 0.9712 0.0000 0.2384
+vn -0.9814 -0.0708 0.1787
+vn 0.9814 -0.0708 0.1787
+vn 0.4493 -0.1736 0.8764
+vn -0.4493 -0.1736 0.8764
+vn 0.4092 0.0000 0.9124
+vn -0.4092 0.0000 0.9124
+vn 0.0000 0.4684 0.8835
+vn 0.0000 -0.4731 0.8810
+vn 0.7052 0.1806 0.6856
+vn -0.7949 0.2790 0.5387
+vn -0.5965 0.7297 0.3343
+vn -0.8955 -0.2513 0.3674
+vn -0.8300 -0.1531 0.5364
+vn -0.8567 0.0000 0.5159
+vn 0.7567 0.0000 0.6538
+vn 0.7294 -0.1026 0.6763
+vn 0.7852 -0.4122 0.4621
+vn 0.4455 0.8205 0.3582
+vn -0.8293 0.1803 -0.5290
+vn 0.5085 0.2718 -0.8171
+vn 0.4940 0.6641 -0.5612
+vn 0.7017 -0.2547 -0.6653
+vn 0.5448 -0.1354 -0.8276
+vn 0.5739 0.0000 -0.8189
+vn -0.6361 0.6823 -0.3603
+vn -0.8518 -0.1024 -0.5137
+vn -0.8290 -0.4641 -0.3120
+vn -0.4672 0.8587 -0.2107
+vn 0.8567 0.0000 -0.5158
+vn 0.7379 0.0000 0.6749
+vn -0.2512 0.0000 -0.9679
+vn 0.6430 0.6608 -0.3871
+vn -0.8701 0.0000 -0.4929
+vn 0.2693 0.0000 -0.9630
+vn 0.0181 -0.9998 0.0102
+vn -0.7146 0.0000 0.6995
+usemtl None
+s off
+f 102//1 85//1 4//1 50//1
+f 50//2 4//2 8//2 51//2
+f 84//3 68//3 6//3 52//3
+f 52//4 6//4 2//4 49//4
+f 67//5 76//5 5//5 1//5
+f 68//6 75//6 2//6 6//6
+f 5//7 32//7 9//7 1//7
+f 32//8 31//8 10//8 9//8
+f 31//9 30//9 11//9 10//9
+f 30//10 29//10 12//10 11//10
+f 64//11 63//11 60//11 55//11
+f 62//12 61//12 57//12 58//12
+f 76//13 69//13 32//13 5//13
+f 69//14 70//14 31//14 32//14
+f 70//15 71//15 30//15 31//15
+f 71//16 72//16 29//16 30//16
+f 72//17 73//17 28//17 29//17
+f 73//18 74//18 27//18 28//18
+f 3//19 20//19 26//19 7//19
+f 20//20 19//20 25//20 26//20
+f 19//21 18//21 24//21 25//21
+f 18//22 17//22 23//22 24//22
+f 17//23 16//23 22//23 23//23
+f 16//24 15//24 21//24 22//24
+f 93//25 87//25 20//25 3//25
+f 87//26 88//26 19//26 20//26
+f 88//27 89//27 18//27 19//27
+f 89//28 90//28 17//28 18//28
+f 90//29 91//29 16//29 17//29
+f 91//30 92//30 15//30 16//30
+f 81//31 82//31 48//31 47//31
+f 80//32 81//32 47//32 46//32
+f 79//33 80//33 46//33 45//33
+f 78//27 79//27 45//27 44//27
+f 77//26 78//26 44//26 43//26
+f 67//25 77//25 43//25 33//25
+f 41//34 99//34 105//34 103//34
+f 98//35 99//35 41//35 40//35
+f 97//36 98//36 40//36 39//36
+f 96//15 97//15 39//15 38//15
+f 95//14 96//14 38//14 37//14
+f 86//13 95//13 37//13 35//13
+f 94//6 85//6 34//6 36//6
+f 93//5 86//5 35//5 33//5
+f 101//37 94//37 36//37 53//37
+f 83//38 75//38 34//38 54//38
+f 82//39 83//39 54//39 48//39
+f 100//40 101//40 53//40 42//40
+f 27//41 52//41 49//41 14//41
+f 74//42 84//42 52//42 27//42
+f 15//43 50//43 51//43 21//43
+f 92//44 102//44 50//44 15//44
+f 60//45 59//45 56//45 55//45
+f 59//46 58//46 57//46 56//46
+f 61//47 65//47 56//47 57//47
+f 66//48 62//48 58//48 59//48
+f 65//49 64//49 55//49 56//49
+f 63//50 66//50 59//50 60//50
+f 29//51 28//51 66//51 63//51
+f 13//52 12//52 64//52 65//52
+f 28//53 27//53 62//53 66//53
+f 14//54 13//54 65//54 61//54
+f 27//55 14//55 61//55 62//55
+f 12//56 29//56 63//56 64//56
+f 42//57 53//57 84//57 74//57
+f 14//58 49//58 83//58 82//58
+f 49//59 2//59 75//59 83//59
+f 1//25 9//25 77//25 67//25
+f 9//26 10//26 78//26 77//26
+f 10//27 11//27 79//27 78//27
+f 11//60 12//60 80//60 79//60
+f 12//61 13//61 81//61 80//61
+f 13//62 14//62 82//62 81//62
+f 41//63 42//63 74//63 73//63
+f 40//64 41//64 73//64 72//64
+f 39//65 40//65 72//65 71//65
+f 38//15 39//15 71//15 70//15
+f 37//14 38//14 70//14 69//14
+f 35//13 37//13 69//13 76//13
+f 36//6 34//6 75//6 68//6
+f 33//5 35//5 76//5 67//5
+f 53//66 36//66 68//66 84//66
+f 48//67 54//67 102//67 92//67
+f 21//68 51//68 101//68 100//68
+f 51//69 8//69 94//69 101//69
+f 3//5 7//5 86//5 93//5
+f 8//6 4//6 85//6 94//6
+f 7//13 26//13 95//13 86//13
+f 26//14 25//14 96//14 95//14
+f 25//15 24//15 97//15 96//15
+f 24//70 23//70 98//70 97//70
+f 23//71 22//71 99//71 98//71
+f 22//72 21//72 100//72 99//72
+f 48//73 92//73 110//73 108//73
+f 46//74 47//74 91//74 90//74
+f 45//75 46//75 90//75 89//75
+f 44//27 45//27 89//27 88//27
+f 43//26 44//26 88//26 87//26
+f 33//25 43//25 87//25 93//25
+f 54//76 34//76 85//76 102//76
+f 105//77 106//77 104//77 103//77
+f 42//78 41//78 103//78 104//78
+f 99//79 100//79 106//79 105//79
+f 100//80 42//80 104//80 106//80
+f 107//81 108//81 110//81 109//81
+f 92//82 91//82 109//82 110//82
+f 91//83 47//83 107//83 109//83
+f 47//84 48//84 108//84 107//84

BIN
resources/blaster.jpg


+ 159 - 0
resources/cruiser.obj

@@ -0,0 +1,159 @@
+# Blender v2.79 (sub 0) OBJ File: 'cruiser.blend'
+# www.blender.org
+mtllib cruiser.mtl
+o Cube
+v 1.523334 -5.735679 0.733036
+v -1.523334 -5.735679 0.733036
+v -1.523333 -5.735679 -0.714332
+v 1.523334 -5.735679 -0.714331
+v 0.649694 5.091955 0.318000
+v -0.649695 5.091955 0.317999
+v -0.649694 5.091955 -0.299296
+v 0.649695 5.091955 -0.299296
+v 1.523334 -5.227901 0.733036
+v 1.926359 -0.333333 0.924501
+v 1.781143 1.556539 0.855513
+v 1.364918 3.244402 0.657779
+v 1.000000 4.466551 0.484419
+v -1.523334 -5.227901 0.733036
+v -1.926360 -0.333333 0.924500
+v -1.781144 1.556539 0.855512
+v -1.364919 3.244402 0.657778
+v -1.000000 4.466551 0.484418
+v -1.523333 -5.227901 -0.714332
+v -1.926359 -0.333333 -0.905796
+v -1.781143 1.556539 -0.836809
+v -1.364918 3.244402 -0.639075
+v -1.000000 4.466551 -0.465714
+v 1.523334 -5.227901 -0.714331
+v 1.926360 -0.333333 -0.905795
+v 1.781144 1.556539 -0.836808
+v 1.364919 3.244402 -0.639074
+v 1.000000 4.466551 -0.465714
+v 1.993718 -2.371884 0.956500
+v 1.168468 -1.603205 0.564452
+v -1.993719 -2.371884 0.956500
+v -1.168468 -1.603205 0.564451
+v -1.993718 -2.371884 -0.937796
+v -1.168468 -1.603205 -0.545748
+v 1.993719 -2.371884 -0.937795
+v 1.168468 -1.603205 -0.545747
+v -1.523333 -4.635482 -1.318107
+v 1.523334 -4.635482 -1.318106
+v -1.637797 -2.567708 -1.203428
+v 1.573567 -2.567708 -1.203427
+v 0.000001 -4.635482 -1.102007
+v -1.580565 -3.601595 -1.617877
+v 1.548451 -3.601595 -1.617877
+v -0.032115 -2.567708 -0.987328
+v -0.016057 -3.601595 -1.401778
+v -0.761666 -4.635482 -1.624804
+v -0.834956 -2.567708 -1.510126
+v -0.798311 -3.601595 -1.924575
+v 0.761668 -4.635482 -1.624804
+v 0.770726 -2.567708 -1.510125
+v 0.766197 -3.601595 -1.924575
+v 0.380834 -4.635482 -1.624804
+v 0.369305 -2.567708 -1.510125
+v 0.375070 -3.601595 -1.924575
+v -0.380833 -4.635482 -1.624804
+v -0.433536 -2.567708 -1.510125
+v -0.407184 -3.601595 -1.924575
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+vn -0.0000 0.2571 0.9664
+vn -0.8725 0.4887 -0.0000
+vn 0.0000 0.2571 -0.9664
+vn 1.0000 0.0000 0.0000
+vn 0.8725 0.4887 0.0000
+vn 0.9582 0.2861 0.0000
+vn 0.9709 0.2394 0.0000
+vn 0.9971 0.0766 0.0000
+vn 0.9867 -0.1625 0.0000
+vn 0.0000 -0.0000 -1.0000
+vn 0.0000 -0.2728 -0.9621
+vn 0.0000 0.0365 -0.9993
+vn 0.0000 0.1164 -0.9932
+vn 0.0000 0.1404 -0.9901
+vn -1.0000 0.0000 -0.0000
+vn -0.8587 -0.5125 -0.0000
+vn -0.9971 0.0766 -0.0000
+vn -0.9709 0.2394 -0.0000
+vn -0.9582 0.2861 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -0.0000 -0.2728 0.9621
+vn -0.0000 0.0365 0.9993
+vn -0.0000 0.1164 0.9932
+vn -0.0000 0.1404 0.9901
+vn -0.0000 -0.0780 0.9970
+vn -0.0000 0.4543 0.8908
+vn -0.9867 -0.1625 -0.0000
+vn -0.6816 0.7317 -0.0000
+vn 0.9256 -0.0865 -0.3685
+vn 0.0000 0.4543 -0.8908
+vn 0.8587 -0.5125 0.0000
+vn 0.6816 0.7317 0.0000
+vn 0.3386 0.3459 -0.8750
+vn -0.9396 -0.1034 -0.3263
+vn 0.0000 -0.7920 -0.6106
+vn 0.0000 0.9034 -0.4288
+vn 0.3560 -0.2649 -0.8961
+vn 0.7949 -0.1539 -0.5869
+vn 0.7716 0.2503 -0.5848
+vn -0.3398 0.3366 -0.8782
+vn -0.3550 -0.2752 -0.8935
+vn 0.0000 -0.2785 -0.9604
+vn 0.0000 0.3721 -0.9282
+vn -0.7760 0.2276 -0.5882
+vn -0.7917 -0.1778 -0.5845
+usemtl Material
+s off
+f 1//1 2//1 3//1 4//1
+f 5//2 8//2 7//2 6//2
+f 13//3 5//3 6//3 18//3
+f 18//4 6//4 7//4 23//4
+f 23//5 7//5 8//5 28//5
+f 9//6 1//6 4//6 24//6
+f 5//7 13//7 28//7 8//7
+f 13//8 12//8 27//8 28//8
+f 12//9 11//9 26//9 27//9
+f 11//10 10//10 25//10 26//10
+f 29//11 9//11 24//11 35//11
+f 3//12 19//12 24//12 4//12
+f 34//13 20//13 25//13 36//13
+f 20//14 21//14 26//14 25//14
+f 21//15 22//15 27//15 26//15
+f 22//16 23//16 28//16 27//16
+f 2//17 14//17 19//17 3//17
+f 32//18 15//18 20//18 34//18
+f 15//19 16//19 21//19 20//19
+f 16//20 17//20 22//20 21//20
+f 17//21 18//21 23//21 22//21
+f 1//22 9//22 14//22 2//22
+f 30//23 10//23 15//23 32//23
+f 10//24 11//24 16//24 15//24
+f 11//25 12//25 17//25 16//25
+f 12//26 13//26 18//26 17//26
+f 9//27 29//27 31//27 14//27
+f 29//28 30//28 32//28 31//28
+f 14//29 31//29 33//29 19//29
+f 31//30 32//30 34//30 33//30
+f 35//31 24//31 38//31 43//31 40//31
+f 33//32 34//32 36//32 35//32
+f 10//33 30//33 36//33 25//33
+f 30//34 29//34 35//34 36//34
+f 51//35 50//35 40//35 43//35
+f 19//36 33//36 39//36 42//36 37//36
+f 24//37 19//37 37//37 46//37 55//37 41//37 52//37 49//37 38//37
+f 33//38 35//38 40//38 50//38 53//38 44//38 56//38 47//38 39//38
+f 49//39 51//39 43//39 38//39
+f 55//40 57//40 45//40 41//40
+f 57//41 56//41 44//41 45//41
+f 42//42 39//42 47//42 48//42
+f 37//43 42//43 48//43 46//43
+f 52//44 54//44 51//44 49//44
+f 54//45 53//45 50//45 51//45
+f 45//46 44//46 53//46 54//46
+f 41//47 45//47 54//47 52//47
+f 48//45 47//45 56//45 57//45
+f 46//44 48//44 57//44 55//44

+ 218 - 0
resources/fighter.obj

@@ -0,0 +1,218 @@
+# Blender v2.79 (sub 0) OBJ File: 'fighter.blend'
+# www.blender.org
+mtllib fighter.mtl
+o Cube
+v 1.000000 -2.007871 -1.000000
+v 1.000000 -2.007871 1.000000
+v -1.000000 -2.007871 1.000000
+v -1.000000 -2.007871 -1.000000
+v 0.537330 1.332513 -0.537330
+v 0.537330 1.332513 0.537331
+v -0.537330 1.332513 0.537330
+v -0.537330 1.332513 -0.537330
+v 1.000000 -0.844945 -1.000000
+v 1.000000 -0.844945 1.000000
+v -1.000000 -0.844945 1.000000
+v -1.000000 -0.844945 -1.000000
+v 3.101492 -2.007871 -1.000000
+v 3.101492 -2.007871 1.000000
+v 3.101492 -0.844945 -0.999999
+v 3.101491 -0.844945 1.000000
+v -3.085759 -2.007871 0.999999
+v -3.085758 -2.007871 -1.000001
+v -3.085759 -0.844945 0.999999
+v -3.085758 -0.844945 -1.000001
+v -1.236911 -1.611402 0.318151
+v -2.271223 -1.611402 0.318151
+v -2.513347 -2.007871 1.000000
+v -2.513346 -2.007871 -1.000001
+v -2.271223 -1.611402 -0.318152
+v -1.236911 -1.611402 -0.318152
+v -1.236911 -1.241415 0.318151
+v -2.271223 -1.241415 0.318151
+v -2.513347 -0.844945 1.000000
+v -1.236911 -1.241415 -0.318152
+v -2.271223 -1.241415 -0.318152
+v -2.513346 -0.844945 -1.000000
+v 0.357122 2.920035 -0.357122
+v 0.357122 2.920035 0.357122
+v -0.357122 2.920035 0.357122
+v -0.357122 2.920035 -0.357122
+v 1.200899 -1.572815 0.251790
+v 2.272034 -1.574492 0.254676
+v 2.499711 -2.007871 1.000000
+v 2.499711 -0.844945 1.000000
+v 2.272034 -1.278323 0.254676
+v 1.200899 -1.280001 0.251790
+v 1.200899 -1.572815 -0.251790
+v 2.272034 -1.574492 -0.254675
+v 2.499711 -2.007871 -1.000000
+v 1.200899 -1.280001 -0.251790
+v 2.272034 -1.278323 -0.254675
+v 2.499711 -0.844945 -0.999999
+v 2.988535 1.069431 -0.624590
+v 2.988534 1.069431 0.624591
+v 2.612668 1.069431 0.624591
+v 2.612668 1.069431 -0.624590
+v -2.982360 1.070724 0.638727
+v -2.982360 1.070724 -0.638728
+v -2.616745 1.070724 0.638727
+v -2.616745 1.070724 -0.638728
+v -2.898924 -0.683572 -0.347204
+v -2.863694 0.555813 -0.224109
+v -2.982360 0.771924 -0.638728
+v -2.616745 0.771924 -0.638728
+v -2.735411 0.555813 -0.224109
+v -2.700181 -0.683572 -0.347204
+v -2.898924 -0.683572 0.347203
+v -2.863694 0.555813 0.224108
+v -2.982360 0.771924 0.638727
+v -2.616745 0.771924 0.638727
+v -2.735411 0.555813 0.224108
+v -2.700181 -0.683572 0.347203
+v 2.703194 -0.656612 -0.323729
+v 2.703194 0.531225 -0.323729
+v 2.612668 0.754177 -0.624590
+v 2.988535 0.754177 -0.624590
+v 2.898008 0.531225 -0.323728
+v 2.898008 -0.656612 -0.323728
+v 2.898008 -0.656612 0.323730
+v 2.898008 0.531225 0.323730
+v 2.988534 0.754177 0.624591
+v 2.612668 0.754177 0.624591
+v 2.703194 0.531225 0.323730
+v 2.703194 -0.656612 0.323730
+vn 0.0000 -1.0000 0.0000
+vn -0.9936 0.1128 -0.0000
+vn 0.9782 0.2078 0.0000
+vn -0.0000 0.2078 0.9782
+vn -0.9782 0.2078 -0.0000
+vn 0.0000 0.0000 -1.0000
+vn 0.0000 0.2078 -0.9782
+vn -0.0000 -0.0000 1.0000
+vn 1.0000 0.0000 0.0000
+vn -1.0000 0.0000 -0.0000
+vn -0.9446 0.0000 -0.3282
+vn 0.9424 0.0000 -0.3346
+vn -0.8584 0.5129 -0.0000
+vn 0.0000 1.0000 0.0000
+vn 0.8534 0.5212 0.0000
+vn -0.9446 -0.0000 0.3282
+vn 0.9424 0.0000 0.3346
+vn -0.8584 -0.5130 -0.0000
+vn 0.8534 -0.5212 0.0000
+vn 0.0000 0.1128 -0.9936
+vn -0.0000 0.1128 0.9936
+vn 0.9936 0.1128 0.0000
+vn 0.9079 0.4192 0.0000
+vn -0.0016 1.0000 -0.0000
+vn -0.8853 0.4651 -0.0000
+vn 0.9658 -0.0000 -0.2593
+vn -0.0027 0.0000 -1.0000
+vn -0.9564 0.0000 -0.2921
+vn 0.9079 -0.4192 0.0000
+vn -0.0016 -1.0000 0.0000
+vn -0.8853 -0.4651 0.0000
+vn 0.9658 0.0000 0.2593
+vn -0.0027 -0.0000 1.0000
+vn -0.9564 -0.0000 0.2921
+vn -0.0000 0.9708 0.2400
+vn 0.0000 0.0988 0.9951
+vn -0.0000 -0.8868 0.4622
+vn 0.6537 0.7568 0.0000
+vn 0.9996 0.0284 0.0000
+vn 0.8765 -0.4813 0.0000
+vn -0.6537 0.7568 -0.0000
+vn -0.9996 0.0284 0.0000
+vn -0.8765 -0.4813 -0.0000
+vn 0.0000 0.9708 -0.2400
+vn 0.0000 0.0988 -0.9951
+vn 0.0000 -0.8868 -0.4622
+vn 0.6793 0.7339 0.0000
+vn 0.9265 -0.3762 0.0000
+vn -0.6793 0.7339 -0.0000
+vn -0.9265 -0.3762 -0.0000
+vn -0.0000 0.9633 0.2683
+vn 0.0000 -0.8034 0.5954
+vn -0.0000 0.9633 -0.2683
+vn 0.0000 -0.8034 -0.5954
+usemtl Material
+s off
+f 1//1 2//1 3//1 4//1
+f 8//2 7//2 35//2 36//2
+f 9//3 5//3 6//3 10//3
+f 10//4 6//4 7//4 11//4
+f 11//5 7//5 8//5 12//5
+f 9//6 1//6 4//6 12//6
+f 5//7 9//7 12//7 8//7
+f 24//1 23//1 17//1 18//1
+f 2//8 10//8 11//8 3//8
+f 40//8 39//8 14//8 16//8
+f 13//9 15//9 16//9 14//9
+f 39//1 45//1 13//1 14//1
+f 45//6 48//6 15//6 13//6
+f 72//6 71//6 52//6 49//6
+f 17//10 19//10 20//10 18//10
+f 23//8 29//8 19//8 17//8
+f 60//6 59//6 54//6 56//6
+f 32//6 24//6 18//6 20//6
+f 12//11 4//11 26//11 30//11
+f 30//6 26//6 25//6 31//6
+f 31//12 25//12 24//12 32//12
+f 11//13 12//13 30//13 27//13
+f 27//14 30//14 31//14 28//14
+f 28//15 31//15 32//15 29//15
+f 3//16 11//16 27//16 21//16
+f 21//8 27//8 28//8 22//8
+f 22//17 28//17 29//17 23//17
+f 4//18 3//18 21//18 26//18
+f 26//1 21//1 22//1 25//1
+f 25//19 22//19 23//19 24//19
+f 33//14 36//14 35//14 34//14
+f 5//20 8//20 36//20 33//20
+f 7//21 6//21 34//21 35//21
+f 6//22 5//22 33//22 34//22
+f 9//23 10//23 42//23 46//23
+f 46//24 42//24 41//24 47//24
+f 47//25 41//25 40//25 48//25
+f 1//26 9//26 46//26 43//26
+f 43//27 46//27 47//27 44//27
+f 44//28 47//28 48//28 45//28
+f 2//29 1//29 43//29 37//29
+f 37//30 43//30 44//30 38//30
+f 38//31 44//31 45//31 39//31
+f 10//32 2//32 37//32 42//32
+f 42//33 37//33 38//33 41//33
+f 41//34 38//34 39//34 40//34
+f 52//14 51//14 50//14 49//14
+f 78//8 77//8 50//8 51//8
+f 71//10 78//10 51//10 52//10
+f 77//9 72//9 49//9 50//9
+f 55//14 56//14 54//14 53//14
+f 59//10 65//10 53//10 54//10
+f 66//9 60//9 56//9 55//9
+f 65//8 66//8 55//8 53//8
+f 19//35 29//35 68//35 63//35
+f 63//36 68//36 67//36 64//36
+f 64//37 67//37 66//37 65//37
+f 29//38 32//38 62//38 68//38
+f 68//39 62//39 61//39 67//39
+f 67//40 61//40 60//40 66//40
+f 20//41 19//41 63//41 57//41
+f 57//42 63//42 64//42 58//42
+f 58//43 64//43 65//43 59//43
+f 32//44 20//44 57//44 62//44
+f 62//45 57//45 58//45 61//45
+f 61//46 58//46 59//46 60//46
+f 16//47 15//47 74//47 75//47
+f 75//9 74//9 73//9 76//9
+f 76//48 73//48 72//48 77//48
+f 48//49 40//49 80//49 69//49
+f 69//10 80//10 79//10 70//10
+f 70//50 79//50 78//50 71//50
+f 40//51 16//51 75//51 80//51
+f 80//8 75//8 76//8 79//8
+f 79//52 76//52 77//52 78//52
+f 15//53 48//53 69//53 74//53
+f 74//6 69//6 70//6 73//6
+f 73//54 70//54 71//54 72//54

+ 144 - 0
resources/fish.obj

@@ -0,0 +1,144 @@
+# Blender v2.79 (sub 0) OBJ File: ''
+# www.blender.org
+mtllib fish.mtl
+o Fish_Mesh
+v -0.025934 -0.930735 0.247159
+v -0.047352 1.497133 0.157841
+v -0.025934 -0.930735 -0.265016
+v -0.047352 1.497133 -0.157841
+v 0.025934 -0.930735 0.247159
+v 0.047352 1.497133 0.157841
+v 0.025934 -0.930735 -0.265016
+v 0.047352 1.497133 -0.157841
+v -0.052007 -0.734213 0.173356
+v -0.052007 -0.376371 0.107881
+v -0.070747 -0.141090 0.235824
+v -0.133233 0.401762 0.444112
+v -0.164172 0.709300 0.547240
+v -0.164172 1.079936 0.547240
+v -0.164172 1.079936 -0.547240
+v -0.164172 0.709300 -0.547240
+v -0.133233 0.401762 -0.444112
+v -0.070747 -0.141090 -0.235824
+v -0.052007 -0.376371 -0.119786
+v -0.052007 -0.734213 -0.167404
+v 0.164172 1.079936 -0.547240
+v 0.164172 0.709300 -0.547240
+v 0.133233 0.401762 -0.444112
+v 0.070747 -0.141090 -0.235824
+v 0.052007 -0.376371 -0.119786
+v 0.052007 -0.734213 -0.167404
+v 0.164172 1.079936 0.547240
+v 0.164172 0.709300 0.547240
+v 0.133233 0.401762 0.444112
+v 0.070747 -0.141090 0.235824
+v 0.052007 -0.376371 0.107881
+v 0.052007 -0.734213 0.173356
+v -0.025934 -0.930735 0.000000
+v -0.047352 1.497133 0.000000
+v 0.025934 -0.930735 0.000000
+v 0.047352 1.497133 0.000000
+v 0.052007 -0.734213 0.000000
+v 0.052007 -0.376371 0.000000
+v 0.070747 -0.141090 0.000000
+v 0.133233 0.401762 0.000000
+v 0.164172 0.709300 0.000000
+v 0.164172 1.079936 0.000000
+v -0.052007 -0.734213 0.000000
+v -0.052007 -0.376371 0.000000
+v -0.070747 -0.141090 0.000000
+v -0.133233 0.401762 0.000000
+v -0.164172 0.709300 0.000000
+v -0.164172 1.079936 0.000000
+v -0.123117 1.322820 0.410391
+v -0.123117 1.322820 -0.410391
+v 0.123117 1.322820 -0.410391
+v 0.123117 1.322820 0.410391
+v 0.123117 1.322820 0.000000
+v -0.123117 1.322820 0.000000
+vn -0.9171 0.3986 0.0000
+vn 0.0000 0.8230 -0.5680
+vn 0.9171 0.3986 0.0000
+vn 0.0000 0.8230 0.5680
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+vn 0.0000 0.3516 0.9362
+vn 0.0000 0.1800 0.9837
+vn 0.0000 -0.4777 0.8785
+vn 0.0000 -0.3582 0.9336
+vn 0.0000 -0.3179 0.9481
+vn 0.0000 0.0000 1.0000
+vn 0.9913 -0.1315 0.0000
+vn 1.0000 0.0000 0.0000
+vn 0.9968 -0.0794 0.0000
+vn 0.9934 -0.1144 0.0000
+vn 0.9950 -0.1001 0.0000
+vn 0.0000 0.4448 -0.8956
+vn 0.0000 0.1319 -0.9913
+vn 0.0000 -0.4423 -0.8969
+vn 0.0000 -0.3582 -0.9336
+vn 0.0000 -0.3179 -0.9481
+vn 0.0000 0.0000 -1.0000
+vn -0.9913 -0.1315 0.0000
+vn -1.0000 -0.0000 0.0000
+vn -0.9968 -0.0794 0.0000
+vn -0.9934 -0.1144 0.0000
+vn -0.9950 -0.1001 0.0000
+vn -0.9860 0.1667 0.0000
+vn 0.9860 0.1667 0.0000
+vn 0.0000 0.4909 0.8712
+vn 0.0000 0.4909 -0.8712
+usemtl None
+s off
+f 54//1 34//1 4//1 50//1
+f 50//2 4//2 8//2 51//2
+f 53//3 36//3 6//3 52//3
+f 52//4 6//4 2//4 49//4
+f 33//5 35//5 5//5 1//5
+f 36//6 34//6 2//6 6//6
+f 5//7 32//7 9//7 1//7
+f 32//8 31//8 10//8 9//8
+f 31//9 30//9 11//9 10//9
+f 30//10 29//10 12//10 11//10
+f 29//11 28//11 13//11 12//11
+f 28//12 27//12 14//12 13//12
+f 35//13 37//13 32//13 5//13
+f 37//14 38//14 31//14 32//14
+f 38//15 39//15 30//15 31//15
+f 39//16 40//16 29//16 30//16
+f 40//17 41//17 28//17 29//17
+f 41//14 42//14 27//14 28//14
+f 3//18 20//18 26//18 7//18
+f 20//19 19//19 25//19 26//19
+f 19//20 18//20 24//20 25//20
+f 18//21 17//21 23//21 24//21
+f 17//22 16//22 22//22 23//22
+f 16//23 15//23 21//23 22//23
+f 33//24 43//24 20//24 3//24
+f 43//25 44//25 19//25 20//25
+f 44//26 45//26 18//26 19//26
+f 45//27 46//27 17//27 18//27
+f 46//28 47//28 16//28 17//28
+f 47//25 48//25 15//25 16//25
+f 13//25 14//25 48//25 47//25
+f 12//28 13//28 47//28 46//28
+f 11//27 12//27 46//27 45//27
+f 10//26 11//26 45//26 44//26
+f 9//25 10//25 44//25 43//25
+f 1//24 9//24 43//24 33//24
+f 22//14 21//14 42//14 41//14
+f 23//17 22//17 41//17 40//17
+f 24//16 23//16 40//16 39//16
+f 25//15 24//15 39//15 38//15
+f 26//14 25//14 38//14 37//14
+f 7//13 26//13 37//13 35//13
+f 8//6 4//6 34//6 36//6
+f 3//5 7//5 35//5 33//5
+f 51//3 8//3 36//3 53//3
+f 49//1 2//1 34//1 54//1
+f 14//29 49//29 54//29 48//29
+f 21//30 51//30 53//30 42//30
+f 27//31 52//31 49//31 14//31
+f 42//30 53//30 52//30 27//30
+f 15//32 50//32 51//32 21//32
+f 48//29 54//29 50//29 15//29

BIN
resources/space-negx.jpg


BIN
resources/space-negy.jpg


BIN
resources/space-negz.jpg


BIN
resources/space-posx.jpg


BIN
resources/space-posy.jpg


BIN
resources/space-posz.jpg


BIN
resources/underwater.jpg


+ 536 - 0
space.js

@@ -0,0 +1,536 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {visibility} from './visibility.js';
+import {particles} from './particles.js';
+import {blaster} from './blaster.js';
+import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/OBJLoader.js';
+
+let _APP = null;
+
+const _NUM_BOIDS = 300;
+const _BOID_SPEED = 25;
+const _BOID_ACCELERATION = _BOID_SPEED / 2.5;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 20.0;
+const _BOID_FORCE_ORIGIN = 50;
+const _BOID_FORCE_ALIGNMENT = 10;
+const _BOID_FORCE_SEPARATION = 20;
+const _BOID_FORCE_COLLISION = 50;
+const _BOID_FORCE_COHESION = 5;
+const _BOID_FORCE_WANDER = 3;
+
+
+class LineRenderer {
+  constructor(game) {
+    this._game = game;
+
+    this._materials = {};
+    this._group = new THREE.Group();
+
+    this._game._graphics.Scene.add(this._group);
+  }
+
+  Reset() {
+    this._lines = [];
+    this._group.remove(...this._group.children);
+  }
+
+  Add(pt1, pt2, hexColour) {
+    const geometry = new THREE.Geometry();
+    geometry.vertices.push(pt1);
+    geometry.vertices.push(pt2);
+
+    let material = this._materials[hexColour];
+    if (!material) {
+      this._materials[hexColour] = new THREE.LineBasicMaterial(
+          {color: hexColour});
+      material = this._materials[hexColour];
+    }
+
+    const line = new THREE.Line(geometry, material);
+    this._lines.push(line);
+    this._group.add(line);
+  }
+}
+
+class ExplodeParticles {
+  constructor(game) {
+    this._particleSystem = new particles.ParticleSystem(
+        game, {texture: "./resources/blaster.jpg"});
+    this._particles = [];
+  }
+
+  Splode(origin) {
+    for (let i = 0; i < 128; i++) {
+      const p = this._particleSystem.CreateParticle();
+      p.Position.copy(origin);
+      p.Velocity = new THREE.Vector3(
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1)
+      );
+      p.Velocity.normalize();
+      p.Velocity.multiplyScalar(125);
+      p.TotalLife = 2.0;
+      p.Life = p.TotalLife;
+      p.Colours = [new THREE.Color(0xFF8000), new THREE.Color(0x800000)];
+      p.Sizes = [3, 12];
+      p.Size = p.Sizes[0];
+      this._particles.push(p);
+    }
+  }
+
+  Update(timeInSeconds) {
+    this._particles = this._particles.filter(p => {
+      return p.Alive;
+    });
+    for (const p of this._particles) {
+      p.Life -= timeInSeconds;
+      if (p.Life <= 0) {
+        p.Alive = false;
+      }
+      p.Position.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
+      p.Velocity.multiplyScalar(0.75);
+      p.Size = math.lerp(p.Life / p.TotalLife, p.Sizes[0], p.Sizes[1]);
+      p.Colour.copy(p.Colours[0]);
+      p.Colour.lerp(p.Colours[1], 1.0 - p.Life / p.TotalLife);
+    }
+    this._particleSystem.Update();
+  }
+};
+
+
+class Boid {
+  constructor(game, params) {
+    this._mesh = new THREE.Mesh(
+        params.geometry,
+        new THREE.MeshStandardMaterial({color: 0x808080}));
+    this._mesh.castShadow = true;
+    this._mesh.receiveShadow = false;
+
+    this._group = new THREE.Group();
+    this._group.add(this._mesh);
+    this._group.position.set(
+        math.rand_range(-250, 250),
+        math.rand_range(-250, 250),
+        math.rand_range(-250, 250));
+    this._direction = new THREE.Vector3(
+        math.rand_range(-1, 1),
+        math.rand_range(-1, 1),
+        math.rand_range(-1, 1));
+    this._velocity = this._direction.clone();
+
+    const speedMultiplier = math.rand_range(params.speedMin, params.speedMax);
+    this._maxSteeringForce = params.maxSteeringForce * speedMultiplier;
+    this._maxSpeed  = params.speed * speedMultiplier;
+    this._acceleration = params.acceleration * speedMultiplier;
+
+    const scale = 1.0 / speedMultiplier;
+    this._radius = scale;
+    this._mesh.scale.setScalar(scale * params.scale);
+    //this._mesh.rotateX(Math.PI / 2);
+
+    this._game = game;
+    game._graphics.Scene.add(this._group);
+    this._visibilityIndex = game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this);
+
+    this._wanderAngle = 0;
+    this._seekGoal = params.seekGoal;
+    this._fireCooldown = 0.0;
+    this._params = params;
+  }
+
+  DisplayDebug() {
+    const geometry = new THREE.SphereGeometry(10, 64, 64);
+    const material = new THREE.MeshBasicMaterial({
+      color: 0xFF0000,
+      transparent: true,
+      opacity: 0.25,
+    });
+    const mesh = new THREE.Mesh(geometry, material);
+    this._group.add(mesh);
+
+    this._mesh.material.color.setHex(0xFF0000);
+    this._displayDebug = true;
+    this._lineRenderer = new LineRenderer(this._game);
+  }
+
+  _UpdateDebug(local) {
+    this._lineRenderer.Reset();
+    this._lineRenderer.Add(
+        this.Position, this.Position.clone().add(this._velocity),
+        0xFFFFFF);
+    for (const e of local) {
+      this._lineRenderer.Add(this.Position, e.Position, 0x00FF00);
+    }
+  }
+
+  get Position() {
+    return this._group.position;
+  }
+
+  get Velocity() {
+    return this._velocity;
+  }
+
+  get Direction() {
+    return this._direction;
+  }
+
+  get Radius() {
+    return this._radius;
+  }
+
+  Step(timeInSeconds) {
+    const local = this._game._visibilityGrid.GetLocalEntities(
+        this.Position, 15);
+
+    this._ApplySteering(timeInSeconds, local);
+
+    const frameVelocity = this._velocity.clone();
+    frameVelocity.multiplyScalar(timeInSeconds);
+    this._group.position.add(frameVelocity);
+
+    this._group.quaternion.setFromUnitVectors(
+        new THREE.Vector3(0, 1, 0), this.Direction);
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this, this._visibilityIndex);
+
+    if (this._displayDebug) {
+      this._UpdateDebug(local);
+    }
+  }
+
+  _ApplySteering(timeInSeconds, local) {
+    const separationVelocity = this._ApplySeparation(local);
+
+    // Only apply alignment and cohesion to allies
+    const allies = local.filter((e) => {
+      return this._seekGoal.equals(e._seekGoal);
+    });
+
+    const enemies = local.filter((e) => {
+      return !this._seekGoal.equals(e._seekGoal);
+    });
+
+    this._fireCooldown -= timeInSeconds;
+    if (enemies.length > 0 && this._fireCooldown <= 0) {
+      const p = this._game._blasters.CreateParticle();
+      p.Start = this.Position.clone();
+      p.End = this.Position.clone();
+      p.Velocity = this.Direction.clone().multiplyScalar(300);
+      p.Length = 50;
+      p.Colours = [
+          this._params.colour.clone(), new THREE.Color(0.0, 0.0, 0.0)];
+      p.Life = 2.0;
+      p.TotalLife = 2.0;
+      p.Width = 0.25;
+
+      if (Math.random() < 0.025) {
+        this._game._explosionSystem.Splode(enemies[0].Position);
+      }
+      this._fireCooldown = 0.25;
+    }
+
+    const alignmentVelocity = this._ApplyAlignment(allies);
+    const cohesionVelocity = this._ApplyCohesion(allies);
+    const originVelocity = this._ApplySeek(this._seekGoal);
+    const wanderVelocity = this._ApplyWander();
+    const collisionVelocity = this._ApplyCollisionAvoidance();
+
+    const steeringForce = new THREE.Vector3(0, 0, 0);
+    steeringForce.add(separationVelocity);
+    steeringForce.add(alignmentVelocity);
+    steeringForce.add(cohesionVelocity);
+    steeringForce.add(originVelocity);
+    steeringForce.add(wanderVelocity);
+    steeringForce.add(collisionVelocity);
+
+    steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+
+    // Clamp the force applied
+    if (steeringForce.length() > this._maxSteeringForce) {
+      steeringForce.normalize();
+      steeringForce.multiplyScalar(this._maxSteeringForce);
+    }
+
+    this._velocity.add(steeringForce);
+
+    // Clamp velocity
+    if (this._velocity.length() > this._maxSpeed) {
+      this._velocity.normalize();
+      this._velocity.multiplyScalar(this._maxSpeed);
+    }
+
+    this._direction = this._velocity.clone();
+    this._direction.normalize();
+  }
+
+  _ApplyCollisionAvoidance() {
+    const colliders = this._game._visibilityGrid.GetGlobalItems();
+
+    const ray = new THREE.Ray(this.Position, this.Direction);
+    const force = new THREE.Vector3(0, 0, 0);
+
+    for (const c of colliders) {
+      if (c.Position.distanceTo(this.Position) > c.QuickRadius) {
+        continue;
+      }
+
+      const result = ray.intersectBox(c.AABB, new THREE.Vector3());
+      if (result) {
+        const distanceToCollision = result.distanceTo(this.Position);
+        if (distanceToCollision < 2) {
+          let a = 0;
+        }
+        const dirToCenter = c.Position.clone().sub(this.Position).normalize();
+        const dirToCollision = result.clone().sub(this.Position).normalize();
+        const steeringDirection = dirToCollision.sub(dirToCenter).normalize();
+        steeringDirection.multiplyScalar(_BOID_FORCE_COLLISION);
+        force.add(steeringDirection);
+      }
+    }
+
+    return force;
+  }
+
+  _ApplyWander() {
+    this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
+    const randomPointOnCircle = new THREE.Vector3(
+        Math.cos(this._wanderAngle),
+        0,
+        Math.sin(this._wanderAngle));
+    const pointAhead = this._direction.clone();
+    pointAhead.multiplyScalar(5);
+    pointAhead.add(randomPointOnCircle);
+    pointAhead.normalize();
+    return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
+  }
+
+  _ApplySeparation(local) {
+    if (local.length == 0) {
+      return new THREE.Vector3(0, 0, 0);
+    }
+
+    const forceVector = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      const distanceToEntity = Math.max(
+          e.Position.distanceTo(this.Position) - 1.5 * (this.Radius + e.Radius),
+          0.001);
+      const directionFromEntity = new THREE.Vector3().subVectors(
+          this.Position, e.Position);
+      const multiplier = (_BOID_FORCE_SEPARATION / distanceToEntity);
+      directionFromEntity.normalize();
+      forceVector.add(
+          directionFromEntity.multiplyScalar(multiplier));
+    }
+    return forceVector;
+  }
+
+  _ApplyAlignment(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    for (let e of local) {
+      const entityDirection = e.Direction;
+      forceVector.add(entityDirection);
+    }
+
+    forceVector.normalize();
+    forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
+
+    return forceVector;
+  }
+
+  _ApplyCohesion(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    if (local.length == 0) {
+      return forceVector;
+    }
+
+    const averagePosition = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      averagePosition.add(e.Position);
+    }
+
+    averagePosition.multiplyScalar(1.0 / local.length);
+
+    const directionToAveragePosition = averagePosition.clone().sub(
+        this.Position);
+    directionToAveragePosition.normalize();
+    directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
+
+    // HACK: Floating point error from accumulation of positions.
+    directionToAveragePosition.y = 0;
+
+    return directionToAveragePosition;
+  }
+
+  _ApplySeek(destination) {
+    const distance = Math.max(0,((
+        this.Position.distanceTo(destination) - 50) / 500)) ** 2;
+    const direction = destination.clone().sub(this.Position);
+    direction.normalize();
+
+    const forceVector = direction.multiplyScalar(
+        _BOID_FORCE_ORIGIN * distance);
+    return forceVector;
+  }
+}
+
+
+class OpenWorldDemo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = [];
+
+    this._bloomPass = this._graphics.AddPostFX(
+        graphics.PostFX.UnrealBloomPass,
+        {
+            threshold: 0.75,
+            strength: 2.5,
+            radius: 0,
+            resolution: {
+              x: 1024,
+              y: 1024,
+            }
+        });
+
+    this._glitchPass = this._graphics.AddPostFX(
+        graphics.PostFX.GlitchPass, {});
+    this._glitchCooldown = 15;
+
+    this._glitchPass.enabled = false;
+
+    this._LoadBackground();
+
+    const geometries = {};
+    const loader = new OBJLoader();
+    loader.load("./resources/fighter.obj", (result) => {
+      geometries.fighter = result.children[0].geometry;
+      loader.load("./resources/cruiser.obj", (result) => {
+        geometries.cruiser = result.children[0].geometry;
+        this._CreateBoids(geometries);
+      });
+    });
+    this._CreateEntities();
+  }
+
+  _LoadBackground() {
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/space-posx.jpg',
+        './resources/space-negx.jpg',
+        './resources/space-posy.jpg',
+        './resources/space-negy.jpg',
+        './resources/space-posz.jpg',
+        './resources/space-negz.jpg',
+    ]);
+    this._graphics._scene.background = texture;
+  }
+
+  _CreateEntities() {
+    // This is 2D but eh, whatever.
+    this._visibilityGrid = new visibility.VisibilityGrid(
+        [new THREE.Vector3(-500, 0, -500), new THREE.Vector3(500, 0, 500)],
+        [100, 100]);
+
+    this._explosionSystem = new ExplodeParticles(this);
+
+    this._blasters = new blaster.BlasterSystem(
+        this, {texture: "./resources/blaster.jpg"});
+  }
+
+  _CreateBoids(geometries) {
+    const positions = [
+        new THREE.Vector3(-200, 50, -100),
+        new THREE.Vector3(0, 0, 0)];
+    const colours = [
+        new THREE.Color(0.5, 0.5, 4.0),
+        new THREE.Color(4.0, 0.5, 0.5)
+    ];
+    for (let i = 0; i < 2; i++) {
+      const p = positions[i];
+      const cruiser = new THREE.Mesh(
+          geometries.cruiser,
+          new THREE.MeshStandardMaterial({
+              color: 0x404040
+          }));
+      cruiser.position.set(p.x, p.y, p.z);
+      cruiser.castShadow = true;
+      cruiser.receiveShadow = true;
+      cruiser.rotation.x = Math.PI / 2;
+      cruiser.scale.setScalar(10, 10, 10);
+      cruiser.updateWorldMatrix();
+      this._graphics.Scene.add(cruiser);
+
+      cruiser.geometry.computeBoundingBox();
+      const b = cruiser.geometry.boundingBox.clone().applyMatrix4(
+          cruiser.matrixWorld);
+
+      this._visibilityGrid.AddGlobalItem({
+        Position: p,
+        AABB: b,
+        QuickRadius: 200,
+        Velocity: new THREE.Vector3(0, 0, 0),
+        Direction: new THREE.Vector3(0, 1, 0),
+      });
+
+      let params = {
+        geometry: geometries.fighter,
+        speedMin: 1.0,
+        speedMax: 1.0,
+        speed: _BOID_SPEED,
+        maxSteeringForce: _BOID_FORCE_MAX,
+        acceleration: _BOID_ACCELERATION,
+        scale: 0.4,
+        seekGoal: p,
+        colour: colours[i]
+      };
+      for (let i = 0; i < _NUM_BOIDS; i++) {
+        const e = new Boid(this, params);
+        this._entities.push(e);
+      }
+    }
+
+    //this._entities[0].DisplayDebug();
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    this._blasters.Update(timeInSeconds);
+    this._explosionSystem.Update(timeInSeconds);
+
+    this._glitchCooldown -= timeInSeconds;
+    if (this._glitchCooldown < 0) {
+      this._glitchCooldown = math.rand_range(5, 10);
+      this._glitchPass.enabled = !this._glitchPass.enabled;
+    }
+
+    this._StepEntities(timeInSeconds);
+  }
+
+  _StepEntities(timeInSeconds) {
+    if (this._entities.length == 0) {
+      return;
+    }
+
+    for (let e of this._entities) {
+      e.Step(timeInSeconds);
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new OpenWorldDemo();
+}
+
+_Main();

+ 81 - 0
visibility.js

@@ -0,0 +1,81 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {math} from './math.js';
+
+export const visibility = (function() {
+  return {
+      VisibilityGrid: class {
+        constructor(bounds, dimensions) {
+          const [x, y] = dimensions;
+          this._cells = [...Array(x)].map(_ => [...Array(y)].map(_ => ({})));
+          this._dimensions = dimensions;
+          this._bounds = bounds;
+          this._cellSize = bounds[1].clone().sub(bounds[0]);
+          this._cellSize.multiply(
+              new THREE.Vector3(1.0 / dimensions[0], 0, 1.0 / dimensions[1]));
+          this._globalItems = [];
+        }
+
+        AddGlobalItem(entity) {
+          this._globalItems.push(entity);
+        }
+
+        GetGlobalItems() {
+          return [...this._globalItems];
+        }
+
+        UpdateItem(uuid, entity, previous=null) {
+          const [x, y] = this._GetCellIndex(entity.Position);
+
+          if (previous) {
+            const [prevX, prevY] = previous;
+            if (prevX == x && prevY == y) {
+              return [x, y];
+            }
+
+            delete this._cells[prevX][prevY][uuid];
+          }
+          this._cells[x][y][uuid] = entity;
+
+          return [x, y];
+        }
+
+        GetLocalEntities(position, radius) {
+          const [x, y] = this._GetCellIndex(position);
+
+          const cellSize = Math.min(this._cellSize.x, this._cellSize.z);
+          const cells = Math.ceil(radius / cellSize);
+
+          let local = [];
+          const xMin = Math.max(x - cells, 0);
+          const yMin = Math.max(y - cells, 0);
+          const xMax = Math.min(this._dimensions[0] - 1, x + cells);
+          const yMax = Math.min(this._dimensions[1] - 1, y + cells);
+          for (let xi = xMin; xi <= xMax; xi++) {
+            for (let yi = yMin; yi <= yMax; yi++) {
+              local.push(...Object.values(this._cells[xi][yi]));
+            }
+          }
+
+          local = local.filter((e) => {
+            const distance = e.Position.distanceTo(position);
+
+            return distance != 0.0 && distance < radius;
+          });
+
+          return local;
+        }
+
+        _GetCellIndex(position) {
+          const x = math.sat((this._bounds[0].x - position.x) / (
+              this._bounds[0].x - this._bounds[1].x));
+          const y = math.sat((this._bounds[0].z - position.z) / (
+              this._bounds[0].z - this._bounds[1].z));
+
+          const xIndex = Math.floor(x * (this._dimensions[0] - 1));
+          const yIndex = Math.floor(y * (this._dimensions[1] - 1));
+
+          return [xIndex, yIndex];
+        }
+      }
+  };
+})();