Explorar el Código

Initial commit.

simondevyoutube hace 5 años
padre
commit
5660e2f861
Se han modificado 9 ficheros con 1444 adiciones y 0 borrados
  1. 371 0
      agent.js
  2. 246 0
      astar.js
  3. 72 0
      base.css
  4. 60 0
      game.js
  5. 117 0
      graphics.js
  6. 16 0
      index.html
  7. 441 0
      main.js
  8. 28 0
      math.js
  9. 93 0
      mazegen.js

+ 371 - 0
agent.js

@@ -0,0 +1,371 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {math} from './math.js';
+
+
+export const agent = (function() {
+
+  const _BOID_FORCE_ORIGIN = 5;
+
+  const _M = new THREE.Matrix4();
+  const _V = new THREE.Vector3();
+  const _A = new THREE.Vector2();
+  const _B = new THREE.Vector2();
+  const _AP = new THREE.Vector2();
+  const _AB = new THREE.Vector2();
+  const _BA = new THREE.Vector2();
+  const _PT2 = new THREE.Vector2();
+  const _PT3 = new THREE.Vector3();
+
+  const _Q = new THREE.Quaternion();
+  const _V_0 = new THREE.Vector3(0, 0, 0);
+  const _V_Y = new THREE.Vector3(0, 1, 0);
+  const _V_SC_0_1 = new THREE.Vector3(0.1, 0.1, 0.1);
+
+  function _Key(x, y) {
+    return x + '.' + y;
+  }
+  
+  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,
+              linewidth: 3,
+            });
+        material = this._materials[hexColour];
+      }
+  
+      const line = new THREE.Line(geometry, material);
+      this._lines.push(line);
+      this._group.add(line);
+    }
+  }
+  
+
+  class _Agent_Base {
+    constructor(game, params) {
+      this._direction = new THREE.Vector3(1, 0, 0);
+      this._velocity = new THREE.Vector3(0, 0, 0);
+
+      this._maxSteeringForce = params.maxSteeringForce;
+      this._maxSpeed  = params.speed;
+      this._acceleration = params.acceleration;
+
+      this._game = game;
+
+      this._astar = params.astar;
+      this._pathNodes = [];
+
+      this._wanderAngle = 0;
+
+      this._displayDebug = false;
+
+      this._InitDebug();
+    }
+
+    _InitDebug() {
+      if (!this._displayDebug) {
+        return;
+      }
+
+      const e = this._astar._nodes[this._astar._end].metadata.position;
+      const endPosition = new THREE.Vector3(e.x, 1, e.y);
+
+      const geometry = new THREE.SphereGeometry(1, 16, 16);
+      const material = new THREE.MeshBasicMaterial({
+        color: 0x80FF80,
+        transparent: true,
+        opacity: 0.25,
+      });
+      const mesh = new THREE.Mesh(geometry, material);
+      this._goalMesh = mesh;
+      this._goalMesh.position.copy(endPosition);
+  
+      this._displayDebug = true;
+      this._lineRenderer = new LineRenderer(this._game);
+      this._lineRenderer.Reset();
+    }
+
+    _UpdateDebug() {
+      if (!this._displayDebug) {
+        return;
+      }
+      this._lineRenderer.Reset();
+      this._UpdateDebugPath();
+    }
+
+    _UpdateDebugPath() {
+      if (!this._displayDebug) {
+        return;
+      }
+      const path = this._pathNodes;
+      for (let i = 0; i < path.length - 1; i++) {
+
+        const _AsV3 = (p) => {
+          const pos = p.metadata.position;
+          return new THREE.Vector3(pos.x + 0.5, 0.25, pos.y + 0.5);
+        }
+
+        const p1 = _AsV3(path[i]);
+        const p2 = _AsV3(path[i+1]);
+
+        this._lineRenderer.Add(p1, p2, 0xFF0000);
+      }
+    }
+
+    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._astar) {
+        if (this._astar.finished) {
+          this._pathNodes = this._astar.Path;
+          this._astar = null;
+        } else if (!this._astar.started) {
+          this._UpdateSearchStartPosition();
+        }
+      }
+
+      this._ApplySteering(timeInSeconds);
+      this._OnStep(timeInSeconds);
+    }
+
+    _UpdateSearchStartPosition() {
+      const p = this.Position;
+      const a = _A.set(p.x, p.z);
+      const k = _Key(Math.floor(a.x), Math.floor(a.y));
+
+      this._astar._start = k; 
+    }
+
+    _ApplySteering(timeInSeconds) {
+      const forces = [
+        this._ApplyPathFollowing(),
+      ];
+
+      const steeringForce = new THREE.Vector3(0, 0, 0);
+      for (const f of forces) {
+        steeringForce.add(f);
+      }
+
+      steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+
+      // Lock movement to x/z dimension
+      steeringForce.multiply(new THREE.Vector3(1, 0, 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.copy(this._velocity);
+      this._direction.normalize();
+    }
+
+    _ApplyPartialPathFollowing() {
+      if (!this._astar) {
+        return _V_0;
+      }
+
+      const end = _A.copy(this._astar._nodes[this._astar._end].metadata.position);
+      end.addScalar(0.5);
+
+      _PT3.set(end.x, this.Position.y, end.y);
+
+      return this._ApplySeek(_PT3);
+    }
+
+    _ApplyPathFollowing() {
+      if (this._pathNodes.length < 2) {
+        return this._ApplyPartialPathFollowing();
+      }
+
+      const _PointOnLine = (p, a, b) => {
+        _AP.subVectors(p, a);
+        _AB.subVectors(b, a);
+
+        const maxLength = _AB.length();
+        const dp = math.clamp(_AP.dot(_AB), 0, maxLength);
+
+        _AB.normalize();
+        _AB.multiplyScalar(dp);
+
+        return _AB.add(a);
+      }
+
+      const p = this.Position;
+      _PT2.set(p.x, p.z);
+
+      const a = _A.copy(this._pathNodes[0].metadata.position);
+      const b = _B.copy(this._pathNodes[1].metadata.position);
+
+      a.addScalar(0.5);
+      b.addScalar(0.5);
+
+      const pt = _PointOnLine(_PT2, a, b);
+      _BA.subVectors(b, a).normalize();
+      pt.add(_BA.multiplyScalar(0.1));
+
+      _PT3.set(pt.x, p.y, pt.y);
+
+      if (this._displayDebug) {
+        this._lineRenderer.Add(p, _PT3, 0x00FF00);
+      }
+
+      if (p.distanceTo(_PT3) < 0.25) {
+        this._pathNodes.shift();
+      }
+
+      return this._ApplySeek(_PT3);
+    }
+
+    _ApplySeek(destination) {
+      const direction = destination.clone().sub(this.Position);
+      direction.normalize();
+
+      const forceVector = direction.multiplyScalar(_BOID_FORCE_ORIGIN);
+      return forceVector;
+    }
+  }
+
+  class _Agent_Mesh extends _Agent_Base {
+    constructor(game, params) {
+      super(game, params);
+
+      this._mesh = new THREE.Mesh(params.geometry, params.material);
+      this._mesh.castShadow = true;
+      this._mesh.receiveShadow = true;
+      this._mesh.scale.setScalar(0.1);
+      this._mesh.rotateX(-Math.PI / 2);
+
+      this._group = new THREE.Group();
+      this._group.add(this._mesh);
+      this._group.position.copy(params.position);
+
+      game._graphics.Scene.add(this._group);
+    }
+
+    _OnStep(timeInSeconds) {
+      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);
+    }
+  }
+
+  class _Agent_Instanced extends _Agent_Base {
+    constructor(game, params) {
+      super(game, params);
+
+      this._mesh = params.mesh;
+
+      this._proxy = new THREE.Object3D();
+      this._proxy.scale.setScalar(0.1);
+      this._proxy.rotateX(-Math.PI / 2);
+
+      this._group = new THREE.Object3D();
+      this._group.position.copy(params.position);
+      this._group.add(this._proxy);
+
+      this._index = params.index;
+    }
+
+    _OnStep(timeInSeconds) {
+      const frameVelocity = this._velocity.clone();
+      frameVelocity.multiplyScalar(timeInSeconds);
+      this._group.position.add(frameVelocity);
+  
+      _M.lookAt(_V_0, this._direction, _V_Y);
+      this._group.quaternion.setFromRotationMatrix(_M);
+      this._group.updateMatrixWorld();
+      this._proxy.updateMatrixWorld();
+      this._mesh.setMatrixAt(this._index, this._proxy.matrixWorld);
+      this._mesh.instanceMatrix.needsUpdate = true;
+    }
+  }
+
+  class _Agent_Instanced_Faster extends _Agent_Base {
+    constructor(game, params) {
+      super(game, params);
+
+      this._mesh = params.mesh;
+
+      this._position = new THREE.Vector3();
+      this._position.copy(params.position);
+
+      this._index = params.index;
+    }
+
+    get Position() {
+      return this._position;
+    }
+
+    _OnStep(timeInSeconds) {
+      const frameVelocity = _V.copy(this._velocity);
+      frameVelocity.multiplyScalar(timeInSeconds);
+      this._position.add(frameVelocity);
+
+      _Q.setFromUnitVectors(_V_Y, this._direction);
+      _M.identity();
+      _M.compose(this._position, _Q, _V_SC_0_1);
+      
+      this._mesh.setMatrixAt(this._index, _M);
+      this._mesh.instanceMatrix.needsUpdate = true;
+    }
+  }
+
+  return {
+    Agent: _Agent_Mesh,
+    Agent_Instanced: _Agent_Instanced_Faster
+  };
+})();

+ 246 - 0
astar.js

@@ -0,0 +1,246 @@
+export const astar = (function() {
+
+  class _OpenSet {
+    constructor() {
+      this._priorityQueue = buckets.PriorityQueue((a, b) => {
+        if (a.fScore > b.fScore) {
+          return -1;
+        }
+        if (a.fScore < b.fScore) {
+          return 1;
+        }
+        if (a.gScore > b.gScore) {
+          return 1;
+        }
+        if (a.gScore < b.gScore) {
+          return -1;
+        }
+        return 0;
+      });
+      this._dict = {};
+    }
+
+    add(k, v) {
+      this._priorityQueue.add(v);
+      this._dict[k] = true;
+    }
+
+    dequeue() {
+      const v = this._priorityQueue.dequeue();
+      delete this._dict[v.key];
+      return v;
+    }
+
+    peek() {
+      return this._priorityQueue.peek();
+    }
+
+    hasKey(k) {
+      return (k in this._dict);
+    }
+
+    size() {
+      return this._priorityQueue.size();
+    }
+  };
+
+  class _AStarClient {
+    constructor(start, end, nodes) {
+      this._start = start;
+      this._end = end;
+      this._nodes = nodes;
+      this._path = null;
+      this._astarInstance = null;
+    }
+
+    Start(instance) {
+      this._astarInstance = instance;
+    }
+
+    get Path() {
+      return this._path;
+    }
+
+    get started() {
+      return this._astarInstance != null;
+    }
+
+    get finished() {
+      if (this._path) {
+        return true;
+      }
+
+      if (!this._astarInstance) {
+        return false;
+      }
+
+      return this._astarInstance.finished;
+    }
+
+    CachePath() {
+      if (!this._astarInstance) {
+        return null;
+      }
+
+      this._path = this._astarInstance.BuildPath();
+      this._path = this._path.map(k => this._astarInstance._nodes[k])
+      this._astarInstance = null;
+    }
+
+    Step() {
+      if (!this._astarInstance) {
+        return;
+      }
+
+      this._astarInstance.Step();
+    }
+  };
+
+  const _MAX_ASTAR = 400;
+
+  class _AStarManager {
+    constructor(nodes, costFunction, weightFunction) {
+      this._nodes = nodes;
+      this._costFunction = costFunction;
+      this._weightFunction = weightFunction;
+      this._live = [];
+      this._clients = [];
+    }
+
+    Step() {
+      for (let c of this._clients) {
+        if (!c._astarInstance && !c.finished && this._live.length < _MAX_ASTAR) {
+          const a = new _AStar(this._nodes, c._start, c._end, this._costFunction, this._weightFunction);
+          c.Start(a);
+          this._live.push(c);
+        }
+      }
+
+      for (let c of this._live) {
+        c.Step();
+        if (c.finished) {
+          c.CachePath();
+        }
+      }
+
+      this._live = this._live.filter(c => !c.finished);
+    }
+
+    CreateClient(start, end) {
+      const c = new _AStarClient(start, end, this._nodes);
+      this._clients.push(c);
+
+      return c;
+    }
+  }
+
+  class _AStar {
+    constructor(nodes, start, end, costFunction, weightFunction) {
+      this._start = start;
+      this._end = end;
+      this._nodes = nodes;
+      this._costFunction = costFunction;
+      this._weightFunction = weightFunction;
+      this._finished = false;
+      this._steps = 0;
+
+      this._data = {};
+      this._data[start] = {
+        key: start,
+        gScore: 0,
+        fScore: costFunction(start, end),
+        cameFrom: null,
+      };
+
+      this._open = new _OpenSet();
+      this._open.add(start, this._data[start]);
+    }
+
+    get finished() {
+      return this._finished;
+    }
+
+    BuildInProgressPath() {
+      const lowestF = this._open.peek();
+
+      const path = [lowestF.key];
+
+      while (true) {
+        const n = this._data[path[path.length - 1]];
+
+        if (n.cameFrom == null) {
+          break;
+        }
+
+        path.push(n.cameFrom);
+      }
+      return path.reverse();
+    }
+
+    BuildPath() {
+      if (!this.finished) {
+        return this.BuildInProgressPath();
+      }
+
+      const path = [this._end];
+
+      while (true) {
+        const n = this._data[path[path.length - 1]];
+
+        if (n.cameFrom == null) {
+          break;
+        }
+
+        path.push(n.cameFrom);
+      }
+      return path.reverse();
+    }
+
+    Step() {
+      if (this.finished) {
+        return;
+      }
+
+      if (this._open.size() > 0) {
+        this._steps += 1;
+
+        const curNode = this._open.dequeue();
+        const k = curNode.key;
+
+        if (k == this._end) {
+          this._finished = true;
+          return;
+        }
+
+        for (const e of this._nodes[k].edges) {
+          // Lazily instantiate graph instead of in constructor.
+          if (!(e in this._data)) {
+            this._data[e] = {
+              key: k,
+              gScore: Number.MAX_VALUE,
+              fScore: Number.MAX_VALUE,
+              cameFrom: null,
+            };
+          }
+         
+          const gScore = this._data[k].gScore + this._weightFunction(k, e);
+          if (gScore < this._data[e].gScore) {
+            this._data[e] = {
+              key: e,
+              gScore: gScore,
+              fScore: gScore + this._costFunction(this._end, e),
+              cameFrom: k,
+            };
+            if (!this._open.hasKey(e)) {
+              this._open.add(e, this._data[e]);
+            }
+          }
+        }
+      }
+    }
+  };
+
+  return {
+    AStarManager: _AStarManager,
+  };
+})();

+ 72 - 0
base.css

@@ -0,0 +1,72 @@
+.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;
+}
+
+#info {
+	position: absolute;
+	display: block;
+	text-align: center;
+  width: 100%;
+	z-index: 100;
+}
+
+.info-text {
+  color: white;
+  font-size: 2em;
+  font-weight: bold;
+  font-family: 'Space Mono', monospace;
+  text-shadow: 
+    0 1px 0 #ccc, 
+    0 2px 0 #c9c9c9, 
+    0 3px 0 #bbb, 
+    0 4px 0 #b9b9b9, 
+    0 5px 0 #aaa, 
+    0 6px 1px rgba(0,0,0,.1), 
+    0 0 5px rgba(0,0,0,.1), 
+    0 1px 3px rgba(0,0,0,.3), 
+    0 3px 5px rgba(0,0,0,.2), 
+    0 5px 10px rgba(0,0,0,.25), 
+    0 10px 10px rgba(0,0,0,.2), 
+    0 20px 20px rgba(0,0,0,.15);
+}
+
+body {
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}

+ 60 - 0
game.js

@@ -0,0 +1,60 @@
+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._controls.update();
+        this._graphics.Render(timeInSeconds);
+
+        this._RAF();
+      }
+    }
+  };
+})();

+ 117 - 0
graphics.js

@@ -0,0 +1,117 @@
+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 {SSAOPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/SSAOPass.js';
+import {UnrealBloomPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/UnrealBloomPass.js';
+
+export const graphics = (function() {
+  return {
+    PostFX: {
+      UnrealBloomPass: UnrealBloomPass,
+      SSAOPass: SSAOPass,
+    },
+    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(16, 2, 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(10, 20, 10);
+        //light.target.position.set(0, 0, 0);
+        light.castShadow = true;
+        light.shadow.bias = -0.005;
+        light.shadow.mapSize.set(4096, 4096);
+        light.shadow.camera.near = 0.01;
+        light.shadow.camera.far = 50;
+        light.shadow.camera.left = 50;
+        light.shadow.camera.right = -50;
+        light.shadow.camera.top = 50;
+        light.shadow.camera.bottom = -50;
+        light.shadow.radius = 1;
+        this._scene.add(light);
+        // cleanup
+        this._shadowLight = 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(this._scene, this._camera);
+        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();
+      }
+    }
+  };
+})();

+ 16 - 0
index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>How Many?</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">
+  <link href="https://fonts.googleapis.com/css?family=Space+Mono&display=swap" rel="stylesheet">
+  <script src="https://github.com/mrdoob/three.js/blob/r112/build/three.module.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/buckets-js/dist/buckets.min.js"></script>
+</head>
+<body>
+  <div id="target"></div>
+  <script src="main.js" type="module">
+  </script>
+</body>
+</html>

+ 441 - 0
main.js

@@ -0,0 +1,441 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {BufferGeometryUtils} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/utils/BufferGeometryUtils.js';
+import {Sky} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/objects/Sky.js';
+
+import {agent} from './agent.js';
+import {astar} from './astar.js';
+import {game} from './game.js';
+import {math} from './math.js';
+import {mazegen} from './mazegen.js';
+
+
+const _BOID_SPEED = 0.25;
+const _BOID_ACCELERATION = _BOID_SPEED / 2.5;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 5.0;
+
+const _TILES_X = 500;
+const _TILES_Y = 20;
+const _TILES_S = 50;
+
+let _APP = null;
+
+
+function _Key(x, y) {
+  return x + '.' + y;
+}
+
+function _ManhattanDistance(n1, n2) {
+  const p1 = n1.metadata.position;
+  const p2 = n2.metadata.position;
+  const dx = Math.abs(p2.x - p1.x);
+  const dy = Math.abs(p2.y - p1.y);
+  return (dx + dy);
+}
+
+function _Distance(n1, n2) {
+  const p1 = n1.metadata.position;
+  const p2 = n2.metadata.position;
+  return p1.distanceTo(p2);
+}
+
+class Graph {
+  constructor() {
+    this._nodes = {};
+  }
+
+  get Nodes() {
+    return this._nodes;
+  }
+
+  AddNode(k, e, m) {
+    this._nodes[k] = {
+      edges: [...e],
+      potentialEdges: [...e],
+      metadata: m
+    };
+  }
+}
+
+function NodesToMesh(scene, nodes) {
+  const material = new THREE.MeshStandardMaterial({color: 0x71b5ef});
+  const material2 = new THREE.MeshStandardMaterial({color: 0xFFFFFF});
+
+  const edges = {};
+  const geometries = [];
+
+  for (const k in nodes) {
+    const curNode = nodes[k];
+    const x = curNode.metadata.position.x;
+    const y = curNode.metadata.position.y;
+    const w = 1;
+    const h = 1;
+    const wallWidth = 0.25;
+    const wallHeight = 0.5;
+
+    const neighbours = [[0, 1], [1, 0], [0, -1], [-1, 0]];
+
+    if (!curNode.metadata.render.visible) {
+      continue;
+    }
+
+    for (let ni = 0; ni < neighbours.length; ni++) {
+      const n = neighbours[ni];
+      const ki = _Key(x + n[0], y + n[1]);
+
+      if (curNode.edges.indexOf(_Key(x, y + 1)) < 0) {
+        // this._gfx.moveTo(w * (x + 0.0), h * (y + 1.0));
+        // this._gfx.lineTo(w * (x + 1.0), h * (y + 1.0));
+        const x1 = w * (x + 0.0);
+        const y1 = h * (y + 1.0);
+        const x2 = w * (x + 1.0);
+        const y2 = h * (y + 1.0);
+
+        const sq = new THREE.BoxBufferGeometry(x2 - x1, wallHeight, wallWidth);
+        const m = new THREE.Matrix4();
+        m.makeTranslation(x1 + 0.5, wallHeight * 0.5, y1);
+        sq.applyMatrix(m);
+        geometries.push(sq);
+      }
+
+      if (curNode.edges.indexOf(_Key(x + 1, y + 0)) < 0) {
+        // this._gfx.moveTo(w * (x + 1.0), h * (y + 0.0));
+        // this._gfx.lineTo(w * (x + 1.0), h * (y + 1.0));
+        const x1 = w * (x + 1.0);
+        const y1 = h * (y + 0.0);
+        const x2 = w * (x + 1.0);
+        const y2 = h * (y + 1.0);
+
+        const sq = new THREE.BoxBufferGeometry(wallWidth, wallHeight, y2 - y1);
+        const m = new THREE.Matrix4();
+        m.makeTranslation(x1, wallHeight * 0.5, y1 + 0.5);
+        sq.applyMatrix(m);
+        geometries.push(sq);
+      }
+
+      if (curNode.edges.indexOf(_Key(x, y - 1)) < 0) {
+        // this._gfx.moveTo(w * (x + 0.0), h * (y + 0.0));
+        // this._gfx.lineTo(w * (x + 1.0), h * (y + 0.0));
+        const x1 = w * (x + 0.0);
+        const y1 = h * (y + 0.0);
+        const x2 = w * (x + 1.0);
+        const y2 = h * (y + 0.0);
+
+        const sq = new THREE.BoxBufferGeometry(x2 - x1, wallHeight, wallWidth);
+        const m = new THREE.Matrix4();
+        m.makeTranslation(x1 + 0.5, wallHeight * 0.5, y1);
+        sq.applyMatrix(m);
+        geometries.push(sq);
+      }
+
+      if (curNode.edges.indexOf(_Key(x - 1, y)) < 0) {
+        // this._gfx.moveTo(w * (x + 0.0), h * (y + 0.0));
+        // this._gfx.lineTo(w * (x + 0.0), h * (y + 1.0));
+        const x1 = w * (x + 0.0);
+        const y1 = h * (y + 0.0);
+        const x2 = w * (x + 0.0);
+        const y2 = h * (y + 1.0);
+
+        const sq = new THREE.BoxBufferGeometry(wallWidth, wallHeight, y2 - y1);
+        const m = new THREE.Matrix4();
+        m.makeTranslation(x1, wallHeight * 0.5, y1 + 0.5);
+        sq.applyMatrix(m);
+        geometries.push(sq);
+      }
+    }
+  }
+
+  for (const k in nodes) {
+    const curNode = nodes[k];
+    curNode.edges = [...new Set(curNode.edges)];
+  }
+
+  const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
+    geometries, false);
+  const mesh = new THREE.Mesh(mergedGeometry, material2);
+  mesh.castShadow = true;
+  mesh.receiveShadow = true;
+  scene.add(mesh);
+
+  const plane = new THREE.Mesh(new THREE.PlaneGeometry(5000, 5000, 1, 1), material);
+  plane.position.set(0, 0, 0);
+  plane.castShadow = false;
+  plane.receiveShadow = true;
+  plane.rotation.x = -Math.PI / 2;
+  scene.add(plane);
+}
+
+class Demo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = [];
+    this._controls.panningMode = 1;
+
+    this._CreateMaze();
+    this._LoadBackground();
+  }
+
+  _LoadBackground() {
+    this._sky = new Sky();
+    this._sky.scale.setScalar(10000);
+    this._graphics.Scene.add(this._sky);
+
+    const sky = {
+      turbidity: 10.0,
+      rayleigh: 2,
+      mieCoefficient: 0.005,
+      mieDirectionalG: 0.8,
+      luminance: 1,
+    };
+
+    const sun = {
+      inclination: 0.31,
+      azimuth: 0.25,
+    };
+
+    for (let k in sky) {
+      this._sky.material.uniforms[k].value = sky[k];
+    }
+
+    const theta = Math.PI * (sun.inclination - 0.5);
+    const phi = 2 * Math.PI * (sun.azimuth - 0.5);
+
+    const sunPosition = new THREE.Vector3();
+    sunPosition.x = Math.cos(phi);
+    sunPosition.y = Math.sin(phi) * Math.sin(theta);
+    sunPosition.z = Math.sin(phi) * Math.cos(theta);
+
+    this._sky.material.uniforms['sunPosition'].value.copy(sunPosition);
+  }
+
+  _CreateMaze() {
+    this._graph = new Graph();
+
+    for (let x = 0; x < _TILES_X; x++) {
+      for (let y = 0; y < _TILES_Y; y++) {
+        const k = _Key(x, y);
+        this._graph.AddNode(
+            k, [],
+            {
+              position: new THREE.Vector2(x, y),
+              weight: 0,
+              render: {
+                visited: false,
+                visible: true,
+              }
+            });
+      }
+    }
+
+    for (let x = 0; x < _TILES_X; x++) {
+      for (let y = 0; y < _TILES_Y; y++) {
+        const k = _Key(x, y);
+
+        for (let xi = -1; xi <= 1; xi++) {
+          for (let yi = -1; yi <= 1; yi++) {
+            if (xi == 0 && yi == 0 || (Math.abs(xi) + Math.abs(yi) != 1)) {
+              continue;
+            }
+
+            const ki = _Key(x + xi, y + yi);
+
+            if (ki in this._graph.Nodes) {
+              this._graph.Nodes[k].potentialEdges.push(ki);
+            }
+          }
+        }
+      }
+    }
+
+    const start = _Key(0, 0);
+    const end = _Key(4, 0);
+
+    this._mazeGenerator = new mazegen.MazeGenerator(this._graph.Nodes);
+    this._mazeIterator = this._mazeGenerator.GenerateIteratively(start);
+    this._mazeDone = () => {
+      const nodes = [];
+      for (let x = 0; x < _TILES_X; x++) {
+        for (let y = 0; y > -_TILES_S; y--) {
+          const k = _Key(x, y);
+          if (k in this._graph.Nodes) {
+            continue;
+          }
+          this._graph.AddNode(
+              k, [],
+              {
+                position: new THREE.Vector2(x, y),
+                weight: 0,
+                render: {
+                  visited: false,
+                  visible: false,
+                }
+              });
+          nodes.push(k);
+        }
+      }
+      for (let x = 0; x < _TILES_X; x++) {
+        for (let y = _TILES_Y - 1; y < _TILES_Y + _TILES_S; y++) {
+          const k = _Key(x, y);
+          if (k in this._graph.Nodes) {
+            continue;
+          }
+          this._graph.AddNode(
+              k, [],
+              {
+                position: new THREE.Vector2(x, y),
+                weight: 0,
+                render: {
+                  visited: false,
+                  visible: false,
+                }
+              });
+          nodes.push(k);
+        }
+      }
+      for (let k of nodes) {
+        const n = this._graph.Nodes[k];
+        const x = n.metadata.position.x;
+        const y = n.metadata.position.y;
+
+        for (let xi = -1; xi <= 1; xi++) {
+          for (let yi = -1; yi <= 1; yi++) {
+            if (xi == 0 && yi == 0 || (Math.abs(xi) + Math.abs(yi) != 1)) {
+              continue;
+            }
+
+            const ki = _Key(x + xi, y + yi);
+
+            if (ki in this._graph.Nodes) {
+              this._graph.Nodes[k].potentialEdges.push(ki);
+            }
+
+            for (let pk of this._graph.Nodes[k].potentialEdges) {
+              this._graph.Nodes[k].edges.push(pk);
+              this._graph.Nodes[pk].edges.push(k);
+            }
+          }
+        }
+      }
+
+      this._CreateEntities();
+    };
+  }
+
+  _CreateEntities() {
+    const geometries = {
+      cone: new THREE.ConeGeometry(1, 2, 32)
+    };
+
+    const material = new THREE.MeshStandardMaterial({color: 0xFF0000});
+    const numInstances = _TILES_X * _TILES_S / 2;
+
+    const mesh = new THREE.InstancedMesh(
+        geometries.cone, material, numInstances);
+    mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
+    mesh.castShadow = true;
+    mesh.receiveShadow = true;
+    mesh.frustumCulled = false;
+
+    let index = 0;
+    const nodes = this._graph.Nodes;
+
+    function _ManhattanDistance(n1, n2) {
+      const p1 = n1.metadata.position;
+      const p2 = n2.metadata.position;
+      const dx = Math.abs(p2.x - p1.x);
+      const dy = Math.abs(p2.y - p1.y);
+      return (dx + dy);
+    }
+    
+    function _Distance(n1, n2) {
+      const p1 = n1.metadata.position;
+      const p2 = n2.metadata.position;
+      return p1.distanceTo(p2);
+    }
+
+    const heuristicFunction = (s, e) => {
+      return 2 * _ManhattanDistance(nodes[s], nodes[e]);
+    };
+
+    const weightFunction = (s, e) => {
+      return _ManhattanDistance(nodes[s], nodes[e]);
+    };
+
+    const mgr = new astar.AStarManager(
+        this._graph.Nodes,
+        heuristicFunction,
+        weightFunction);
+
+    this._entities.push(mgr);
+
+    for (let j = 0; j < _TILES_S / 2; j++) {
+      for (let i = 0; i < _TILES_X; i++) {
+        const xe = math.clamp(math.rand_int(i - 20, i + 20), 0, _TILES_X - 1);
+        const start = _Key(i, -j - 1);
+        const end = _Key(xe, _TILES_Y + 5);
+    
+        let params = {
+          geometry: geometries.cone,
+          material: material,
+          mesh: mesh,
+          index: index++,
+          speed: _BOID_SPEED,
+          maxSteeringForce: _BOID_FORCE_MAX,
+          acceleration: _BOID_ACCELERATION,
+          position: new THREE.Vector3(i, 0.25, -j - 1),
+          astar: mgr.CreateClient(start, end),
+        };
+        const e = new agent.Agent_Instanced(this, params);
+        this._entities.push(e);
+      }
+    }
+
+    this._graphics._camera.position.set(_TILES_X / 2, 7, 12);
+    this._controls.target.set(_TILES_X / 2, 0, -5);
+    this._controls.update();
+
+    console.log('AGENTS: ' + this._entities.length)
+
+    this._graphics.Scene.add( mesh );
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    this._StepMazeGeneration();
+    this._StepEntities(timeInSeconds);
+  }
+
+  _StepMazeGeneration() {
+    for (let i = 0; i < 100; i++) {
+      if (this._mazeIterator) {
+        const r = this._mazeIterator.next();
+        if (r.done) {
+          console.log('DONE');
+          this._mazeGenerator.Randomize();
+          this._mazeDone();
+          NodesToMesh(this._graphics.Scene, this._graph.Nodes);
+          this._graphics._shadowLight.position.set(_TILES_X * 0.5, 10, _TILES_Y * 0.5);
+          this._graphics._shadowLight.target.position.set(_TILES_X * 0.5 - 5, 0, _TILES_Y * 0.5 - 5);
+          this._graphics._shadowLight.target.updateWorldMatrix();
+          this._mazeIterator = null;
+        }
+      }
+    }
+  }
+
+  _StepEntities(timeInSeconds) {
+    for (let e of this._entities) {
+      e.Step(timeInSeconds);
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new Demo();
+}
+
+_Main();

+ 28 - 0
math.js

@@ -0,0 +1,28 @@
+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;
+    },
+
+    rand_int: function(a, b) {
+      return Math.round(Math.random() * (b - a) + a);
+    },
+
+    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);
+    },
+  };
+})();

+ 93 - 0
mazegen.js

@@ -0,0 +1,93 @@
+export const mazegen = (function() {
+
+  function RouletteSelect(src) {
+    const roll = Math.random() * src.length;
+
+    let sum = 0;
+    for (let i = 0; i < src.length; i++) {
+      sum += 1.0;
+      if (roll < sum) {
+        const res = src[i];
+        src = src.splice(i, 1);
+        return res;
+      }
+    }
+  }
+
+  function _Key(x, y) {
+    return x + '.' + y;
+  }
+
+  return {
+    MazeGenerator: class {
+      constructor(nodes) {
+        this._nodes = nodes;
+        this._visited = {};
+      }
+
+      *GenerateIteratively(nodeKey) {
+        this._visited[nodeKey] = true;
+
+        const node = this._nodes[nodeKey];
+
+        const neighbours = [...node.potentialEdges];
+        while (neighbours.length > 0) {
+          const ki = RouletteSelect(neighbours);
+
+          if (!(ki in this._visited)) {
+            const adjNode = this._nodes[ki];
+
+            node.edges.push(ki);
+            adjNode.edges.push(nodeKey);
+
+            yield* this.GenerateIteratively(ki);
+          }
+        }
+      }
+
+      Randomize() {
+        for (let k in this._nodes) {
+          const n = this._nodes[k];
+          if (n.potentialEdges < 3) {
+            continue;
+          }
+
+          const neighbours = [...n.potentialEdges];
+          while (n.edges.length < 3) {
+            const ki = RouletteSelect(neighbours);
+
+            if (!(ki in n.edges)) {
+              const adjNode = this._nodes[ki];
+  
+              n.edges.push(ki);
+              adjNode.edges.push(k);
+            }    
+          }
+        }
+      }
+
+      GenerateMaze(start) {
+        this._ProcessNode(start);
+      }
+
+      _ProcessNode(nodeKey) {
+        this._visited[nodeKey] = true;
+
+        const node = this._nodes[nodeKey];
+
+        const neighbours = [...node.potentialEdges];
+        while (neighbours.length > 0) {
+          const ki = RouletteSelect(neighbours);
+
+          if (!(ki in this._visited)) {
+            const adjNode = this._nodes[ki];
+
+            node.edges.push(ki);
+            adjNode.edges.push(nodeKey);
+            this._ProcessNode(ki);
+          }
+        }
+      }
+    }
+  };
+})();