@c-frame/physx
Version:
Physics for A-Frame using Nvidia PhysX
38 lines (30 loc) • 241 kB
JavaScript
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("__webpack_require__(/*! ./src/physics */ \"./src/physics.js\");\n__webpack_require__(/*! aframe-stats-panel */ \"./node_modules/aframe-stats-panel/index.js\");\n\n//# sourceURL=webpack://@c-frame/physx/./index.js?");
/***/ }),
/***/ "./node_modules/aframe-stats-panel/index.js":
/*!**************************************************!*\
!*** ./node_modules/aframe-stats-panel/index.js ***!
\**************************************************/
/***/ (() => {
eval("AFRAME.registerComponent('stats-panel', {\r\n schema: {\r\n merge: {type: 'boolean', default: true}\r\n },\r\n\r\n init() {\r\n\r\n const container = document.querySelector('.rs-container')\r\n\r\n if (container && this.data.merge) {\r\n //stats panel exists, just merge into it.\r\n this.container = container\r\n return;\r\n }\r\n\r\n // if stats panel doesn't exist, add one to support our custom stats.\r\n this.base = document.createElement('div')\r\n this.base.classList.add('rs-base')\r\n const body = document.body || document.getElementsByTagName('body')[0]\r\n\r\n if (container && !this.data.merge) {\r\n this.base.style.top = \"auto\"\r\n this.base.style.bottom = \"20px\"\r\n }\r\n\r\n body.appendChild(this.base)\r\n\r\n this.container = document.createElement('div')\r\n this.container.classList.add('rs-container')\r\n this.base.appendChild(this.container)\r\n }\r\n});\r\n\r\nAFRAME.registerComponent('stats-group', {\r\n multiple: true,\r\n schema: {\r\n label: {type: 'string'}\r\n },\r\n\r\n init() {\r\n\r\n let container\r\n const baseComponent = this.el.components['stats-panel']\r\n if (baseComponent) {\r\n container = baseComponent.container\r\n }\r\n else {\r\n container = document.querySelector('.rs-container')\r\n }\r\n\r\n if (!container) {\r\n console.warn(`Couldn't find stats container to add stats to.\r\n Add either stats or stats-panel component to a-scene`)\r\n return;\r\n }\r\n \r\n this.groupHeader = document.createElement('h1')\r\n this.groupHeader.innerHTML = this.data.label\r\n container.appendChild(this.groupHeader)\r\n\r\n this.group = document.createElement('div')\r\n this.group.classList.add('rs-group')\r\n // rs-group hs style flex-direction of 'column-reverse'\r\n // No idea why it's like that, but it's not what we want for our stats.\r\n // We prefer them rendered in the order speified.\r\n // So override this style.\r\n this.group.style.flexDirection = 'column'\r\n this.group.style.webKitFlexDirection = 'column'\r\n container.appendChild(this.group)\r\n }\r\n});\r\n\r\nAFRAME.registerComponent('stats-row', {\r\n multiple: true,\r\n schema: {\r\n // name of the group to add the stats row to.\r\n group: {type: 'string'},\r\n\r\n // name of an event to listen for\r\n event: {type: 'string'},\r\n\r\n // property from event to output in stats panel\r\n properties: {type: 'array'},\r\n\r\n // label for the row in the stats panel\r\n label: {type: 'string'}\r\n },\r\n\r\n init () {\r\n\r\n const groupComponentName = \"stats-group__\" + this.data.group\r\n const groupComponent = this.el.components[groupComponentName] ||\r\n this.el.sceneEl.components[groupComponentName] ||\r\n this.el.components[\"stats-group\"] ||\r\n this.el.sceneEl.components[\"stats-group\"]\r\n\r\n if (!groupComponent) {\r\n console.warn(`Couldn't find stats group ${groupComponentName}`)\r\n return;\r\n }\r\n \r\n this.counter = document.createElement('div')\r\n this.counter.classList.add('rs-counter-base')\r\n groupComponent.group.appendChild(this.counter)\r\n\r\n this.counterId = document.createElement('div')\r\n this.counterId.classList.add('rs-counter-id')\r\n this.counterId.innerHTML = this.data.label\r\n this.counter.appendChild(this.counterId)\r\n\r\n this.counterValues = {}\r\n this.data.properties.forEach((property) => {\r\n const counterValue = document.createElement('div')\r\n counterValue.classList.add('rs-counter-value')\r\n counterValue.innerHTML = \"...\"\r\n this.counter.appendChild(counterValue)\r\n this.counterValues[property] = counterValue\r\n })\r\n\r\n this.updateStatsData = this.updateStatsData.bind(this)\r\n this.el.addEventListener(this.data.event, this.updateStatsData)\r\n\r\n this.splitCache = {}\r\n },\r\n\r\n updateStatsData(e) {\r\n\r\n if (!this.data.properties) return\r\n \r\n this.data.properties.forEach((property) => {\r\n const split = this.splitDot(property);\r\n let value = e.detail;\r\n for (i = 0; i < split.length; i++) {\r\n value = value[split[i]];\r\n }\r\n this.counterValues[property].innerHTML = value\r\n })\r\n },\r\n\r\n splitDot (path) {\r\n if (path in this.splitCache) { return this.splitCache[path]; }\r\n this.splitCache[path] = path.split('.');\r\n return this.splitCache[path];\r\n }\r\n\r\n});\r\n\r\nAFRAME.registerComponent('stats-collector', {\r\n multiple: true,\r\n\r\n schema: {\r\n // name of an event to listen for\r\n inEvent: {type: 'string'},\r\n\r\n // property from event to output in stats panel\r\n properties: {type: 'array'},\r\n\r\n // frequency of output in terms of events received.\r\n outputFrequency: {type: 'number', default: 100},\r\n\r\n // name of event to emit\r\n outEvent: {type: 'string'},\r\n \r\n // outputs (generated for each property)\r\n // Combination of: mean, max, percentile__XX.X (where XX.X is a number)\r\n outputs: {type: 'array'},\r\n\r\n // Whether to output to console as well as generating events\r\n // If a string is specified, this is output to console, together with the event data\r\n // If no string is specified, nothing is output to console.\r\n outputToConsole: {type: 'string'}\r\n },\r\n\r\n init() {\r\n \r\n this.statsData = {}\r\n this.resetData()\r\n this.outputDetail = {}\r\n this.data.properties.forEach((property) => {\r\n this.outputDetail[property] = {}\r\n })\r\n\r\n this.statsReceived = this.statsReceived.bind(this)\r\n this.el.addEventListener(this.data.inEvent, this.statsReceived)\r\n },\r\n \r\n resetData() {\r\n\r\n this.counter = 0\r\n this.data.properties.forEach((property) => {\r\n \r\n // For calculating percentiles like 0.01 and 99.9% we'll want to store\r\n // additional data - something like this...\r\n // Store off outliers, and discard data.\r\n // const min = Math.min(...this.statsData[property])\r\n // this.lowOutliers[property].push(min)\r\n // const max = Math.max(...this.statsData[property])\r\n // this.highOutliers[property].push(max)\r\n\r\n this.statsData[property] = []\r\n })\r\n },\r\n\r\n statsReceived(e) {\r\n\r\n this.updateStatsData(e.detail)\r\n\r\n this.counter++ \r\n if (this.counter === this.data.outputFrequency) {\r\n this.outputData()\r\n this.resetData()\r\n }\r\n },\r\n\r\n updateStatsData(detail) {\r\n\r\n this.data.properties.forEach((property) => {\r\n let value = detail;\r\n value = value[property];\r\n this.statsData[property].push(value)\r\n })\r\n },\r\n\r\n outputData() {\r\n this.data.properties.forEach((property) => {\r\n this.data.outputs.forEach((output) => {\r\n this.outputDetail[property][output] = this.computeOutput(output, this.statsData[property])\r\n })\r\n })\r\n\r\n if (this.data.outEvent) {\r\n this.el.emit(this.data.outEvent, this.outputDetail)\r\n }\r\n\r\n if (this.data.outputToConsole) {\r\n console.log(this.data.outputToConsole, this.outputDetail)\r\n }\r\n },\r\n\r\n computeOutput(outputInstruction, data) {\r\n\r\n const outputInstructions = outputInstruction.split(\"__\")\r\n const outputType = outputInstructions[0]\r\n let output\r\n\r\n switch (outputType) {\r\n case \"mean\":\r\n output = data.reduce((a, b) => a + b, 0) / data.length;\r\n break;\r\n \r\n case \"max\":\r\n output = Math.max(...data)\r\n break;\r\n\r\n case \"min\":\r\n output = Math.min(...data)\r\n break;\r\n\r\n case \"percentile\":\r\n const sorted = data.sort((a, b) => a - b)\r\n // decimal percentiles encoded like 99+9 rather than 99.9 due to \".\" being used as a \r\n // separator for nested properties.\r\n const percentileString = outputInstructions[1].replace(\"_\", \".\")\r\n const proportion = +percentileString / 100\r\n\r\n // Note that this calculation of the percentile is inaccurate when there is insufficient data\r\n // e.g. for 0.1th or 99.9th percentile when only 100 data points.\r\n // Greater accuracy would require storing off more data (specifically outliers) and folding these\r\n // into the computation.\r\n const position = (data.length - 1) * proportion\r\n const base = Math.floor(position)\r\n const delta = position - base;\r\n if (sorted[base + 1] !== undefined) {\r\n output = sorted[base] + delta * (sorted[base + 1] - sorted[base]);\r\n } else {\r\n output = sorted[base];\r\n }\r\n break;\r\n }\r\n return output.toFixed(2)\r\n }\r\n});\r\n\n\n//# sourceURL=webpack://@c-frame/physx/./node_modules/aframe-stats-panel/index.js?");
/***/ }),
/***/ "./src/physics.js":
/*!************************!*\
!*** ./src/physics.js ***!
\************************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("// This is a modification of the physics/PhysX libraries\n// created by Lee Stemkoski\n// from the VARTISTE project @ https://vartiste.xyz/ \n// by Zachary Capalbo https://github.com/zach-capalbo/vartiste\n// with the goal of creating a simplified standalone codebase.\n// Further performance modifications by Diarmid Mackenzie.\n\n// original documentation: https://vartiste.xyz/docs.html#physics.js\n\n// Came via: https://github.com/stemkoski/A-Frame-Examples/blob/66f05fe5cf89879996f1f6a4c0475ce475e8796a/js/physics.js\n// and then via: https://github.com/diarmidmackenzie/christmas-scene/blob/a94ae7e7167937f10d34df8429fb71641e343bb1/lib/physics.js\n// ======================================================================\n\nlet PHYSX = __webpack_require__(/*! ./physx.release.js */ \"./src/physx.release.js\");\n\n// patching in Pool functions\nvar poolSize = 0\n\nfunction sysPool(name, type) {\n if (this.system._pool[name]) return this.system._pool[name]\n this.system._pool[name] = new type()\n // console.log(\"SysPooling\", type.name)\n return this.system._pool[name]\n}\n\nfunction pool(name, type) {\n if (this._pool[name]) return this._pool[name]\n this._pool[name] = new type()\n // console.log(\"Pooling\", type.name)\n return this._pool[name]\n}\n\nclass Pool {\n static init(where, {useSystem = false} = {}) {\n if (useSystem)\n {\n if (!where.system) {\n console.error(\"No system for system pool\", where.attrName)\n }\n if (!where.system._pool) where.system._pool = {};\n\n where.pool = sysPool;\n }\n else\n {\n where._pool = {}\n where.pool = pool;\n }\n }\n}\n\n// ==================================================================================================\n\n// patching in required Util functions from VARTISTE\n\nUtil = {}\n\nPool.init(Util);\n\n// Copies `matrix` into `obj`'s (a `THREE.Object3D`) `matrix`, and decomposes\n// it to `obj`'s position, rotation, and scale\nUtil.applyMatrix = function(matrix, obj) {\n obj.matrix.copy(matrix)\n matrix.decompose(obj.position, obj.rotation, obj.scale)\n}\n\nUtil.traverseCondition = function(obj3D, condition, fn) \n{\n if (!condition(obj3D)) return;\n\n fn(obj3D)\n for (let c of obj3D.children)\n {\n this.traverseCondition(c, condition, fn)\n }\n}\n\nUtil.positionObject3DAtTarget = function(obj, target, {scale, transformOffset, transformRoot} = {}) \n{\n if (typeof transformRoot === 'undefined') transformRoot = obj.parent\n\n target.updateWorldMatrix()\n let destMat = this.pool('dest', THREE.Matrix4)\n destMat.copy(target.matrixWorld)\n\n if (transformOffset) {\n let transformMat = this.pool('transformMat', THREE.Matrix4)\n transformMat.makeTranslation(transformOffset.x, transformOffset.y, transformOffset.z)\n destMat.multiply(transformMat)\n }\n\n if (scale) {\n let scaleVect = this.pool('scale', THREE.Vector3)\n scaleVect.setFromMatrixScale(destMat)\n scaleVect.set(scale.x / scaleVect.x, scale.y / scaleVect.y, scale.z / scaleVect.z)\n destMat.scale(scaleVect)\n }\n\n let invMat = this.pool('inv', THREE.Matrix4)\n\n transformRoot.updateWorldMatrix()\n invMat.copy(transformRoot.matrixWorld).invert()\n destMat.premultiply(invMat)\n\n Util.applyMatrix(destMat, obj)\n}\n\n// untested functions\n\n// Executes function `fn` when `entity` has finished loading, or immediately\n// if it has already loaded. `entity` may be a single `a-entity` element, or\n// an array of `a-entity` elements. If `fn` is not provided, it will return a\n// `Promise` that will resolve when `entity` is loaded (or immediately if\n// `entity` is already loaded).\nUtil.whenLoaded = function(entity, fn) {\n if (Array.isArray(entity) && fn) return whenLoadedAll(entity, fn)\n if (Array.isArray(entity)) return awaitLoadingAll(entity)\n if (fn) return whenLoadedSingle(entity, fn)\n return awaitLoadingSingle(entity)\n}\n\nfunction whenLoadedSingle(entity, fn) {\n if (entity.hasLoaded)\n {\n fn()\n }\n else\n {\n entity.addEventListener('loaded', fn)\n }\n}\n\nfunction whenLoadedAll(entities, fn) {\n let allLoaded = entities.map(() => false)\n for (let i = 0; i < entities.length; ++i)\n {\n let ii = i\n let entity = entities[ii]\n whenLoadedSingle(entity, () => {\n allLoaded[ii] = true\n if (allLoaded.every(t => t)) fn()\n })\n }\n}\n\nfunction awaitLoadingSingle(entity) {\n return new Promise((r, e) => whenLoadedSingle(entity, r))\n}\n\nasync function awaitLoadingAll(entities) {\n for (let entity of entities)\n {\n await awaitLoadingSingle(entity)\n }\n}\n\nUtil.whenComponentInitialized = function(el, component, fn) {\n if (el && el.components[component] && el.components[component].initialized) {\n return Promise.resolve(fn ? fn() : undefined)\n }\n\n return new Promise((r, e) => {\n if (el && el.components[component] && el.components[component].initialized) {\n return Promise.resolve(fn ? fn() : undefined)\n }\n\n let listener = (e) => {\n if (e.detail.name === component) {\n el.removeEventListener('componentinitialized', listener);\n if (fn) fn();\n r();\n }\n };\n el.addEventListener('componentinitialized', listener)\n })\n}\n\n// ========================================================================================\n\n// Extra utility functions for dealing with PhysX\n\nconst PhysXUtil = {\n // Gets the world position transform of the given object3D in PhysX format\n object3DPhysXTransform: (() => {\n let pos = new THREE.Vector3();\n let quat = new THREE.Quaternion();\n return function (obj) {\n obj.getWorldPosition(pos);\n obj.getWorldQuaternion(quat);\n\n return {\n translation: {\n x: pos.x,\n y: pos.y,\n z: pos.z,\n },\n rotation: {\n w: quat.w, // PhysX uses WXYZ quaternions,\n x: quat.x,\n y: quat.y,\n z: quat.z,\n },\n }\n }\n })(),\n\n // Converts a THREE.Matrix4 into a PhysX transform\n matrixToTransform: (() => {\n let pos = new THREE.Vector3();\n let quat = new THREE.Quaternion();\n let scale = new THREE.Vector3();\n let scaleInv = new THREE.Matrix4();\n let mat2 = new THREE.Matrix4();\n return function (matrix) {\n matrix.decompose(pos, quat, scale);\n\n return {\n translation: {\n x: pos.x,\n y: pos.y,\n z: pos.z,\n },\n rotation: {\n w: quat.w, // PhysX uses WXYZ quaternions,\n x: quat.x,\n y: quat.y,\n z: quat.z,\n },\n }\n }\n })(),\n\n // Converts an arry of layer numbers to an integer bitmask\n layersToMask: (() => {\n let layers = new THREE.Layers();\n return function(layerArray) {\n layers.disableAll();\n for (let layer of layerArray)\n {\n layers.enable(parseInt(layer));\n }\n return layers.mask;\n };\n })(),\n\n axisArrayToEnums: function(axes) {\n let enumAxes = []\n for (let axis of axes)\n {\n if (axis === 'swing') {\n enumAxes.push(PhysX.PxD6Axis.eSWING1)\n enumAxes.push(PhysX.PxD6Axis.eSWING2)\n continue\n }\n let enumKey = `e${axis.toUpperCase()}`\n if (!(enumKey in PhysX.PxD6Axis))\n {\n console.warn(`Unknown axis ${axis} (PxD6Axis::${enumKey})`)\n }\n enumAxes.push(PhysX.PxD6Axis[enumKey])\n }\n return enumAxes;\n }\n};\n\nlet PhysX\n\n// Implements the a physics system using an emscripten compiled PhysX engine.\n//\n//\n// If `autoLoad` is `true`, or when you call `startPhysX`, the `physx` system will\n// automatically load and initialize the physics system with reasonable defaults\n// and a ground plane. All you have to do is add [`physx-body`](#physx-body) to\n// the bodies that you want to be part of the simulation. The system will take\n// try to take care of things like collision meshes, position updates, etc\n// automatically. The simplest physics scene looks something like:\n//\n//```\n// <a-scene physx=\"autoLoad: true\">\n// <a-assets><a-asset-item id=\"#mymodel\" src=\"...\"></a-asset-item></a-assets>\n//\n// <a-box physx-body=\"type: static\" color=\"green\" position=\"0 0 -3\"></a-box>\n// <a-sphere physx-body=\"type: dynamic\" position=\"0.4 2 -3\" color=\"blue\"></a-sphere>\n// <a-entity physx-body=\"type: dynamic\" position=\"0 5 -3\" gltf-model=\"#mymodel\"></a-entity>\n// </a-scene>\n//```\n//\n// If you want a little more control over how things behave, you can set the\n// [`physx-material`](#physx-material) component on the objects in your\n// simulation, or use [`physx-joint`s](#physx-joint),\n// [`physx-constraint`s](#physx-constraint) and [`physx-driver`s](#physx-driver)\n// to add some complexity to your scene.\n//\n// If you need more low-level control, the PhysX bindings are exposed through\n// the `PhysX` property of the system. So for instance, if you wanted to make\n// use of the [`PxCapsuleGeometry`](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxapi/files/classPxCapsuleGeometry.html)\n// in your own component, you would call:\n//\n//```\n// let myGeometry = new this.el.sceneEl.PhysX.PxCapsuleGeometry(1.0, 2.0)\n//```\n//\n// The system uses [my fork](https://github.com/zach-capalbo/PhysX) of PhysX, built using the [Docker Wrapper](https://github.com/ashconnell/physx-js). To see what's exposed to JavaScript, see [PxWebBindings.cpp](https://github.com/zach-capalbo/PhysX/blob/emscripten_wip/physx/source/physxwebbindings/src/PxWebBindings.cpp)\n//\n// For a complete example of how to use this, you can see the\n// [aframe-vartiste-toolkit Physics\n// Playground](https://glitch.com/edit/#!/fascinated-hip-period?path=index.html)\n//\n// It is also helpful to refer to the [NVIDIA PhysX\n// documentation](https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Index.html)\nAFRAME.registerSystem('physx', {\n schema: {\n // Amount of time to wait after loading before starting the physics. Can be\n // useful if there is still some things loading or initializing elsewhere in\n // the scene\n delay: {default: 5000},\n\n // Throttle for running the physics simulation. On complex scenes, you can\n // increase this to avoid dropping video frames\n throttle: {default: 10},\n\n // If true, the PhysX will automatically be loaded and started. If false,\n // you will have to call `startPhysX()` manually to load and start the\n // physics engine\n autoLoad: {default: false},\n\n // Simulation speed multiplier. Increase or decrease to speed up or slow\n // down simulation time\n speed: {default: 1.0},\n\n // URL for the PhysX WASM bundle.\n wasmUrl: {default: \"../../wasm/physx.release.wasm\"},\n\n // If true, sets up a default scene with a ground plane and bounding\n // cylinder.\n useDefaultScene: {default: true},\n\n // NYI\n wrapBounds: {default: false},\n\n // Which collision layers the ground belongs to\n groundCollisionLayers: {default: [2]},\n\n // Which collision layers will collide with the ground\n groundCollisionMask: {default: [1,2,3,4]},\n\n // Global gravity vector\n gravity: {type: 'vec3', default: {x: 0, y: -9.8, z: 0}},\n\n // Whether to output stats, and how to output them. One or more of \"console\", \"events\", \"panel\"\n stats: {type: 'array', default: []}\n },\n init() {\n this.PhysXUtil = PhysXUtil;\n\n // for logging.\n this.cumTimeEngine = 0;\n this.cumTimeWrapper = 0;\n this.tickCounter = 0;\n\n\n this.objects = new Map();\n this.shapeMap = new Map();\n this.jointMap = new Map();\n this.boundaryShapes = new Set();\n this.worldHelper = new THREE.Object3D();\n this.el.object3D.add(this.worldHelper);\n this.tock = AFRAME.utils.throttleTick(this.tock, this.data.throttle, this)\n this.collisionObject = {thisShape: null, otherShape:null, points: [], impulses: [], otherComponent: null};\n\n let defaultTarget = document.createElement('a-entity')\n this.el.append(defaultTarget)\n this.defaultTarget = defaultTarget\n\n this.initializePhysX = new Promise((r, e) => {\n this.fulfillPhysXPromise = r;\n })\n\n this.initStats()\n\n this.el.addEventListener('inspectortoggle', (e) => {\n console.log(\"Inspector toggle\", e)\n if (e.detail === true)\n {\n this.running = false\n }\n })\n },\n\n initStats() {\n // Data used for performance monitoring.\n this.statsToConsole = this.data.stats.includes(\"console\")\n this.statsToEvents = this.data.stats.includes(\"events\")\n this.statsToPanel = this.data.stats.includes(\"panel\")\n\n this.bodyTypeToStatsPropertyMap = {\n \"static\": \"staticBodies\",\n \"dynamic\": \"dynamicBodies\",\n \"kinematic\": \"kinematicBodies\",\n }\n\n if (this.statsToConsole || this.statsToEvents || this.statsToPanel) {\n this.trackPerf = true;\n this.tickCounter = 0;\n this.statsTickData = {};\n this.statsBodyData = {};\n\n const scene = this.el.sceneEl;\n scene.setAttribute(\"stats-collector\", `inEvent: physics-tick-data;\n properties: engine, after, total;\n outputFrequency: 100;\n outEvent: physics-tick-summary;\n outputs: percentile__50, percentile__90, max`);\n }\n\n if (this.statsToPanel) {\n const scene = this.el.sceneEl;\n const space = \"   \"\n \n scene.setAttribute(\"stats-panel\", \"\")\n scene.setAttribute(\"stats-group__bodies\", `label: Physics Bodies`)\n scene.setAttribute(\"stats-row__b1\", `group: bodies;\n event:physics-body-data;\n properties: staticBodies;\n label: Static`)\n scene.setAttribute(\"stats-row__b2\", `group: bodies;\n event:physics-body-data;\n properties: dynamicBodies;\n label: Dynamic`)\n scene.setAttribute(\"stats-row__b3\", `group: bodies;\n event:physics-body-data;\n properties: kinematicBodies;\n label: Kinematic`)\n scene.setAttribute(\"stats-group__tick\", `label: Physics Ticks: Median${space}90th%${space}99th%`)\n scene.setAttribute(\"stats-row__1\", `group: tick; \n event:physics-tick-summary; \n properties: engine.percentile__50, \n engine.percentile__90, \n engine.max;\n label: Engine`)\n scene.setAttribute(\"stats-row__2\", `group: tick;\n event:physics-tick-summary;\n properties: after.percentile__50, \n after.percentile__90, \n after.max; \n label: After`)\n\n scene.setAttribute(\"stats-row__3\", `group: tick;\n event:physics-tick-summary;\n properties: total.percentile__50, \n total.percentile__90, \n total.max;\n label: Total`)\n }\n },\n findWasm() {\n return this.data.wasmUrl;\n },\n // Loads PhysX and starts the simulation\n async startPhysX() {\n this.running = true;\n let self = this;\n let resolveInitialized;\n let initialized = new Promise((r, e) => resolveInitialized = r)\n let instance = PHYSX({\n locateFile() {\n return self.findWasm()\n },\n onRuntimeInitialized() {\n resolveInitialized();\n }\n });\n if (instance instanceof Promise) instance = await instance;\n this.PhysX = instance;\n PhysX = instance;\n await initialized;\n self.startPhysXScene()\n self.physXInitialized = true\n self.fulfillPhysXPromise()\n self.el.emit('physx-started', {})\n },\n startPhysXScene() {\n console.info(\"Starting PhysX scene\")\n const foundation = PhysX.PxCreateFoundation(\n PhysX.PX_PHYSICS_VERSION,\n new PhysX.PxDefaultAllocator(),\n new PhysX.PxDefaultErrorCallback()\n );\n this.foundation = foundation\n const physxSimulationCallbackInstance = PhysX.PxSimulationEventCallback.implement({\n onContactBegin: (shape0, shape1, points, impulses) => {\n let c0 = this.shapeMap.get(shape0.$$.ptr)\n let c1 = this.shapeMap.get(shape1.$$.ptr)\n\n if (c1 === c0) return;\n\n if (c0 && c0.data.emitCollisionEvents) {\n this.collisionObject.thisShape = shape0\n this.collisionObject.otherShape = shape1\n this.collisionObject.points = points\n this.collisionObject.impulses = impulses\n this.collisionObject.otherComponent = c1\n c0.el.emit('contactbegin', this.collisionObject)\n }\n\n if (c1 && c1.data.emitCollisionEvents) {\n this.collisionObject.thisShape = shape1\n this.collisionObject.otherShape = shape0\n this.collisionObject.points = points\n this.collisionObject.impulses = impulses\n this.collisionObject.otherComponent = c0\n c1.el.emit('contactbegin', this.collisionObject)\n }\n },\n onContactEnd: (shape0, shape1) => {\n let c0 = this.shapeMap.get(shape0.$$.ptr)\n let c1 = this.shapeMap.get(shape1.$$.ptr)\n\n if (c1 === c0) return;\n\n if (c0 && c0.data.emitCollisionEvents) {\n this.collisionObject.thisShape = shape0\n this.collisionObject.otherShape = shape1\n this.collisionObject.points = null\n this.collisionObject.impulses = null\n this.collisionObject.otherComponent = c1\n c0.el.emit('contactend', this.collisionObject)\n }\n\n if (c1 && c1.data.emitCollisionEvents) {\n this.collisionObject.thisShape = shape1\n this.collisionObject.otherShape = shape0\n this.collisionObject.points = null\n this.collisionObject.impulses = null\n this.collisionObject.otherComponent = c0\n c1.el.emit('contactend', this.collisionObject)\n }\n },\n onContactPersist: () => {},\n onTriggerBegin: () => {},\n onTriggerEnd: () => {},\n onConstraintBreak: (joint) => {\n let component = this.jointMap.get(joint.$$.ptr);\n\n if (!component) return;\n\n component.el.emit('constraintbreak', {})\n },\n });\n let tolerance = new PhysX.PxTolerancesScale();\n // tolerance.length /= 10;\n // console.log(\"Tolerances\", tolerance.length, tolerance.speed);\n this.physics = PhysX.PxCreatePhysics(\n PhysX.PX_PHYSICS_VERSION,\n foundation,\n tolerance,\n false,\n null\n )\n PhysX.PxInitExtensions(this.physics, null);\n\n this.cooking = PhysX.PxCreateCooking(\n PhysX.PX_PHYSICS_VERSION,\n foundation,\n new PhysX.PxCookingParams(tolerance)\n )\n\n const sceneDesc = PhysX.getDefaultSceneDesc(\n this.physics.getTolerancesScale(),\n 0,\n physxSimulationCallbackInstance\n )\n this.scene = this.physics.createScene(sceneDesc)\n\n this.setupDefaultEnvironment()\n },\n setupDefaultEnvironment() {\n this.defaultActorFlags = new PhysX.PxShapeFlags(\n PhysX.PxShapeFlag.eSCENE_QUERY_SHAPE.value |\n PhysX.PxShapeFlag.eSIMULATION_SHAPE.value\n )\n this.defaultFilterData = new PhysX.PxFilterData(PhysXUtil.layersToMask(this.data.groundCollisionLayers), PhysXUtil.layersToMask(this.data.groundCollisionMask), 0, 0);\n\n this.scene.setGravity(this.data.gravity)\n\n if (this.data.useDefaultScene)\n {\n this.createGroundPlane()\n this.createBoundingCylinder()\n }\n\n\n this.defaultTarget.setAttribute('physx-body', 'type', 'static')\n\n },\n createGroundPlane() {\n let geometry = new PhysX.PxPlaneGeometry();\n // let geometry = new PhysX.PxBoxGeometry(10, 1, 10);\n let material = this.physics.createMaterial(0.8, 0.8, 0.1);\n\n const shape = this.physics.createShape(geometry, material, false, this.defaultActorFlags)\n shape.setQueryFilterData(this.defaultFilterData)\n shape.setSimulationFilterData(this.defaultFilterData)\n const transform = {\n translation: {\n x: 0,\n y: 0,\n z: -5,\n },\n rotation: {\n w: 0.707107, // PhysX uses WXYZ quaternions,\n x: 0,\n y: 0,\n z: 0.707107,\n },\n }\n let body = this.physics.createRigidStatic(transform)\n body.attachShape(shape)\n this.scene.addActor(body, null)\n this.ground = body\n this.rigidBody = body\n },\n createBoundingCylinder() {\n const numPlanes = 16\n let geometry = new PhysX.PxPlaneGeometry();\n let material = this.physics.createMaterial(0.1, 0.1, 0.8);\n let spherical = new THREE.Spherical();\n spherical.radius = 30;\n let quat = new THREE.Quaternion();\n let pos = new THREE.Vector3;\n let euler = new THREE.Euler();\n\n for (let i = 0; i < numPlanes; ++i)\n {\n spherical.theta = i * 2.0 * Math.PI / numPlanes;\n pos.setFromSphericalCoords(spherical.radius, spherical.theta, spherical.phi)\n pos.x = - pos.y\n pos.y = 0;\n euler.set(0, spherical.theta, 0);\n quat.setFromEuler(euler)\n\n const shape = this.physics.createShape(geometry, material, false, this.defaultActorFlags)\n shape.setQueryFilterData(this.defaultFilterData)\n shape.setSimulationFilterData(this.defaultFilterData)\n const transform = {\n translation: {\n x: pos.x,\n y: pos.y,\n z: pos.z,\n },\n rotation: {\n w: quat.w, // PhysX uses WXYZ quaternions,\n x: quat.x,\n y: quat.y,\n z: quat.z,\n },\n }\n this.boundaryShapes.add(shape.$$.ptr)\n let body = this.physics.createRigidStatic(transform)\n body.attachShape(shape)\n this.scene.addActor(body, null)\n }\n },\n async registerComponentBody(component, {type}) {\n await this.initializePhysX;\n\n // const shape = this.physics.createShape(geometry, material, false, flags)\n const transform = PhysXUtil.object3DPhysXTransform(component.el.object3D);\n\n let body\n if (type === 'dynamic' || type === 'kinematic')\n {\n body = this.physics.createRigidDynamic(transform)\n\n // body.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_CCD, true);\n // body.setMaxContactImpulse(1e2);\n }\n else\n {\n body = this.physics.createRigidStatic(transform)\n }\n\n let attemptToUseDensity = true;\n let seenAnyDensity = false;\n let densities = new PhysX.VectorPxReal()\n for (let shape of component.createShapes(this.physics, this.defaultActorFlags))\n {\n body.attachShape(shape)\n\n if (isFinite(shape.density))\n {\n seenAnyDensity = true\n densities.push_back(shape.density)\n }\n else\n {\n attemptToUseDensity = false\n\n if (seenAnyDensity)\n {\n console.warn(\"Densities not set for all shapes. Will use total mass instead.\", component.el)\n }\n }\n }\n if (type === 'dynamic' || type === 'kinematic') {\n if (attemptToUseDensity && seenAnyDensity)\n {\n console.log(\"Setting density vector\", densities)\n body.updateMassAndInertia(densities)\n }\n else {\n body.setMassAndUpdateInertia(component.data.mass)\n }\n }\n densities.delete()\n this.scene.addActor(body, null)\n this.objects.set(component.el.object3D, body)\n component.rigidBody = body\n },\n registerShape(shape, component) {\n this.shapeMap.set(shape.$$.ptr, component);\n },\n registerJoint(joint, component) {\n this.jointMap.set(joint.$$.ptr, component);\n },\n removeBody(component) {\n let body = component.rigidBody\n this.objects.delete(component.el.object3D)\n body.release()\n },\n tock(t, dt) {\n if (t < this.data.delay) return\n if (!this.physXInitialized && this.data.autoLoad && !this.running) this.startPhysX()\n if (!this.physXInitialized) return\n if (!this.running) return\n\n const engineStartTime = performance.now();\n\n this.scene.simulate(THREE.MathUtils.clamp(dt * this.data.speed / 1000, 0, 0.03 * this.data.speed), true)\n //this.scene.simulate(0.02, true) // (experiment with fixed interval)\n this.scene.fetchResults(true)\n\n const engineEndTime = performance.now();\n\n for (let [obj, body] of this.objects)\n {\n // no updates needed for static objects.\n if (obj.el.components['physx-body'].data.type === 'static') continue;\n\n const transform = body.getGlobalPose()\n this.worldHelper.position.copy(transform.translation);\n this.worldHelper.quaternion.copy(transform.rotation);\n obj.getWorldScale(this.worldHelper.scale)\n Util.positionObject3DAtTarget(obj, this.worldHelper);\n }\n\n if (this.trackPerf) {\n const afterEndTime = performance.now();\n\n this.statsTickData.engine = engineEndTime - engineStartTime\n this.statsTickData.after = afterEndTime - engineEndTime\n this.statsTickData.total = afterEndTime - engineStartTime\n this.el.emit(\"physics-tick-data\", this.statsTickData)\n\n this.tickCounter++;\n\n if (this.tickCounter === 100) {\n\n this.countBodies()\n\n if (this.statsToConsole) {\n console.log(\"Physics tick stats:\", this.statsData)\n }\n\n if (this.statsToEvents || this.statsToPanel) {\n this.el.emit(\"physics-body-data\", this.statsBodyData)\n }\n\n this.tickCounter = 0;\n }\n }\n },\n\n countBodies() {\n\n // Aditional statistics beyond simple body counts should be possible.\n // They could be accessed via PxScene::getSimulationStatistics()\n // https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Manual/Statistics.html\n // https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxapi/files/classPxSimulationStatistics.html\n // However this part of the API is not yet exposed in the\n // WASM PhysX build we are using\n // See: https://github.com/zach-capalbo/PhysX/blob/emscripten_wip/physx/source/physxwebbindings/src/PxWebBindings.cpp\n \n const statsData = this.statsBodyData\n statsData.staticBodies = 0\n statsData.kinematicBodies = 0\n statsData.dynamicBodies = 0\n\n this.objects.forEach((pxBody, object3D) => {\n const el = object3D.el\n const type = el.components['physx-body'].data.type\n const property = this.bodyTypeToStatsPropertyMap[type]\n statsData[property]++\n })\n },\n})\n\n// Controls physics properties for individual shapes or rigid bodies. You can\n// set this either on an entity with the `phyx-body` component, or on a shape or\n// model contained in an entity with the `physx-body` component. If it's set on\n// a `physx-body`, it will be the default material for all shapes in that body.\n// If it's set on an element containing geometry or a model, it will be the\n// material used for that shape only.\n//\n// For instance, in the following scene fragment:\n//```\n// <a-entity id=\"bodyA\" physx-body physx-material=\"staticFriction: 0.5\">\n// <a-box id=\"shape1\" physx-material=\"staticFriction: 1.0\"></a-box>\n// <a-sphere id=\"shape2\"></a-sphere>\n// </a-entity>\n// <a-cone id=\"bodyB\" physx-body></a-cone>\n//```\n//\n// `shape1`, which is part of the `bodyA` rigid body, will have static friction\n// of 1.0, since it has a material set on it. `shape2`, which is also part of\n// the `bodyA` rigid body, will have a static friction of 0.5, since that is\n// the body default. `bodyB` will have the component default of 0.2, since it is\n// a separate body.\nAFRAME.registerComponent('physx-material', {\n schema: {\n // Static friction\n staticFriction: {default: 0.2},\n // Dynamic friction\n dynamicFriction: {default: 0.2},\n // Restitution, or \"bounciness\"\n restitution: {default: 0.2},\n\n // Density for the shape. If densities are specified for _all_ shapes in a\n // rigid body, then the rigid body's mass properties will be automatically\n // calculated based on the different densities. However, if density\n // information is not specified for every shape, then the mass defined in\n // the overarching [`physx-body`](#physx-body) will be used instead.\n density: {type: 'number', default: NaN},\n\n // Which collision layers this shape is present on\n collisionLayers: {default: [1], type: 'array'},\n // Array containing all layers that this shape should collide with\n collidesWithLayers: {default: [1,2,3,4], type: 'array'},\n\n // If `collisionGroup` is greater than 0, this shape will *not* collide with\n // any other shape with the same `collisionGroup` value\n collisionGroup: {default: 0},\n\n // If >= 0, this will set the PhysX contact offset, indicating how far away\n // from the shape simulation contact events should begin.\n contactOffset: {default: -1.0},\n\n // If >= 0, this will set the PhysX rest offset\n restOffset: {default: -1.0},\n }\n})\n\n// Turns an entity into a PhysX rigid body. This is the main component for\n// creating physics objects.\n//\n// **Types**\n//\n// There are 3 types of supported rigid bodies. The type can be set by using the\n// `type` proeprty, but once initialized cannot be changed.\n//\n// - `dynamic` objects are objects that will have physics simulated on them. The\n// entity's world position, scale, and rotation will be used as the starting\n// condition for the simulation, however once the simulation starts the\n// entity's position and rotation will be replaced each frame with the results\n// of the simulation.\n// - `static` objects are objects that cannot move. They cab be used to create\n// collidable objects for `dynamic` objects, or for anchor points for joints.\n// - `kinematic` objects are objects that can be moved programmatically, but\n// will not be moved by the simulation. They can however, interact with and\n// collide with dynamic objects. Each frame, the entity's `object3D` will be\n// used to set the position and rotation for the simulation object.\n//\n// **Shapes**\n//\n// When the component is initialized, and on the `object3dset` event, all\n// visible meshes that are descendents of this entity will have shapes created\n// for them. Each individual mesh will have its own convex hull automatically\n// generated for it. This means you can have reasonably accurate collision\n// meshes both from building up shapes with a-frame geometry primitives, and\n// from importing 3D models.\n//\n// Visible meshes can be excluded from this shape generation process by setting\n// the `physx-no-collision` attribute on the corresponding `a-entity` element.\n// Invisible meshes can be included into this shape generation process by\n// settingt the `physx-hidden-collision` attribute on the corresponding\n// `a-entity` element. This can be especially useful when using an external tool\n// (like [Blender V-HACD](https://github.com/andyp123/blender_vhacd)) to create\n// a low-poly convex collision mesh for a high-poly or concave mesh. This leads\n// to this pattern for such cases:\n//\n// ```\n// <a-entity physx-body=\"type: dynamic\">\n// <a-entity gltf-model=\"HighPolyOrConcaveURL.gltf\" physx-no-collision=\"\"></a-entity>\n// <a-entity gltf-model=\"LowPolyConvexURL.gltf\" physx-hidden-collision=\"\" visible=\"false\"></a-entity>\n// </a-entity>\n// ```\n//\n// Note, in such cases that if you are setting material properties on individual\n// shapes, then the property should go on the collision mesh entity\n//\n// **Use with the [Manipulator](#manipulator) component**\n//\n// If a dynamic entity is grabbed by the [Manipulator](#manipulator) component,\n// it will temporarily become a kinematic object. This means that collisions\n// will no longer impede its movement, and it will track the manipulator\n// exactly, (subject to any manipulator constraints, such as\n// [`manipulator-weight`](#manipulator-weight)). If you would rather have the\n// object remain dynamic, you will need to [redirect the grab](#redirect-grab)\n// to a `physx-joint` instead, or even easier, use the\n// [`dual-wieldable`](#dual-wieldable) component.\n//\n// As soon as the dynamic object is released, it will revert back to a dynamic\n// object. Objects with the type `kinematic` will remain kinematic.\n//\n// Static objects should not be moved. If a static object can be the target of a\n// manipulator grab (or any other kind of movement), it should be `kinematic`\n// instead.\nAFRAME.registerComponent('physx-body', {\n dependencies: ['physx-material'],\n schema: {\n // **[dynamic, static, kinematic]** Type of the rigid body to create\n type: {default: 'dynamic', oneOf: ['dynamic', 'static', 'kinematic']},\n\n // Total mass of the body\n mass: {default: 1.0},\n\n // If > 0, will set the rigid body's angular damping\n angularDamping: {default: 0.0},\n\n // If > 0, will set the rigid body's linear damping\n linearDamping: {default: 0.0},\n\n // If set to `true`, it will emit `contactbegin` and `contactend` events\n // when collisions occur\n emitCollisionEvents: {default: false},\n\n // If set to `true`, the object will receive extra attention by the\n // simulation engine (at a performance cost).\n highPrecision: {default: false},\n\n shapeOffset: {type: 'vec3', default: {x: 0, y: 0, z: 0}}\n },\n events: {\n stateadded: function(e) {\n if (e.detail === 'grabbed') {\n this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, true)\n }\n },\n stateremoved: function(e) {\n if (e.detail === 'grabbed') {\n if (this.floating) {\n this.rigidBody.setLinearVelocity({x: 0, y: 0, z: 0}, true)\n }\n if (this.data.type !== 'kinematic')\n {\n this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, false)\n }\n }\n },\n 'bbuttonup': function(e) {\n this.toggleGravity()\n },\n componentchanged: function(e) {\n if (e.name === 'physx-material')\n {\n this.el.emit('object3dset', {})\n }\n },\n object3dset: function(e) {\n if (this.rigidBody) {\n for (let shape of this.shapes)\n {\n this.rigidBody.detachShape(shape, false)\n }\n\n let attemptToUseDensity = true;\n let seenAnyDensity = false;\n let densities = new PhysX.VectorPxReal()\n let component = this\n let type = this.data.type\n let body = this.rigidBody\n for (let shape of component.createShapes(this.system.physics, this.system.defaultActorFlags))\n {\n body.attachShape(shape)\n\n if (isFinite(shape.density))\n {\n seenAnyDensity = true\n densities.push_back(shape.density)\n }\n else\n {\n attemptToUseDensity = false\n\n if (seenAnyDensity)\n {\n console.warn(\"Densities not set for all shapes. Will use total mass instead.\", component.el)\n }\n }\n }\n if (type === 'dynamic' || type === 'kinematic') {\n if (attemptToUseDensity && seenAnyDensity)\n {\n console.log(\"Setting density vector\", densities)\n body.updateMassAndInertia(densities)\n }\n else {\n body.setMassAndUpdateInertia(component.data.mass)\n }\n }\n }\n },\n contactbegin: function(e) {\n // console.log(\"Collision\", e.detail.points)\n }\n },\n init() {\n this.system = this.el.sceneEl.systems.physx\n this.physxRegisteredPromise = this.system.registerComponentBody(this, {type: this.data.type})\n this.el.setAttribute('grab-options', 'scalable', false)\n\n this.kinematicMove = this.kinematicMove.bind(this)\n if (this.el.sceneEl.systems['button-caster'])\n {\n this.el.sceneEl.systems['button-caster'].install(['bbutton'])\n }\n\n this.physxRegisteredPromise.then(() => this.update())\n },\n update(oldData) {\n if (!this.rigidBody) return;\n\n if (this.data.type === 'dynamic')\n {\n this.rigidBody.setAngularDamping(this.data.angularDamping)\n this.rigidBody.setLinearDamping(this.data.linearDamping)\n this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, false)\n }\n\n if (this.data.highPrecision)\n {\n if (this.data.type === 'dynamic') {\n this.rigidBody.setSolverIterationCounts(4, 2);\n this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_CCD, true)\n }\n else if (this.data.type === 'kinematic') {\n this.rigidBody.setSolverIterationCounts(4, 2);\n this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, true);\n }\n }\n\n if (!oldData || this.data.mass !== oldData.mass) this.el.emit('object3dset', {})\n },\n remove() {\n if (!this.rigidBody) return;\n this.system.removeBody(this)\n },\n createGeometry(o) {\n if (o.el.hasAttribute('geometry'))\n {\n let geometry = o.el.getAttribute('geometry');\n switch(geometry.primitive)\n {\n case 'sphere':\n return new PhysX.PxSphereGeometry(geometry.radius * this.el.object3D.scale.x * 0.98)\n case 'box':\n return new PhysX.PxBoxGeometry(geometry.width / 2, geometry.height / 2, geometry.depth / 2)\n default:\n return this.createConvexMeshGeometry(o.el.getObject3D('mesh'));\n }\n }\n },\n createConvexMeshGeometry(mesh, rootAncestor) {\n let vectors = new PhysX.PxVec3Vector()\n\n let g = mesh.geometry.attributes.position\n if (!g) return;\n if (g.count < 3) return;\n if (g.itemSize != 3) return;\n let t = new THREE.Vector3;\n\n if (rootAncestor)\n {\n let matrix = new THREE.Matrix4();\n mesh.updateMatrix();\n matrix.copy(mesh.matrix)\n let ancestor = mesh.parent;\n while(ancestor && ancestor !== rootAncestor)\n {\n ancestor.updateMatrix();\n matrix.premultiply(ancestor.matrix);\n ancestor = ancestor.parent;\n }\n for (let i = 0; i < g.co