@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
232 lines (191 loc) • 5.89 kB
JavaScript
import { assert } from "../assert.js";
import { combine_hash } from "../collection/array/combine_hash.js";
import { EPSILON } from "../math/EPSILON.js";
import { epsilonEquals } from "../math/epsilonEquals.js";
import { PI2 } from "../math/PI2.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";
import Vector3 from "./Vector3.js";
/**
* Represents a direction vector+angle
* Mainly used inside particle systems for pick a motion direction at spawn
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class ConicRay {
/**
* Must be normalized
* @readonly
* @type {Vector3}
*/
direction = new Vector3(0, 1, 0);
/**
* Half-angle of the cone that is the angle between the center axis and the edge of the cone
*
* @type {number} In radians
*/
angle = 0;
toJSON() {
return {
direction: this.direction.toJSON(),
angle: this.angle
};
}
fromJSON(json) {
this.direction.fromJSON(json.direction);
this.angle = json.angle;
}
/**
*
* @param {BinaryBuffer} buffer
*/
toBinaryBuffer(buffer) {
buffer.writeFloat64(this.angle);
this.direction.toBinaryBufferFloat32(buffer);
}
/**
*
* @param {BinaryBuffer} buffer
*/
fromBinaryBuffer(buffer) {
this.angle = buffer.readFloat64();
this.direction.fromBinaryBufferFloat32(buffer);
//normalize direction
this.direction.normalize();
}
/**
*
* @param {ConicRay} other
* @returns {boolean}
*/
equals(other) {
return this.angle === other.angle
&& this.direction.equals(other.direction)
;
}
/**
*
* @param {ConicRay} other
* @param {number} [tolerance]
* @returns {boolean}
*/
roughlyEquals(other, tolerance = EPSILON) {
return epsilonEquals(this.angle, other.angle, tolerance)
&& this.direction.roughlyEquals(other.direction, tolerance);
}
/**
*
* @return {number}
*/
hash() {
return combine_hash(
computeHashFloat(this.angle),
this.direction.hash()
);
}
/**
*
* @param {ConicRay} other
*/
copy(other) {
this.direction.copy(other.direction);
this.angle = other.angle;
}
/**
* Includes boundary matches
* @param {Vector3} v3
* @returns {boolean}
*/
containsDirectionVector(v3) {
const angular_distance = this.direction.angleTo(v3);
return angular_distance <= this.angle;
}
/**
* NOTE: Heavily based on a stackoverflow answer
* @see https://stackoverflow.com/questions/38997302/create-random-unit-vector-inside-a-defined-conical-region/39003745#39003745
* @param {function} random
* @param {Vector3} result
*/
sampleRandomDirection(random, result) {
const coneAngle = this.angle;
if (coneAngle === 0) {
//fast path, just copy ray direction
result.copy(this.direction);
return;
}
// Generate points on the spherical cap around the north pole [1].
// [1] See https://math.stackexchange.com/a/205589/81266
const angleCosine = Math.cos(coneAngle);
const z = random() * (1 - angleCosine) + angleCosine;
const phi = random() * PI2;
const zSqr = z * z;
const zSqrtNeg = Math.sqrt(1 - zSqr);
const x = zSqrtNeg * Math.cos(phi);
const y = zSqrtNeg * Math.sin(phi);
// If the spherical cap is centered around the north pole, we're done.
const direction = this.direction;
if (direction._equals(0, 0, 1)) {
result.set(x, y, z);
return;
}
assert.ok(direction.isNormalized(), 'direction vector must be normalized');
// Find the rotation axis `u` and rotation angle `rot` [1]
const dX = direction.x;
const dY = direction.y;
const dZ = direction.z;
//compute u
//compute rotation angle
const rot = Math.acos(dZ);
// Convert rotation axis and angle to 3x3 rotation matrix [2]
// [2] See https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle
//write matrix
const c = dZ;
const s = Math.sin(rot);
const t = 1 - c;
const tx = -t * dY;
const ty = t * dX;
//build relevant terms of the matrix
const n11 = c - tx * dY;
const n12 = tx * dX;
const n22 = ty * dX + c;
const n13 = s * dX;
const n32 = -s * dY;
// Rotate [x; y; z] from north pole to `coneDir`.
const _x = n11 * x + n12 * y + n13 * z;
const _y = n12 * x + n22 * y + -n32 * z;
const _z = -n13 * x + n32 * y + c * z;
result.set(_x, _y, _z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} half_angle
* @returns {ConicRay}
*/
static fromScalars(
x,
y,
z,
half_angle
) {
assert.isNumber(half_angle, 'angle');
assert.greaterThanOrEqual(half_angle, 0, 'half_angle must be non-negative');
const r = new ConicRay();
r.angle = half_angle;
r.direction.set(x, y, z);
r.direction.normalize();
return r;
}
}
/**
* @readonly
* @type {boolean}
*/
ConicRay.prototype.isConicRay = true;
/**
* @readonly
* @type {string}
*/
ConicRay.typeName = "ConicRay";