UNPKG

three-boids

Version:
1,344 lines (1,342 loc) 55 kB
(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory(require("three"), require("three-mesh-bvh")) : typeof define === "function" && define.amd ? define(["three", "three-mesh-bvh"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global["three-boids"] = factory(global.THREE, global.threeMeshBvh)); })(this, function(THREE, threeMeshBvh) { "use strict";var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _BoidLogic_instances, initBoids_fn, accumulatorObject_fn, solidWall_fn, transparentWall_fn, _BoidController_instances, setBoidLogic_fn, debugCount_fn, debugValues_fn, debugSolidBorderBox_fn, debugProtectedRange_fn, debugVisualRange_fn, debugVisionRange_fn, _RayController_instances, checkEnviroment_fn, debugUpdate_fn, debugRays_fn, debugSetPointSphere_fn, debugRemovePointSphere_fn, debugTweakRays_fn, _Octree_instances, setUpBounds_fn, addObjects_fn, drawBox_fn, removeBox_fn, _Boids_instances, slowUpdate_fn; function _interopNamespaceDefault(e) { const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } }); if (e) { for (const k in e) { if (k !== "default") { const d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: () => e[k] }); } } } n.default = e; return Object.freeze(n); } const THREE__namespace = /* @__PURE__ */ _interopNamespaceDefault(THREE); const boidConfig = { values: { count: 1, visualRange: 0.75046, protectedRange: 0.38377, enviromentVision: 0.5, objectAvoidFactor: 1, cohesionFactor: 408e-5, matchingFactor: 0.06312, seperationFactor: 0.19269, minSpeed: 2.379, maxSpeed: 5.206, wallTransparent: false, turnFactor: 0.201 }, vision: { count: 101, rayAngleLimit: 0.115, far: 0.5 } }; class BoidLogic { /** * * @param {int} boidCount * @param {THREE.Box3} box */ constructor(boidCount, box) { __privateAdd(this, _BoidLogic_instances); this.boundingBox = box; this.setUpTweakableValues(); __privateMethod(this, _BoidLogic_instances, initBoids_fn).call(this, boidCount); } /** * sets up the base values based on a config file */ setUpTweakableValues() { this.visualRange = boidConfig.values.visualRange || defaultValue(1, "VisualRange"); this.protectedRange = boidConfig.values.protectedRange || defaultValue(0.5, "protectedRange"); this.cohesionFactor = boidConfig.values.cohesionFactor || defaultValue(39e-4, "cohesionFactor"); this.matchingFactor = boidConfig.values.matchingFactor || defaultValue(0.0287, "matchingFactor"); this.seperationFactor = boidConfig.values.seperationFactor || defaultValue(0.01395, "seperationFactor"); this.minSpeed = boidConfig.values.minSpeed / 100 || defaultValue(5e-3, "minSpeed"); this.maxSpeed = boidConfig.values.maxSpeed / 100 || defaultValue(0.01, "maxSpeed"); this.wallTransparent = boidConfig.values.wallTransparent || defaultValue(false, "wallTransparent"); this.turnFactor = boidConfig.values.turnFactor / 100 || defaultValue(0.2, "turnFactor"); this.objectAvoidFactor = boidConfig.values.objectAvoidFactor || defaultValue(2, "object avoid"); } /** * Creates and adds new boids, with randomized aceleration and position * * @param {int} count */ addBoids(count) { for (let i = 0; i < count; i++) { const x = (Math.random() - 0.5) * 2 * this.boundingBox.max.x; const y = (Math.random() - 0.5) * 2 * this.boundingBox.max.y; const z = (Math.random() - 0.5) * 2 * this.boundingBox.max.z; const vx = (Math.random() - 0.5) * 2 * this.maxSpeed; const vy = (Math.random() - 0.5) * 2 * this.maxSpeed; const vz = (Math.random() - 0.5) * 2 * this.maxSpeed; this.boidArray.push(new Boid(x, y, z, vy, vx, vz)); } } /** * Removes boid position references * * @param {int} count - amount of boids to remove */ removeBoids(count) { while (count) { this.boidArray.pop(); count--; } } /** * Updates the boid positions based on other boids and environment objects * * @param {[obj]} environmentObjects - array of environment objects close to boids */ update(environmentObjects, deltaTime) { const PROTECTED_RANGE_SQUARED = this.protectedRange ** 2; const VISUAL_RANGE_SQUARED = this.visualRange ** 2; this.boidArray.forEach((boid, i) => { let accum = __privateMethod(this, _BoidLogic_instances, accumulatorObject_fn).call(this); this.boidArray.forEach((otherBoid, n) => { const dx = boid.position.x - otherBoid.position.x; const dy = boid.position.y - otherBoid.position.y; const dz = boid.position.z - otherBoid.position.z; if (Math.abs(dx) < this.visualRange && Math.abs(dy) < this.visualRange && Math.abs(dz) < this.visualRange) { const distanceSquared = dx ** 2 + dy ** 2 + dz ** 2; if (distanceSquared < PROTECTED_RANGE_SQUARED) { const exp = (1 - distanceSquared / PROTECTED_RANGE_SQUARED) ** 2; accum.close_dx += dx * exp; accum.close_dy += dy * exp; accum.close_dz += dz * exp; } else if (distanceSquared < VISUAL_RANGE_SQUARED) { const exp = (1 - distanceSquared / VISUAL_RANGE_SQUARED) ** 2; accum.xpos_avg += otherBoid.position.x; accum.ypos_avg += otherBoid.position.y; accum.zpos_avg += otherBoid.position.z; accum.xvel_avg += otherBoid.velocity.x * exp; accum.yvel_avg += otherBoid.velocity.y * exp; accum.zvel_avg += otherBoid.velocity.z * exp; accum.neighboring_boids++; } } }); if (!environmentObjects[i]) { if (accum.neighboring_boids > 0) { accum.xpos_avg /= accum.neighboring_boids; accum.ypos_avg /= accum.neighboring_boids; accum.zpos_avg /= accum.neighboring_boids; accum.xvel_avg /= accum.neighboring_boids; accum.yvel_avg /= accum.neighboring_boids; accum.zvel_avg /= accum.neighboring_boids; boid.velocity.x += (accum.xpos_avg - boid.position.x) * this.cohesionFactor; boid.velocity.x += (accum.xvel_avg - boid.velocity.x) * this.matchingFactor; boid.velocity.y += (accum.ypos_avg - boid.position.y) * this.cohesionFactor; boid.velocity.y += (accum.yvel_avg - boid.velocity.y) * this.matchingFactor; boid.velocity.z += (accum.zpos_avg - boid.position.z) * this.cohesionFactor; boid.velocity.z += (accum.zvel_avg - boid.velocity.z) * this.matchingFactor; } boid.velocity.x += accum.close_dx * this.seperationFactor; boid.velocity.y += accum.close_dy * this.seperationFactor; boid.velocity.z += accum.close_dz * this.seperationFactor; } else { const avoidObjExp = (1 - environmentObjects[i].distance) ** 3; const dx = boid.position.x - environmentObjects[i].position.x; const dy = boid.position.y - environmentObjects[i].position.y; const dz = boid.position.z - environmentObjects[i].position.z; boid.velocity.x += dx * avoidObjExp * this.objectAvoidFactor; boid.velocity.y += dy * avoidObjExp * this.objectAvoidFactor; boid.velocity.z += dz * avoidObjExp * this.objectAvoidFactor; } boid = this.wallTransparent ? __privateMethod(this, _BoidLogic_instances, transparentWall_fn).call(this, boid) : __privateMethod(this, _BoidLogic_instances, solidWall_fn).call(this, boid); const speed = Math.sqrt(boid.velocity.x ** 2 + boid.velocity.y ** 2 + boid.velocity.z ** 2); if (speed < this.minSpeed) { boid.velocity.x = boid.velocity.x / speed * this.minSpeed; boid.velocity.y = boid.velocity.y / speed * this.minSpeed; boid.velocity.z = boid.velocity.z / speed * this.minSpeed; } if (speed > this.maxSpeed) { boid.velocity.x = boid.velocity.x / speed * this.maxSpeed; boid.velocity.y = boid.velocity.y / speed * this.maxSpeed; boid.velocity.z = boid.velocity.z / speed * this.maxSpeed; } const currentPosition = boid.position.clone(); boid.position.x += boid.velocity.x * deltaTime; boid.position.y += boid.velocity.y * deltaTime; boid.position.z += boid.velocity.z * deltaTime; const m4 = new THREE__namespace.Matrix4(); m4.lookAt(currentPosition, boid.position, new THREE__namespace.Vector3(0, 1, 0)); boid.rotationMatrix = m4; }); } //returns the main boid getMain() { return this.boidArray[0]; } } _BoidLogic_instances = new WeakSet(); /** * * @param {int} boidCount */ initBoids_fn = function(boidCount) { this.boidCount = boidCount || defaultValue(1, "boidCount"); this.boidArray = []; this.addBoids(this.boidCount); }; /** * An object containing relevant physics accumulations * * @returns accumulator obj */ accumulatorObject_fn = function() { const accum = { xpos_avg: 0, //position averages ypos_avg: 0, zpos_avg: 0, xvel_avg: 0, //velocity averages yvel_avg: 0, zvel_avg: 0, neighboring_boids: 0, //count of neighboring boids within visual range close_dx: 0, close_dy: 0, close_dz: 0 }; return accum; }; /** * Keeps boids within a bounding box. * Bounding box acts as a notice to turn around * * @param {obj} boid * @returns */ solidWall_fn = function(boid) { if (this.boundingBox.max.y < boid.position.y) { boid.velocity.y -= this.turnFactor; } if (this.boundingBox.min.y > boid.position.y) { boid.velocity.y += this.turnFactor; } if (this.boundingBox.max.x < boid.position.x) { boid.velocity.x -= this.turnFactor; } if (this.boundingBox.min.x > boid.position.x) { boid.velocity.x += this.turnFactor; } if (this.boundingBox.max.z < boid.position.z) { boid.velocity.z -= this.turnFactor; } if (this.boundingBox.min.z > boid.position.z) { boid.velocity.z += this.turnFactor; } return boid; }; /** * Keeps boids within a bounding box. * Bounding box acts as 'portal'. * * @param {obj} boid * @returns */ transparentWall_fn = function(boid) { if (this.boundingBox.max.y < boid.y) { boid.y = this.boundingBox.min.y; } if (this.boundingBox.max.x < boid.x) { boid.x = this.boundingBox.min.x; } if (this.boundingBox.min.x > boid.x) { boid.x = this.boundingBox.max.x; } if (this.boundingBox.min.y > boid.y) { boid.y = this.boundingBox.max.y; } if (this.boundingBox.max.z < boid.z) { boid.z = this.boundingBox.min.z; } if (this.boundingBox.min.z > boid.z) { boid.z = this.boundingBox.max.z; } return boid; }; class Boid { constructor(x, y, z, vx, vy, vz) { this.position = new THREE__namespace.Vector3(x, y, z); this.velocity = new THREE__namespace.Vector3(vx, vy, vz); this.rotationMatrix = new THREE__namespace.Matrix4(); this.targetX = 0; this.targetY = 0; this.targetZ = 0; } } function defaultValue(x, name) { console.log(`Defaulted on ${name}`); return x; } class BoidController { /** constructor() * * * boidObject arr = setUp (boidArray) * * */ constructor(count = 200, box3, scene) { __privateAdd(this, _BoidController_instances); this.scene = scene; this.boundingBox = box3; this.boidMeshes = []; this.boidCount = null; __privateMethod(this, _BoidController_instances, setBoidLogic_fn).call(this, count); } getBoidArray() { return this.boidLogic.boidArray; } getMainBoid() { return this.boidLogic.boidArray[0]; } /** * * @param {int} count */ addBoids(count) { this.boidLogic.addBoids(count); this.changeModelCount(this.getBoidArray().length); } /** * * @param {int} count */ removeBoids(count) { this.boidLogic.removeBoids(count); this.changeModelCount(this.getBoidArray().length); } //#endregion //#region utils /** * * @param {[obj]} environmenObjects */ update(environmenObjects, deltaTime) { this.boidLogic.update(environmenObjects, deltaTime); if (this.dummy) { for (let i = 0; i < this.getBoidArray().length; i++) { const boid = this.getBoidArray()[i]; for (let n = 0; n < this.dummy.length; n++) { this.dummy[n].position.copy(boid.position); this.dummy[n].quaternion.setFromRotationMatrix(boid.rotationMatrix); this.dummy[n].updateMatrix(); this.boidInstancedMesh[n].setMatrixAt(i, this.dummy[n].matrix); this.boidInstancedMesh[n].instanceMatrix.needsUpdate = true; } } } if (this.debug) { if (this.debug.protectedRange) { this.debug.protectedRange.position.copy(this.getMainBoid().position); } if (this.debug.visualRange) { this.debug.visualRange.position.copy(this.getMainBoid().position); } } } //#endregion //#region //createDummyMesh createDummyMesh(model) { this.dummy = []; if (!model.scene) { this.dummy.push(model); return; } const baseMesh = this.getBaseMesh(model.scene); this.dummy.push(...baseMesh.children); return; } getBaseMesh(mesh, parent) { if (mesh.children.length < 1) { return parent; } parent = mesh; return this.getBaseMesh(mesh.children[0], parent); } setModels(model, minScale, defaultMaterial) { this.modelScale = minScale; this.createDummyMesh(model); if (!this.localBoidBoundingBox) { this.localBoidBoundingBox = new THREE__namespace.Box3(new THREE__namespace.Vector3(0, 0, 0), new THREE__namespace.Vector3(0, 0, 0)); } this.dummy.forEach((obj) => { this.localBoidBoundingBox.expandByObject(obj); }); this.localBoidBoundingBox.min.multiplyScalar(0.1 * this.modelScale); this.localBoidBoundingBox.max.multiplyScalar(0.1 * this.modelScale); this.boidInstancedMesh = []; this.dummy.forEach( (dummyMesh, i) => { let material = dummyMesh.material; if (defaultMaterial) { material = defaultMaterial; } this.boidInstancedMesh[i] = new THREE__namespace.InstancedMesh(dummyMesh.geometry, material, this.getBoidArray().length); } ); for (let i = 0; i < this.getBoidArray().length; i++) { const boid = this.boidLogic.boidArray[i]; const scale = Math.max(Math.random(), this.modelScale); for (let n = 0; n < this.dummy.length; n++) { this.dummy[n].position.copy(boid.position); this.dummy[n].scale.set(0.1 * scale, 0.1 * scale, 0.1 * scale); this.dummy[n].updateMatrix(); this.boidInstancedMesh[n].setMatrixAt(i, this.dummy[n].matrix); } } for (let i = 0; i < this.boidInstancedMesh.length; i++) { this.scene.add(this.boidInstancedMesh[i]); } } removeInstancedMesh() { this.boidInstancedMesh.forEach((obj) => { this.scene.remove(obj); obj.geometry.dispose(); obj.material.dispose(); }); } removeDummyMesh() { this.dummy.forEach((obj) => { this.scene.remove(obj); obj.geometry.dispose(); obj.material.dispose(); }); } changeModelCount() { this.removeInstancedMesh(); this.dummy.forEach( (dummyMesh, i) => { let material = dummyMesh.material; this.boidInstancedMesh[i] = new THREE__namespace.InstancedMesh(dummyMesh.geometry, material, this.getBoidArray().length); } ); for (let i = 0; i < this.getBoidArray().length; i++) { const boid = this.boidLogic.boidArray[i]; const scale = Math.max(Math.random(), this.modelScale); for (let n = 0; n < this.dummy.length; n++) { this.dummy[n].position.copy(boid.position); this.dummy[n].scale.set(0.1 * scale, 0.1 * scale, 0.1 * scale); this.dummy[n].updateMatrix(); this.boidInstancedMesh[n].setMatrixAt(i, this.dummy[n].matrix); } } for (let i = 0; i < this.boidInstancedMesh.length; i++) { this.scene.add(this.boidInstancedMesh[i]); } } changeModelMesh(newModel, minScale, defaultMaterial) { this.removeDummyMesh(); this.removeInstancedMesh(); this.setModels(newModel, minScale, defaultMaterial); } //#endregion //#region DEBUG /** * Set up the debug panel * * @param {*} gui lil-gui instance */ viewDebug(gui) { this.debug = {}; this.debug.folder = gui.addFolder("Boids"); this.debug.boidCount = this.getBoidArray().length; __privateMethod(this, _BoidController_instances, debugSolidBorderBox_fn).call(this); __privateMethod(this, _BoidController_instances, debugCount_fn).call(this); __privateMethod(this, _BoidController_instances, debugValues_fn).call(this); __privateMethod(this, _BoidController_instances, debugVisionRange_fn).call(this); } //#endregion } _BoidController_instances = new WeakSet(); //#region boids /** * inititate the boid logic * * @param {int} count */ setBoidLogic_fn = function(count) { this.boidLogic = new BoidLogic(count, this.boundingBox); }; /** * Setup boid Count tweak */ debugCount_fn = function() { this.debug.folder.add(this.debug, "boidCount").name("Entity Count").min(4).max(1e3).step(4).onFinishChange((count) => { if (count > this.getBoidArray().length) { this.addBoids(count - this.getBoidArray().length); } if (count < this.getBoidArray().length) { this.removeBoids(this.getBoidArray().length - count); } }); }; /** * Setup boid values tweaks */ debugValues_fn = function() { this.debug.folder.add(boidConfig.values, "objectAvoidFactor").name("Object Avoid Factor").min(0).max(4).step(1e-5).onChange((num) => { this.boidLogic.objectAvoidFactor = num; }); this.debug.folder.add(boidConfig.values, "enviromentVision").name("Object Visual range").min(0).max(2).step(1e-5); this.debug.folder.add(boidConfig.values, "cohesionFactor").name("Cohesion Factor").min(0).max(0.05).step(1e-5).onChange((num) => { this.boidLogic.cohesionFactor = num; }); this.debug.folder.add(boidConfig.values, "matchingFactor").name("Matching Factor").min(0).max(0.1).step(1e-5).onChange((num) => { this.boidLogic.matchingFactor = num; }); this.debug.folder.add(boidConfig.values, "seperationFactor").name("Seperation Factor").min(0).max(0.5).step(1e-5).onChange((num) => { this.boidLogic.seperationFactor = num; }); this.debug.folder.add(boidConfig.values, "turnFactor").name("Turn Factor").min(0).max(1).step(1e-4).onChange((num) => { this.boidLogic.turnFactor = num / 100; }); this.debug.folder.add(boidConfig.values, "minSpeed").name("Min Speed").min(0).max(10).step(1e-3).onChange((num) => { this.boidLogic.minSpeed = num / 100; }); this.debug.folder.add(boidConfig.values, "maxSpeed").name("Max Speed").min(0).max(10).step(1e-3).onChange((num) => { this.boidLogic.maxSpeed = num / 100; }); }; /** * set up border box tweak */ debugSolidBorderBox_fn = function() { this.debug.showBoundingBox = false; this.debug.folder.add(this.debug, "showBoundingBox").name("Show Bounding Box").onChange(() => { if (!this.debug.boundingBoxHelper) { this.debug.boundingBoxHelper = new THREE__namespace.Box3Helper(this.boundingBox, new THREE__namespace.Color("#ff1084")); this.scene.add(this.debug.boundingBoxHelper); } else { this.scene.remove(this.debug.boundingBoxHelper); this.debug.boundingBoxHelper.dispose(); this.debug.boundingBoxHelper = null; } }); }; /** * Setup protected range visualization */ debugProtectedRange_fn = function(needsUpdate = false) { if (!needsUpdate) { const material = new THREE__namespace.MeshBasicMaterial({ color: "red", opacity: 0.5, transparent: true, depthWrite: false }); const geometry = new THREE__namespace.SphereGeometry(); this.debug.protectedRange = new THREE__namespace.Mesh(geometry, material); const updateScale = new THREE__namespace.Vector3(1, 1, 1).multiplyScalar(boidConfig.values.protectedRange); this.debug.protectedRange.scale.copy(updateScale); this.debug.protectedRange.position.copy(this.getMainBoid().position); this.scene.add(this.debug.protectedRange); } else { const updateScale = new THREE__namespace.Vector3(1, 1, 1).multiplyScalar(boidConfig.values.protectedRange); this.debug.protectedRange.scale.copy(updateScale); this.scene.add(this.debug.protectedRange); } }; /** * Setup visual range visualization */ debugVisualRange_fn = function(needsUpdate = false) { if (!needsUpdate) { const material = new THREE__namespace.MeshBasicMaterial({ color: "#5bff33", opacity: 0.5, transparent: true, depthWrite: false }); const geometry = new THREE__namespace.SphereGeometry(); this.debug.visualRange = new THREE__namespace.Mesh(geometry, material); const updateScale = new THREE__namespace.Vector3(1, 1, 1).multiplyScalar(boidConfig.values.visualRange); this.debug.visualRange.position.copy(this.getMainBoid().position); this.debug.visualRange.scale.copy(updateScale); this.scene.add(this.debug.visualRange); } else { const updateScale = new THREE__namespace.Vector3(1, 1, 1).multiplyScalar(boidConfig.values.visualRange); this.debug.visualRange.scale.copy(updateScale); this.scene.add(this.debug.visualRange); } }; /** * Setup vision teak */ debugVisionRange_fn = function() { this.debug.showProtectedRange = false; this.debug.showVisualRange = false; this.debug.folder.add(this.debug, "showProtectedRange").name("Show Protected Range").onChange((bool) => { if (!bool) { this.scene.remove(this.debug.protectedRange); this.debug.protectedRange.material.dispose(); this.debug.protectedRange.geometry.dispose(); } else { __privateMethod(this, _BoidController_instances, debugProtectedRange_fn).call(this); } }); this.debug.folder.add(boidConfig.values, "protectedRange").name("Protected range").min(0.1).max(2).step(1e-5).onChange((num) => { this.boidLogic.protectedRange = num; if (this.debug.protectedRange && this.debug.showProtectedRange) { __privateMethod(this, _BoidController_instances, debugProtectedRange_fn).call(this, true); } }); this.debug.folder.add(this.debug, "showVisualRange").name("Show Visual Range").onChange((bool) => { if (!bool) { this.scene.remove(this.debug.visualRange); this.debug.visualRange.material.dispose(); this.debug.visualRange.geometry.dispose(); } else { __privateMethod(this, _BoidController_instances, debugVisualRange_fn).call(this); } }); this.debug.folder.add(boidConfig.values, "visualRange").name("Visual range").min(0.5).max(3).step(1e-5).onChange((num) => { this.boidLogic.visualRange = num; if (this.debug.visualRange && this.debug.showVisualRange) { __privateMethod(this, _BoidController_instances, debugVisualRange_fn).call(this, true); } }); }; class RaySphere { /** * raySphere controlls the positioning and firing of rays for one object * - uses a point geometry to position targets for rays. * */ constructor() { this.debug = {}; this.init(); this.pointSphere = this.setUpPointSphere(); this.rayCaster = this.setUpRayCaster(); } /** * sets up start parameters */ init() { this.rayCount = boidConfig.vision.count; this.rayAngleLimit = boidConfig.vision.rayAngleLimit; this.rayFar = boidConfig.vision.far; } /** * * @returns clone of the pointsphere */ getPointSphere() { return this.pointSphere.clone(); } //#region sphere methods /** * updates point sphere mesh * * @returns mesh */ updatePointSphere() { this.rayPositions_vec3Array = this.fibonacci_sphere_vec3(); this.rayPositions_floatArray = this.toFloatArr(this.rayPositions_vec3Array); const pointsGeometry = new THREE__namespace.BufferGeometry(); pointsGeometry.setAttribute("position", new THREE__namespace.BufferAttribute(this.rayPositions_floatArray, 3)); this.pointSphere.geometry.dispose(); this.pointSphere.geometry = pointsGeometry; } /** * Creates the point sphere points mesh using a bufferattribute derived from the global float array * * @returns mesh */ setUpPointSphere() { this.rayPositions_vec3Array = this.fibonacci_sphere_vec3(); this.rayPositions_floatArray = this.toFloatArr(this.rayPositions_vec3Array); const pointsGeometry = new THREE__namespace.BufferGeometry(); pointsGeometry.setAttribute("position", new THREE__namespace.BufferAttribute(this.rayPositions_floatArray, 3)); const pointsMaterial = new THREE__namespace.PointsMaterial({ color: "green", size: 0.04 // sizeAttenuation:true, }); const particleMesh = new THREE__namespace.Points(pointsGeometry, pointsMaterial); return particleMesh; } /** * returns points placed quasi equidistantly around a sphere using the Fibonacci sphere algorithm * - radius is normalized * - cuttoff value determines the z limit for the points on the sphere * - adapted from https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere/44164075#44164075 * @returns a THREE.Vector3 array */ fibonacci_sphere_vec3() { const points = []; const phi = Math.PI * (Math.sqrt(5) - 1); for (let i = 0; i < this.rayCount; i++) { let y = 1 - i / (this.rayCount - 1) * 2; let radius = Math.sqrt(1 - y * y); let theta = phi * i; let x = Math.cos(theta) * radius; let z = Math.sin(theta) * radius; if (z < this.rayAngleLimit) { const normalizedTarget = new THREE__namespace.Vector3(x, y, z); normalizedTarget.normalize(); points.push(normalizedTarget); } } return points; } /** * Rotates the point sphere to match the given mesh rotation. * Returns an array of the sphere vertices shifted to the world position * * note! it may be quicker to use the boidPoistion array instead of the boidMesh * possibly dont need the mesh. may be better with just the vec3 rotation (euler) * * @param {*} mesh object mesh * @returns THREE.Vector3 Array */ rotateTo(mesh) { this.pointSphere.quaternion.setFromRotationMatrix(mesh.rotationMatrix); return this.toWorldVertices(); } //#endregion //#region ray casting /** * sets up the raycaster. * - current layer set to 1 * @returns THREE.Raycaster */ setUpRayCaster() { const rayCaster = new THREE__namespace.Raycaster(); rayCaster.layers.set(1); rayCaster.far = this.rayFar; rayCaster.firstHitOnly = true; return rayCaster; } /** * Aims the raycaster and checks for intersections * - averages the found intersections distances and position * - normalized the final distance with the raycaster Far distance * * @param {[THREE.Vector3]} rayTargets array of vec3 directions(normalized) * @param {THREE.Vector3} origin * @returns {Object} {distance: int ,position: obj} */ castRays(rayTargets, origin, environment) { const objectArr = []; const sum = { distance: 0, position: { x: 0, y: 0, z: 0 } }; let foundArr = []; for (let i = 0; i < rayTargets.length; i++) { this.rayCaster.set(origin, rayTargets[i]); if (environment.length > 1) { foundArr = this.rayCaster.intersectObjects(environment); } else { foundArr = this.rayCaster.intersectObject(environment[0]); } if (foundArr.length > 0) { objectArr.push(...foundArr); } } if (objectArr.length > 0) { for (const obj of objectArr) { sum.distance += obj.distance; sum.position.x += obj.point.x; sum.position.z += obj.point.z; sum.position.y += obj.point.y; } if (objectArr.length > 1) { sum.distance /= objectArr.length; sum.position.x /= objectArr.length; sum.position.y /= objectArr.length; sum.position.z /= objectArr.length; } sum.distance /= this.rayCaster.far; } return sum.distance ? sum : null; } //#endregion //#region utils /** * converts the vertices of the pointsphere mesh to world space * @returns [THREE.Vector3] array */ toWorldVertices() { const positionAttribute = this.pointSphere.geometry.getAttribute("position"); const rotatedVerticies = []; for (let i = 0; i < positionAttribute.count; i++) { const vertex = new THREE__namespace.Vector3(); vertex.fromBufferAttribute(positionAttribute, i); this.pointSphere.localToWorld(vertex); rotatedVerticies.push(vertex); } return rotatedVerticies; } /** * Converts a THREE.Vector3 array to a Float32Array * @param {*} arr * @returns */ toFloatArr(arr) { const floatArr = new Float32Array(arr.length * 3); arr.forEach((vec, i) => { const i3 = i * 3; floatArr[i3] = vec.x; floatArr[i3 + 1] = vec.y; floatArr[i3 + 2] = vec.z; }); return floatArr; } /** * Converts a Float32Array to a THREE.Vector3 array * @param {*} arr * @returns */ toVec3Arr(arr) { const vec3Arr = []; for (let i = 0; i < arr.length / 3; i++) { const i3 = i * 3; const vec = new THREE__namespace.Vector3( arr[i3], //x arr[i3 + 1], //y arr[i3 + 2] //z ); vec3Arr.push(vec); } return vec3Arr; } //#endregion } class RayController { constructor(environmentOctree) { __privateAdd(this, _RayController_instances); this.environmentOctree = environmentOctree; this.raySphere = new RaySphere(); this.stagger = { count: 0 }; } /** * Performs the raycast check on surrounding environment objects. * these checks are staggered to improve performance. * the stagger parameter partitions the array into sections, only one section is checked per function call * * @param {*} boidPoistions array of three.js meshes * @param {*} stagger the amount of partitions for the boidPositions. Keep to factor of the length of the array, no greater than length/2 * @returns */ update(boidPoistions, boidBoundingBox, stagger) { this.stagger.count++; if (this.stagger.count == 1e4) { this.stagger.count = 0; } const window = boidPoistions.length / stagger; const shift = this.stagger.count % stagger; const iStart = window * shift; const iEnd = window * (shift + 1); if (iStart % 1 != 0 || iEnd % 1 != 0) { throw "Boid Array length is not divisible by stagger"; } if (this.debug && this.debug.showRays) { __privateMethod(this, _RayController_instances, debugUpdate_fn).call(this, boidPoistions[0]); } return __privateMethod(this, _RayController_instances, checkEnviroment_fn).call(this, boidPoistions, boidBoundingBox, iStart, iEnd); } /** * sets up debug panel for raycasting. * Includes: * - Show/Hide ray targets * - Tweak ray count * - Tweak Ray angle * - Tweak Ray Distance * * @param {*} gui lil-gui instance * @param {*} scene three.js scene */ setDebug(gui, scene, mainBoid) { this.debug = {}; const folder = gui.addFolder("Environment Vision"); __privateMethod(this, _RayController_instances, debugRays_fn).call(this, folder, scene, mainBoid); __privateMethod(this, _RayController_instances, debugTweakRays_fn).call(this, folder, scene, mainBoid); } } _RayController_instances = new WeakSet(); // NOTE: it may be better to use the standard boid position array,instead of the vec3 arr /** * checks the environment to see if there are world objects within the boids vision * * @param {[THREE.Vector3]} boidPositions * @returns {foundIntersections{boidindex,{distance,position}}} found intersections */ checkEnviroment_fn = function(boidPositions, boidBoundingBox, iStart, iEnd) { if (iStart == null || iEnd == null) { iStart = 0; iEnd = boidPositions.length; } const foundIntersections = {}; for (let i = iStart; i < iEnd; i++) { let enviromentObjects = []; if (boidBoundingBox) { const size = boidBoundingBox.max.clone(); size.sub(boidBoundingBox.min); const boundingBox = new THREE__namespace.Box3().setFromCenterAndSize(boidPositions[i].position, size); enviromentObjects = this.environmentOctree.getObjects(boundingBox); } let environmentIntersections; if (enviromentObjects.length > 0) { const targets = this.raySphere.rotateTo(boidPositions[i]); environmentIntersections = this.raySphere.castRays(targets, boidPositions[i].position, enviromentObjects); } if (environmentIntersections) { foundIntersections[i] = environmentIntersections; } } return foundIntersections; }; debugUpdate_fn = function(boid) { this.debug.pointSphere.position.copy(boid.position); this.debug.pointSphere.quaternion.setFromRotationMatrix(boid.rotationMatrix); }; debugRays_fn = function(folder, scene, mainBoid) { this.debug.showRays = false; folder.add(this.debug, "showRays").onChange((bool) => { if (bool) { __privateMethod(this, _RayController_instances, debugSetPointSphere_fn).call(this, scene, mainBoid); } else { __privateMethod(this, _RayController_instances, debugRemovePointSphere_fn).call(this, scene); } }); }; debugSetPointSphere_fn = function(scene, mainBoid) { this.debug.pointSphere = this.raySphere.getPointSphere(); const scale = new THREE__namespace.Vector3(1, 1, 1); scale.multiplyScalar(this.raySphere.rayFar); this.debug.pointSphere.scale.copy(scale); __privateMethod(this, _RayController_instances, debugUpdate_fn).call(this, mainBoid); scene.add(this.debug.pointSphere); }; debugRemovePointSphere_fn = function(scene) { scene.remove(this.debug.pointSphere); this.debug.pointSphere.material.dispose(); this.debug.pointSphere.geometry.dispose(); }; debugTweakRays_fn = function(folder, scene, mainBoid) { folder.add(this.raySphere, "rayCount").min(1).max(500).step(1).name("Ray Count").onChange((num) => { this.raySphere.updatePointSphere(); if (this.debug.showRays) { __privateMethod(this, _RayController_instances, debugRemovePointSphere_fn).call(this, scene); __privateMethod(this, _RayController_instances, debugSetPointSphere_fn).call(this, scene, mainBoid); } }); folder.add(this.raySphere, "rayAngleLimit").min(-1).max(1).step(1e-3).name("Ray Angle").onChange(() => { this.raySphere.updatePointSphere(); if (this.debug.showRays) { __privateMethod(this, _RayController_instances, debugRemovePointSphere_fn).call(this, scene); __privateMethod(this, _RayController_instances, debugSetPointSphere_fn).call(this, scene, mainBoid); } }); folder.add(this.raySphere, "rayFar").min(0).max(1).step(1e-3).name("Ray Distance").onChange(() => { this.raySphere.updatePointSphere(); if (this.debug.showRays) { __privateMethod(this, _RayController_instances, debugRemovePointSphere_fn).call(this, scene); __privateMethod(this, _RayController_instances, debugSetPointSphere_fn).call(this, scene, mainBoid); } }); }; class OctreeNode { /** * * @param {*} box * @param {*} minNodeSize minimum possible size per node * @param {*} depth depth of current node */ constructor(box, minNodeSize, depth) { this.depth = depth || 1; this.nodeBounds = box; this.minSize = minNodeSize; this.worldObjects = []; this.containsObject = false; this.nodeSize = new THREE__namespace.Vector3(); this.nodeBounds.getSize(this.nodeSize); this.childBounds = []; this.setUpChildNodes(); this.children = null; } /** * sets the child bounding boxes up. * finds the center of the space in each division, uses that and the size to create a new box */ setUpChildNodes() { const quarter = this.nodeSize.y / 4; const childLength = this.nodeSize.y / 2; const childSize = new THREE__namespace.Vector3(childLength, childLength, childLength); const center = new THREE__namespace.Vector3(); this.nodeBounds.getCenter(center); this.childBounds[0] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(-quarter, quarter, -quarter)), childSize ); this.childBounds[1] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(quarter, quarter, -quarter)), childSize ); this.childBounds[2] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(-quarter, quarter, quarter)), childSize ); this.childBounds[3] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(quarter, quarter, quarter)), childSize ); this.childBounds[4] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(-quarter, -quarter, -quarter)), childSize ); this.childBounds[5] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(quarter, -quarter, -quarter)), childSize ); this.childBounds[6] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(-quarter, -quarter, quarter)), childSize ); this.childBounds[7] = new THREE__namespace.Box3().setFromCenterAndSize( center.clone().add(new THREE__namespace.Vector3(quarter, -quarter, quarter)), childSize ); } /** * entry point to recursive tree build * * @param {*} worldObj */ addObject(worldObj) { this.divideAndAdd(worldObj); } /** * Recursively builds the tree object * * @param {*} worldObj * @returns */ divideAndAdd(worldObj) { if (this.nodeSize.y <= this.minSize) { this.worldObjects.push(worldObj); this.containsObject = true; return; } if (this.children == null) { this.children = []; } let dividing = false; for (let i = 0; i < 8; i++) { if (this.children[i] == null) { this.children[i] = new OctreeNode(this.childBounds[i], this.minSize, this.depth + 1); } if (this.children[i].nodeBounds.intersectsBox(new THREE__namespace.Box3().setFromObject(worldObj))) { dividing = true; this.children[i].divideAndAdd(worldObj); if (this.children[i].containsObject) { this.containsObject = true; } } } if (dividing == false) { this.containsObject = false; this.children = null; } } } class Octree { constructor(worldObjects, minNodeSize) { __privateAdd(this, _Octree_instances); const bounds = __privateMethod(this, _Octree_instances, setUpBounds_fn).call(this, worldObjects); this.rootNode = new OctreeNode(bounds, minNodeSize); __privateMethod(this, _Octree_instances, addObjects_fn).call(this, worldObjects); } /** * Recursively checks children, returns a list of THREE.JS Meshes that intersect with the provided object or box3 * * @param {*} mesh * @param {*} scene * @returns */ getObjects(obj, scene) { let box = obj; if (obj.isBox3 == null || obj.isBox3 == false) { box = new THREE__namespace.Box3().setFromObject(obj); } const boundingSphere = new THREE__namespace.Sphere(); box.getBoundingSphere(boundingSphere); const intersections = this.intersectsObject(this.rootNode, boundingSphere, scene); const uniqueIntersections = [...new Map(intersections.map((item) => [item.uuid, item])).values()]; return uniqueIntersections; } /** * Recursively checks children, returns the layers of the thee.js meshes * @param {*} mesh * @param {*} scene * @returns */ getLayers(mesh, scene) { const box = new THREE__namespace.Box3().setFromObject(mesh); const testLayers = this.intersectsLayers(this.rootNode, box, scene); const unique = [...new Map(testLayers.map((item) => [item.mask, item])).values()]; return unique; } /** * * Recursively checks children, returns true if the object is intersecting with any object * @param {*} mesh * @param {*} scene * @returns */ intersects(mesh, scene) { const box = new THREE__namespace.Box3().setFromObject(mesh); return this.isIntersecting(this.rootNode, box, scene); } /** * Recursively checks children, returns a list of THREE.JS Meshes that intersect with the provided box3 * @param {OctreeNode} node * @param {Box3} box * @param {Object3d} scene * @returns Three.Mesh[] */ intersectsObject(node, sphere) { const array = []; if (node.nodeBounds.intersectsSphere(sphere) && node.containsObject) { if (node.children == null) { return node.worldObjects; } for (let i = 0; i < 8; i++) { if (node.childBounds[i].intersectsSphere(sphere)) { const value = this.intersectsObject(node.children[i], sphere); if (value) { const unique = [...new Map(value.map((item) => [item.uuid, item])).values()]; array.push(...unique); } } } } return array; } /** * Recursively checks children, returns the layers of the thee.js meshes * @param {OctreeNode} node * @param {Box3} box * @param {Object3d} scene * @returns int[] */ intersectsLayers(node, box, scene) { const array = []; if (node.nodeBounds.intersectsBox(box) && node.containsObject) { if (node.children == null) { if (scene) { this.debugDraw(node.nodeBounds, "green", scene); this.debugDraw(box, "red", scene); } const temp = []; if (node.worldObjects.length > 0) { node.worldObjects.forEach((worldObj) => { temp.push(worldObj.layers); }); } return temp; } for (let i = 0; i < 8; i++) { if (node.childBounds[i].intersectsBox(box)) { const value = this.intersectsLayers(node.children[i], box, scene); if (value) { array.push(...value); } } } } return array; } /** * Recursively checks children, returns true if the object is intersecting with any object * * @param {OctreeNode} node * @param {Box3} box * @param {Object3d} scene * @returns Three.Mesh[] */ isIntersecting(node, box, scene) { let value = false; if (node.nodeBounds.intersectsBox(box) && node.containsObject) { if (node.children == null) { if (scene) { this.debugDraw(node.nodeBounds, "green", scene); this.debugDraw(box, "red", scene); } return true; } for (let i = 0; i < 8; i++) { if (node.childBounds[i].intersectsBox(box)) { const holder = this.isIntersecting(node.children[i], box, scene); if (holder) { value = true; } } } } return value; } /** * Adds the current octree visualization to the scene * * @param {*} scene */ showOctree(scene) { __privateMethod(this, _Octree_instances, drawBox_fn).call(this, this.rootNode, scene); } /** * Removes the octree visualization from the scene * * @param {*} scene */ hideOctree(scene) { __privateMethod(this, _Octree_instances, removeBox_fn).call(this, this.rootNode, scene); } debug(gui, scene) { const folder = gui.addFolder().title("Enviroment Optimizations"); const debug = {}; debug.showOctree = false; console.log(this); folder.add(debug, "showOctree").name("View Octree Structure").onChange((bool) => { if (bool) { this.showOctree(scene); } else { this.hideOctree(scene); } }); } } _Octree_instances = new WeakSet(); /**Grows a bounding box to cover all the objects in the array * * @param {*} worldObjects * @returns Box3 */ setUpBounds_fn = function(worldObjects) { const bounds = new THREE__namespace.Box3(); worldObjects.forEach((mesh) => { bounds.expandByObject(mesh); }); const size = new THREE__namespace.Vector3(); bounds.getSize(size); const maxSize = Math.max(...size); const sizeVector = new THREE__namespace.Vector3(maxSize, maxSize, maxSize); sizeVector.multiplyScalar(0.5); const boundsCenter = new THREE__namespace.Vector3(); bounds.getCenter(boundsCenter); bounds.set(boundsCenter.clone().sub(sizeVector), boundsCenter.add(sizeVector)); return bounds; }; /** * * @param {*} worldObjects */ addObjects_fn = function(worldObjects) { worldObjects.forEach((obj) => { this.rootNode.addObject(obj); }); }; //debug /** * Recursively adds box visualizations of the octree to the scene * * @param {*} node * @param {*} scene * @param {*} count * @returns */ drawBox_fn = function(node, scene, count) { count = count != null ? count : 1; if (node.children == null) { const center = new THREE__namespace.Vector3(); const scale = new THREE__namespace.Vector3(); node.nodeBounds.getCenter(center); node.nodeBounds.getSize(scale); scale.multiplyScalar(0.999); const box = new THREE__namespace.Box3().setFromCenterAndSize(center, scale); let color; switch (count) { case 1: color = "#ffffff"; break; case 2: color = "#c8c2ff"; break; case 3: color = "#7363ff"; break; case 4: color = "#1a00ff"; break; case 5: color = "#ff00ec"; break; case 6: color = "#ff004b"; break; case 7: color = "#ff0000"; break; case 8: color = "#ffd000"; break; case 9: color = "#a4ff00"; break; default: color = "#00ff87"; } const helper = new THREE__namespace.Box3Helper(box, color); node.boxHelper = helper; scene.add(helper); return; } node.children.forEach( (child) => { __privateMethod(this, _Octree_instances, drawBox_fn).call(this, child, scene, count + 1); } ); }; /** * Recursively removes box visualization from the scene * * @param {*} node * @param {*} scene * @returns */ removeBox_fn = function(node, scene) { if (node.children == null) { if (node.boxHelper) { scene.remove(node.boxHelper); node.boxHelper.dispose(); node.boxHelper = null; } return; } node.children.forEach( (child) => { __privateMethod(this, _Octree_instances, removeBox_fn).call(this, child,