@thi.ng/boids
Version:
n-dimensional boids simulation with modular behavior system
125 lines (124 loc) • 3.5 kB
JavaScript
import { identity } from "@thi.ng/api";
import { DIST_SQ2, DIST_SQ3 } from "@thi.ng/distance/squared";
import { VectorState, defVector } from "@thi.ng/timestep/state";
import { integrateAll, interpolateAll } from "@thi.ng/timestep/timestep";
import { VEC2 } from "@thi.ng/vectors/vec2-api";
import { VEC3 } from "@thi.ng/vectors/vec3-api";
import { blendedBehaviorUpdate } from "./behaviors/update.js";
import { Radial } from "./region.js";
class Boid {
pos;
vel;
api;
accel;
behaviors;
region;
opts;
/**
* Pre-allocated vector for force accumulation, used by/for behavior
* updates.
*/
force;
constructor(opts, api, distance, pos, vel) {
this.api = api;
this.opts = { maxForce: 1, ...opts };
this.accel = this.opts.accel;
this.behaviors = this.opts.behaviors;
const update = this.opts.update || blendedBehaviorUpdate;
const constrain = this.opts.constrain || identity;
const { add, limit, maddN } = api;
this.vel = defVector(
api,
vel,
(vel2) => limit(vel2, add(vel2, vel2, update(this)), this.opts.maxSpeed)
);
this.pos = defVector(
api,
pos,
(pos2, dt) => constrain(maddN(pos2, this.vel.curr, dt, pos2), this)
);
this.region = new Radial(distance, pos, 1);
this.force = api.zeroes();
}
/**
* Integration step of the thi.ng/timestep update cycle. See
* [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html)
*
* @param dt
* @param ctx
*/
integrate(dt, ctx) {
integrateAll(dt, ctx, this.vel, this.pos);
}
/**
* Interplation step of the thi.ng/timestep update cycle. See
* [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html)
*
* @param alpha
* @param ctx
*/
interpolate(alpha, ctx) {
interpolateAll(alpha, ctx, this.vel, this.pos);
}
/**
* Queries the spatial index for other boids in the current region, or if
* `pos` is given also moves the search region to new position before
* querying.
*
* @remarks
* IMPORTANT: The returned array will always contain the current boid itself
* too. Filtering has been left out here for performance reasons and is left
* to downstream code.
*
* @param r
* @param pos
*/
neighbors(r, pos) {
const region = this.region;
if (pos) region.target = pos;
region.setRadius(r);
return this.accel.queryNeighborhood(region).deref();
}
steerTowards(target, out = target) {
return this.limitForce(this.api.sub(out, target, this.pos.curr));
}
/**
* Mutably divides given `force` by `num` (if > 0) and limits result via
* {@link Boid.limitForce}.
*
* @param force
* @param num
*/
averageForce(force, num) {
return this.limitForce(
num > 0 ? this.api.mulN(force, force, 1 / num) : force
);
}
/**
* If force > 0, computes: `limit(normalize(force, maxSpeed) - vel, maxForce)`.
* Otherwise, returns input as is.
*
* @param force
*/
limitForce(force) {
const { limit, magSq, msubN } = this.api;
const m = magSq(force);
return m > 0 ? limit(
force,
msubN(
force,
force,
this.opts.maxSpeed / Math.sqrt(m),
this.vel.curr
),
this.opts.maxForce
) : force;
}
}
const defBoid2 = (pos, vel, opts) => new Boid(opts, VEC2, DIST_SQ2, pos, vel);
const defBoid3 = (pos, vel, opts) => new Boid(opts, VEC3, DIST_SQ3, pos, vel);
export {
Boid,
defBoid2,
defBoid3
};