UNPKG

@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
(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