three-boids
Version:
Javascript Boid Simulation library
1,344 lines (1,342 loc) • 55 kB
JavaScript
(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,