UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

232 lines (191 loc) 5.89 kB
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";