@c-frame/aframe-physics-system
Version:
Physics system for A-Frame VR, built on Cannon.js & Ammo.js
1,783 lines (1,583 loc) • 557 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
var CANNON = require('cannon-es');
require('./src/components/math');
require('./src/components/body/ammo-body');
require('./src/components/body/body');
require('./src/components/body/dynamic-body');
require('./src/components/body/static-body');
require('./src/components/shape/shape');
require('./src/components/shape/ammo-shape')
require('./src/components/ammo-constraint');
require('./src/components/constraint');
require('./src/components/spring');
require('./src/system');
module.exports = {
registerAll: function () {
console.warn('registerAll() is deprecated. Components are automatically registered.');
}
};
// Export CANNON.js.
window.CANNON = window.CANNON || CANNON;
},{"./src/components/ammo-constraint":9,"./src/components/body/ammo-body":10,"./src/components/body/body":11,"./src/components/body/dynamic-body":12,"./src/components/body/static-body":13,"./src/components/constraint":14,"./src/components/math":15,"./src/components/shape/ammo-shape":17,"./src/components/shape/shape":18,"./src/components/spring":19,"./src/system":29,"cannon-es":5}],2:[function(require,module,exports){
/**
* CANNON.shape2mesh
*
* Source: https://schteppe.github.io/cannon.js/build/cannon.demo.js
* Author: @schteppe
*/
var CANNON = require('cannon-es');
CANNON.shape2mesh = function(body){
var obj = new THREE.Object3D();
function createBufferGeometry(positions, faces) {
var geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
geometry.setIndex(faces);
geometry.computeBoundingSphere();
return geometry;
}
for (var l = 0; l < body.shapes.length; l++) {
var shape = body.shapes[l];
var mesh;
switch(shape.type){
case CANNON.Shape.types.SPHERE:
var sphere_geometry = new THREE.SphereGeometry( shape.radius, 8, 8);
mesh = new THREE.Mesh( sphere_geometry, this.currentMaterial );
break;
case CANNON.Shape.types.PARTICLE:
mesh = new THREE.Mesh( this.particleGeo, this.particleMaterial );
var s = this.settings;
mesh.scale.set(s.particleSize,s.particleSize,s.particleSize);
break;
case CANNON.Shape.types.PLANE:
var geometry = new THREE.PlaneGeometry(10, 10, 4, 4);
mesh = new THREE.Object3D();
var submesh = new THREE.Object3D();
var ground = new THREE.Mesh( geometry, this.currentMaterial );
ground.scale.set(100, 100, 100);
submesh.add(ground);
ground.castShadow = true;
ground.receiveShadow = true;
mesh.add(submesh);
break;
case CANNON.Shape.types.BOX:
var box_geometry = new THREE.BoxGeometry( shape.halfExtents.x*2,
shape.halfExtents.y*2,
shape.halfExtents.z*2 );
mesh = new THREE.Mesh( box_geometry, this.currentMaterial );
break;
case CANNON.Shape.types.CONVEXPOLYHEDRON:
// Add vertices
var positions = []
for (var i = 0; i < shape.vertices.length; i++) {
var v = shape.vertices[i];
positions.push(v.x, v.y, v.z);
}
var faces = []
for(var i=0; i < shape.faces.length; i++){
var face = shape.faces[i];
// add triangles
var a = face[0];
for (var j = 1; j < face.length - 1; j++) {
var b = face[j];
var c = face[j + 1];
faces.push(a, b, c);
}
}
var geo = createBufferGeometry(positions, faces);
mesh = new THREE.Mesh( geo, this.currentMaterial );
break;
case CANNON.Shape.types.HEIGHTFIELD:
var v0 = new CANNON.Vec3();
var v1 = new CANNON.Vec3();
var v2 = new CANNON.Vec3();
var positions = [];
var faces = [];
for (var xi = 0; xi < shape.data.length - 1; xi++) {
for (var yi = 0; yi < shape.data[xi].length - 1; yi++) {
for (var k = 0; k < 2; k++) {
shape.getConvexTrianglePillar(xi, yi, k===0);
v0.copy(shape.pillarConvex.vertices[0]);
v1.copy(shape.pillarConvex.vertices[1]);
v2.copy(shape.pillarConvex.vertices[2]);
v0.vadd(shape.pillarOffset, v0);
v1.vadd(shape.pillarOffset, v1);
v2.vadd(shape.pillarOffset, v2);
positions.push(
v0.x, v0.y, v0.z,
v1.x, v1.y, v1.z,
v2.x, v2.y, v2.z
);
var i = positions.length / 3 - 3;
faces.push(i, i+1, i+2);
}
}
}
var geometry = createBufferGeometry(positions, faces);
mesh = new THREE.Mesh(geometry, this.currentMaterial);
break;
case CANNON.Shape.types.TRIMESH:
var geometry = new THREE.BufferGeometry();
var v0 = new CANNON.Vec3();
var v1 = new CANNON.Vec3();
var v2 = new CANNON.Vec3();
var positions = [];
var faces = [];
for (var i = 0; i < shape.indices.length / 3; i++) {
shape.getTriangleVertices(i, v0, v1, v2);
positions.push(
v0.x, v0.y, v0.z,
v1.x, v1.y, v1.z,
v2.x, v2.y, v2.z
);
var j = positions.length / 3 - 3;
faces.push(j, j+1, j+2);
}
var geometry = createBufferGeometry(positions, faces);
mesh = new THREE.Mesh(geometry, this.currentMaterial);
break;
default:
throw "Visual type not recognized: "+shape.type;
}
mesh.receiveShadow = true;
mesh.castShadow = true;
if(mesh.children){
for(var i=0; i<mesh.children.length; i++){
mesh.children[i].castShadow = true;
mesh.children[i].receiveShadow = true;
if(mesh.children[i]){
for(var j=0; j<mesh.children[i].length; j++){
mesh.children[i].children[j].castShadow = true;
mesh.children[i].children[j].receiveShadow = true;
}
}
}
}
var o = body.shapeOffsets[l];
var q = body.shapeOrientations[l];
mesh.position.set(o.x, o.y, o.z);
mesh.quaternion.set(q.x, q.y, q.z, q.w);
obj.add(mesh);
}
return obj;
};
module.exports = CANNON.shape2mesh;
},{"cannon-es":5}],3:[function(require,module,exports){
AFRAME.registerComponent('stats-panel', {
schema: {
merge: {type: 'boolean', default: true}
},
init() {
const container = document.querySelector('.rs-container')
if (container && this.data.merge) {
//stats panel exists, just merge into it.
this.container = container
return;
}
// if stats panel doesn't exist, add one to support our custom stats.
this.base = document.createElement('div')
this.base.classList.add('rs-base')
const body = document.body || document.getElementsByTagName('body')[0]
if (container && !this.data.merge) {
this.base.style.top = "auto"
this.base.style.bottom = "20px"
}
body.appendChild(this.base)
this.container = document.createElement('div')
this.container.classList.add('rs-container')
this.base.appendChild(this.container)
}
});
AFRAME.registerComponent('stats-group', {
multiple: true,
schema: {
label: {type: 'string'}
},
init() {
let container
const baseComponent = this.el.components['stats-panel']
if (baseComponent) {
container = baseComponent.container
}
else {
container = document.querySelector('.rs-container')
}
if (!container) {
console.warn(`Couldn't find stats container to add stats to.
Add either stats or stats-panel component to a-scene`)
return;
}
this.groupHeader = document.createElement('h1')
this.groupHeader.innerHTML = this.data.label
container.appendChild(this.groupHeader)
this.group = document.createElement('div')
this.group.classList.add('rs-group')
// rs-group hs style flex-direction of 'column-reverse'
// No idea why it's like that, but it's not what we want for our stats.
// We prefer them rendered in the order speified.
// So override this style.
this.group.style.flexDirection = 'column'
this.group.style.webKitFlexDirection = 'column'
container.appendChild(this.group)
}
});
AFRAME.registerComponent('stats-row', {
multiple: true,
schema: {
// name of the group to add the stats row to.
group: {type: 'string'},
// name of an event to listen for
event: {type: 'string'},
// property from event to output in stats panel
properties: {type: 'array'},
// label for the row in the stats panel
label: {type: 'string'}
},
init () {
const groupComponentName = "stats-group__" + this.data.group
const groupComponent = this.el.components[groupComponentName] ||
this.el.sceneEl.components[groupComponentName] ||
this.el.components["stats-group"] ||
this.el.sceneEl.components["stats-group"]
if (!groupComponent) {
console.warn(`Couldn't find stats group ${groupComponentName}`)
return;
}
this.counter = document.createElement('div')
this.counter.classList.add('rs-counter-base')
groupComponent.group.appendChild(this.counter)
this.counterId = document.createElement('div')
this.counterId.classList.add('rs-counter-id')
this.counterId.innerHTML = this.data.label
this.counter.appendChild(this.counterId)
this.counterValues = {}
this.data.properties.forEach((property) => {
const counterValue = document.createElement('div')
counterValue.classList.add('rs-counter-value')
counterValue.innerHTML = "..."
this.counter.appendChild(counterValue)
this.counterValues[property] = counterValue
})
this.updateStatsData = this.updateStatsData.bind(this)
this.el.addEventListener(this.data.event, this.updateStatsData)
this.splitCache = {}
},
updateStatsData(e) {
if (!this.data.properties) return
this.data.properties.forEach((property) => {
const split = this.splitDot(property);
let value = e.detail;
for (i = 0; i < split.length; i++) {
value = value[split[i]];
}
this.counterValues[property].innerHTML = value
})
},
splitDot (path) {
if (path in this.splitCache) { return this.splitCache[path]; }
this.splitCache[path] = path.split('.');
return this.splitCache[path];
}
});
AFRAME.registerComponent('stats-collector', {
multiple: true,
schema: {
// name of an event to listen for
inEvent: {type: 'string'},
// property from event to output in stats panel
properties: {type: 'array'},
// frequency of output in terms of events received.
outputFrequency: {type: 'number', default: 100},
// name of event to emit
outEvent: {type: 'string'},
// outputs (generated for each property)
// Combination of: mean, max, percentile__XX.X (where XX.X is a number)
outputs: {type: 'array'},
// Whether to output to console as well as generating events
// If a string is specified, this is output to console, together with the event data
// If no string is specified, nothing is output to console.
outputToConsole: {type: 'string'}
},
init() {
this.statsData = {}
this.resetData()
this.outputDetail = {}
this.data.properties.forEach((property) => {
this.outputDetail[property] = {}
})
this.statsReceived = this.statsReceived.bind(this)
this.el.addEventListener(this.data.inEvent, this.statsReceived)
},
resetData() {
this.counter = 0
this.data.properties.forEach((property) => {
// For calculating percentiles like 0.01 and 99.9% we'll want to store
// additional data - something like this...
// Store off outliers, and discard data.
// const min = Math.min(...this.statsData[property])
// this.lowOutliers[property].push(min)
// const max = Math.max(...this.statsData[property])
// this.highOutliers[property].push(max)
this.statsData[property] = []
})
},
statsReceived(e) {
this.updateStatsData(e.detail)
this.counter++
if (this.counter === this.data.outputFrequency) {
this.outputData()
this.resetData()
}
},
updateStatsData(detail) {
this.data.properties.forEach((property) => {
let value = detail;
value = value[property];
this.statsData[property].push(value)
})
},
outputData() {
this.data.properties.forEach((property) => {
this.data.outputs.forEach((output) => {
this.outputDetail[property][output] = this.computeOutput(output, this.statsData[property])
})
})
if (this.data.outEvent) {
this.el.emit(this.data.outEvent, this.outputDetail)
}
if (this.data.outputToConsole) {
console.log(this.data.outputToConsole, this.outputDetail)
}
},
computeOutput(outputInstruction, data) {
const outputInstructions = outputInstruction.split("__")
const outputType = outputInstructions[0]
let output
switch (outputType) {
case "mean":
output = data.reduce((a, b) => a + b, 0) / data.length;
break;
case "max":
output = Math.max(...data)
break;
case "min":
output = Math.min(...data)
break;
case "percentile":
const sorted = data.sort((a, b) => a - b)
// decimal percentiles encoded like 99+9 rather than 99.9 due to "." being used as a
// separator for nested properties.
const percentileString = outputInstructions[1].replace("_", ".")
const proportion = +percentileString / 100
// Note that this calculation of the percentile is inaccurate when there is insufficient data
// e.g. for 0.1th or 99.9th percentile when only 100 data points.
// Greater accuracy would require storing off more data (specifically outliers) and folding these
// into the computation.
const position = (data.length - 1) * proportion
const base = Math.floor(position)
const delta = position - base;
if (sorted[base + 1] !== undefined) {
output = sorted[base] + delta * (sorted[base + 1] - sorted[base]);
} else {
output = sorted[base];
}
break;
}
return output.toFixed(2)
}
});
},{}],4:[function(require,module,exports){
/* global Ammo,THREE */
THREE.AmmoDebugConstants = {
NoDebug: 0,
DrawWireframe: 1,
DrawAabb: 2,
DrawFeaturesText: 4,
DrawContactPoints: 8,
NoDeactivation: 16,
NoHelpText: 32,
DrawText: 64,
ProfileTimings: 128,
EnableSatComparison: 256,
DisableBulletLCP: 512,
EnableCCD: 1024,
DrawConstraints: 1 << 11, //2048
DrawConstraintLimits: 1 << 12, //4096
FastWireframe: 1 << 13, //8192
DrawNormals: 1 << 14, //16384
DrawOnTop: 1 << 15, //32768
MAX_DEBUG_DRAW_MODE: 0xffffffff
};
/**
* An implementation of the btIDebugDraw interface in Ammo.js, for debug rendering of Ammo shapes
* @class AmmoDebugDrawer
* @param {THREE.Scene} scene
* @param {Ammo.btCollisionWorld} world
* @param {object} [options]
*/
THREE.AmmoDebugDrawer = function(scene, world, options) {
this.scene = scene;
this.world = world;
options = options || {};
this.debugDrawMode = options.debugDrawMode || THREE.AmmoDebugConstants.DrawWireframe;
var drawOnTop = this.debugDrawMode & THREE.AmmoDebugConstants.DrawOnTop || false;
var maxBufferSize = options.maxBufferSize || 1000000;
this.geometry = new THREE.BufferGeometry();
var vertices = new Float32Array(maxBufferSize * 3);
var colors = new Float32Array(maxBufferSize * 3);
this.geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3).setUsage(THREE.DynamicDrawUsage));
this.geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage));
this.index = 0;
var material = new THREE.LineBasicMaterial({
vertexColors: true,
depthTest: !drawOnTop
});
this.mesh = new THREE.LineSegments(this.geometry, material);
if (drawOnTop) this.mesh.renderOrder = 999;
this.mesh.frustumCulled = false;
this.enabled = false;
this.debugDrawer = new Ammo.DebugDrawer();
this.debugDrawer.drawLine = this.drawLine.bind(this);
this.debugDrawer.drawContactPoint = this.drawContactPoint.bind(this);
this.debugDrawer.reportErrorWarning = this.reportErrorWarning.bind(this);
this.debugDrawer.draw3dText = this.draw3dText.bind(this);
this.debugDrawer.setDebugMode = this.setDebugMode.bind(this);
this.debugDrawer.getDebugMode = this.getDebugMode.bind(this);
this.debugDrawer.enable = this.enable.bind(this);
this.debugDrawer.disable = this.disable.bind(this);
this.debugDrawer.update = this.update.bind(this);
this.world.setDebugDrawer(this.debugDrawer);
};
THREE.AmmoDebugDrawer.prototype = function() {
return this.debugDrawer;
};
THREE.AmmoDebugDrawer.prototype.enable = function() {
this.enabled = true;
this.scene.add(this.mesh);
};
THREE.AmmoDebugDrawer.prototype.disable = function() {
this.enabled = false;
this.scene.remove(this.mesh);
};
THREE.AmmoDebugDrawer.prototype.update = function() {
if (!this.enabled) {
return;
}
if (this.index != 0) {
this.geometry.attributes.position.needsUpdate = true;
this.geometry.attributes.color.needsUpdate = true;
}
this.index = 0;
this.world.debugDrawWorld();
this.geometry.setDrawRange(0, this.index);
};
THREE.AmmoDebugDrawer.prototype.drawLine = function(from, to, color) {
const heap = Ammo.HEAPF32;
const r = heap[(color + 0) / 4];
const g = heap[(color + 4) / 4];
const b = heap[(color + 8) / 4];
const fromX = heap[(from + 0) / 4];
const fromY = heap[(from + 4) / 4];
const fromZ = heap[(from + 8) / 4];
this.geometry.attributes.position.setXYZ(this.index, fromX, fromY, fromZ);
this.geometry.attributes.color.setXYZ(this.index++, r, g, b);
const toX = heap[(to + 0) / 4];
const toY = heap[(to + 4) / 4];
const toZ = heap[(to + 8) / 4];
this.geometry.attributes.position.setXYZ(this.index, toX, toY, toZ);
this.geometry.attributes.color.setXYZ(this.index++, r, g, b);
};
//TODO: figure out how to make lifeTime work
THREE.AmmoDebugDrawer.prototype.drawContactPoint = function(pointOnB, normalOnB, distance, lifeTime, color) {
const heap = Ammo.HEAPF32;
const r = heap[(color + 0) / 4];
const g = heap[(color + 4) / 4];
const b = heap[(color + 8) / 4];
const x = heap[(pointOnB + 0) / 4];
const y = heap[(pointOnB + 4) / 4];
const z = heap[(pointOnB + 8) / 4];
this.geometry.attributes.position.setXYZ(this.index, x, y, z);
this.geometry.attributes.color.setXYZ(this.index++, r, g, b);
const dx = heap[(normalOnB + 0) / 4] * distance;
const dy = heap[(normalOnB + 4) / 4] * distance;
const dz = heap[(normalOnB + 8) / 4] * distance;
this.geometry.attributes.position.setXYZ(this.index, x + dx, y + dy, z + dz);
this.geometry.attributes.color.setXYZ(this.index++, r, g, b);
};
THREE.AmmoDebugDrawer.prototype.reportErrorWarning = function(warningString) {
if (Ammo.hasOwnProperty("Pointer_stringify")) {
console.warn(Ammo.Pointer_stringify(warningString));
} else if (!this.warnedOnce) {
this.warnedOnce = true;
console.warn("Cannot print warningString, please rebuild Ammo.js using 'debug' flag");
}
};
THREE.AmmoDebugDrawer.prototype.draw3dText = function(location, textString) {
//TODO
console.warn("TODO: draw3dText");
};
THREE.AmmoDebugDrawer.prototype.setDebugMode = function(debugMode) {
this.debugDrawMode = debugMode;
};
THREE.AmmoDebugDrawer.prototype.getDebugMode = function() {
return this.debugDrawMode;
};
},{}],5:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.World = exports.Vec3Pool = exports.Vec3 = exports.Trimesh = exports.Transform = exports.Spring = exports.SplitSolver = exports.Sphere = exports.Solver = exports.Shape = exports.SPHSystem = exports.SHAPE_TYPES = exports.SAPBroadphase = exports.RotationalMotorEquation = exports.RotationalEquation = exports.RigidVehicle = exports.RaycastVehicle = exports.RaycastResult = exports.Ray = exports.RAY_MODES = exports.Quaternion = exports.Pool = exports.PointToPointConstraint = exports.Plane = exports.Particle = exports.ObjectCollisionMatrix = exports.Narrowphase = exports.NaiveBroadphase = exports.Material = exports.Mat3 = exports.LockConstraint = exports.JacobianElement = exports.HingeConstraint = exports.Heightfield = exports.GridBroadphase = exports.GSSolver = exports.FrictionEquation = exports.EventTarget = exports.Equation = exports.DistanceConstraint = exports.Cylinder = exports.ConvexPolyhedron = exports.ContactMaterial = exports.ContactEquation = exports.Constraint = exports.ConeTwistConstraint = exports.COLLISION_TYPES = exports.Broadphase = exports.Box = exports.Body = exports.BODY_TYPES = exports.BODY_SLEEP_STATES = exports.ArrayCollisionMatrix = exports.AABB = void 0;
/**
* Records what objects are colliding with each other
* @class ObjectCollisionMatrix
* @constructor
*/
class ObjectCollisionMatrix {
// The matrix storage.
constructor() {
this.matrix = {};
}
/**
* @method get
* @param {Body} i
* @param {Body} j
* @return {boolean}
*/
get(bi, bj) {
let {
id: i
} = bi;
let {
id: j
} = bj;
if (j > i) {
const temp = j;
j = i;
i = temp;
}
return i + "-" + j in this.matrix;
}
/**
* @method set
* @param {Body} i
* @param {Body} j
* @param {boolean} value
*/
set(bi, bj, value) {
let {
id: i
} = bi;
let {
id: j
} = bj;
if (j > i) {
const temp = j;
j = i;
i = temp;
}
if (value) {
this.matrix[i + "-" + j] = true;
} else {
delete this.matrix[i + "-" + j];
}
}
/**
* Empty the matrix
* @method reset
*/
reset() {
this.matrix = {};
}
/**
* Set max number of objects
* @method setNumObjects
* @param {Number} n
*/
setNumObjects(n) {}
}
/**
* A 3x3 matrix.
* @class Mat3
* @constructor
* @param {Array} elements A vector of length 9, containing all matrix elements. Optional.
* @author schteppe / http://github.com/schteppe
*/
exports.ObjectCollisionMatrix = ObjectCollisionMatrix;
class Mat3 {
constructor(elements = [0, 0, 0, 0, 0, 0, 0, 0, 0]) {
this.elements = elements;
}
/**
* Sets the matrix to identity
* @method identity
* @todo Should perhaps be renamed to setIdentity() to be more clear.
* @todo Create another function that immediately creates an identity matrix eg. eye()
*/
identity() {
const e = this.elements;
e[0] = 1;
e[1] = 0;
e[2] = 0;
e[3] = 0;
e[4] = 1;
e[5] = 0;
e[6] = 0;
e[7] = 0;
e[8] = 1;
}
/**
* Set all elements to zero
* @method setZero
*/
setZero() {
const e = this.elements;
e[0] = 0;
e[1] = 0;
e[2] = 0;
e[3] = 0;
e[4] = 0;
e[5] = 0;
e[6] = 0;
e[7] = 0;
e[8] = 0;
}
/**
* Sets the matrix diagonal elements from a Vec3
* @method setTrace
* @param {Vec3} vec3
*/
setTrace(vector) {
const e = this.elements;
e[0] = vector.x;
e[4] = vector.y;
e[8] = vector.z;
}
/**
* Gets the matrix diagonal elements
* @method getTrace
* @return {Vec3}
*/
getTrace(target = new Vec3()) {
const e = this.elements;
target.x = e[0];
target.y = e[4];
target.z = e[8];
}
/**
* Matrix-Vector multiplication
* @method vmult
* @param {Vec3} v The vector to multiply with
* @param {Vec3} target Optional, target to save the result in.
*/
vmult(v, target = new Vec3()) {
const e = this.elements;
const x = v.x;
const y = v.y;
const z = v.z;
target.x = e[0] * x + e[1] * y + e[2] * z;
target.y = e[3] * x + e[4] * y + e[5] * z;
target.z = e[6] * x + e[7] * y + e[8] * z;
return target;
}
/**
* Matrix-scalar multiplication
* @method smult
* @param {Number} s
*/
smult(s) {
for (let i = 0; i < this.elements.length; i++) {
this.elements[i] *= s;
}
}
/**
* Matrix multiplication
* @method mmult
* @param {Mat3} matrix Matrix to multiply with from left side.
* @return {Mat3} The result.
*/
mmult(matrix, target = new Mat3()) {
const {
elements
} = matrix;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let sum = 0.0;
for (let k = 0; k < 3; k++) {
sum += elements[i + k * 3] * this.elements[k + j * 3];
}
target.elements[i + j * 3] = sum;
}
}
return target;
}
/**
* Scale each column of the matrix
* @method scale
* @param {Vec3} v
* @return {Mat3} The result.
*/
scale(vector, target = new Mat3()) {
const e = this.elements;
const t = target.elements;
for (let i = 0; i !== 3; i++) {
t[3 * i + 0] = vector.x * e[3 * i + 0];
t[3 * i + 1] = vector.y * e[3 * i + 1];
t[3 * i + 2] = vector.z * e[3 * i + 2];
}
return target;
}
/**
* Solve Ax=b
* @method solve
* @param {Vec3} b The right hand side
* @param {Vec3} target Optional. Target vector to save in.
* @return {Vec3} The solution x
* @todo should reuse arrays
*/
solve(b, target = new Vec3()) {
// Construct equations
const nr = 3; // num rows
const nc = 4; // num cols
const eqns = [];
let i;
let j;
for (i = 0; i < nr * nc; i++) {
eqns.push(0);
}
for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
eqns[i + nc * j] = this.elements[i + 3 * j];
}
}
eqns[3 + 4 * 0] = b.x;
eqns[3 + 4 * 1] = b.y;
eqns[3 + 4 * 2] = b.z; // Compute right upper triangular version of the matrix - Gauss elimination
let n = 3;
const k = n;
let np;
const kp = 4; // num rows
let p;
do {
i = k - n;
if (eqns[i + nc * i] === 0) {
// the pivot is null, swap lines
for (j = i + 1; j < k; j++) {
if (eqns[i + nc * j] !== 0) {
np = kp;
do {
// do ligne( i ) = ligne( i ) + ligne( k )
p = kp - np;
eqns[p + nc * i] += eqns[p + nc * j];
} while (--np);
break;
}
}
}
if (eqns[i + nc * i] !== 0) {
for (j = i + 1; j < k; j++) {
const multiplier = eqns[i + nc * j] / eqns[i + nc * i];
np = kp;
do {
// do ligne( k ) = ligne( k ) - multiplier * ligne( i )
p = kp - np;
eqns[p + nc * j] = p <= i ? 0 : eqns[p + nc * j] - eqns[p + nc * i] * multiplier;
} while (--np);
}
}
} while (--n); // Get the solution
target.z = eqns[2 * nc + 3] / eqns[2 * nc + 2];
target.y = (eqns[1 * nc + 3] - eqns[1 * nc + 2] * target.z) / eqns[1 * nc + 1];
target.x = (eqns[0 * nc + 3] - eqns[0 * nc + 2] * target.z - eqns[0 * nc + 1] * target.y) / eqns[0 * nc + 0];
if (isNaN(target.x) || isNaN(target.y) || isNaN(target.z) || target.x === Infinity || target.y === Infinity || target.z === Infinity) {
throw "Could not solve equation! Got x=[" + target.toString() + "], b=[" + b.toString() + "], A=[" + this.toString() + "]";
}
return target;
}
/**
* Get an element in the matrix by index. Index starts at 0, not 1!!!
* @method e
* @param {Number} row
* @param {Number} column
* @param {Number} value Optional. If provided, the matrix element will be set to this value.
* @return {Number}
*/
e(row, column, value) {
if (value === undefined) {
return this.elements[column + 3 * row];
} else {
// Set value
this.elements[column + 3 * row] = value;
}
}
/**
* Copy another matrix into this matrix object.
* @method copy
* @param {Mat3} source
* @return {Mat3} this
*/
copy(matrix) {
for (let i = 0; i < matrix.elements.length; i++) {
this.elements[i] = matrix.elements[i];
}
return this;
}
/**
* Returns a string representation of the matrix.
* @method toString
* @return string
*/
toString() {
let r = '';
const sep = ',';
for (let i = 0; i < 9; i++) {
r += this.elements[i] + sep;
}
return r;
}
/**
* reverse the matrix
* @method reverse
* @param {Mat3} target Optional. Target matrix to save in.
* @return {Mat3} The solution x
*/
reverse(target = new Mat3()) {
// Construct equations
const nr = 3; // num rows
const nc = 6; // num cols
const eqns = [];
let i;
let j;
for (i = 0; i < nr * nc; i++) {
eqns.push(0);
}
for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
eqns[i + nc * j] = this.elements[i + 3 * j];
}
}
eqns[3 + 6 * 0] = 1;
eqns[3 + 6 * 1] = 0;
eqns[3 + 6 * 2] = 0;
eqns[4 + 6 * 0] = 0;
eqns[4 + 6 * 1] = 1;
eqns[4 + 6 * 2] = 0;
eqns[5 + 6 * 0] = 0;
eqns[5 + 6 * 1] = 0;
eqns[5 + 6 * 2] = 1; // Compute right upper triangular version of the matrix - Gauss elimination
let n = 3;
const k = n;
let np;
const kp = nc; // num rows
let p;
do {
i = k - n;
if (eqns[i + nc * i] === 0) {
// the pivot is null, swap lines
for (j = i + 1; j < k; j++) {
if (eqns[i + nc * j] !== 0) {
np = kp;
do {
// do line( i ) = line( i ) + line( k )
p = kp - np;
eqns[p + nc * i] += eqns[p + nc * j];
} while (--np);
break;
}
}
}
if (eqns[i + nc * i] !== 0) {
for (j = i + 1; j < k; j++) {
const multiplier = eqns[i + nc * j] / eqns[i + nc * i];
np = kp;
do {
// do line( k ) = line( k ) - multiplier * line( i )
p = kp - np;
eqns[p + nc * j] = p <= i ? 0 : eqns[p + nc * j] - eqns[p + nc * i] * multiplier;
} while (--np);
}
}
} while (--n); // eliminate the upper left triangle of the matrix
i = 2;
do {
j = i - 1;
do {
const multiplier = eqns[i + nc * j] / eqns[i + nc * i];
np = nc;
do {
p = nc - np;
eqns[p + nc * j] = eqns[p + nc * j] - eqns[p + nc * i] * multiplier;
} while (--np);
} while (j--);
} while (--i); // operations on the diagonal
i = 2;
do {
const multiplier = 1 / eqns[i + nc * i];
np = nc;
do {
p = nc - np;
eqns[p + nc * i] = eqns[p + nc * i] * multiplier;
} while (--np);
} while (i--);
i = 2;
do {
j = 2;
do {
p = eqns[nr + j + nc * i];
if (isNaN(p) || p === Infinity) {
throw "Could not reverse! A=[" + this.toString() + "]";
}
target.e(i, j, p);
} while (j--);
} while (i--);
return target;
}
/**
* Set the matrix from a quaterion
* @method setRotationFromQuaternion
* @param {Quaternion} q
*/
setRotationFromQuaternion(q) {
const x = q.x;
const y = q.y;
const z = q.z;
const w = q.w;
const x2 = x + x;
const y2 = y + y;
const z2 = z + z;
const xx = x * x2;
const xy = x * y2;
const xz = x * z2;
const yy = y * y2;
const yz = y * z2;
const zz = z * z2;
const wx = w * x2;
const wy = w * y2;
const wz = w * z2;
const e = this.elements;
e[3 * 0 + 0] = 1 - (yy + zz);
e[3 * 0 + 1] = xy - wz;
e[3 * 0 + 2] = xz + wy;
e[3 * 1 + 0] = xy + wz;
e[3 * 1 + 1] = 1 - (xx + zz);
e[3 * 1 + 2] = yz - wx;
e[3 * 2 + 0] = xz - wy;
e[3 * 2 + 1] = yz + wx;
e[3 * 2 + 2] = 1 - (xx + yy);
return this;
}
/**
* Transpose the matrix
* @method transpose
* @param {Mat3} target Optional. Where to store the result.
* @return {Mat3} The target Mat3, or a new Mat3 if target was omitted.
*/
transpose(target = new Mat3()) {
const Mt = target.elements;
const M = this.elements;
for (let i = 0; i !== 3; i++) {
for (let j = 0; j !== 3; j++) {
Mt[3 * i + j] = M[3 * j + i];
}
}
return target;
}
}
/**
* 3-dimensional vector
* @class Vec3
* @constructor
* @param {Number} x
* @param {Number} y
* @param {Number} z
* @author schteppe
* @example
* const v = new Vec3(1, 2, 3);
* console.log('x=' + v.x); // x=1
*/
exports.Mat3 = Mat3;
class Vec3 {
constructor(x = 0.0, y = 0.0, z = 0.0) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Vector cross product
* @method cross
* @param {Vec3} v
* @param {Vec3} target Optional. Target to save in.
* @return {Vec3}
*/
cross(vector, target = new Vec3()) {
const vx = vector.x;
const vy = vector.y;
const vz = vector.z;
const x = this.x;
const y = this.y;
const z = this.z;
target.x = y * vz - z * vy;
target.y = z * vx - x * vz;
target.z = x * vy - y * vx;
return target;
}
/**
* Set the vectors' 3 elements
* @method set
* @param {Number} x
* @param {Number} y
* @param {Number} z
* @return Vec3
*/
set(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
/**
* Set all components of the vector to zero.
* @method setZero
*/
setZero() {
this.x = this.y = this.z = 0;
}
/**
* Vector addition
* @method vadd
* @param {Vec3} v
* @param {Vec3} target Optional.
* @return {Vec3}
*/
vadd(vector, target) {
if (target) {
target.x = vector.x + this.x;
target.y = vector.y + this.y;
target.z = vector.z + this.z;
} else {
return new Vec3(this.x + vector.x, this.y + vector.y, this.z + vector.z);
}
}
/**
* Vector subtraction
* @method vsub
* @param {Vec3} v
* @param {Vec3} target Optional. Target to save in.
* @return {Vec3}
*/
vsub(vector, target) {
if (target) {
target.x = this.x - vector.x;
target.y = this.y - vector.y;
target.z = this.z - vector.z;
} else {
return new Vec3(this.x - vector.x, this.y - vector.y, this.z - vector.z);
}
}
/**
* Get the cross product matrix a_cross from a vector, such that a x b = a_cross * b = c
* @method crossmat
* @see http://www8.cs.umu.se/kurser/TDBD24/VT06/lectures/Lecture6.pdf
* @return {Mat3}
*/
crossmat() {
return new Mat3([0, -this.z, this.y, this.z, 0, -this.x, -this.y, this.x, 0]);
}
/**
* Normalize the vector. Note that this changes the values in the vector.
* @method normalize
* @return {Number} Returns the norm of the vector
*/
normalize() {
const x = this.x;
const y = this.y;
const z = this.z;
const n = Math.sqrt(x * x + y * y + z * z);
if (n > 0.0) {
const invN = 1 / n;
this.x *= invN;
this.y *= invN;
this.z *= invN;
} else {
// Make something up
this.x = 0;
this.y = 0;
this.z = 0;
}
return n;
}
/**
* Get the version of this vector that is of length 1.
* @method unit
* @param {Vec3} target Optional target to save in
* @return {Vec3} Returns the unit vector
*/
unit(target = new Vec3()) {
const x = this.x;
const y = this.y;
const z = this.z;
let ninv = Math.sqrt(x * x + y * y + z * z);
if (ninv > 0.0) {
ninv = 1.0 / ninv;
target.x = x * ninv;
target.y = y * ninv;
target.z = z * ninv;
} else {
target.x = 1;
target.y = 0;
target.z = 0;
}
return target;
}
/**
* Get the length of the vector
* @method length
* @return {Number}
*/
length() {
const x = this.x;
const y = this.y;
const z = this.z;
return Math.sqrt(x * x + y * y + z * z);
}
/**
* Get the squared length of the vector.
* @method lengthSquared
* @return {Number}
*/
lengthSquared() {
return this.dot(this);
}
/**
* Get distance from this point to another point
* @method distanceTo
* @param {Vec3} p
* @return {Number}
*/
distanceTo(p) {
const x = this.x;
const y = this.y;
const z = this.z;
const px = p.x;
const py = p.y;
const pz = p.z;
return Math.sqrt((px - x) * (px - x) + (py - y) * (py - y) + (pz - z) * (pz - z));
}
/**
* Get squared distance from this point to another point
* @method distanceSquared
* @param {Vec3} p
* @return {Number}
*/
distanceSquared(p) {
const x = this.x;
const y = this.y;
const z = this.z;
const px = p.x;
const py = p.y;
const pz = p.z;
return (px - x) * (px - x) + (py - y) * (py - y) + (pz - z) * (pz - z);
}
/**
* Multiply all the components of the vector with a scalar.
* @method scale
* @param {Number} scalar
* @param {Vec3} target The vector to save the result in.
* @return {Vec3}
*/
scale(scalar, target = new Vec3()) {
const x = this.x;
const y = this.y;
const z = this.z;
target.x = scalar * x;
target.y = scalar * y;
target.z = scalar * z;
return target;
}
/**
* Multiply the vector with an other vector, component-wise.
* @method vmult
* @param {Number} vector
* @param {Vec3} target The vector to save the result in.
* @return {Vec3}
*/
vmul(vector, target = new Vec3()) {
target.x = vector.x * this.x;
target.y = vector.y * this.y;
target.z = vector.z * this.z;
return target;
}
/**
* Scale a vector and add it to this vector. Save the result in "target". (target = this + vector * scalar)
* @method addScaledVector
* @param {Number} scalar
* @param {Vec3} vector
* @param {Vec3} target The vector to save the result in.
* @return {Vec3}
*/
addScaledVector(scalar, vector, target = new Vec3()) {
target.x = this.x + scalar * vector.x;
target.y = this.y + scalar * vector.y;
target.z = this.z + scalar * vector.z;
return target;
}
/**
* Calculate dot product
* @method dot
* @param {Vec3} v
* @return {Number}
*/
dot(vector) {
return this.x * vector.x + this.y * vector.y + this.z * vector.z;
}
/**
* @method isZero
* @return bool
*/
isZero() {
return this.x === 0 && this.y === 0 && this.z === 0;
}
/**
* Make the vector point in the opposite direction.
* @method negate
* @param {Vec3} target Optional target to save in
* @return {Vec3}
*/
negate(target = new Vec3()) {
target.x = -this.x;
target.y = -this.y;
target.z = -this.z;
return target;
}
/**
* Compute two artificial tangents to the vector
* @method tangents
* @param {Vec3} t1 Vector object to save the first tangent in
* @param {Vec3} t2 Vector object to save the second tangent in
*/
tangents(t1, t2) {
const norm = this.length();
if (norm > 0.0) {
const n = Vec3_tangents_n;
const inorm = 1 / norm;
n.set(this.x * inorm, this.y * inorm, this.z * inorm);
const randVec = Vec3_tangents_randVec;
if (Math.abs(n.x) < 0.9) {
randVec.set(1, 0, 0);
n.cross(randVec, t1);
} else {
randVec.set(0, 1, 0);
n.cross(randVec, t1);
}
n.cross(t1, t2);
} else {
// The normal length is zero, make something up
t1.set(1, 0, 0);
t2.set(0, 1, 0);
}
}
/**
* Converts to a more readable format
* @method toString
* @return string
*/
toString() {
return this.x + "," + this.y + "," + this.z;
}
/**
* Converts to an array
* @method toArray
* @return Array
*/
toArray() {
return [this.x, this.y, this.z];
}
/**
* Copies value of source to this vector.
* @method copy
* @param {Vec3} source
* @return {Vec3} this
*/
copy(vector) {
this.x = vector.x;
this.y = vector.y;
this.z = vector.z;
return this;
}
/**
* Do a linear interpolation between two vectors
* @method lerp
* @param {Vec3} v
* @param {Number} t A number between 0 and 1. 0 will make this function return u, and 1 will make it return v. Numbers in between will generate a vector in between them.
* @param {Vec3} target
*/
lerp(vector, t, target) {
const x = this.x;
const y = this.y;
const z = this.z;
target.x = x + (vector.x - x) * t;
target.y = y + (vector.y - y) * t;
target.z = z + (vector.z - z) * t;
}
/**
* Check if a vector equals is almost equal to another one.
* @method almostEquals
* @param {Vec3} v
* @param {Number} precision
* @return bool
*/
almostEquals(vector, precision = 1e-6) {
if (Math.abs(this.x - vector.x) > precision || Math.abs(this.y - vector.y) > precision || Math.abs(this.z - vector.z) > precision) {
return false;
}
return true;
}
/**
* Check if a vector is almost zero
* @method almostZero
* @param {Number} precision
*/
almostZero(precision = 1e-6) {
if (Math.abs(this.x) > precision || Math.abs(this.y) > precision || Math.abs(this.z) > precision) {
return false;
}
return true;
}
/**
* Check if the vector is anti-parallel to another vector.
* @method isAntiparallelTo
* @param {Vec3} v
* @param {Number} precision Set to zero for exact comparisons
* @return {Boolean}
*/
isAntiparallelTo(vector, precision) {
this.negate(antip_neg);
return antip_neg.almostEquals(vector, precision);
}
/**
* Clone the vector
* @method clone
* @return {Vec3}
*/
clone() {
return new Vec3(this.x, this.y, this.z);
}
}
exports.Vec3 = Vec3;
Vec3.ZERO = new Vec3(0, 0, 0);
Vec3.UNIT_X = new Vec3(1, 0, 0);
Vec3.UNIT_Y = new Vec3(0, 1, 0);
Vec3.UNIT_Z = new Vec3(0, 0, 1);
/**
* Compute two artificial tangents to the vector
* @method tangents
* @param {Vec3} t1 Vector object to save the first tangent in
* @param {Vec3} t2 Vector object to save the second tangent in
*/
const Vec3_tangents_n = new Vec3();
const Vec3_tangents_randVec = new Vec3();
const antip_neg = new Vec3();
/**
* Axis aligned bounding box class.
* @class AABB
* @constructor
* @param {Object} [options]
* @param {Vec3} [options.upperBound] The upper bound of the bounding box.
* @param {Vec3} [options.lowerBound] The lower bound of the bounding box
*/
class AABB {
// The lower bound of the bounding box
// The upper bound of the bounding box
constructor(options = {}) {
this.lowerBound = new Vec3();
this.upperBound = new Vec3();
if (options.lowerBound) {
this.lowerBound.copy(options.lowerBound);
}
if (options.upperBound) {
this.upperBound.copy(options.upperBound);
}
}
/**
* Set the AABB bounds from a set of points.
* @method setFromPoints
* @param {Array} points An array of Vec3's.
* @param {Vec3} position Optional.
* @param {Quaternion} quaternion Optional.
* @param {number} skinSize Optional.
* @return {AABB} The self object
*/
setFromPoints(points, position, quaternion, skinSize) {
const l = this.lowerBound;
const u = this.upperBound;
const q = quaternion; // Set to the first point
l.copy(points[0]);
if (q) {
q.vmult(l, l);
}
u.copy(l);
for (let i = 1; i < points.length; i++) {
let p = points[i];
if (q) {
q.vmult(p, tmp);
p = tmp;
}
if (p.x > u.x) {
u.x = p.x;
}
if (p.x < l.x) {
l.x = p.x;
}
if (p.y > u.y) {
u.y = p.y;
}
if (p.y < l.y) {
l.y = p.y;
}
if (p.z > u.z) {
u.z = p.z;
}
if (p.z < l.z) {
l.z = p.z;
}
} // Add offset
if (position) {
position.vadd(l, l);
position.vadd(u, u);
}
if (skinSize) {
l.x -= skinSize;
l.y -= skinSize;
l.z -= skinSize;
u.x += skinSize;
u.y += skinSize;
u.z += skinSize;
}
return this;
}
/**
* Copy bounds from an AABB to this AABB
* @method copy
* @param {AABB} aabb Source to copy from
* @return {AABB} The this object, for chainability
*/
copy(aabb) {
this.lowerBound.copy(aabb.lowerBound);
this.upperBound.copy(aabb.upperBound);
return this;
}
/**
* Clone an AABB
* @method clone
*/
clone() {
return new AABB().copy(this);
}
/**
* Extend this AABB so that it covers the given AABB too.
* @method extend
* @param {AABB} aabb
*/
extend(aabb) {
this.lowerBound.x = Math.min(this.lowerBound.x, aabb.lowerBound.x);
this.upperBound.x = Math.max(this.upperBound.x, aabb.upperBound.x);
this.lowerBound.y = Math.min(this.lowerBound.y, aabb.lowerBound.y);
this.upperBound.y = Math.max(this.upperBound.y, aabb.upperBound.y);
this.lowerBound.z = Math.min(this.lowerBound.z, aabb.lowerBound.z);
this.upperBound.z = Math.max(this.upperBound.z, aabb.upperBound.z);
}
/**
* Returns true if the given AABB overlaps this AABB.
* @method overlaps
* @param {AABB} aabb
* @return {Boolean}
*/
overlaps(aabb) {
const l1 = this.lowerBound;
const u1 = this.upperBound;
const l2 = aabb.lowerBound;
const u2 = aabb.upperBound; // l2 u2
// |---------|
// |--------|
// l1 u1
const overlapsX = l2.x <= u1.x && u1.x <= u2.x || l1.x <= u2.x && u2.x <= u1.x;
const overlapsY = l2.y <= u1.y && u1.y <= u2.y || l1.y <= u2.y && u2.y <= u1.y;
const overlapsZ = l2.z <= u1.z && u1.z <= u2.z || l1.z <= u2.z && u2.z <= u1.z;
return overlapsX && overlapsY && overlapsZ;
} // Mostly for debugging
volume() {
const l = this.lowerBound;
const u = this.upperBound;
return (u.x - l.x) * (u.y - l.y) * (u.z - l.z);
}
/**
* Returns true if the given AABB is fully contained in this AABB.
* @method contains
* @param {AABB} aabb
* @return {Boolean}
*/
contains(aabb) {
const l1 = this.lowerBound;
const u1 = this.upperBound;
const l2 = aabb.lowerBound;
const u2 = aabb.upperBound; // l2 u2
// |---------|
// |---------------|
// l1 u1
return l1.x <= l2.x && u1.x >= u2.x && l1.y <= l2.y && u1.y >= u2.y && l1.z <= l2.z && u1.z >= u2.z;
}
/**
* @method getCorners
* @param {Vec3} a
* @param {Vec3} b
* @param {Vec3} c
* @param {Vec3} d
* @param {Vec3} e
* @param {Vec3} f
* @param {Vec3} g
* @param {Vec3} h
*/
getCorners(a, b, c, d, e, f, g, h) {
const l = this.lowerBound;
const u = this.upperBound;
a.copy(l);
b.set(u.x, l.y, l.z);
c.set(u.x, u.y, l.z);
d.set(l.x, u.y, u.z);
e.set(u.x, l.y, u.z);
f.set(l.x, u.y, l.z);
g.set(l.x, l.y, u.z);
h.copy(u);
}
/**
* Get the representation of an AABB in another frame.
* @method toLocalFrame
* @param {Transform} frame
* @param {AABB} target
* @return {AABB} The "target" AABB object.
*/
toLocalFrame(frame, target) {
const corners = transformIntoFrame_corners;
const a = corners[0];
const b = corners[1];
const c = corners[2];
const d = corners[3];
const e = corners[4];
const f = corners[5];
const g = corners[6];
const h = corners[7]; // Get corners in current frame
this.getCorners(a, b, c, d, e, f, g, h); // Transform them to new local frame
for (let i = 0; i !== 8; i++) {
const corner = corners[i];
frame.pointToLocal(corner, corner);
}
return target.setFromPoints(corners);
}
/**
* Get the representation of an AABB in the global frame.
* @method toWorldFrame
* @param {Transform} frame
* @param {AABB} target
* @return {AABB} The "target" AABB object.
*/
toWorldFrame(frame, target) {
const corners = transform