├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .jshintrc ├── AmmoDriver.md ├── LICENSE ├── README.md ├── dist ├── aframe-physics-system.js └── aframe-physics-system.min.js ├── docs └── readme.md ├── examples ├── README.md ├── ammo.html ├── cannon.html ├── components │ ├── force-pushable.js │ ├── grab.js │ └── rain-of-entities.js ├── compound.html ├── constraints-ammo.html ├── constraints.html ├── materials.html ├── spring.html ├── stress.html ├── sweeper.html └── ttl.html ├── index.js ├── lib └── CANNON-shape2mesh.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── ammo-constraint.js │ ├── body │ │ ├── ammo-body.js │ │ ├── body.js │ │ ├── dynamic-body.js │ │ └── static-body.js │ ├── constraint.js │ ├── math │ │ ├── README.md │ │ ├── index.js │ │ └── velocity.js │ ├── shape │ │ ├── ammo-shape.js │ │ └── shape.js │ └── spring.js ├── constants.js ├── drivers │ ├── ammo-driver.js │ ├── driver.js │ ├── event.js │ ├── local-driver.js │ ├── network-driver.js │ ├── webworkify-debug.js │ ├── worker-driver.js │ └── worker.js ├── system.js └── utils │ ├── math.js │ └── protocol.js └── tests ├── .jshintrc ├── __init.test.js ├── helpers.js ├── karma.conf.js ├── math └── velocity.test.js └── physics ├── body.test.js └── system └── physics.test.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build distribution 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'src/**' 8 | - 'package.json' 9 | - 'package-lock.json' 10 | - 'lib/**' 11 | jobs: 12 | build: 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build distributions 24 | run: npm run dist 25 | - name: Update built distributions 26 | run: | 27 | git config user.name aframe-physics-system 28 | git config user.email aframe-physics-system@github.com 29 | git add dist 30 | git update-index --refresh 31 | git diff-index --quiet HEAD dist || git commit -m "Update built distributions" 32 | git push 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Browser testing CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-18.04, macos-10.15, windows-2019] 8 | browser: [ChromeHeadless, FirefoxHeadless] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Run tests 20 | run: npx karma start ./tests/karma.conf.js --browsers ${{ matrix.browser }} --single-run 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | **/bundle.js 4 | npm-debug.log 5 | .idea 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "THREE": false, 4 | "AFRAME": false, 5 | "window": false, 6 | "document": false, 7 | "require": false, 8 | "console": false, 9 | "module": false 10 | }, 11 | "bitwise": false, 12 | "browser": true, 13 | "eqeqeq": true, 14 | "esnext": true, 15 | "expr": true, 16 | "forin": true, 17 | "immed": true, 18 | "latedef": "nofunc", 19 | "laxbreak": true, 20 | "maxlen": 100, 21 | "newcap": true, 22 | "noarg": true, 23 | "noempty": true, 24 | "noyield": true, 25 | "quotmark": "single", 26 | "smarttabs": false, 27 | "trailing": true, 28 | "undef": true, 29 | "unused": true, 30 | "white": false 31 | } 32 | -------------------------------------------------------------------------------- /AmmoDriver.md: -------------------------------------------------------------------------------- 1 | # Ammo Driver 2 | 3 | [Ammo.js](https://github.com/kripken/ammo.js/) is an [Emscripten](https://emscripten.org/) port of [Bullet](https://github.com/bulletphysics/bullet3), a widely used open-source physics engine. 4 | 5 | ## Contents 6 | 7 | - [Considerations](#considerations-before-use) 8 | - [Installation](#installation) 9 | - [Basics](#basics) 10 | - [Components](#components) 11 | - [`ammo-body`](#ammo-body) 12 | - [`ammo-shape`](#ammo-shape) 13 | - [`ammo-constraint`](#ammo-constraint) 14 | - [Using the Ammo.js API](#using-the-ammojs-api) 15 | - [Events](#events) 16 | - [System Configuration](#system-configuration) 17 | 18 | ## Considerations Before Use 19 | 20 | The Ammo.js driver provides many features and new functionality that the existing Cannon.js integration lacks. However, there are several things to keep in mind before using the Ammo.js driver: 21 | 22 | - The Ammo.js binaries are not a dependency of `Aframe-Physics-System`. You will need to include this into your project yourself. See: [Including the Ammo.js Build](#including-the-ammojs-build). 23 | - The Ammo.js binaries are several times larger than the Cannon.js binary. This shouldn't matter for most usages unless working in very memory sensitive environments. 24 | - new Ammo specific components provide a simple interface for interacting with the Ammo.js code, however it is possible to directly use Ammo.js classes and functions. It is recommended to familiarize yourself with [Emscripten](https://emscripten.org/) if you do so. See: [Using the Ammo.js API](#using-the-ammojs-api). 25 | 26 | ## Installation 27 | 28 | Initial installation is the same as for Cannon.js. See: [Scripts](https://github.com/donmccurdy/aframe-physics-system/blob/master/README.md#installation), then see [Including the Ammo.js Build](#including-the-ammojs-build). 29 | 30 | ### Including the Ammo.js build 31 | 32 | Ammo.js is not a dependency of this project. As a result, it must be included into your project manually. Recommended options are: [script tag](#script-tag) or [NPM and Webpack](#npm-and-webpack). 33 | The latest [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly) build is available either via the [Ammo.js github](http://kripken.github.io/ammo.js/builds/ammo.wasm.js) (`http://kripken.github.io/ammo.js/builds/ammo.wasm.js`) or the [Mozilla Reality fork](https://mixedreality.mozilla.org/ammo.js/builds/ammo.wasm.js) (`https://mixedreality.mozilla.org/ammo.js/builds/ammo.wasm.js`) maintained by the [Mozilla Hubs](https://github.com/mozilla/hubs) team. The latter is especially optimized for use with the Ammo Driver and includes [some functionality](#hacd-and-vhacd) not yet available in the main repository. 34 | 35 | #### Script Tag 36 | 37 | This is the easiest way to include Ammo.js in your project and is recommended for most AFrame projects. Simply add the following to your html file: 38 | 39 | ```html 40 | 41 | or 42 | 43 | ``` 44 | 45 | #### NPM and Webpack 46 | 47 | For more advanced projects that use npm and webpack, first `npm install` whichever version of ammo.js desired. 48 | `npm install github:mozillareality/ammo.js#hubs/master` 49 | or 50 | `npm install github:kripken/ammo.js#master` 51 | Then, the following is a workaround to allow webpack to load the .wasm binary correctly. Include the following in your `package.json`'s `main` script (or some other path as configured by your `webpack.config.json`): 52 | 53 | ```js 54 | const Ammo = require("ammo.js/builds/ammo.wasm.js"); 55 | const AmmoWasm = require("ammo.js/builds/ammo.wasm.wasm"); 56 | window.Ammo = Ammo.bind(undefined, { 57 | locateFile(path) { 58 | if (path.endsWith(".wasm")) { 59 | return AmmoWasm; 60 | } 61 | return path; 62 | } 63 | }); 64 | require("aframe-physics-system"); //note this require must happen after the above 65 | ``` 66 | 67 | Finally, add the following rule to your `webpack.config.json`: 68 | 69 | ```js 70 | { 71 | test: /\.(wasm)$/, 72 | type: "javascript/auto", 73 | use: { 74 | loader: "file-loader", 75 | options: { 76 | outputPath: "assets/wasm", //set this whatever path you desire 77 | name: "[name]-[hash].[ext]" 78 | } 79 | } 80 | }, 81 | ``` 82 | 83 | See [this gist](https://gist.github.com/surma/b2705b6cca29357ebea1c9e6e15684cc) for more information. 84 | 85 | ## Basics 86 | 87 | To begin using the Ammo.js driver, `driver: ammo` must be set in the declaration for the physics system on the `a-scene`. Similar to the old API, `debug: true` will enable wireframe debugging of physics shapes/bodies, however this can further be configured via `debugDrawMode`. See [AmmoDebugDrawer](https://github.com/InfiniteLee/ammo-debug-drawer/blob/0b2c323ef65b4fd414235b6a5e705cfc1201c765/AmmoDebugDrawer.js#L3) for debugDrawMode options. 88 | 89 | ```html 90 | 91 | 92 | 93 | ``` 94 | 95 | To create a physics body, both an `ammo-body` and at least one `ammo-shape` component should be added to an entity. 96 | 97 | ```html 98 | 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | See [examples/ammo.html](/examples/ammo.html) for a working sample. 106 | 107 | ## Components 108 | 109 | ### `ammo-body` 110 | 111 | An `ammo-body` component may be added to any entity in a scene. While having only an `ammo-body` will technically give you a valid physics body in the scene, only after adding an `ammo-shape` will your entity begin to collide with other objects. 112 | 113 | | Property | Default | Description | 114 | | ------------------------ |----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | 115 | | type | `dynamic` | Options: `dynamic`, `static`, `kinematic`. See [ammo-body type](#ammo-body-type). | 116 | | loadedEvent | — | Optional event to wait for before the body attempt to initialize. | 117 | | mass | `1` | Simulated mass of the object, >= 0. | 118 | | gravity | `0 -9.8 0` | Set the gravity for this specific object. | 119 | | linearDamping | `0.01` | Resistance to movement. | 120 | | angularDamping | `0.01` | Resistance to rotation. | 121 | | linearSleepingThreshold | `1.6` | Minimum movement cutoff before a body can enter `activationState: wantsDeactivation` | 122 | | angularSleepingThreshold | `2.5` | Minimum rotation cutoff before a body can enter `activationState: wantsDeactivation` | 123 | | angularFactor | `1 1 1` | Constrains how much the body is allowed to rotate on an axis. E.g. `1 0 1` will prevent rotation around y axis. | 124 | | activationState | `active` | Options: `active`, `islandSleeping`, `wantsDeactivation`, `disableDeactivation`, `disableSimulation`. See [Activation States](#activation-states) | 125 | | emitCollisionEvents | `false` | Set to true to enable firing of `collidestart` and `collideend` events on this entity. See [Events](#events). | 126 | | disableCollision | `false` | Set to true to disable object from colliding with all others. | 127 | | collisionFilterGroup | `1` | 32-bit bitmask to determine what collision "group" this object belongs to. See: [Collision Filtering](#collision-filtering). | 128 | | collisionFilterMask | `1` | 32-bit bitmask to determine what collision "groups" this object should collide with. See: [Collision Filtering](#collision-filtering). | 129 | | scaleAutoUpdate | `true` | Should the shapes of the objecct be automatically scaled to match the scale of the entity. | 130 | 131 | #### `ammo-body` type 132 | 133 | The `type` of an ammo body can be one of the following: 134 | 135 | - `dynamic`: A freely-moving object. Dynamic bodies have mass, collide with other bodies, bounce or slow during collisions, and fall if gravity is enabled. 136 | - `static`: A fixed-position object. Other bodies may collide with static bodies, but static bodies themselves are unaffected by gravity and collisions. These bodies should typically not be moved after initialization as they cannot impart forces on `dynamic` bodies. 137 | - `kinematic`: Like a `static` body, except that they can be moved via updating the position of the entity. Unlike a `static` body, they impart forces on `dynamic` bodies when moved. Useful for animated or remote (networked) objects. 138 | 139 | #### Activation States 140 | 141 | Activation states are only used for `type: dynamic` bodies. Most bodies should be left at the default `activationState: active` so that they can go to sleep (sleeping bodies are very cheap). It can be useful to set bodies to `activationState: disableDeactivation` if also using an `ammo-constraint` as constraints will stop functioning if the body goes to sleep, however they should be used sparingly. Each activation state has a color used for wireframe rendering when debug is enabled. 142 | 143 | | state | debug rendering color | description | 144 | | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 145 | | `active` | white | Waking state. Bodies will enter this state if collisions with other bodies occur. This is the default state. | 146 | | `islandSleeping` | green | Sleeping state. Bodies will enter this state if they fall below `linearSleepingThreshold` and `angularSleepingThreshold` and no other `active` or `disableDeactivation` bodies are nearby. | 147 | | `wantsDeactivation` | cyan | Intermediary state between `active` and `islandSleeping`. Bodies will enter this state if they fall below `linearSleepingThreshold` and `angularSleepingThreshold`. | 148 | | `disableDeactivation` | red | Forced `active` state. Bodies set to this state will never enter `islandSleeping` or `wantsDeactivation`. | 149 | | `disableSimulation` | yellow | Bodies in this state will be completely ignored by the physics system. | 150 | 151 | #### Collision Filtering 152 | 153 | Collision filtering allows you to control what bodies are allowed to collide with others. For Ammo.js, they are represented as two 32-bit bitmasks, `collisionFilterGroup` and `collisionFilterMask`. 154 | 155 | Using collision filtering requires basic understanding of the [bitwise OR](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#(Bitwise_OR)) (`a | b`) and [bitwise AND](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#(Bitwise_AND)) (`a & b`) operations. 156 | 157 | Example: 158 | Imagine 3 groups of objects, `A`, `B`, and `C`. We will say their bit values are as follows: 159 | 160 | ```js 161 | collisionGroups: { 162 | A: 1, 163 | B: 2, 164 | C: 4 165 | } 166 | ``` 167 | 168 | Assume all A objects should only collide with other A objects, and only B objects should collide with other B objects. 169 | 170 | ```html 171 | 172 | 173 | 174 | 175 | ``` 176 | 177 | Now Assume all C objects can collide with either A or B objects. 178 | 179 | ```html 180 | 181 | 182 | 183 | 184 | 185 | 186 | ``` 187 | 188 | Note that the `collisionFilterMask` for `A` and `B` changed to `5` and `6` respectively. This is because the bitwise `OR` of collision groups `A` and `C` is `1 | 4 = 5` and for `B` and `C` is `2 | 4 = 6` . The `collisionFilterMask` for `C` is `7` because `1 | 2 | 4 = 7`. When two bodies collide, both bodies compare their `collisionFilterMask` with the colliding body's `collisionFilterGroup` using the bitwise `AND` operator and checks for equality with `0`. If the result of the `AND` for either pair is equal to `0`, the objects are not allowed to collide. 189 | 190 | ```js 191 | // Object α (alpha) in group A and object β (beta) in group B overlap. 192 | 193 | // α checks if it can collide with β. (α's collisionFilterMask AND β's collisionFilterGroup) 194 | (5 & 2) = 0; 195 | 196 | // β checks if it can collide with α. (β's collisionFilterMask AND α's collisionFilterGroup) 197 | (6 & 1) = 0; 198 | 199 | // Both checks equal 0; α and β do not collide. 200 | 201 | // Now, object γ (gamma) in group C is overlapping with object β. 202 | 203 | // β checks if it can collide with γ. (β's collisionFilterMask AND γ's collisionFilterGroup) 204 | (6 & 7) = 6; 205 | 206 | // γ checks if it can collide with β. (γ's collisionFilterMask AND β's collisionFilterGroup) 207 | (7 & 2) = 2; 208 | 209 | // Neither check equals 0; β and γ collide. 210 | ```` 211 | 212 | See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Flags_and_bitmasks for more information about bitmasks. 213 | 214 | ### `ammo-shape` 215 | 216 | Any entity with an `ammo-body` component can also have 1 or more `ammo-shape` components. The `ammo-shape` component is what defines the collision shape of the entity. `ammo-shape` components can be added and removed at any time. The actual work of generating a `btCollisionShape` is done via an external library, [Three-to-Ammo](https://github.com/infinitelee/three-to-ammo). 217 | 218 | | Property | Dependencies | Default | Description | 219 | | ------------------- | --------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | 220 | | type | — | `hull` | Options: `box`, `cylinder`, `sphere`, `capsule`, `cone`, `hull`, `hacd`, `vhacd`, `mesh`, `heightfield`. see [Shape Types](#shape-types). | 221 | | fit | — | `all` | Options: `all`, `manual`. Use `manual` if defining `halfExtents` or `sphereRadius` below. See [Shape Fit](#shape-fit). | 222 | | halfExtents | `fit: manual` and `type: box, cylinder, capsule, cone` | `1 1 1` | Set the halfExtents to use. | 223 | | minHalfExtent | `fit: all` and `type: box, cylinder, capsule, cone` | `0` | The minimum value for any axis of the halfExtents. | 224 | | maxHalfExtent | `fit: all` and `type: box, cylinder, capsule, cone` | `Number.POSITIVE_INFINITY` | The maximum value for any axis of the halfExtents. | 225 | | sphereRadius | `fit: manual` and `type: sphere` | `NaN` | Set the radius for spheres. | 226 | | cylinderAxis | — | `y` | Options: `x`, `y`, `z`. Override default axis for `cylinder`, `capsule`, and `cone` types. | 227 | | margin | — | `0.01` | The amount of 'padding' to add around the shape. Larger values have better performance but reduce collision shape precision. | 228 | | offset | — | `0 0 0` | Where to position the shape relative to the origin of the entity. | 229 | | heightfieldData | `fit: manual` and `type: heightfield` | `[]` | An array of arrays of float values that represent a height at a fixed interval `heightfieldDistance` | 230 | | heightfieldDistance | `fit: manual` and `type: heightfield` | `1` | The distance between each height value in both the x and z direction in `heightfieldData` | 231 | | includeInvisible | `fit: all` | `false` | Should invisible meshes be included when using `fit: all` | 232 | 233 | #### Shape Types 234 | 235 | - **Primitives** 236 | - **Box** (`box`) – Requires `halfExtents` if using `fit: manual`. 237 | - **Cylinder** (`cylinder`) – Requires `halfExtents` if using `fit: manual`. Use `cylinderAxis` to change which axis the length of the cylinder is aligned. 238 | - **Sphere** (`sphere`) – Requires `sphereRadius` if using `fit: manual`. 239 | - **Capsule** (`capsule`) – Requires `halfExtents` if using `fit: manual`. Use `cylinderAxis` to change which axis the length of the capsule is aligned. 240 | - **Cone** (`cone`) – Requires `halfExtents` if using `fit: manual`. Use `cylinderAxis` to change which axis the point of the cone is aligned. 241 | - **Hull** (`hull`) – Wraps a model in a convex hull, like a shrink-wrap. Not quite as performant as primitives, but still very fast. 242 | - **Hull Approximate Convex Decomposition** (`hacd`) – This is an experimental feature that generates multiple convex hulls to approximate any convex or concave shape. 243 | - **Volumetric Hull Approximate Convex Decomposition** (`vhacd`) – Also experimental, this is `hacd` with a different algorithm. See: http://kmamou.blogspot.com/2014/11/v-hacd-v20-is-here.html for more information. 244 | - **Mesh** (`mesh`) – Creates a 1:1 concave collision shape with the triangles of the meshes of the entity. May only be used on `static` bodies. This is the least performant shape, however they can work very well for static environments if the following is observed: 245 | - Avoid using meshes with very high triangle density relative to size of convex objects (primitives and hulls) colliding with the mesh. E.g. avoid meshes where an object could collide with dozens or more triangles in a single spot. 246 | - Avoid very high poly meshes in general and use mesh decimation (simplification) if possible. 247 | - **Heightfield** (`heightfield`) – Similar to a mesh shape, but you must provide an array of heights and the distance between those values. E.g. `heightfieldData: [[0, 0, 0], [0, 1, 0], [0, 0, 0]]` and `heightfieldDistance: 1` will create a 3x3 meter heightfield with a height of 0 except for the center with a height of 1. 248 | 249 | #### Shape Fit 250 | 251 | - `fit: all` – Requires a mesh to exist on the entity. The specified shape will be created to contain all the vertices of the mesh. 252 | - `fit: manual` – Does not require a mesh, however you must specifiy either the `halfExtents` or `sphereRadius` manually. This is not supported for `hull`, `hacd`, `vhacd` and `mesh` types. 253 | 254 | ### `ammo-constraint` 255 | 256 | The `ammo-constraint` component is used to bind `ammo-bodies` together using hinges, fixed distances, or fixed attachment points. Note that an `ammo-shape` is not required for `ammo-constraint` to work, however you may get strange results with some constraint types. 257 | 258 | Example: 259 | 260 | ```html 261 | 262 | 263 | ``` 264 | 265 | | Property | Dependencies | Default | Description | 266 | | ----------- | -------------------------------------- | ------- | ----------------------------------------------------------------------------------------- | 267 | | type | — | `lock` | Options: `lock`, `fixed`, `spring`, `slider`, `hinge`, `coneTwist`, `pointToPoint`. | 268 | | target | — | — | Selector for a single entity to which current entity should be bound. | 269 | | pivot | `type: pointToPoint, coneTwist, hinge` | `0 0 0` | Offset of the hinge or point-to-point constraint, defined locally in this element's body. | 270 | | targetPivot | `type: pointToPoint, coneTwist, hinge` | `0 0 0` | Offset of the hinge or point-to-point constraint, defined locally in the target's body. | 271 | | axis | `type: hinge` | `0 0 1` | An axis that each body can rotate around, defined locally to this element's body. | 272 | | targetAxis | `type: hinge` | `0 0 1` | An axis that each body can rotate around, defined locally to the target's body. | 273 | 274 | ## Using the Ammo.js API 275 | 276 | The Ammo.js API lacks any usage documentation. Instead, it is recommended to read the [Bullet 2.83 documentation](https://github.com/bulletphysics/bullet3/tree/master/docs), the [Bullet forums](https://pybullet.org/Bullet/phpBB3/) and the [Emscripten WebIDL Binder documentation](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html). Note that the linked Bullet documentation is for Bullet 2.83, where as Ammo.js is using 2.82, so some features described in the documentation may not be available. 277 | 278 | Some things to note: 279 | 280 | - Not all classes and properties in Bullet are available. Each class and property has to be 'exposed' via a definition in [ammo.idl](https://github.com/MozillaReality/ammo.js/blob/hubs/master/ammo.idl). 281 | - There is no automatic garbage collection for instantiated Bullet objects. Any time you use the `new` keyword for a Bullet class you must also at some point (when you are done with the object) release the memory by calling `Ammo.destroy`. 282 | 283 | ```js 284 | const vector3 = new Ammo.btVector3(); 285 | ... do stuff 286 | Ammo.destroy(vector3); 287 | ``` 288 | 289 | - Exposed properties on classes can be accessed via specially generated `get_()` and `set_()` functions. E.g. `rayResultCallback.get_m_collisionObject();` 290 | - Sometimes when calling certain functions you will receive a pointer object instead of an instance of the class you are expecting. Use `Ammo.wrapPointer` to "wrap" the pointer in the class you expected. E.g. `Ammo.wrapPointer(ptr, Ammo.btRigidBody);` 291 | - Conversely, sometimes you need the pointer of an object. Use `Ammo.getPointer`. E.g. `const ptr = Ammo.getPointer(object);`. 292 | 293 | In A-Frame, each entity's `btRigidBody` instance is exposed on the `el.body` property. To apply a quick push to an object, you might do the following: 294 | 295 | ```html 296 | 297 | 298 | 299 | 300 | ``` 301 | 302 | ```javascript 303 | var el = sceneEl.querySelector('#nyan'); 304 | const force = new Ammo.btVector3(0, 1, -0); 305 | const pos = new Ammo.btVector3(el.object3D.position.x, el.object3D.position.y, el.object3D.position.z); 306 | el.body.applyForce(force, pos); 307 | Ammo.destroy(force); 308 | Ammo.destroy(pos); 309 | ``` 310 | 311 | ## Events 312 | 313 | | event | description | 314 | | ------------- | --------------------------------------------------------------------------------------------------- | 315 | | `body-loaded` | Fired when physics body (`el.body`) has been created. | 316 | | `collidestart` | Fired when two bodies collide. `emitCollisionEvents: true` must be set on the `ammo-body`. | 317 | | `collideend` | Fired when two bodies stop colliding. `emitCollisionEvents: true` must be set on the `ammo-body`. | 318 | 319 | ### Collisions 320 | 321 | `ammo-driver` generates events when a collision has started or ended, which are propagated onto the associated A-Frame entity. Example: 322 | 323 | ```javascript 324 | var playerEl = document.querySelector("[camera]"); 325 | playerEl.addEventListener("collide", function(e) { 326 | console.log("Player has collided with body #" + e.detail.targetEl.id); 327 | e.detail.targetEl; // Other entity, which playerEl touched. 328 | }); 329 | ``` 330 | 331 | The current map of collisions can be accessed via `AFRAME.scenes[0].systems.physics.driver.collisions`. This will return a map keyed by each `btRigidBody` (by pointer) with value of an array of each other `btRigidBody` it is currently colliding with. 332 | 333 | ## System Configuration 334 | 335 | | Property | Default | Description | 336 | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | 337 | | driver | `local` | [`local`, `worker`, `ammo`] | 338 | | debug | `true` | Whether to show wireframes for debugging. | 339 | | debugDrawMode | `0` | See [AmmoDebugDrawer](https://github.com/InfiniteLee/ammo-debug-drawer/blob/0b2c323ef65b4fd414235b6a5e705cfc1201c765/AmmoDebugDrawer.js#L3) | 340 | | gravity | `-9.8` | Force of gravity (in m/s^2). | 341 | | iterations | `10` | The number of solver iterations determines quality of the constraints in the world. | 342 | | maxSubSteps | `4` | The max number of physics steps to calculate per tick. | 343 | | fixedTimeStep | `0.01667` | The internal framerate of the physics simulation. | 344 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ----------- 2 | 3 | ### a-frame-physics-system is now maintained at: [c-frame/aframe-physics-system](https://github.com/c-frame/aframe-physics-system) 4 | 5 | ### Available on npm as [@c-frame/aframe-physics-system](https://www.npmjs.com/package/@c-frame/aframe-physics-system) 6 | 7 | ------------- 8 | 9 | 10 | 11 | # Physics for A-Frame VR 12 | 13 | [![Latest NPM release](https://img.shields.io/npm/v/aframe-physics-system.svg)](https://www.npmjs.com/package/aframe-physics-system) 14 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/n5ro/aframe-physics-system/master/LICENSE) 15 | ![Build](https://github.com/n5ro/aframe-physics-system/workflows/Build%20distribution/badge.svg) 16 | ![Test](https://github.com/n5ro/aframe-physics-system/workflows/Browser%20testing%20CI/badge.svg) 17 | 18 | Components for A-Frame physics integration. 19 | Supports [CANNON.js](http://schteppe.github.io/cannon.js/) and [Ammo.js](https://github.com/kripken/ammo.js/) 20 | 21 | 22 | ## New Features 23 | 24 | Ammo.js driver support has been added. Please see [Ammo Driver](/AmmoDriver.md) for documentation. CANNON.js support may be deprecated in the future. 25 | 26 | ## Contents 27 | 28 | + [Installation](#installation) 29 | + [Basics](#basics) 30 | + [Components](#components) 31 | + [`dynamic-body` and `static-body`](#dynamic-body-and-static-body) 32 | + [`shape`](#shape) 33 | + [`constraint`](#constraint) 34 | + [`spring`](#spring) 35 | + [Using the CANNON.js API](#using-the-cannonjs-api) 36 | + [Events](#events) 37 | + [System Configuration](#system-configuration) 38 | + [Examples](#examples) 39 | 40 | ## Installation 41 | 42 | ### Scripts 43 | 44 | In the [dist/](https://github.com/donmccurdy/aframe-physics-system/tree/master/dist) folder, download the full or minified build. Include the script on your page, and all components are automatically registered for you: 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | CDN builds for aframe-physics-system@v$npm_package_version: 51 | 52 | - [aframe-physics-system.js](https://cdn.jsdelivr.net/gh/n5ro/aframe-physics-system@v$npm_package_version/dist/aframe-physics-system.js) *(development)* 53 | - [aframe-physics-system.min.js](https://cdn.jsdelivr.net/gh/n5ro/aframe-physics-system@v$npm_package_version/dist/aframe-physics-system.min.js) *(production)* 54 | 55 | ### npm 56 | 57 | ``` 58 | npm install --save aframe-physics-system 59 | ``` 60 | 61 | ```javascript 62 | // my-app.js 63 | require('aframe-physics-system'); 64 | ``` 65 | 66 | Once installed, you'll need to compile your JavaScript using something like [Browserify](http://browserify.org/) or [Webpack](http://webpack.github.io/). Example: 67 | 68 | ```bash 69 | npm install -g browserify 70 | browserify my-app.js -o bundle.js 71 | ``` 72 | 73 | `bundle.js` may then be included in your page. See [here](http://browserify.org/#middle-section) for a better introduction to Browserify. 74 | 75 | #### npm + webpack 76 | 77 | When using webpack, you need to ensure that your `loader` for `.js` files includes this dependency. The example below assumes you're using Babel. 78 | 79 | ```js 80 | { 81 | test: /\.js$/, 82 | include: ['src', require.resolve('aframe-physics-system') ], 83 | use: { 84 | loader: 'babel-loader', // or whatever loader you're using to parse modules 85 | options: {} 86 | } 87 | } 88 | ``` 89 | 90 | > **Note**: You cannot use `exclude: /node_modules` for your `.js` loader. You must instead use `include` and pass an array of directories as dependencies to transpile. 91 | 92 | ## Basics 93 | 94 | ```html 95 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ``` 113 | 114 | ## Components 115 | 116 | ### `dynamic-body` and `static-body` 117 | 118 | The `dynamic-body` and `static-body` components may be added to any `` that contains a mesh. Generally, each scene will have at least one `static-body` for the ground, and one or more `dynamic-body` instances that the player can interact with. 119 | 120 | - **dynamic-body**: A freely-moving object. Dynamic bodies have mass, collide with other objects, bounce or slow during collisions, and fall if gravity is enabled. 121 | - **static-body**: A fixed-position or animated object. Other objects may collide with static bodies, but static bodies themselves are unaffected by gravity and collisions. 122 | 123 | | Property | Dependencies | Default | Description | 124 | |----------------|------------------|---------|-----------------------------------------------------| 125 | | shape | — | `auto` | `auto`, `box`, `cylinder`, `sphere`, `hull`, `none` | 126 | | mass | `dynamic-body` | 5 | Simulated mass of the object, > 0. | 127 | | linearDamping | `dynamic-body` | 0.01 | Resistance to movement. | 128 | | angularDamping | `dynamic-body` | 0.01 | Resistance to rotation. | 129 | | sphereRadius | `shape:sphere` | — | Override default radius of bounding sphere. | 130 | | cylinderAxis | `shape:cylinder` | — | Override default axis of bounding cylinder. | 131 | 132 | #### Body Shapes 133 | 134 | Body components will attempt to find an appropriate CANNON.js shape to fit your model. When defining an object you may choose a shape or leave the default, `auto`. Select a shape carefully, as there are performance implications with different choices: 135 | 136 | * **None** (`none`) – Does not add collision geometry. Use this when adding collision shapes manually, through the `shape` component or custom JavaScript. 137 | * **Auto** (`auto`) – Chooses automatically from the available shapes. 138 | * **Box** (`box`) – Great performance, compared to Hull or Trimesh shapes, and may be fitted to custom models. 139 | * **Cylinder** (`cylinder`) – See `box`. Adds `cylinderAxis` option. 140 | * **Sphere** (`sphere`) – See `box`. Adds `sphereRadius` option. 141 | * **Convex** (`hull`) – Wraps a model like shrink-wrap. Convex shapes are more performant and better supported than Trimesh, but may still have some performance impact when used as dynamic objects. 142 | * **Primitives** – Plane/Cylinder/Sphere. Used automatically with the corresponding A-Frame primitives. 143 | * **Trimesh** (`mesh`) – *Deprecated.* Trimeshes adapt to fit custom geometry (e.g. a `.OBJ` or `.DAE` file), but have very minimal support. Arbitrary trimesh shapes are difficult to model in any JS physics engine, will "fall through" certain other shapes, and have serious performance limitations. 144 | 145 | For more details, see the CANNON.js [collision matrix](https://github.com/schteppe/cannon.js#features). 146 | 147 | Example using a bounding box for a custom model: 148 | 149 | ```html 150 | 151 | 152 | 153 | 154 | 155 | ``` 156 | 157 | ### `shape` 158 | 159 | Compound shapes require a bit of work to set up, but allow you to use multiple primitives to define a physics shape around custom models. These will generally perform better, and behave more accurately, than `mesh` or `hull` shapes. For example, a chair might be modeled as a cylinder-shaped seat, on four long cylindrical legs. 160 | 161 | Example: 162 | 163 | ```html 164 | 173 | 174 | ``` 175 | 176 | | Property | Shapes | Default | Description | 177 | |-------------|------------|-----------|-------------| 178 | |shape | — | `box` | `box`, `sphere`, or `cylinder` | 179 | |offset | — | `0 0 0` | Position of shape relative to body. | 180 | |orientation | — | `0 0 0 1` | Rotation of shape relative to body. | 181 | |radius | `sphere` | `1` | Sphere radius. | 182 | |halfExtents | `box` | `1 1 1` | Box half-extents. Use `0.5 0.5 0.5` for a 1x1x1 box. | 183 | |radiusTop | `cylinder` | `1` | Cylinder upper radius. | 184 | |radiusBottom | `cylinder` | `1` | Cylinder lower radius. | 185 | |height | `cylinder` | `1` | Cylinder height. | 186 | |numSegments | `cylinder` | `8` | Cylinder subdivisions. | 187 | 188 | ### `constraint` 189 | 190 | The `constraint` component is used to bind physics bodies together using hinges, fixed distances, or fixed attachment points. 191 | 192 | Example: 193 | 194 | ```html 195 | 196 | 197 | ``` 198 | 199 | | Property | Dependencies | Default | Description | 200 | | --- | --- | --- | --- | 201 | | type | — | `lock` | Type of constraint. Options: `lock`, `distance`, `hinge`, `coneTwist`, `pointToPoint`. | 202 | | target | — | — | Selector for a single entity to which current entity should be bound. | 203 | | maxForce | — | 1e6 | Maximum force that may be exerted to enforce this constraint. | 204 | | collideConnected | — | true | If true, connected bodies may collide with one another. | 205 | | wakeUpBodies | — | true | If true, sleeping bodies are woken up by this constraint. | 206 | | distance | `type:distance` | auto | Distance at which bodies should be fixed. Default, or 0, for current distance. | 207 | | pivot | `type: pointToPoint, coneTwist, hinge` | 0 0 0 | Offset of the hinge or point-to-point constraint, defined locally in this element's body. | 208 | | targetPivot | `type: pointToPoint, coneTwist, hinge` | 0 0 0 | Offset of the hinge or point-to-point constraint, defined locally in the target's body. | 209 | | axis | `type: coneTwist, hinge` | 0 0 1 | An axis that each body can rotate around, defined locally to this element's body. | 210 | | targetAxis | `type: coneTwist, hinge` | 0 0 1 | An axis that each body can rotate around, defined locally to the target's body. | 211 | 212 | ### `spring` 213 | 214 | The `spring` component connects two bodies, and applies forces as the bodies become farther apart. 215 | 216 | Example: 217 | 218 | ```html 219 | 220 | 225 | ``` 226 | 227 | | Property | Default | Description | 228 | | ------------ | --- | -------------------------------------------------------------- | 229 | | target | — | Target (other) body for the constraint. | 230 | | restLength | 1 | Length of the spring, when no force acts upon it. | 231 | | stiffness | 100 | How much will the spring suppress force. | 232 | | damping | 1 | Stretch factor of the spring. | 233 | | localAnchorA | — | Where to hook the spring to body A, in local body coordinates. | 234 | | localAnchorB | — | Where to hook the spring to body B, in local body coordinates. | 235 | 236 | ## Using the CANNON.js API 237 | 238 | For more advanced physics, use the CANNON.js API with custom JavaScript and A-Frame components. The [CANNON.js documentation](http://schteppe.github.io/cannon.js/docs/) and source code offer good resources for learning to work with physics in JavaScript. 239 | 240 | In A-Frame, each entity's `CANNON.Body` instance is exposed on the `el.body` property. To apply a quick push to an object, you might do the following: 241 | 242 | ```html 243 | 244 | 245 | 246 | 247 | ``` 248 | 249 | ```javascript 250 | var el = sceneEl.querySelector('#nyan'); 251 | el.body.applyImpulse( 252 | /* impulse */ new CANNON.Vec3(0, 1, -1), 253 | /* world position */ new CANNON.Vec3().copy(el.getComputedAttribute('position')) 254 | ); 255 | ``` 256 | 257 | ## Events 258 | 259 | | event | description | 260 | |-------|-------------| 261 | | `body-loaded` | Fired when physics body (`el.body`) has been created. | 262 | | `collide` | Fired when two objects collide. Touching objects may fire `collide` events frequently. Unavailable with `driver: worker`. | 263 | 264 | ### Collisions 265 | 266 | > **NOTE:** Collision events are currently only supported with the local driver, and will not be fired with `physics="driver: worker"` enabled. 267 | 268 | CANNON.js generates events when a collision is detected, which are propagated onto the associated A-Frame entity. Example: 269 | 270 | ```javascript 271 | var playerEl = document.querySelector('[camera]'); 272 | playerEl.addEventListener('collide', function (e) { 273 | console.log('Player has collided with body #' + e.detail.body.id); 274 | 275 | e.detail.target.el; // Original entity (playerEl). 276 | e.detail.body.el; // Other entity, which playerEl touched. 277 | e.detail.contact; // Stats about the collision (CANNON.ContactEquation). 278 | e.detail.contact.ni; // Normal (direction) of the collision (CANNON.Vec3). 279 | }); 280 | ``` 281 | 282 | Note that CANNON.js cannot perfectly detect collisions with very fast-moving bodies. Doing so requires Continuous Collision Detection, which can be both slow and difficult to implement. If this is an issue for your scene, consider (1) slowing objects down, (2) detecting collisions manually (collisions with the floor are easy – `position.y - height / 2 <= 0`), or (3) attempting a PR to CANNON.js. See: [Collision with fast bodies](https://github.com/schteppe/cannon.js/issues/202). 283 | 284 | ## System Configuration 285 | 286 | Contact materials define what happens when two objects meet, including physical properties such as friction and restitution (bounciness). The default, scene-wide contact materials may be configured on the scene element: 287 | 288 | ```html 289 | 290 | 291 | 292 | ``` 293 | > NOTE: It is possible to run physics on a Web Worker using the `physics="driver: worker"` option. 294 | > Using a worker is helpful for maintaining a smooth framerate, because physics simulation does 295 | > not block the main thread. However, scenes needing highly-responsive interaction (for example, 296 | > tossing and catching objects) may prefer to run physics locally, where feedback from the physics 297 | > system will be immediate. 298 | 299 | | Property | Default | Description | 300 | |---------------------------------|---------|----------------------------------------------------| 301 | | debug | true | Whether to show wireframes for debugging. | 302 | | gravity | -9.8 | Force of gravity (in m/s^2). | 303 | | iterations | 10 | The number of solver iterations determines quality of the constraints in the world. The more iterations, the more correct simulation. More iterations need more computations though. If you have a large gravity force in your world, you will need more iterations. | 304 | | maxInterval | 0.0667 | Maximum simulated time (in milliseconds) that may be taken by the physics engine per frame. Effectively prevents weird "jumps" when the player returns to the scene after a few minutes, at the expense of pausing physics during this time. | 305 | | friction | 0.01 | Coefficient of friction. | 306 | | restitution | 0.3 | Coefficient of restitution (bounciness). | 307 | | contactEquationStiffness | 1e8 | Stiffness of the produced contact equations. | 308 | | contactEquationRelaxation | 3 | Relaxation time of the produced contact equations. | 309 | | frictionEquationStiffness | 1e8 | Stiffness of the produced friction equations. | 310 | | frictionEquationRegularization | 3 | Relaxation time of the produced friction equations | 311 | | driver | local | [`local`, `worker`] | 312 | | workerFps | 60 | Steps per second to be used in physics simulation on worker. | 313 | | workerInterpolate | true | Whether the main thread should interpolate physics frames from the worker. | 314 | | workerInterpBufferSize | 2 | Number of physics frames to be 'padded' before displaying. Advanced. | 315 | | workerDebug | false | If true, the worker codepaths are used on the main thread. This is slow, because physics snapshots are needlessly serialized, but helpful for debugging. | 316 | 317 | More advanced configuration, including specifying different collision behaviors for different objects, is available through the CANNON.js JavaScript API. 318 | 319 | Resources: 320 | 321 | * [CANNON.World](http://schteppe.github.io/cannon.js/docs/classes/World.html) 322 | * [CANNON.ContactMaterial](http://schteppe.github.io/cannon.js/docs/classes/ContactMaterial.html) 323 | 324 | ## Examples 325 | 326 | To help demonstrate the features and capabilities of `aframe-physics-system` a 327 | collection of examples have been prepared. Please see 328 | [Examples](examples/README.md) for a summary and link to each of the 329 | prepared examples. 330 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | To help demonstrate the features and capabilities of `aframe-physics-system` 4 | the following collection of examples have been prepared. 5 | 6 | - [**Ammo sandbox**](ammo.html) 7 | Demonstration of many [Ammo driver](../AmmoDriver.md) features in a single 8 | example. 9 | 10 | - [**Cannon.js sandbox**](cannon.html) 11 | Demonstration of many Cannon driver features in a single example. 12 | 13 | - [**Compound**](compound.html) 14 | Construct a [compound shape](../README.md#shape) and simulate collision with 15 | a ground plane using the Cannon driver. 16 | 17 | - [**Constraints with Ammo**](constraints-ammo.html) 18 | Demonstration of many 19 | [Ammo driver constraints](../AmmoDriver.md#ammo-constraint) including cone 20 | twist, hinge, lock, point to point, and slider constraints. 21 | 22 | - [**Constraints with Cannon**](constraints.html) 23 | Demonstration of many 24 | [Cannon driver constraints](../README.md#constraint) including cone twist, 25 | hinge, lock, point to point, and distance constraints. 26 | 27 | - [**Materials**](materials.html) 28 | Bounce simulation with 29 | [restitution (bounciness)](../README.md#system-configuration) of 1 using the 30 | Cannon driver. 31 | 32 | - [**Spring**](spring.html) 33 | Four vertical [springs](../README.md#spring) each between two boxes with an 34 | assortment of damping and stiffness values using the Cannon driver. 35 | 36 | - [**Stress**](stress.html) 37 | Apply [strong impulse](../README.md#using-the-cannonjs-api) to a cube when the 38 | user clicks with a mouse using the Cannon driver. Cubes are arranged in four 39 | 4x3 walls. 40 | 41 | - [**Sweeper**](sweeper.html) 42 | Animate a long wall moving along the z-axis along the initial view direction 43 | using the velocity component and the Cannon driver. 44 | 45 | - [**Time to live**](ttl.html) 46 | Remove a [dynamic body](../README.md#dynamic-body-and-static-body) from the 47 | scene after 100 frames using the Cannon driver. 48 | -------------------------------------------------------------------------------- /examples/ammo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples • AmmoDriver 5 | 6 | 7 | 8 | 9 | 55 | 56 | 57 | 58 | 59 | 67 | 76 | 84 | 94 | 104 | 113 | 124 | 134 | 144 | 145 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /examples/cannon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples • CannonDriver 5 | 6 | 7 | 8 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/components/force-pushable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Force Pushable component. 3 | * 4 | * Applies behavior to the current entity such that cursor clicks will apply a 5 | * strong impulse, pushing the entity away from the viewer. 6 | * 7 | * Requires: physics 8 | */ 9 | AFRAME.registerComponent('force-pushable', { 10 | schema: { 11 | force: { default: 10 } 12 | }, 13 | init: function () { 14 | this.pStart = new THREE.Vector3(); 15 | this.sourceEl = this.el.sceneEl.querySelector('[camera]'); 16 | this.el.addEventListener('click', this.forcePush.bind(this)); 17 | }, 18 | forcePush: function () { 19 | var force, 20 | el = this.el, 21 | pStart = this.pStart.copy(this.sourceEl.getAttribute('position')); 22 | 23 | // Compute direction of force, normalize, then scale. 24 | force = el.body.position.vsub(pStart); 25 | force.normalize(); 26 | force.scale(this.data.force, force); 27 | 28 | el.body.applyImpulse(force, el.body.position); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /examples/components/grab.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerComponent('grab', { 2 | init: function () { 3 | this.system = this.el.sceneEl.systems.physics; 4 | 5 | this.GRABBED_STATE = 'grabbed'; 6 | 7 | this.grabbing = false; 8 | this.hitEl = /** @type {AFRAME.Element} */ null; 9 | this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics; 10 | this.constraint = /** @type {CANNON.Constraint} */ null; 11 | 12 | // Bind event handlers 13 | this.onHit = this.onHit.bind(this); 14 | this.onGripOpen = this.onGripOpen.bind(this); 15 | this.onGripClose = this.onGripClose.bind(this); 16 | }, 17 | 18 | play: function () { 19 | var el = this.el; 20 | el.addEventListener('hit', this.onHit); 21 | el.addEventListener('gripdown', this.onGripClose); 22 | el.addEventListener('gripup', this.onGripOpen); 23 | el.addEventListener('trackpaddown', this.onGripClose); 24 | el.addEventListener('trackpadup', this.onGripOpen); 25 | el.addEventListener('triggerdown', this.onGripClose); 26 | el.addEventListener('triggerup', this.onGripOpen); 27 | }, 28 | 29 | pause: function () { 30 | var el = this.el; 31 | el.removeEventListener('hit', this.onHit); 32 | el.removeEventListener('gripdown', this.onGripClose); 33 | el.removeEventListener('gripup', this.onGripOpen); 34 | el.removeEventListener('trackpaddown', this.onGripClose); 35 | el.removeEventListener('trackpadup', this.onGripOpen); 36 | el.removeEventListener('triggerdown', this.onGripClose); 37 | el.removeEventListener('triggerup', this.onGripOpen); 38 | }, 39 | 40 | onGripClose: function (evt) { 41 | this.grabbing = true; 42 | }, 43 | 44 | onGripOpen: function (evt) { 45 | var hitEl = this.hitEl; 46 | this.grabbing = false; 47 | if (!hitEl) { return; } 48 | hitEl.removeState(this.GRABBED_STATE); 49 | this.hitEl = undefined; 50 | this.system.removeConstraint(this.constraint); 51 | this.constraint = null; 52 | }, 53 | 54 | onHit: function (evt) { 55 | var hitEl = evt.detail.el; 56 | // If the element is already grabbed (it could be grabbed by another controller). 57 | // If the hand is not grabbing the element does not stick. 58 | // If we're already grabbing something you can't grab again. 59 | if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; } 60 | hitEl.addState(this.GRABBED_STATE); 61 | this.hitEl = hitEl; 62 | this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body, {maxForce: 100}); 63 | this.system.addConstraint(this.constraint); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /examples/components/rain-of-entities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rain of Entities component. 3 | * 4 | * Creates a spawner on the scene, which periodically generates new entities 5 | * and drops them from the sky. Objects falling below altitude=0 will be 6 | * recycled after a few seconds. 7 | * 8 | * Requires: physics 9 | */ 10 | AFRAME.registerComponent('rain-of-entities', { 11 | schema: { 12 | tagName: { default: 'a-box' }, 13 | components: { default: ['dynamic-body', 'force-pushable', 'color|#39BB82', 'scale|0.2 0.2 0.2'] }, 14 | spread: { default: 10, min: 0 }, 15 | maxCount: { default: 10, min: 0 }, 16 | interval: { default: 1000, min: 0 }, 17 | lifetime: { default: 10000, min: 0 } 18 | }, 19 | init: function () { 20 | this.boxes = []; 21 | this.timeout = setInterval(this.spawn.bind(this), this.data.interval); 22 | }, 23 | spawn: function () { 24 | if (this.boxes.length >= this.data.maxCount) { 25 | clearTimeout(this.timeout); 26 | return; 27 | } 28 | 29 | var data = this.data, 30 | physics = this.el.sceneEl.systems.physics, 31 | box = document.createElement(data.tagName); 32 | 33 | this.boxes.push(box); 34 | this.el.appendChild(box); 35 | 36 | box.setAttribute('position', this.randomPosition()); 37 | data.components.forEach(function (s) { 38 | var parts = s.split('|'); 39 | box.setAttribute(parts[0], parts[1] || ''); 40 | }); 41 | 42 | // Recycling is important, kids. 43 | setInterval(function () { 44 | if (box.body.position.y > 0) return; 45 | box.body.position.copy(this.randomPosition()); 46 | box.body.quaternion.set(0, 0, 0, 1); 47 | box.body.velocity.set(0, 0, 0); 48 | box.body.angularVelocity.set(0, 0, 0); 49 | box.body.updateProperties(); 50 | }.bind(this), this.data.lifetime); 51 | 52 | var colliderEls = this.el.sceneEl.querySelectorAll('[sphere-collider]'); 53 | for (var i = 0; i < colliderEls.length; i++) { 54 | colliderEls[i].components['sphere-collider'].update(); 55 | } 56 | }, 57 | randomPosition: function () { 58 | var spread = this.data.spread; 59 | return { 60 | x: Math.random() * spread - spread / 2, 61 | y: 3, 62 | z: Math.random() * spread - spread / 2 63 | }; 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /examples/compound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Compound 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/constraints-ammo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Examples • Constraints • AmmoDriver 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 109 | 110 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/constraints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Examples • Constraints 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /examples/materials.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Materials 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/spring.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Examples • Spring 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/stress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Stress Test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/sweeper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Sweeper 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/ttl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples • TTL 5 | 6 | 7 | 8 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'); 2 | 3 | require('./src/components/math'); 4 | require('./src/components/body/ammo-body'); 5 | require('./src/components/body/body'); 6 | require('./src/components/body/dynamic-body'); 7 | require('./src/components/body/static-body'); 8 | require('./src/components/shape/shape'); 9 | require('./src/components/shape/ammo-shape') 10 | require('./src/components/ammo-constraint'); 11 | require('./src/components/constraint'); 12 | require('./src/components/spring'); 13 | require('./src/system'); 14 | 15 | module.exports = { 16 | registerAll: function () { 17 | console.warn('registerAll() is deprecated. Components are automatically registered.'); 18 | } 19 | }; 20 | 21 | // Export CANNON.js. 22 | window.CANNON = window.CANNON || CANNON; 23 | -------------------------------------------------------------------------------- /lib/CANNON-shape2mesh.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CANNON.shape2mesh 3 | * 4 | * Source: http://schteppe.github.io/cannon.js/build/cannon.demo.js 5 | * Author: @schteppe 6 | */ 7 | var CANNON = require('cannon-es'); 8 | 9 | CANNON.shape2mesh = function(body){ 10 | var obj = new THREE.Object3D(); 11 | 12 | for (var l = 0; l < body.shapes.length; l++) { 13 | var shape = body.shapes[l]; 14 | 15 | var mesh; 16 | 17 | switch(shape.type){ 18 | 19 | case CANNON.Shape.types.SPHERE: 20 | var sphere_geometry = new THREE.SphereGeometry( shape.radius, 8, 8); 21 | mesh = new THREE.Mesh( sphere_geometry, this.currentMaterial ); 22 | break; 23 | 24 | case CANNON.Shape.types.PARTICLE: 25 | mesh = new THREE.Mesh( this.particleGeo, this.particleMaterial ); 26 | var s = this.settings; 27 | mesh.scale.set(s.particleSize,s.particleSize,s.particleSize); 28 | break; 29 | 30 | case CANNON.Shape.types.PLANE: 31 | var geometry = new THREE.PlaneGeometry(10, 10, 4, 4); 32 | mesh = new THREE.Object3D(); 33 | var submesh = new THREE.Object3D(); 34 | var ground = new THREE.Mesh( geometry, this.currentMaterial ); 35 | ground.scale.set(100, 100, 100); 36 | submesh.add(ground); 37 | 38 | ground.castShadow = true; 39 | ground.receiveShadow = true; 40 | 41 | mesh.add(submesh); 42 | break; 43 | 44 | case CANNON.Shape.types.BOX: 45 | var box_geometry = new THREE.BoxGeometry( shape.halfExtents.x*2, 46 | shape.halfExtents.y*2, 47 | shape.halfExtents.z*2 ); 48 | mesh = new THREE.Mesh( box_geometry, this.currentMaterial ); 49 | break; 50 | 51 | case CANNON.Shape.types.CONVEXPOLYHEDRON: 52 | var geo = new THREE.Geometry(); 53 | 54 | // Add vertices 55 | for (var i = 0; i < shape.vertices.length; i++) { 56 | var v = shape.vertices[i]; 57 | geo.vertices.push(new THREE.Vector3(v.x, v.y, v.z)); 58 | } 59 | 60 | for(var i=0; i < shape.faces.length; i++){ 61 | var face = shape.faces[i]; 62 | 63 | // add triangles 64 | var a = face[0]; 65 | for (var j = 1; j < face.length - 1; j++) { 66 | var b = face[j]; 67 | var c = face[j + 1]; 68 | geo.faces.push(new THREE.Face3(a, b, c)); 69 | } 70 | } 71 | geo.computeBoundingSphere(); 72 | geo.computeFaceNormals(); 73 | mesh = new THREE.Mesh( geo, this.currentMaterial ); 74 | break; 75 | 76 | case CANNON.Shape.types.HEIGHTFIELD: 77 | var geometry = new THREE.Geometry(); 78 | 79 | var v0 = new CANNON.Vec3(); 80 | var v1 = new CANNON.Vec3(); 81 | var v2 = new CANNON.Vec3(); 82 | for (var xi = 0; xi < shape.data.length - 1; xi++) { 83 | for (var yi = 0; yi < shape.data[xi].length - 1; yi++) { 84 | for (var k = 0; k < 2; k++) { 85 | shape.getConvexTrianglePillar(xi, yi, k===0); 86 | v0.copy(shape.pillarConvex.vertices[0]); 87 | v1.copy(shape.pillarConvex.vertices[1]); 88 | v2.copy(shape.pillarConvex.vertices[2]); 89 | v0.vadd(shape.pillarOffset, v0); 90 | v1.vadd(shape.pillarOffset, v1); 91 | v2.vadd(shape.pillarOffset, v2); 92 | geometry.vertices.push( 93 | new THREE.Vector3(v0.x, v0.y, v0.z), 94 | new THREE.Vector3(v1.x, v1.y, v1.z), 95 | new THREE.Vector3(v2.x, v2.y, v2.z) 96 | ); 97 | var i = geometry.vertices.length - 3; 98 | geometry.faces.push(new THREE.Face3(i, i+1, i+2)); 99 | } 100 | } 101 | } 102 | geometry.computeBoundingSphere(); 103 | geometry.computeFaceNormals(); 104 | mesh = new THREE.Mesh(geometry, this.currentMaterial); 105 | break; 106 | 107 | case CANNON.Shape.types.TRIMESH: 108 | var geometry = new THREE.Geometry(); 109 | 110 | var v0 = new CANNON.Vec3(); 111 | var v1 = new CANNON.Vec3(); 112 | var v2 = new CANNON.Vec3(); 113 | for (var i = 0; i < shape.indices.length / 3; i++) { 114 | shape.getTriangleVertices(i, v0, v1, v2); 115 | geometry.vertices.push( 116 | new THREE.Vector3(v0.x, v0.y, v0.z), 117 | new THREE.Vector3(v1.x, v1.y, v1.z), 118 | new THREE.Vector3(v2.x, v2.y, v2.z) 119 | ); 120 | var j = geometry.vertices.length - 3; 121 | geometry.faces.push(new THREE.Face3(j, j+1, j+2)); 122 | } 123 | geometry.computeBoundingSphere(); 124 | geometry.computeFaceNormals(); 125 | mesh = new THREE.Mesh(geometry, this.currentMaterial); 126 | break; 127 | 128 | default: 129 | throw "Visual type not recognized: "+shape.type; 130 | } 131 | 132 | mesh.receiveShadow = true; 133 | mesh.castShadow = true; 134 | if(mesh.children){ 135 | for(var i=0; i (https://www.donmccurdy.com)", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/n5ro/aframe-physics-system/issues" 40 | }, 41 | "homepage": "https://github.com/n5ro/aframe-physics-system#readme", 42 | "dependencies": { 43 | "ammo-debug-drawer": "^0.1.0", 44 | "cannon-es": "^0.9.1", 45 | "three-to-ammo": "^0.1.0", 46 | "three-to-cannon": "^3.0.2", 47 | "webworkify": "^1.4.0" 48 | }, 49 | "peerDependencies": { 50 | "aframe": ">=0.5.0" 51 | }, 52 | "devDependencies": { 53 | "aframe": "^1.1.0", 54 | "browserify": "^16.5.1", 55 | "browserify-css": "^0.15.0", 56 | "browserify-shim": "^3.8.14", 57 | "budo": "^11.6.3", 58 | "chai": "^3.5.0", 59 | "chai-shallow-deep-equal": "^1.4.6", 60 | "envify": "^4.1.0", 61 | "karma": "^4.4.1", 62 | "karma-browserify": "^6.1.0", 63 | "karma-chai-shallow-deep-equal": "0.0.4", 64 | "karma-chrome-launcher": "^2.2.0", 65 | "karma-env-preprocessor": "^0.1.1", 66 | "karma-firefox-launcher": "^1.3.0", 67 | "karma-mocha": "^1.3.0", 68 | "karma-mocha-reporter": "^2.2.5", 69 | "karma-sinon-chai": "^1.3.4", 70 | "mocha": "^6.2.3", 71 | "replace": "^1.2.0", 72 | "sinon": "^2.4.1", 73 | "sinon-chai": "^2.14.0", 74 | "three": "^0.123.0", 75 | "uglify-es": "^3.3.9" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ammo-constraint.js: -------------------------------------------------------------------------------- 1 | /* global Ammo */ 2 | const CONSTRAINT = require("../constants").CONSTRAINT; 3 | 4 | module.exports = AFRAME.registerComponent("ammo-constraint", { 5 | multiple: true, 6 | 7 | schema: { 8 | // Type of constraint. 9 | type: { 10 | default: CONSTRAINT.LOCK, 11 | oneOf: [ 12 | CONSTRAINT.LOCK, 13 | CONSTRAINT.FIXED, 14 | CONSTRAINT.SPRING, 15 | CONSTRAINT.SLIDER, 16 | CONSTRAINT.HINGE, 17 | CONSTRAINT.CONE_TWIST, 18 | CONSTRAINT.POINT_TO_POINT 19 | ] 20 | }, 21 | 22 | // Target (other) body for the constraint. 23 | target: { type: "selector" }, 24 | 25 | // Offset of the hinge or point-to-point constraint, defined locally in the body. Used for hinge, coneTwist pointToPoint constraints. 26 | pivot: { type: "vec3" }, 27 | targetPivot: { type: "vec3" }, 28 | 29 | // An axis that each body can rotate around, defined locally to that body. Used for hinge constraints. 30 | axis: { type: "vec3", default: { x: 0, y: 0, z: 1 } }, 31 | targetAxis: { type: "vec3", default: { x: 0, y: 0, z: 1 } } 32 | }, 33 | 34 | init: function() { 35 | this.system = this.el.sceneEl.systems.physics; 36 | this.constraint = null; 37 | }, 38 | 39 | remove: function() { 40 | if (!this.constraint) return; 41 | 42 | this.system.removeConstraint(this.constraint); 43 | this.constraint = null; 44 | }, 45 | 46 | update: function() { 47 | const el = this.el, 48 | data = this.data; 49 | 50 | this.remove(); 51 | 52 | if (!el.body || !data.target.body) { 53 | (el.body ? data.target : el).addEventListener("body-loaded", this.update.bind(this, {}), { once: true }); 54 | return; 55 | } 56 | 57 | this.constraint = this.createConstraint(); 58 | this.system.addConstraint(this.constraint); 59 | }, 60 | 61 | /** 62 | * @return {Ammo.btTypedConstraint} 63 | */ 64 | createConstraint: function() { 65 | let constraint; 66 | const data = this.data, 67 | body = this.el.body, 68 | targetBody = data.target.body; 69 | 70 | const bodyTransform = body 71 | .getCenterOfMassTransform() 72 | .invert() 73 | .op_mul(targetBody.getWorldTransform()); 74 | const targetTransform = new Ammo.btTransform(); 75 | targetTransform.setIdentity(); 76 | 77 | switch (data.type) { 78 | case CONSTRAINT.LOCK: { 79 | constraint = new Ammo.btGeneric6DofConstraint(body, targetBody, bodyTransform, targetTransform, true); 80 | const zero = new Ammo.btVector3(0, 0, 0); 81 | //TODO: allow these to be configurable 82 | constraint.setLinearLowerLimit(zero); 83 | constraint.setLinearUpperLimit(zero); 84 | constraint.setAngularLowerLimit(zero); 85 | constraint.setAngularUpperLimit(zero); 86 | Ammo.destroy(zero); 87 | break; 88 | } 89 | //TODO: test and verify all other constraint types 90 | case CONSTRAINT.FIXED: { 91 | //btFixedConstraint does not seem to debug render 92 | bodyTransform.setRotation(body.getWorldTransform().getRotation()); 93 | targetTransform.setRotation(targetBody.getWorldTransform().getRotation()); 94 | constraint = new Ammo.btFixedConstraint(body, targetBody, bodyTransform, targetTransform); 95 | break; 96 | } 97 | case CONSTRAINT.SPRING: { 98 | constraint = new Ammo.btGeneric6DofSpringConstraint(body, targetBody, bodyTransform, targetTransform, true); 99 | //TODO: enableSpring, setStiffness and setDamping 100 | break; 101 | } 102 | case CONSTRAINT.SLIDER: { 103 | //TODO: support setting linear and angular limits 104 | constraint = new Ammo.btSliderConstraint(body, targetBody, bodyTransform, targetTransform, true); 105 | constraint.setLowerLinLimit(-1); 106 | constraint.setUpperLinLimit(1); 107 | // constraint.setLowerAngLimit(); 108 | // constraint.setUpperAngLimit(); 109 | break; 110 | } 111 | case CONSTRAINT.HINGE: { 112 | const pivot = new Ammo.btVector3(data.pivot.x, data.pivot.y, data.pivot.z); 113 | const targetPivot = new Ammo.btVector3(data.targetPivot.x, data.targetPivot.y, data.targetPivot.z); 114 | 115 | const axis = new Ammo.btVector3(data.axis.x, data.axis.y, data.axis.z); 116 | const targetAxis = new Ammo.btVector3(data.targetAxis.x, data.targetAxis.y, data.targetAxis.z); 117 | 118 | constraint = new Ammo.btHingeConstraint(body, targetBody, pivot, targetPivot, axis, targetAxis, true); 119 | 120 | Ammo.destroy(pivot); 121 | Ammo.destroy(targetPivot); 122 | Ammo.destroy(axis); 123 | Ammo.destroy(targetAxis); 124 | break; 125 | } 126 | case CONSTRAINT.CONE_TWIST: { 127 | const pivotTransform = new Ammo.btTransform(); 128 | pivotTransform.setIdentity(); 129 | pivotTransform.getOrigin().setValue(data.pivot.x, data.pivot.y, data.pivot.z); 130 | const targetPivotTransform = new Ammo.btTransform(); 131 | targetPivotTransform.setIdentity(); 132 | targetPivotTransform.getOrigin().setValue(data.targetPivot.x, data.targetPivot.y, data.targetPivot.z); 133 | constraint = new Ammo.btConeTwistConstraint(body, targetBody, pivotTransform, targetPivotTransform); 134 | Ammo.destroy(pivotTransform); 135 | Ammo.destroy(targetPivotTransform); 136 | break; 137 | } 138 | case CONSTRAINT.POINT_TO_POINT: { 139 | const pivot = new Ammo.btVector3(data.pivot.x, data.pivot.y, data.pivot.z); 140 | const targetPivot = new Ammo.btVector3(data.targetPivot.x, data.targetPivot.y, data.targetPivot.z); 141 | 142 | constraint = new Ammo.btPoint2PointConstraint(body, targetBody, pivot, targetPivot); 143 | 144 | Ammo.destroy(pivot); 145 | Ammo.destroy(targetPivot); 146 | break; 147 | } 148 | default: 149 | throw new Error("[constraint] Unexpected type: " + data.type); 150 | } 151 | 152 | Ammo.destroy(bodyTransform); 153 | Ammo.destroy(targetTransform); 154 | 155 | return constraint; 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /src/components/body/ammo-body.js: -------------------------------------------------------------------------------- 1 | /* global Ammo,THREE */ 2 | const AmmoDebugDrawer = require("ammo-debug-drawer"); 3 | const threeToAmmo = require("three-to-ammo"); 4 | const CONSTANTS = require("../../constants"), 5 | ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE, 6 | COLLISION_FLAG = CONSTANTS.COLLISION_FLAG, 7 | SHAPE = CONSTANTS.SHAPE, 8 | TYPE = CONSTANTS.TYPE, 9 | FIT = CONSTANTS.FIT; 10 | 11 | const ACTIVATION_STATES = [ 12 | ACTIVATION_STATE.ACTIVE_TAG, 13 | ACTIVATION_STATE.ISLAND_SLEEPING, 14 | ACTIVATION_STATE.WANTS_DEACTIVATION, 15 | ACTIVATION_STATE.DISABLE_DEACTIVATION, 16 | ACTIVATION_STATE.DISABLE_SIMULATION 17 | ]; 18 | 19 | const RIGID_BODY_FLAGS = { 20 | NONE: 0, 21 | DISABLE_WORLD_GRAVITY: 1 22 | }; 23 | 24 | function almostEqualsVector3(epsilon, u, v) { 25 | return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon; 26 | } 27 | 28 | function almostEqualsBtVector3(epsilon, u, v) { 29 | return Math.abs(u.x() - v.x()) < epsilon && Math.abs(u.y() - v.y()) < epsilon && Math.abs(u.z() - v.z()) < epsilon; 30 | } 31 | 32 | function almostEqualsQuaternion(epsilon, u, v) { 33 | return ( 34 | (Math.abs(u.x - v.x) < epsilon && 35 | Math.abs(u.y - v.y) < epsilon && 36 | Math.abs(u.z - v.z) < epsilon && 37 | Math.abs(u.w - v.w) < epsilon) || 38 | (Math.abs(u.x + v.x) < epsilon && 39 | Math.abs(u.y + v.y) < epsilon && 40 | Math.abs(u.z + v.z) < epsilon && 41 | Math.abs(u.w + v.w) < epsilon) 42 | ); 43 | } 44 | 45 | let AmmoBody = { 46 | schema: { 47 | loadedEvent: { default: "" }, 48 | mass: { default: 1 }, 49 | gravity: { type: "vec3", default: { x: 0, y: -9.8, z: 0 } }, 50 | linearDamping: { default: 0.01 }, 51 | angularDamping: { default: 0.01 }, 52 | linearSleepingThreshold: { default: 1.6 }, 53 | angularSleepingThreshold: { default: 2.5 }, 54 | angularFactor: { type: "vec3", default: { x: 1, y: 1, z: 1 } }, 55 | activationState: { 56 | default: ACTIVATION_STATE.ACTIVE_TAG, 57 | oneOf: ACTIVATION_STATES 58 | }, 59 | type: { default: "dynamic", oneOf: [TYPE.STATIC, TYPE.DYNAMIC, TYPE.KINEMATIC] }, 60 | emitCollisionEvents: { default: false }, 61 | disableCollision: { default: false }, 62 | collisionFilterGroup: { default: 1 }, //32-bit mask, 63 | collisionFilterMask: { default: 1 }, //32-bit mask 64 | scaleAutoUpdate: { default: true } 65 | }, 66 | 67 | /** 68 | * Initializes a body component, assigning it to the physics system and binding listeners for 69 | * parsing the elements geometry. 70 | */ 71 | init: function() { 72 | this.system = this.el.sceneEl.systems.physics; 73 | this.shapeComponents = []; 74 | 75 | if (this.data.loadedEvent === "") { 76 | this.loadedEventFired = true; 77 | } else { 78 | this.el.addEventListener( 79 | this.data.loadedEvent, 80 | () => { 81 | this.loadedEventFired = true; 82 | }, 83 | { once: true } 84 | ); 85 | } 86 | 87 | if (this.system.initialized && this.loadedEventFired) { 88 | this.initBody(); 89 | } 90 | }, 91 | 92 | /** 93 | * Parses an element's geometry and component metadata to create an Ammo body instance for the 94 | * component. 95 | */ 96 | initBody: (function() { 97 | const pos = new THREE.Vector3(); 98 | const quat = new THREE.Quaternion(); 99 | const boundingBox = new THREE.Box3(); 100 | 101 | return function() { 102 | const el = this.el, 103 | data = this.data; 104 | 105 | this.localScaling = new Ammo.btVector3(); 106 | 107 | const obj = this.el.object3D; 108 | obj.getWorldPosition(pos); 109 | obj.getWorldQuaternion(quat); 110 | 111 | this.prevScale = new THREE.Vector3(1, 1, 1); 112 | this.prevNumChildShapes = 0; 113 | 114 | this.msTransform = new Ammo.btTransform(); 115 | this.msTransform.setIdentity(); 116 | this.rotation = new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w); 117 | 118 | this.msTransform.getOrigin().setValue(pos.x, pos.y, pos.z); 119 | this.msTransform.setRotation(this.rotation); 120 | 121 | this.motionState = new Ammo.btDefaultMotionState(this.msTransform); 122 | 123 | this.localInertia = new Ammo.btVector3(0, 0, 0); 124 | 125 | this.compoundShape = new Ammo.btCompoundShape(true); 126 | 127 | this.rbInfo = new Ammo.btRigidBodyConstructionInfo( 128 | data.mass, 129 | this.motionState, 130 | this.compoundShape, 131 | this.localInertia 132 | ); 133 | this.body = new Ammo.btRigidBody(this.rbInfo); 134 | this.body.setActivationState(ACTIVATION_STATES.indexOf(data.activationState) + 1); 135 | this.body.setSleepingThresholds(data.linearSleepingThreshold, data.angularSleepingThreshold); 136 | 137 | this.body.setDamping(data.linearDamping, data.angularDamping); 138 | 139 | const angularFactor = new Ammo.btVector3(data.angularFactor.x, data.angularFactor.y, data.angularFactor.z); 140 | this.body.setAngularFactor(angularFactor); 141 | Ammo.destroy(angularFactor); 142 | 143 | const gravity = new Ammo.btVector3(data.gravity.x, data.gravity.y, data.gravity.z); 144 | if (!almostEqualsBtVector3(0.001, gravity, this.system.driver.physicsWorld.getGravity())) { 145 | this.body.setGravity(gravity); 146 | this.body.setFlags(RIGID_BODY_FLAGS.DISABLE_WORLD_GRAVITY); 147 | } 148 | Ammo.destroy(gravity); 149 | 150 | this.updateCollisionFlags(); 151 | 152 | this.el.body = this.body; 153 | this.body.el = el; 154 | 155 | this.isLoaded = true; 156 | 157 | this.el.emit("body-loaded", { body: this.el.body }); 158 | 159 | this._addToSystem(); 160 | }; 161 | })(), 162 | 163 | tick: function() { 164 | if (this.system.initialized && !this.isLoaded && this.loadedEventFired) { 165 | this.initBody(); 166 | } 167 | }, 168 | 169 | _updateShapes: (function() { 170 | const needsPolyhedralInitialization = [SHAPE.HULL, SHAPE.HACD, SHAPE.VHACD]; 171 | return function() { 172 | let updated = false; 173 | 174 | const obj = this.el.object3D; 175 | if (this.data.scaleAutoUpdate && this.prevScale && !almostEqualsVector3(0.001, obj.scale, this.prevScale)) { 176 | this.prevScale.copy(obj.scale); 177 | updated = true; 178 | 179 | this.localScaling.setValue(this.prevScale.x, this.prevScale.y, this.prevScale.z); 180 | this.compoundShape.setLocalScaling(this.localScaling); 181 | } 182 | 183 | if (this.shapeComponentsChanged) { 184 | this.shapeComponentsChanged = false; 185 | updated = true; 186 | for (let i = 0; i < this.shapeComponents.length; i++) { 187 | const shapeComponent = this.shapeComponents[i]; 188 | if (shapeComponent.getShapes().length === 0) { 189 | this._createCollisionShape(shapeComponent); 190 | } 191 | const collisionShapes = shapeComponent.getShapes(); 192 | for (let j = 0; j < collisionShapes.length; j++) { 193 | const collisionShape = collisionShapes[j]; 194 | if (!collisionShape.added) { 195 | this.compoundShape.addChildShape(collisionShape.localTransform, collisionShape); 196 | collisionShape.added = true; 197 | } 198 | } 199 | } 200 | 201 | if (this.data.type === TYPE.DYNAMIC) { 202 | this.updateMass(); 203 | } 204 | 205 | this.system.driver.updateBody(this.body); 206 | } 207 | 208 | //call initializePolyhedralFeatures for hull shapes if debug is turned on and/or scale changes 209 | if (this.system.debug && (updated || !this.polyHedralFeaturesInitialized)) { 210 | for (let i = 0; i < this.shapeComponents.length; i++) { 211 | const collisionShapes = this.shapeComponents[i].getShapes(); 212 | for (let j = 0; j < collisionShapes.length; j++) { 213 | const collisionShape = collisionShapes[j]; 214 | if (needsPolyhedralInitialization.indexOf(collisionShape.type) !== -1) { 215 | collisionShape.initializePolyhedralFeatures(0); 216 | } 217 | } 218 | } 219 | this.polyHedralFeaturesInitialized = true; 220 | } 221 | }; 222 | })(), 223 | 224 | _createCollisionShape: function(shapeComponent) { 225 | const data = shapeComponent.data; 226 | const collisionShapes = threeToAmmo.createCollisionShapes(shapeComponent.getMesh(), data); 227 | shapeComponent.addShapes(collisionShapes); 228 | return; 229 | }, 230 | 231 | /** 232 | * Registers the component with the physics system. 233 | */ 234 | play: function() { 235 | if (this.isLoaded) { 236 | this._addToSystem(); 237 | } 238 | }, 239 | 240 | _addToSystem: function() { 241 | if (!this.addedToSystem) { 242 | this.system.addBody(this.body, this.data.collisionFilterGroup, this.data.collisionFilterMask); 243 | 244 | if (this.data.emitCollisionEvents) { 245 | this.system.driver.addEventListener(this.body); 246 | } 247 | 248 | this.system.addComponent(this); 249 | this.addedToSystem = true; 250 | } 251 | }, 252 | 253 | /** 254 | * Unregisters the component with the physics system. 255 | */ 256 | pause: function() { 257 | if (this.addedToSystem) { 258 | this.system.removeComponent(this); 259 | this.system.removeBody(this.body); 260 | this.addedToSystem = false; 261 | } 262 | }, 263 | 264 | /** 265 | * Updates the rigid body instance, where possible. 266 | */ 267 | update: function(prevData) { 268 | if (this.isLoaded) { 269 | if (!this.hasUpdated) { 270 | //skip the first update 271 | this.hasUpdated = true; 272 | return; 273 | } 274 | 275 | const data = this.data; 276 | 277 | if (prevData.type !== data.type || prevData.disableCollision !== data.disableCollision) { 278 | this.updateCollisionFlags(); 279 | } 280 | 281 | if (prevData.activationState !== data.activationState) { 282 | this.body.forceActivationState(ACTIVATION_STATES.indexOf(data.activationState) + 1); 283 | if (data.activationState === ACTIVATION_STATE.ACTIVE_TAG) { 284 | this.body.activate(true); 285 | } 286 | } 287 | 288 | if ( 289 | prevData.collisionFilterGroup !== data.collisionFilterGroup || 290 | prevData.collisionFilterMask !== data.collisionFilterMask 291 | ) { 292 | const broadphaseProxy = this.body.getBroadphaseProxy(); 293 | broadphaseProxy.set_m_collisionFilterGroup(data.collisionFilterGroup); 294 | broadphaseProxy.set_m_collisionFilterMask(data.collisionFilterMask); 295 | this.system.driver.broadphase 296 | .getOverlappingPairCache() 297 | .removeOverlappingPairsContainingProxy(broadphaseProxy, this.system.driver.dispatcher); 298 | } 299 | 300 | if (prevData.linearDamping != data.linearDamping || prevData.angularDamping != data.angularDamping) { 301 | this.body.setDamping(data.linearDamping, data.angularDamping); 302 | } 303 | 304 | if (!almostEqualsVector3(0.001, prevData.gravity, data.gravity)) { 305 | const gravity = new Ammo.btVector3(data.gravity.x, data.gravity.y, data.gravity.z); 306 | if (!almostEqualsBtVector3(0.001, gravity, this.system.driver.physicsWorld.getGravity())) { 307 | this.body.setFlags(RIGID_BODY_FLAGS.DISABLE_WORLD_GRAVITY); 308 | } else { 309 | this.body.setFlags(RIGID_BODY_FLAGS.NONE); 310 | } 311 | this.body.setGravity(gravity); 312 | Ammo.destroy(gravity); 313 | } 314 | 315 | if ( 316 | prevData.linearSleepingThreshold != data.linearSleepingThreshold || 317 | prevData.angularSleepingThreshold != data.angularSleepingThreshold 318 | ) { 319 | this.body.setSleepingThresholds(data.linearSleepingThreshold, data.angularSleepingThreshold); 320 | } 321 | 322 | if (!almostEqualsVector3(0.001, prevData.angularFactor, data.angularFactor)) { 323 | const angularFactor = new Ammo.btVector3(data.angularFactor.x, data.angularFactor.y, data.angularFactor.z); 324 | this.body.setAngularFactor(angularFactor); 325 | Ammo.destroy(angularFactor); 326 | } 327 | 328 | //TODO: support dynamic update for other properties 329 | } 330 | }, 331 | 332 | /** 333 | * Removes the component and all physics and scene side effects. 334 | */ 335 | remove: function() { 336 | if (this.triMesh) Ammo.destroy(this.triMesh); 337 | if (this.localScaling) Ammo.destroy(this.localScaling); 338 | if (this.compoundShape) Ammo.destroy(this.compoundShape); 339 | if (this.body) { 340 | Ammo.destroy(this.body); 341 | delete this.body; 342 | } 343 | Ammo.destroy(this.rbInfo); 344 | Ammo.destroy(this.msTransform); 345 | Ammo.destroy(this.motionState); 346 | Ammo.destroy(this.localInertia); 347 | Ammo.destroy(this.rotation); 348 | }, 349 | 350 | beforeStep: function() { 351 | this._updateShapes(); 352 | if (this.data.type !== TYPE.DYNAMIC) { 353 | this.syncToPhysics(); 354 | } 355 | }, 356 | 357 | step: function() { 358 | if (this.data.type === TYPE.DYNAMIC) { 359 | this.syncFromPhysics(); 360 | } 361 | }, 362 | 363 | /** 364 | * Updates the rigid body's position, velocity, and rotation, based on the scene. 365 | */ 366 | syncToPhysics: (function() { 367 | const q = new THREE.Quaternion(); 368 | const v = new THREE.Vector3(); 369 | const q2 = new THREE.Vector3(); 370 | const v2 = new THREE.Vector3(); 371 | return function() { 372 | const el = this.el, 373 | parentEl = el.parentEl, 374 | body = this.body; 375 | 376 | if (!body) return; 377 | 378 | this.motionState.getWorldTransform(this.msTransform); 379 | 380 | if (parentEl.isScene) { 381 | v.copy(el.object3D.position); 382 | q.copy(el.object3D.quaternion); 383 | } else { 384 | el.object3D.getWorldPosition(v); 385 | el.object3D.getWorldQuaternion(q); 386 | } 387 | 388 | const position = this.msTransform.getOrigin(); 389 | v2.set(position.x(), position.y(), position.z()); 390 | 391 | const quaternion = this.msTransform.getRotation(); 392 | q2.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w()); 393 | 394 | if (!almostEqualsVector3(0.001, v, v2) || !almostEqualsQuaternion(0.001, q, q2)) { 395 | if (!this.body.isActive()) { 396 | this.body.activate(true); 397 | } 398 | this.msTransform.getOrigin().setValue(v.x, v.y, v.z); 399 | this.rotation.setValue(q.x, q.y, q.z, q.w); 400 | this.msTransform.setRotation(this.rotation); 401 | this.motionState.setWorldTransform(this.msTransform); 402 | 403 | if (this.data.type === TYPE.STATIC) { 404 | this.body.setCenterOfMassTransform(this.msTransform); 405 | } 406 | } 407 | }; 408 | })(), 409 | 410 | /** 411 | * Updates the scene object's position and rotation, based on the physics simulation. 412 | */ 413 | syncFromPhysics: (function() { 414 | const v = new THREE.Vector3(), 415 | q1 = new THREE.Quaternion(), 416 | q2 = new THREE.Quaternion(); 417 | return function() { 418 | this.motionState.getWorldTransform(this.msTransform); 419 | const position = this.msTransform.getOrigin(); 420 | const quaternion = this.msTransform.getRotation(); 421 | 422 | const el = this.el, 423 | parentEl = el.parentEl, 424 | body = this.body; 425 | 426 | if (!body) return; 427 | if (!parentEl) return; 428 | 429 | if (parentEl.isScene) { 430 | el.object3D.position.set(position.x(), position.y(), position.z()); 431 | el.object3D.quaternion.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w()); 432 | } else { 433 | q1.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w()); 434 | parentEl.object3D.getWorldQuaternion(q2); 435 | q1.multiply(q2.invert()); 436 | el.object3D.quaternion.copy(q1); 437 | 438 | v.set(position.x(), position.y(), position.z()); 439 | parentEl.object3D.worldToLocal(v); 440 | el.object3D.position.copy(v); 441 | } 442 | }; 443 | })(), 444 | 445 | addShapeComponent: function(shapeComponent) { 446 | if (shapeComponent.data.type === SHAPE.MESH && this.data.type !== TYPE.STATIC) { 447 | console.warn("non-static mesh colliders not supported"); 448 | return; 449 | } 450 | 451 | this.shapeComponents.push(shapeComponent); 452 | this.shapeComponentsChanged = true; 453 | }, 454 | 455 | removeShapeComponent: function(shapeComponent) { 456 | const index = this.shapeComponents.indexOf(shapeComponent); 457 | if (this.compoundShape && index !== -1 && this.body) { 458 | const shapes = shapeComponent.getShapes(); 459 | for (var i = 0; i < shapes.length; i++) { 460 | this.compoundShape.removeChildShape(shapes[i]); 461 | } 462 | this.shapeComponentsChanged = true; 463 | this.shapeComponents.splice(index, 1); 464 | } 465 | }, 466 | 467 | updateMass: function() { 468 | const shape = this.body.getCollisionShape(); 469 | const mass = this.data.type === TYPE.DYNAMIC ? this.data.mass : 0; 470 | shape.calculateLocalInertia(mass, this.localInertia); 471 | this.body.setMassProps(mass, this.localInertia); 472 | this.body.updateInertiaTensor(); 473 | }, 474 | 475 | updateCollisionFlags: function() { 476 | let flags = this.data.disableCollision ? 4 : 0; 477 | switch (this.data.type) { 478 | case TYPE.STATIC: 479 | flags |= COLLISION_FLAG.STATIC_OBJECT; 480 | break; 481 | case TYPE.KINEMATIC: 482 | flags |= COLLISION_FLAG.KINEMATIC_OBJECT; 483 | break; 484 | default: 485 | this.body.applyGravity(); 486 | break; 487 | } 488 | this.body.setCollisionFlags(flags); 489 | 490 | this.updateMass(); 491 | 492 | // TODO: enable CCD if dynamic? 493 | // this.body.setCcdMotionThreshold(0.001); 494 | // this.body.setCcdSweptSphereRadius(0.001); 495 | 496 | this.system.driver.updateBody(this.body); 497 | }, 498 | 499 | getVelocity: function() { 500 | return this.body.getLinearVelocity(); 501 | } 502 | }; 503 | 504 | module.exports.definition = AmmoBody; 505 | module.exports.Component = AFRAME.registerComponent("ammo-body", AmmoBody); 506 | -------------------------------------------------------------------------------- /src/components/body/body.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'), 2 | mesh2shape = require('three-to-cannon').threeToCannon; 3 | 4 | require('../../../lib/CANNON-shape2mesh'); 5 | 6 | var Body = { 7 | dependencies: ['velocity'], 8 | 9 | schema: { 10 | mass: {default: 5, if: {type: 'dynamic'}}, 11 | linearDamping: { default: 0.01, if: {type: 'dynamic'}}, 12 | angularDamping: { default: 0.01, if: {type: 'dynamic'}}, 13 | shape: {default: 'auto', oneOf: ['auto', 'box', 'cylinder', 'sphere', 'hull', 'mesh', 'none']}, 14 | cylinderAxis: {default: 'y', oneOf: ['x', 'y', 'z']}, 15 | sphereRadius: {default: NaN}, 16 | type: {default: 'dynamic', oneOf: ['static', 'dynamic']} 17 | }, 18 | 19 | /** 20 | * Initializes a body component, assigning it to the physics system and binding listeners for 21 | * parsing the elements geometry. 22 | */ 23 | init: function () { 24 | this.system = this.el.sceneEl.systems.physics; 25 | 26 | if (this.el.sceneEl.hasLoaded) { 27 | this.initBody(); 28 | } else { 29 | this.el.sceneEl.addEventListener('loaded', this.initBody.bind(this)); 30 | } 31 | }, 32 | 33 | /** 34 | * Parses an element's geometry and component metadata to create a CANNON.Body instance for the 35 | * component. 36 | */ 37 | initBody: function () { 38 | var el = this.el, 39 | data = this.data; 40 | 41 | var obj = this.el.object3D; 42 | var pos = obj.position; 43 | var quat = obj.quaternion; 44 | 45 | this.body = new CANNON.Body({ 46 | mass: data.type === 'static' ? 0 : data.mass || 0, 47 | material: this.system.getMaterial('defaultMaterial'), 48 | position: new CANNON.Vec3(pos.x, pos.y, pos.z), 49 | quaternion: new CANNON.Quaternion(quat.x, quat.y, quat.z, quat.w), 50 | linearDamping: data.linearDamping, 51 | angularDamping: data.angularDamping, 52 | type: data.type === 'dynamic' ? CANNON.Body.DYNAMIC : CANNON.Body.STATIC, 53 | }); 54 | 55 | // Matrix World must be updated at root level, if scale is to be applied – updateMatrixWorld() 56 | // only checks an object's parent, not the rest of the ancestors. Hence, a wrapping entity with 57 | // scale="0.5 0.5 0.5" will be ignored. 58 | // Reference: https://github.com/mrdoob/three.js/blob/master/src/core/Object3D.js#L511-L541 59 | // Potential fix: https://github.com/mrdoob/three.js/pull/7019 60 | this.el.object3D.updateMatrixWorld(true); 61 | 62 | if(data.shape !== 'none') { 63 | var options = data.shape === 'auto' ? undefined : AFRAME.utils.extend({}, this.data, { 64 | type: mesh2shape.Type[data.shape.toUpperCase()] 65 | }); 66 | 67 | var shape = mesh2shape(this.el.object3D, options); 68 | 69 | if (!shape) { 70 | el.addEventListener('object3dset', this.initBody.bind(this)); 71 | return; 72 | } 73 | this.body.addShape(shape, shape.offset, shape.orientation); 74 | 75 | // Show wireframe 76 | if (this.system.debug) { 77 | this.shouldUpdateWireframe = true; 78 | } 79 | 80 | this.isLoaded = true; 81 | } 82 | 83 | this.el.body = this.body; 84 | this.body.el = el; 85 | 86 | // If component wasn't initialized when play() was called, finish up. 87 | if (this.isPlaying) { 88 | this._play(); 89 | } 90 | 91 | if (this.isLoaded) { 92 | this.el.emit('body-loaded', {body: this.el.body}); 93 | } 94 | }, 95 | 96 | addShape: function(shape, offset, orientation) { 97 | if (this.data.shape !== 'none') { 98 | console.warn('shape can only be added if shape property is none'); 99 | return; 100 | } 101 | 102 | if (!shape) { 103 | console.warn('shape cannot be null'); 104 | return; 105 | } 106 | 107 | if (!this.body) { 108 | console.warn('shape cannot be added before body is loaded'); 109 | return; 110 | } 111 | this.body.addShape(shape, offset, orientation); 112 | 113 | if (this.system.debug) { 114 | this.shouldUpdateWireframe = true; 115 | } 116 | 117 | this.shouldUpdateBody = true; 118 | }, 119 | 120 | tick: function () { 121 | if (this.shouldUpdateBody) { 122 | this.isLoaded = true; 123 | 124 | this._play(); 125 | 126 | this.el.emit('body-loaded', {body: this.el.body}); 127 | this.shouldUpdateBody = false; 128 | } 129 | 130 | if (this.shouldUpdateWireframe) { 131 | this.createWireframe(this.body); 132 | this.shouldUpdateWireframe = false; 133 | } 134 | }, 135 | 136 | /** 137 | * Registers the component with the physics system, if ready. 138 | */ 139 | play: function () { 140 | if (this.isLoaded) this._play(); 141 | }, 142 | 143 | /** 144 | * Internal helper to register component with physics system. 145 | */ 146 | _play: function () { 147 | this.syncToPhysics(); 148 | this.system.addComponent(this); 149 | this.system.addBody(this.body); 150 | if (this.wireframe) this.el.sceneEl.object3D.add(this.wireframe); 151 | }, 152 | 153 | /** 154 | * Unregisters the component with the physics system. 155 | */ 156 | pause: function () { 157 | if (this.isLoaded) this._pause(); 158 | }, 159 | 160 | _pause: function () { 161 | this.system.removeComponent(this); 162 | if (this.body) this.system.removeBody(this.body); 163 | if (this.wireframe) this.el.sceneEl.object3D.remove(this.wireframe); 164 | }, 165 | 166 | /** 167 | * Updates the CANNON.Body instance, where possible. 168 | */ 169 | update: function (prevData) { 170 | if (!this.body) return; 171 | 172 | var data = this.data; 173 | 174 | if (prevData.type != undefined && data.type != prevData.type) { 175 | this.body.type = data.type === 'dynamic' ? CANNON.Body.DYNAMIC : CANNON.Body.STATIC; 176 | } 177 | 178 | this.body.mass = data.mass || 0; 179 | if (data.type === 'dynamic') { 180 | this.body.linearDamping = data.linearDamping; 181 | this.body.angularDamping = data.angularDamping; 182 | } 183 | if (data.mass !== prevData.mass) { 184 | this.body.updateMassProperties(); 185 | } 186 | if (this.body.updateProperties) this.body.updateProperties(); 187 | }, 188 | 189 | /** 190 | * Removes the component and all physics and scene side effects. 191 | */ 192 | remove: function () { 193 | if (this.body) { 194 | delete this.body.el; 195 | delete this.body; 196 | } 197 | delete this.el.body; 198 | delete this.wireframe; 199 | }, 200 | 201 | beforeStep: function () { 202 | if (this.body.mass === 0) { 203 | this.syncToPhysics(); 204 | } 205 | }, 206 | 207 | step: function () { 208 | if (this.body.mass !== 0) { 209 | this.syncFromPhysics(); 210 | } 211 | }, 212 | 213 | /** 214 | * Creates a wireframe for the body, for debugging. 215 | * TODO(donmccurdy) – Refactor this into a standalone utility or component. 216 | * @param {CANNON.Body} body 217 | * @param {CANNON.Shape} shape 218 | */ 219 | createWireframe: function (body) { 220 | if (this.wireframe) { 221 | this.el.sceneEl.object3D.remove(this.wireframe); 222 | delete this.wireframe; 223 | } 224 | this.wireframe = new THREE.Object3D(); 225 | this.el.sceneEl.object3D.add(this.wireframe); 226 | 227 | var offset, mesh; 228 | var orientation = new THREE.Quaternion(); 229 | for (var i = 0; i < this.body.shapes.length; i++) 230 | { 231 | offset = this.body.shapeOffsets[i], 232 | orientation.copy(this.body.shapeOrientations[i]), 233 | mesh = CANNON.shape2mesh(this.body).children[i]; 234 | 235 | var wireframe = new THREE.LineSegments( 236 | new THREE.EdgesGeometry(mesh.geometry), 237 | new THREE.LineBasicMaterial({color: 0xff0000}) 238 | ); 239 | 240 | if (offset) { 241 | wireframe.position.copy(offset); 242 | } 243 | 244 | if (orientation) { 245 | wireframe.quaternion.copy(orientation); 246 | } 247 | 248 | this.wireframe.add(wireframe); 249 | } 250 | 251 | this.syncWireframe(); 252 | }, 253 | 254 | /** 255 | * Updates the debugging wireframe's position and rotation. 256 | */ 257 | syncWireframe: function () { 258 | var offset, 259 | wireframe = this.wireframe; 260 | 261 | if (!this.wireframe) return; 262 | 263 | // Apply rotation. If the shape required custom orientation, also apply 264 | // that on the wireframe. 265 | wireframe.quaternion.copy(this.body.quaternion); 266 | if (wireframe.orientation) { 267 | wireframe.quaternion.multiply(wireframe.orientation); 268 | } 269 | 270 | // Apply position. If the shape required custom offset, also apply that on 271 | // the wireframe. 272 | wireframe.position.copy(this.body.position); 273 | if (wireframe.offset) { 274 | offset = wireframe.offset.clone().applyQuaternion(wireframe.quaternion); 275 | wireframe.position.add(offset); 276 | } 277 | 278 | wireframe.updateMatrix(); 279 | }, 280 | 281 | /** 282 | * Updates the CANNON.Body instance's position, velocity, and rotation, based on the scene. 283 | */ 284 | syncToPhysics: (function () { 285 | var q = new THREE.Quaternion(), 286 | v = new THREE.Vector3(); 287 | return function () { 288 | var el = this.el, 289 | parentEl = el.parentEl, 290 | body = this.body; 291 | 292 | if (!body) return; 293 | 294 | if (el.components.velocity) body.velocity.copy(el.getAttribute('velocity')); 295 | 296 | if (parentEl.isScene) { 297 | body.quaternion.copy(el.object3D.quaternion); 298 | body.position.copy(el.object3D.position); 299 | } else { 300 | el.object3D.getWorldQuaternion(q); 301 | body.quaternion.copy(q); 302 | el.object3D.getWorldPosition(v); 303 | body.position.copy(v); 304 | } 305 | 306 | if (this.body.updateProperties) this.body.updateProperties(); 307 | if (this.wireframe) this.syncWireframe(); 308 | }; 309 | }()), 310 | 311 | /** 312 | * Updates the scene object's position and rotation, based on the physics simulation. 313 | */ 314 | syncFromPhysics: (function () { 315 | var v = new THREE.Vector3(), 316 | q1 = new THREE.Quaternion(), 317 | q2 = new THREE.Quaternion(); 318 | return function () { 319 | var el = this.el, 320 | parentEl = el.parentEl, 321 | body = this.body; 322 | 323 | if (!body) return; 324 | if (!parentEl) return; 325 | 326 | if (parentEl.isScene) { 327 | el.object3D.quaternion.copy(body.quaternion); 328 | el.object3D.position.copy(body.position); 329 | } else { 330 | q1.copy(body.quaternion); 331 | parentEl.object3D.getWorldQuaternion(q2); 332 | q1.premultiply(q2.invert()); 333 | el.object3D.quaternion.copy(q1); 334 | 335 | v.copy(body.position); 336 | parentEl.object3D.worldToLocal(v); 337 | el.object3D.position.copy(v); 338 | } 339 | 340 | if (this.wireframe) this.syncWireframe(); 341 | }; 342 | }()) 343 | }; 344 | 345 | module.exports.definition = Body; 346 | module.exports.Component = AFRAME.registerComponent('body', Body); 347 | -------------------------------------------------------------------------------- /src/components/body/dynamic-body.js: -------------------------------------------------------------------------------- 1 | var Body = require('./body'); 2 | 3 | /** 4 | * Dynamic body. 5 | * 6 | * Moves according to physics simulation, and may collide with other objects. 7 | */ 8 | var DynamicBody = AFRAME.utils.extend({}, Body.definition); 9 | 10 | module.exports = AFRAME.registerComponent('dynamic-body', DynamicBody); 11 | -------------------------------------------------------------------------------- /src/components/body/static-body.js: -------------------------------------------------------------------------------- 1 | var Body = require('./body'); 2 | 3 | /** 4 | * Static body. 5 | * 6 | * Solid body with a fixed position. Unaffected by gravity and collisions, but 7 | * other objects may collide with it. 8 | */ 9 | var StaticBody = AFRAME.utils.extend({}, Body.definition); 10 | 11 | StaticBody.schema = AFRAME.utils.extend({}, Body.definition.schema, { 12 | type: {default: 'static', oneOf: ['static', 'dynamic']}, 13 | mass: {default: 0} 14 | }); 15 | 16 | module.exports = AFRAME.registerComponent('static-body', StaticBody); 17 | -------------------------------------------------------------------------------- /src/components/constraint.js: -------------------------------------------------------------------------------- 1 | var CANNON = require("cannon-es"); 2 | 3 | module.exports = AFRAME.registerComponent("constraint", { 4 | multiple: true, 5 | 6 | schema: { 7 | // Type of constraint. 8 | type: { default: "lock", oneOf: ["coneTwist", "distance", "hinge", "lock", "pointToPoint"] }, 9 | 10 | // Target (other) body for the constraint. 11 | target: { type: "selector" }, 12 | 13 | // Maximum force that should be applied to constraint the bodies. 14 | maxForce: { default: 1e6, min: 0 }, 15 | 16 | // If true, bodies can collide when they are connected. 17 | collideConnected: { default: true }, 18 | 19 | // Wake up bodies when connected. 20 | wakeUpBodies: { default: true }, 21 | 22 | // The distance to be kept between the bodies. If 0, will be set to current distance. 23 | distance: { default: 0, min: 0 }, 24 | 25 | // Offset of the hinge or point-to-point constraint, defined locally in the body. 26 | pivot: { type: "vec3" }, 27 | targetPivot: { type: "vec3" }, 28 | 29 | // An axis that each body can rotate around, defined locally to that body. 30 | axis: { type: "vec3", default: { x: 0, y: 0, z: 1 } }, 31 | targetAxis: { type: "vec3", default: { x: 0, y: 0, z: 1 } } 32 | }, 33 | 34 | init: function() { 35 | this.system = this.el.sceneEl.systems.physics; 36 | this.constraint = /* {CANNON.Constraint} */ null; 37 | }, 38 | 39 | remove: function() { 40 | if (!this.constraint) return; 41 | 42 | this.system.removeConstraint(this.constraint); 43 | this.constraint = null; 44 | }, 45 | 46 | update: function() { 47 | var el = this.el, 48 | data = this.data; 49 | 50 | this.remove(); 51 | 52 | if (!el.body || !data.target.body) { 53 | (el.body ? data.target : el).addEventListener("body-loaded", this.update.bind(this, {})); 54 | return; 55 | } 56 | 57 | this.constraint = this.createConstraint(); 58 | this.system.addConstraint(this.constraint); 59 | }, 60 | 61 | /** 62 | * Creates a new constraint, given current component data. The CANNON.js constructors are a bit 63 | * different for each constraint type. A `.type` property is added to each constraint, because 64 | * `instanceof` checks are not reliable for some types. These types are needed for serialization. 65 | * @return {CANNON.Constraint} 66 | */ 67 | createConstraint: function() { 68 | var constraint, 69 | data = this.data, 70 | pivot = new CANNON.Vec3(data.pivot.x, data.pivot.y, data.pivot.z), 71 | targetPivot = new CANNON.Vec3(data.targetPivot.x, data.targetPivot.y, data.targetPivot.z), 72 | axis = new CANNON.Vec3(data.axis.x, data.axis.y, data.axis.z), 73 | targetAxis = new CANNON.Vec3(data.targetAxis.x, data.targetAxis.y, data.targetAxis.z); 74 | 75 | var constraint; 76 | 77 | switch (data.type) { 78 | case "lock": 79 | constraint = new CANNON.LockConstraint(this.el.body, data.target.body, { maxForce: data.maxForce }); 80 | constraint.type = "LockConstraint"; 81 | break; 82 | 83 | case "distance": 84 | constraint = new CANNON.DistanceConstraint(this.el.body, data.target.body, data.distance, data.maxForce); 85 | constraint.type = "DistanceConstraint"; 86 | break; 87 | 88 | case "hinge": 89 | constraint = new CANNON.HingeConstraint(this.el.body, data.target.body, { 90 | pivotA: pivot, 91 | pivotB: targetPivot, 92 | axisA: axis, 93 | axisB: targetAxis, 94 | maxForce: data.maxForce 95 | }); 96 | constraint.type = "HingeConstraint"; 97 | break; 98 | 99 | case "coneTwist": 100 | constraint = new CANNON.ConeTwistConstraint(this.el.body, data.target.body, { 101 | pivotA: pivot, 102 | pivotB: targetPivot, 103 | axisA: axis, 104 | axisB: targetAxis, 105 | maxForce: data.maxForce 106 | }); 107 | constraint.type = "ConeTwistConstraint"; 108 | break; 109 | 110 | case "pointToPoint": 111 | constraint = new CANNON.PointToPointConstraint( 112 | this.el.body, 113 | pivot, 114 | data.target.body, 115 | targetPivot, 116 | data.maxForce 117 | ); 118 | constraint.type = "PointToPointConstraint"; 119 | break; 120 | 121 | default: 122 | throw new Error("[constraint] Unexpected type: " + data.type); 123 | } 124 | 125 | constraint.collideConnected = data.collideConnected; 126 | return constraint; 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /src/components/math/README.md: -------------------------------------------------------------------------------- 1 | # Math 2 | 3 | Helpers for physics and controls components. 4 | 5 | - **velocity**: Updates an entity's position at each clock tick, according to a constant (or animateable) velocity. 6 | 7 | ## Usage 8 | 9 | Velocity: 10 | 11 | ```html 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /src/components/math/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'velocity': require('./velocity'), 3 | 4 | registerAll: function (AFRAME) { 5 | if (this._registered) return; 6 | 7 | AFRAME = AFRAME || window.AFRAME; 8 | 9 | if (!AFRAME.components['velocity']) AFRAME.registerComponent('velocity', this.velocity); 10 | 11 | this._registered = true; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/math/velocity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Velocity, in m/s. 3 | */ 4 | module.exports = AFRAME.registerComponent('velocity', { 5 | schema: {type: 'vec3'}, 6 | 7 | init: function () { 8 | this.system = this.el.sceneEl.systems.physics; 9 | 10 | if (this.system) { 11 | this.system.addComponent(this); 12 | } 13 | }, 14 | 15 | remove: function () { 16 | if (this.system) { 17 | this.system.removeComponent(this); 18 | } 19 | }, 20 | 21 | tick: function (t, dt) { 22 | if (!dt) return; 23 | if (this.system) return; 24 | this.afterStep(t, dt); 25 | }, 26 | 27 | afterStep: function (t, dt) { 28 | if (!dt) return; 29 | 30 | var physics = this.el.sceneEl.systems.physics || {data: {maxInterval: 1 / 60}}, 31 | 32 | // TODO - There's definitely a bug with getComputedAttribute and el.data. 33 | velocity = this.el.getAttribute('velocity') || {x: 0, y: 0, z: 0}, 34 | position = this.el.object3D.position || {x: 0, y: 0, z: 0}; 35 | 36 | dt = Math.min(dt, physics.data.maxInterval * 1000); 37 | 38 | this.el.object3D.position.set( 39 | position.x + velocity.x * dt / 1000, 40 | position.y + velocity.y * dt / 1000, 41 | position.z + velocity.z * dt / 1000 42 | ); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/shape/ammo-shape.js: -------------------------------------------------------------------------------- 1 | /* global Ammo,THREE */ 2 | const threeToAmmo = require("three-to-ammo"); 3 | const CONSTANTS = require("../../constants"), 4 | SHAPE = CONSTANTS.SHAPE, 5 | FIT = CONSTANTS.FIT; 6 | 7 | var AmmoShape = { 8 | schema: { 9 | type: { 10 | default: SHAPE.HULL, 11 | oneOf: [ 12 | SHAPE.BOX, 13 | SHAPE.CYLINDER, 14 | SHAPE.SPHERE, 15 | SHAPE.CAPSULE, 16 | SHAPE.CONE, 17 | SHAPE.HULL, 18 | SHAPE.HACD, 19 | SHAPE.VHACD, 20 | SHAPE.MESH, 21 | SHAPE.HEIGHTFIELD 22 | ] 23 | }, 24 | fit: { default: FIT.ALL, oneOf: [FIT.ALL, FIT.MANUAL] }, 25 | halfExtents: { type: "vec3", default: { x: 1, y: 1, z: 1 } }, 26 | minHalfExtent: { default: 0 }, 27 | maxHalfExtent: { default: Number.POSITIVE_INFINITY }, 28 | sphereRadius: { default: NaN }, 29 | cylinderAxis: { default: "y", oneOf: ["x", "y", "z"] }, 30 | margin: { default: 0.01 }, 31 | offset: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, 32 | orientation: { type: "vec4", default: { x: 0, y: 0, z: 0, w: 1 } }, 33 | heightfieldData: { default: [] }, 34 | heightfieldDistance: { default: 1 }, 35 | includeInvisible: { default: false } 36 | }, 37 | 38 | multiple: true, 39 | 40 | init: function() { 41 | this.system = this.el.sceneEl.systems.physics; 42 | this.collisionShapes = []; 43 | 44 | let bodyEl = this.el; 45 | this.body = bodyEl.components["ammo-body"] || null; 46 | while (!this.body && bodyEl.parentNode != this.el.sceneEl) { 47 | bodyEl = bodyEl.parentNode; 48 | if (bodyEl.components["ammo-body"]) { 49 | this.body = bodyEl.components["ammo-body"]; 50 | } 51 | } 52 | if (!this.body) { 53 | console.warn("body not found"); 54 | return; 55 | } 56 | if (this.data.fit !== FIT.MANUAL) { 57 | if (!this.el.object3DMap.mesh) { 58 | console.error("Cannot use FIT.ALL without object3DMap.mesh"); 59 | return; 60 | } 61 | this.mesh = this.el.object3DMap.mesh; 62 | } 63 | this.body.addShapeComponent(this); 64 | }, 65 | 66 | getMesh: function() { 67 | return this.mesh || null; 68 | }, 69 | 70 | addShapes: function(collisionShapes) { 71 | this.collisionShapes = collisionShapes; 72 | }, 73 | 74 | getShapes: function() { 75 | return this.collisionShapes; 76 | }, 77 | 78 | remove: function() { 79 | if (!this.body) { 80 | return; 81 | } 82 | 83 | this.body.removeShapeComponent(this); 84 | 85 | while (this.collisionShapes.length > 0) { 86 | const collisionShape = this.collisionShapes.pop(); 87 | collisionShape.destroy(); 88 | Ammo.destroy(collisionShape.localTransform); 89 | } 90 | } 91 | }; 92 | 93 | module.exports.definition = AmmoShape; 94 | module.exports.Component = AFRAME.registerComponent("ammo-shape", AmmoShape); 95 | -------------------------------------------------------------------------------- /src/components/shape/shape.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'); 2 | 3 | var Shape = { 4 | schema: { 5 | shape: {default: 'box', oneOf: ['box', 'sphere', 'cylinder']}, 6 | offset: {type: 'vec3', default: {x: 0, y: 0, z: 0}}, 7 | orientation: {type: 'vec4', default: {x: 0, y: 0, z: 0, w: 1}}, 8 | 9 | // sphere 10 | radius: {type: 'number', default: 1, if: {shape: ['sphere']}}, 11 | 12 | // box 13 | halfExtents: {type: 'vec3', default: {x: 0.5, y: 0.5, z: 0.5}, if: {shape: ['box']}}, 14 | 15 | // cylinder 16 | radiusTop: {type: 'number', default: 1, if: {shape: ['cylinder']}}, 17 | radiusBottom: {type: 'number', default: 1, if: {shape: ['cylinder']}}, 18 | height: {type: 'number', default: 1, if: {shape: ['cylinder']}}, 19 | numSegments: {type: 'int', default: 8, if: {shape: ['cylinder']}} 20 | }, 21 | 22 | multiple: true, 23 | 24 | init: function() { 25 | if (this.el.sceneEl.hasLoaded) { 26 | this.initShape(); 27 | } else { 28 | this.el.sceneEl.addEventListener('loaded', this.initShape.bind(this)); 29 | } 30 | }, 31 | 32 | initShape: function() { 33 | this.bodyEl = this.el; 34 | var bodyType = this._findType(this.bodyEl); 35 | var data = this.data; 36 | 37 | while (!bodyType && this.bodyEl.parentNode != this.el.sceneEl) { 38 | this.bodyEl = this.bodyEl.parentNode; 39 | bodyType = this._findType(this.bodyEl); 40 | } 41 | 42 | if (!bodyType) { 43 | console.warn('body not found'); 44 | return; 45 | } 46 | 47 | var scale = new THREE.Vector3(); 48 | this.bodyEl.object3D.getWorldScale(scale); 49 | var shape, offset, orientation; 50 | 51 | if (data.hasOwnProperty('offset')) { 52 | offset = new CANNON.Vec3( 53 | data.offset.x * scale.x, 54 | data.offset.y * scale.y, 55 | data.offset.z * scale.z 56 | ); 57 | } 58 | 59 | if (data.hasOwnProperty('orientation')) { 60 | orientation = new CANNON.Quaternion(); 61 | orientation.copy(data.orientation); 62 | } 63 | 64 | switch(data.shape) { 65 | case 'sphere': 66 | shape = new CANNON.Sphere(data.radius * scale.x); 67 | break; 68 | case 'box': 69 | var halfExtents = new CANNON.Vec3( 70 | data.halfExtents.x * scale.x, 71 | data.halfExtents.y * scale.y, 72 | data.halfExtents.z * scale.z 73 | ); 74 | shape = new CANNON.Box(halfExtents); 75 | break; 76 | case 'cylinder': 77 | shape = new CANNON.Cylinder( 78 | data.radiusTop * scale.x, 79 | data.radiusBottom * scale.x, 80 | data.height * scale.y, 81 | data.numSegments 82 | ); 83 | 84 | //rotate by 90 degrees similar to mesh2shape:createCylinderShape 85 | var quat = new CANNON.Quaternion(); 86 | quat.setFromEuler(90 * THREE.Math.DEG2RAD, 0, 0, 'XYZ').normalize(); 87 | orientation.mult(quat, orientation); 88 | break; 89 | default: 90 | console.warn(data.shape + ' shape not supported'); 91 | return; 92 | } 93 | 94 | if (this.bodyEl.body) { 95 | this.bodyEl.components[bodyType].addShape(shape, offset, orientation); 96 | } else { 97 | this.bodyEl.addEventListener('body-loaded', function() { 98 | this.bodyEl.components[bodyType].addShape(shape, offset, orientation); 99 | }, {once: true}); 100 | } 101 | }, 102 | 103 | _findType: function(el) { 104 | if (el.hasAttribute('body')) { 105 | return 'body'; 106 | } else if (el.hasAttribute('dynamic-body')) { 107 | return 'dynamic-body'; 108 | } else if (el.hasAttribute('static-body')) { 109 | return'static-body'; 110 | } 111 | return null; 112 | }, 113 | 114 | remove: function() { 115 | if (this.bodyEl.parentNode) { 116 | console.warn('removing shape component not currently supported'); 117 | } 118 | } 119 | }; 120 | 121 | module.exports.definition = Shape; 122 | module.exports.Component = AFRAME.registerComponent('shape', Shape); 123 | -------------------------------------------------------------------------------- /src/components/spring.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'); 2 | 3 | module.exports = AFRAME.registerComponent('spring', { 4 | 5 | multiple: true, 6 | 7 | schema: { 8 | // Target (other) body for the constraint. 9 | target: {type: 'selector'}, 10 | 11 | // Length of the spring, when no force acts upon it. 12 | restLength: {default: 1, min: 0}, 13 | 14 | // How much will the spring suppress the force. 15 | stiffness: {default: 100, min: 0}, 16 | 17 | // Stretch factor of the spring. 18 | damping: {default: 1, min: 0}, 19 | 20 | // Offsets. 21 | localAnchorA: {type: 'vec3', default: {x: 0, y: 0, z: 0}}, 22 | localAnchorB: {type: 'vec3', default: {x: 0, y: 0, z: 0}}, 23 | }, 24 | 25 | init: function() { 26 | this.system = this.el.sceneEl.systems.physics; 27 | this.system.addComponent(this); 28 | this.isActive = true; 29 | this.spring = /* {CANNON.Spring} */ null; 30 | }, 31 | 32 | update: function(oldData) { 33 | var el = this.el; 34 | var data = this.data; 35 | 36 | if (!data.target) { 37 | console.warn('Spring: invalid target specified.'); 38 | return; 39 | } 40 | 41 | // wait until the CANNON bodies is created and attached 42 | if (!el.body || !data.target.body) { 43 | (el.body ? data.target : el).addEventListener('body-loaded', this.update.bind(this, {})); 44 | return; 45 | } 46 | 47 | // create the spring if necessary 48 | this.createSpring(); 49 | // apply new data to the spring 50 | this.updateSpring(oldData); 51 | }, 52 | 53 | updateSpring: function(oldData) { 54 | if (!this.spring) { 55 | console.warn('Spring: Component attempted to change spring before its created. No changes made.'); 56 | return; 57 | } 58 | var data = this.data; 59 | var spring = this.spring; 60 | 61 | // Cycle through the schema and check if an attribute has changed. 62 | // if so, apply it to the spring 63 | Object.keys(data).forEach(function(attr) { 64 | if (data[attr] !== oldData[attr]) { 65 | if (attr === 'target') { 66 | // special case for the target selector 67 | spring.bodyB = data.target.body; 68 | return; 69 | } 70 | spring[attr] = data[attr]; 71 | } 72 | }) 73 | }, 74 | 75 | createSpring: function() { 76 | if (this.spring) return; // no need to create a new spring 77 | this.spring = new CANNON.Spring(this.el.body); 78 | }, 79 | 80 | // If the spring is valid, update the force each tick the physics are calculated 81 | step: function(t, dt) { 82 | return this.spring && this.isActive ? this.spring.applyForce() : void 0; 83 | }, 84 | 85 | // resume updating the force when component upon calling play() 86 | play: function() { 87 | this.isActive = true; 88 | }, 89 | 90 | // stop updating the force when component upon calling stop() 91 | pause: function() { 92 | this.isActive = false; 93 | }, 94 | 95 | //remove the event listener + delete the spring 96 | remove: function() { 97 | if (this.spring) 98 | delete this.spring; 99 | this.spring = null; 100 | } 101 | }) 102 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | GRAVITY: -9.8, 3 | MAX_INTERVAL: 4 / 60, 4 | ITERATIONS: 10, 5 | CONTACT_MATERIAL: { 6 | friction: 0.01, 7 | restitution: 0.3, 8 | contactEquationStiffness: 1e8, 9 | contactEquationRelaxation: 3, 10 | frictionEquationStiffness: 1e8, 11 | frictionEquationRegularization: 3 12 | }, 13 | ACTIVATION_STATE: { 14 | ACTIVE_TAG: "active", 15 | ISLAND_SLEEPING: "islandSleeping", 16 | WANTS_DEACTIVATION: "wantsDeactivation", 17 | DISABLE_DEACTIVATION: "disableDeactivation", 18 | DISABLE_SIMULATION: "disableSimulation" 19 | }, 20 | COLLISION_FLAG: { 21 | STATIC_OBJECT: 1, 22 | KINEMATIC_OBJECT: 2, 23 | NO_CONTACT_RESPONSE: 4, 24 | CUSTOM_MATERIAL_CALLBACK: 8, //this allows per-triangle material (friction/restitution) 25 | CHARACTER_OBJECT: 16, 26 | DISABLE_VISUALIZE_OBJECT: 32, //disable debug drawing 27 | DISABLE_SPU_COLLISION_PROCESSING: 64 //disable parallel/SPU processing 28 | }, 29 | TYPE: { 30 | STATIC: "static", 31 | DYNAMIC: "dynamic", 32 | KINEMATIC: "kinematic" 33 | }, 34 | SHAPE: { 35 | BOX: "box", 36 | CYLINDER: "cylinder", 37 | SPHERE: "sphere", 38 | CAPSULE: "capsule", 39 | CONE: "cone", 40 | HULL: "hull", 41 | HACD: "hacd", 42 | VHACD: "vhacd", 43 | MESH: "mesh", 44 | HEIGHTFIELD: "heightfield" 45 | }, 46 | FIT: { 47 | ALL: "all", 48 | MANUAL: "manual" 49 | }, 50 | CONSTRAINT: { 51 | LOCK: "lock", 52 | FIXED: "fixed", 53 | SPRING: "spring", 54 | SLIDER: "slider", 55 | HINGE: "hinge", 56 | CONE_TWIST: "coneTwist", 57 | POINT_TO_POINT: "pointToPoint" 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/drivers/ammo-driver.js: -------------------------------------------------------------------------------- 1 | /* global THREE */ 2 | const Driver = require("./driver"); 3 | 4 | if (typeof window !== 'undefined') { 5 | window.AmmoModule = window.Ammo; 6 | window.Ammo = null; 7 | } 8 | 9 | const EPS = 10e-6; 10 | 11 | function AmmoDriver() { 12 | this.collisionConfiguration = null; 13 | this.dispatcher = null; 14 | this.broadphase = null; 15 | this.solver = null; 16 | this.physicsWorld = null; 17 | this.debugDrawer = null; 18 | 19 | this.els = new Map(); 20 | this.eventListeners = []; 21 | this.collisions = new Map(); 22 | this.collisionKeys = []; 23 | this.currentCollisions = new Map(); 24 | } 25 | 26 | AmmoDriver.prototype = new Driver(); 27 | AmmoDriver.prototype.constructor = AmmoDriver; 28 | 29 | module.exports = AmmoDriver; 30 | 31 | /* @param {object} worldConfig */ 32 | AmmoDriver.prototype.init = function(worldConfig) { 33 | //Emscripten doesn't use real promises, just a .then() callback, so it necessary to wrap in a real promise. 34 | return new Promise(resolve => { 35 | AmmoModule().then(result => { 36 | Ammo = result; 37 | this.epsilon = worldConfig.epsilon || EPS; 38 | this.debugDrawMode = worldConfig.debugDrawMode || THREE.AmmoDebugConstants.NoDebug; 39 | this.maxSubSteps = worldConfig.maxSubSteps || 4; 40 | this.fixedTimeStep = worldConfig.fixedTimeStep || 1 / 60; 41 | this.collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); 42 | this.dispatcher = new Ammo.btCollisionDispatcher(this.collisionConfiguration); 43 | this.broadphase = new Ammo.btDbvtBroadphase(); 44 | this.solver = new Ammo.btSequentialImpulseConstraintSolver(); 45 | this.physicsWorld = new Ammo.btDiscreteDynamicsWorld( 46 | this.dispatcher, 47 | this.broadphase, 48 | this.solver, 49 | this.collisionConfiguration 50 | ); 51 | this.physicsWorld.setForceUpdateAllAabbs(false); 52 | this.physicsWorld.setGravity( 53 | new Ammo.btVector3(0, worldConfig.hasOwnProperty("gravity") ? worldConfig.gravity : -9.8, 0) 54 | ); 55 | this.physicsWorld.getSolverInfo().set_m_numIterations(worldConfig.solverIterations); 56 | resolve(); 57 | }); 58 | }); 59 | }; 60 | 61 | /* @param {Ammo.btCollisionObject} body */ 62 | AmmoDriver.prototype.addBody = function(body, group, mask) { 63 | this.physicsWorld.addRigidBody(body, group, mask); 64 | this.els.set(Ammo.getPointer(body), body.el); 65 | }; 66 | 67 | /* @param {Ammo.btCollisionObject} body */ 68 | AmmoDriver.prototype.removeBody = function(body) { 69 | this.physicsWorld.removeRigidBody(body); 70 | this.removeEventListener(body); 71 | const bodyptr = Ammo.getPointer(body); 72 | this.els.delete(bodyptr); 73 | this.collisions.delete(bodyptr); 74 | this.collisionKeys.splice(this.collisionKeys.indexOf(bodyptr), 1); 75 | this.currentCollisions.delete(bodyptr); 76 | }; 77 | 78 | AmmoDriver.prototype.updateBody = function(body) { 79 | if (this.els.has(Ammo.getPointer(body))) { 80 | this.physicsWorld.updateSingleAabb(body); 81 | } 82 | }; 83 | 84 | /* @param {number} deltaTime */ 85 | AmmoDriver.prototype.step = function(deltaTime) { 86 | this.physicsWorld.stepSimulation(deltaTime, this.maxSubSteps, this.fixedTimeStep); 87 | 88 | const numManifolds = this.dispatcher.getNumManifolds(); 89 | for (let i = 0; i < numManifolds; i++) { 90 | const persistentManifold = this.dispatcher.getManifoldByIndexInternal(i); 91 | const numContacts = persistentManifold.getNumContacts(); 92 | const body0ptr = Ammo.getPointer(persistentManifold.getBody0()); 93 | const body1ptr = Ammo.getPointer(persistentManifold.getBody1()); 94 | let collided = false; 95 | 96 | for (let j = 0; j < numContacts; j++) { 97 | const manifoldPoint = persistentManifold.getContactPoint(j); 98 | const distance = manifoldPoint.getDistance(); 99 | if (distance <= this.epsilon) { 100 | collided = true; 101 | break; 102 | } 103 | } 104 | 105 | if (collided) { 106 | if (!this.collisions.has(body0ptr)) { 107 | this.collisions.set(body0ptr, []); 108 | this.collisionKeys.push(body0ptr); 109 | } 110 | if (this.collisions.get(body0ptr).indexOf(body1ptr) === -1) { 111 | this.collisions.get(body0ptr).push(body1ptr); 112 | if (this.eventListeners.indexOf(body0ptr) !== -1) { 113 | this.els.get(body0ptr).emit("collidestart", { targetEl: this.els.get(body1ptr) }); 114 | } 115 | if (this.eventListeners.indexOf(body1ptr) !== -1) { 116 | this.els.get(body1ptr).emit("collidestart", { targetEl: this.els.get(body0ptr) }); 117 | } 118 | } 119 | if (!this.currentCollisions.has(body0ptr)) { 120 | this.currentCollisions.set(body0ptr, new Set()); 121 | } 122 | this.currentCollisions.get(body0ptr).add(body1ptr); 123 | } 124 | } 125 | 126 | for (let i = 0; i < this.collisionKeys.length; i++) { 127 | const body0ptr = this.collisionKeys[i]; 128 | const body1ptrs = this.collisions.get(body0ptr); 129 | for (let j = body1ptrs.length - 1; j >= 0; j--) { 130 | const body1ptr = body1ptrs[j]; 131 | if (this.currentCollisions.get(body0ptr).has(body1ptr)) { 132 | continue; 133 | } 134 | if (this.eventListeners.indexOf(body0ptr) !== -1) { 135 | this.els.get(body0ptr).emit("collideend", { targetEl: this.els.get(body1ptr) }); 136 | } 137 | if (this.eventListeners.indexOf(body1ptr) !== -1) { 138 | this.els.get(body1ptr).emit("collideend", { targetEl: this.els.get(body0ptr) }); 139 | } 140 | body1ptrs.splice(j, 1); 141 | } 142 | this.currentCollisions.get(body0ptr).clear(); 143 | } 144 | 145 | if (this.debugDrawer) { 146 | this.debugDrawer.update(); 147 | } 148 | }; 149 | 150 | /* @param {?} constraint */ 151 | AmmoDriver.prototype.addConstraint = function(constraint) { 152 | this.physicsWorld.addConstraint(constraint, false); 153 | }; 154 | 155 | /* @param {?} constraint */ 156 | AmmoDriver.prototype.removeConstraint = function(constraint) { 157 | this.physicsWorld.removeConstraint(constraint); 158 | }; 159 | 160 | /* @param {Ammo.btCollisionObject} body */ 161 | AmmoDriver.prototype.addEventListener = function(body) { 162 | this.eventListeners.push(Ammo.getPointer(body)); 163 | }; 164 | 165 | /* @param {Ammo.btCollisionObject} body */ 166 | AmmoDriver.prototype.removeEventListener = function(body) { 167 | const ptr = Ammo.getPointer(body); 168 | if (this.eventListeners.indexOf(ptr) !== -1) { 169 | this.eventListeners.splice(this.eventListeners.indexOf(ptr), 1); 170 | } 171 | }; 172 | 173 | AmmoDriver.prototype.destroy = function() { 174 | Ammo.destroy(this.collisionConfiguration); 175 | Ammo.destroy(this.dispatcher); 176 | Ammo.destroy(this.broadphase); 177 | Ammo.destroy(this.solver); 178 | Ammo.destroy(this.physicsWorld); 179 | Ammo.destroy(this.debugDrawer); 180 | }; 181 | 182 | /** 183 | * @param {THREE.Scene} scene 184 | * @param {object} options 185 | */ 186 | AmmoDriver.prototype.getDebugDrawer = function(scene, options) { 187 | if (!this.debugDrawer) { 188 | options = options || {}; 189 | options.debugDrawMode = options.debugDrawMode || this.debugDrawMode; 190 | this.debugDrawer = new THREE.AmmoDebugDrawer(scene, this.physicsWorld, options); 191 | } 192 | return this.debugDrawer; 193 | }; 194 | -------------------------------------------------------------------------------- /src/drivers/driver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Driver - defines limited API to local and remote physics controllers. 3 | */ 4 | 5 | function Driver () {} 6 | 7 | module.exports = Driver; 8 | 9 | /****************************************************************************** 10 | * Lifecycle 11 | */ 12 | 13 | /* @param {object} worldConfig */ 14 | Driver.prototype.init = abstractMethod; 15 | 16 | /* @param {number} deltaMS */ 17 | Driver.prototype.step = abstractMethod; 18 | 19 | Driver.prototype.destroy = abstractMethod; 20 | 21 | /****************************************************************************** 22 | * Bodies 23 | */ 24 | 25 | /* @param {CANNON.Body} body */ 26 | Driver.prototype.addBody = abstractMethod; 27 | 28 | /* @param {CANNON.Body} body */ 29 | Driver.prototype.removeBody = abstractMethod; 30 | 31 | /** 32 | * @param {CANNON.Body} body 33 | * @param {string} methodName 34 | * @param {Array} args 35 | */ 36 | Driver.prototype.applyBodyMethod = abstractMethod; 37 | 38 | /** @param {CANNON.Body} body */ 39 | Driver.prototype.updateBodyProperties = abstractMethod; 40 | 41 | /****************************************************************************** 42 | * Materials 43 | */ 44 | 45 | /** @param {object} materialConfig */ 46 | Driver.prototype.addMaterial = abstractMethod; 47 | 48 | /** 49 | * @param {string} materialName1 50 | * @param {string} materialName2 51 | * @param {object} contactMaterialConfig 52 | */ 53 | Driver.prototype.addContactMaterial = abstractMethod; 54 | 55 | /****************************************************************************** 56 | * Constraints 57 | */ 58 | 59 | /* @param {CANNON.Constraint} constraint */ 60 | Driver.prototype.addConstraint = abstractMethod; 61 | 62 | /* @param {CANNON.Constraint} constraint */ 63 | Driver.prototype.removeConstraint = abstractMethod; 64 | 65 | /****************************************************************************** 66 | * Contacts 67 | */ 68 | 69 | /** @return {Array} */ 70 | Driver.prototype.getContacts = abstractMethod; 71 | 72 | /*****************************************************************************/ 73 | 74 | function abstractMethod () { 75 | throw new Error('Method not implemented.'); 76 | } 77 | -------------------------------------------------------------------------------- /src/drivers/event.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | INIT: 'init', 3 | STEP: 'step', 4 | 5 | // Bodies. 6 | ADD_BODY: 'add-body', 7 | REMOVE_BODY: 'remove-body', 8 | APPLY_BODY_METHOD: 'apply-body-method', 9 | UPDATE_BODY_PROPERTIES: 'update-body-properties', 10 | 11 | // Materials. 12 | ADD_MATERIAL: 'add-material', 13 | ADD_CONTACT_MATERIAL: 'add-contact-material', 14 | 15 | // Constraints. 16 | ADD_CONSTRAINT: 'add-constraint', 17 | REMOVE_CONSTRAINT: 'remove-constraint', 18 | 19 | // Events. 20 | COLLIDE: 'collide' 21 | }; 22 | -------------------------------------------------------------------------------- /src/drivers/local-driver.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'), 2 | Driver = require('./driver'); 3 | 4 | function LocalDriver () { 5 | this.world = null; 6 | this.materials = {}; 7 | this.contactMaterial = null; 8 | } 9 | 10 | LocalDriver.prototype = new Driver(); 11 | LocalDriver.prototype.constructor = LocalDriver; 12 | 13 | module.exports = LocalDriver; 14 | 15 | /****************************************************************************** 16 | * Lifecycle 17 | */ 18 | 19 | /* @param {object} worldConfig */ 20 | LocalDriver.prototype.init = function (worldConfig) { 21 | var world = new CANNON.World(); 22 | world.quatNormalizeSkip = worldConfig.quatNormalizeSkip; 23 | world.quatNormalizeFast = worldConfig.quatNormalizeFast; 24 | world.solver.iterations = worldConfig.solverIterations; 25 | world.gravity.set(0, worldConfig.gravity, 0); 26 | world.broadphase = new CANNON.NaiveBroadphase(); 27 | 28 | this.world = world; 29 | }; 30 | 31 | /* @param {number} deltaMS */ 32 | LocalDriver.prototype.step = function (deltaMS) { 33 | this.world.step(deltaMS); 34 | }; 35 | 36 | LocalDriver.prototype.destroy = function () { 37 | delete this.world; 38 | delete this.contactMaterial; 39 | this.materials = {}; 40 | }; 41 | 42 | /****************************************************************************** 43 | * Bodies 44 | */ 45 | 46 | /* @param {CANNON.Body} body */ 47 | LocalDriver.prototype.addBody = function (body) { 48 | this.world.addBody(body); 49 | }; 50 | 51 | /* @param {CANNON.Body} body */ 52 | LocalDriver.prototype.removeBody = function (body) { 53 | this.world.removeBody(body); 54 | }; 55 | 56 | /** 57 | * @param {CANNON.Body} body 58 | * @param {string} methodName 59 | * @param {Array} args 60 | */ 61 | LocalDriver.prototype.applyBodyMethod = function (body, methodName, args) { 62 | body['__' + methodName].apply(body, args); 63 | }; 64 | 65 | /** @param {CANNON.Body} body */ 66 | LocalDriver.prototype.updateBodyProperties = function () {}; 67 | 68 | /****************************************************************************** 69 | * Materials 70 | */ 71 | 72 | /** 73 | * @param {string} name 74 | * @return {CANNON.Material} 75 | */ 76 | LocalDriver.prototype.getMaterial = function (name) { 77 | return this.materials[name]; 78 | }; 79 | 80 | /** @param {object} materialConfig */ 81 | LocalDriver.prototype.addMaterial = function (materialConfig) { 82 | this.materials[materialConfig.name] = new CANNON.Material(materialConfig); 83 | this.materials[materialConfig.name].name = materialConfig.name; 84 | }; 85 | 86 | /** 87 | * @param {string} matName1 88 | * @param {string} matName2 89 | * @param {object} contactMaterialConfig 90 | */ 91 | LocalDriver.prototype.addContactMaterial = function (matName1, matName2, contactMaterialConfig) { 92 | var mat1 = this.materials[matName1], 93 | mat2 = this.materials[matName2]; 94 | this.contactMaterial = new CANNON.ContactMaterial(mat1, mat2, contactMaterialConfig); 95 | this.world.addContactMaterial(this.contactMaterial); 96 | }; 97 | 98 | /****************************************************************************** 99 | * Constraints 100 | */ 101 | 102 | /* @param {CANNON.Constraint} constraint */ 103 | LocalDriver.prototype.addConstraint = function (constraint) { 104 | if (!constraint.type) { 105 | if (constraint instanceof CANNON.LockConstraint) { 106 | constraint.type = 'LockConstraint'; 107 | } else if (constraint instanceof CANNON.DistanceConstraint) { 108 | constraint.type = 'DistanceConstraint'; 109 | } else if (constraint instanceof CANNON.HingeConstraint) { 110 | constraint.type = 'HingeConstraint'; 111 | } else if (constraint instanceof CANNON.ConeTwistConstraint) { 112 | constraint.type = 'ConeTwistConstraint'; 113 | } else if (constraint instanceof CANNON.PointToPointConstraint) { 114 | constraint.type = 'PointToPointConstraint'; 115 | } 116 | } 117 | this.world.addConstraint(constraint); 118 | }; 119 | 120 | /* @param {CANNON.Constraint} constraint */ 121 | LocalDriver.prototype.removeConstraint = function (constraint) { 122 | this.world.removeConstraint(constraint); 123 | }; 124 | 125 | /****************************************************************************** 126 | * Contacts 127 | */ 128 | 129 | /** @return {Array} */ 130 | LocalDriver.prototype.getContacts = function () { 131 | return this.world.contacts; 132 | }; 133 | -------------------------------------------------------------------------------- /src/drivers/network-driver.js: -------------------------------------------------------------------------------- 1 | var Driver = require('./driver'); 2 | 3 | function NetworkDriver () { 4 | throw new Error('[NetworkDriver] Driver not implemented.'); 5 | } 6 | 7 | NetworkDriver.prototype = new Driver(); 8 | NetworkDriver.prototype.constructor = NetworkDriver; 9 | 10 | module.exports = NetworkDriver; 11 | -------------------------------------------------------------------------------- /src/drivers/webworkify-debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stub version of webworkify, for debugging code outside of a webworker. 3 | */ 4 | function webworkifyDebug (worker) { 5 | var targetA = new EventTarget(), 6 | targetB = new EventTarget(); 7 | 8 | targetA.setTarget(targetB); 9 | targetB.setTarget(targetA); 10 | 11 | worker(targetA); 12 | return targetB; 13 | } 14 | 15 | module.exports = webworkifyDebug; 16 | 17 | /****************************************************************************** 18 | * EventTarget 19 | */ 20 | 21 | function EventTarget () { 22 | this.listeners = []; 23 | } 24 | 25 | EventTarget.prototype.setTarget = function (target) { 26 | this.target = target; 27 | }; 28 | 29 | EventTarget.prototype.addEventListener = function (type, fn) { 30 | this.listeners.push(fn); 31 | }; 32 | 33 | EventTarget.prototype.dispatchEvent = function (type, event) { 34 | for (var i = 0; i < this.listeners.length; i++) { 35 | this.listeners[i](event); 36 | } 37 | }; 38 | 39 | EventTarget.prototype.postMessage = function (msg) { 40 | this.target.dispatchEvent('message', {data: msg}); 41 | }; 42 | -------------------------------------------------------------------------------- /src/drivers/worker-driver.js: -------------------------------------------------------------------------------- 1 | /* global performance */ 2 | 3 | var webworkify = require('webworkify'), 4 | webworkifyDebug = require('./webworkify-debug'), 5 | Driver = require('./driver'), 6 | Event = require('./event'), 7 | worker = require('./worker'), 8 | protocol = require('../utils/protocol'); 9 | 10 | var ID = protocol.ID; 11 | 12 | /****************************************************************************** 13 | * Constructor 14 | */ 15 | 16 | function WorkerDriver (options) { 17 | this.fps = options.fps; 18 | this.engine = options.engine; 19 | this.interpolate = options.interpolate; 20 | // Approximate number of physics steps to 'pad' rendering. 21 | this.interpBufferSize = options.interpolationBufferSize; 22 | this.debug = options.debug; 23 | 24 | this.bodies = {}; 25 | this.contacts = []; 26 | 27 | // https://gafferongames.com/post/snapshot_interpolation/ 28 | this.frameDelay = this.interpBufferSize * 1000 / this.fps; 29 | this.frameBuffer = []; 30 | 31 | this.worker = this.debug 32 | ? webworkifyDebug(worker) 33 | : webworkify(worker); 34 | this.worker.addEventListener('message', this._onMessage.bind(this)); 35 | } 36 | 37 | WorkerDriver.prototype = new Driver(); 38 | WorkerDriver.prototype.constructor = WorkerDriver; 39 | 40 | module.exports = WorkerDriver; 41 | 42 | /****************************************************************************** 43 | * Lifecycle 44 | */ 45 | 46 | /* @param {object} worldConfig */ 47 | WorkerDriver.prototype.init = function (worldConfig) { 48 | this.worker.postMessage({ 49 | type: Event.INIT, 50 | worldConfig: worldConfig, 51 | fps: this.fps, 52 | engine: this.engine 53 | }); 54 | }; 55 | 56 | /** 57 | * Increments the physics world forward one step, if interpolation is enabled. 58 | * If disabled, increments are performed as messages arrive. 59 | * @param {number} deltaMS 60 | */ 61 | WorkerDriver.prototype.step = function () { 62 | if (!this.interpolate) return; 63 | 64 | // Get the two oldest frames that haven't expired. Ideally we would use all 65 | // available frames to keep things smooth, but lerping is easier and faster. 66 | var prevFrame = this.frameBuffer[0]; 67 | var nextFrame = this.frameBuffer[1]; 68 | var timestamp = performance.now(); 69 | while (prevFrame && nextFrame && timestamp - prevFrame.timestamp > this.frameDelay) { 70 | this.frameBuffer.shift(); 71 | prevFrame = this.frameBuffer[0]; 72 | nextFrame = this.frameBuffer[1]; 73 | } 74 | 75 | if (!prevFrame || !nextFrame) return; 76 | 77 | var mix = (timestamp - prevFrame.timestamp) / this.frameDelay; 78 | mix = (mix - (1 - 1 / this.interpBufferSize)) * this.interpBufferSize; 79 | 80 | for (var id in prevFrame.bodies) { 81 | if (prevFrame.bodies.hasOwnProperty(id) && nextFrame.bodies.hasOwnProperty(id)) { 82 | protocol.deserializeInterpBodyUpdate( 83 | prevFrame.bodies[id], 84 | nextFrame.bodies[id], 85 | this.bodies[id], 86 | mix 87 | ); 88 | } 89 | } 90 | }; 91 | 92 | WorkerDriver.prototype.destroy = function () { 93 | this.worker.terminate(); 94 | delete this.worker; 95 | }; 96 | 97 | /** {Event} event */ 98 | WorkerDriver.prototype._onMessage = function (event) { 99 | if (event.data.type === Event.STEP) { 100 | var data = event.data, 101 | bodies = data.bodies; 102 | 103 | this.contacts = event.data.contacts; 104 | 105 | // If interpolation is enabled, store the frame. If not, update all bodies 106 | // immediately. 107 | if (this.interpolate) { 108 | this.frameBuffer.push({timestamp: performance.now(), bodies: bodies}); 109 | } else { 110 | for (var id in bodies) { 111 | if (bodies.hasOwnProperty(id)) { 112 | protocol.deserializeBodyUpdate(bodies[id], this.bodies[id]); 113 | } 114 | } 115 | } 116 | 117 | } else if (event.data.type === Event.COLLIDE) { 118 | var body = this.bodies[event.data.bodyID]; 119 | var target = this.bodies[event.data.targetID]; 120 | var contact = protocol.deserializeContact(event.data.contact, this.bodies); 121 | if (!body._listeners || !body._listeners.collide) return; 122 | for (var i = 0; i < body._listeners.collide.length; i++) { 123 | body._listeners.collide[i]({target: target, body: body, contact: contact}); 124 | } 125 | 126 | } else { 127 | throw new Error('[WorkerDriver] Unexpected message type.'); 128 | } 129 | }; 130 | 131 | /****************************************************************************** 132 | * Bodies 133 | */ 134 | 135 | /* @param {CANNON.Body} body */ 136 | WorkerDriver.prototype.addBody = function (body) { 137 | protocol.assignID('body', body); 138 | this.bodies[body[ID]] = body; 139 | this.worker.postMessage({type: Event.ADD_BODY, body: protocol.serializeBody(body)}); 140 | }; 141 | 142 | /* @param {CANNON.Body} body */ 143 | WorkerDriver.prototype.removeBody = function (body) { 144 | this.worker.postMessage({type: Event.REMOVE_BODY, bodyID: body[ID]}); 145 | delete this.bodies[body[ID]]; 146 | }; 147 | 148 | /** 149 | * @param {CANNON.Body} body 150 | * @param {string} methodName 151 | * @param {Array} args 152 | */ 153 | WorkerDriver.prototype.applyBodyMethod = function (body, methodName, args) { 154 | switch (methodName) { 155 | case 'applyForce': 156 | case 'applyImpulse': 157 | this.worker.postMessage({ 158 | type: Event.APPLY_BODY_METHOD, 159 | bodyID: body[ID], 160 | methodName: methodName, 161 | args: [args[0].toArray(), args[1].toArray()] 162 | }); 163 | break; 164 | default: 165 | throw new Error('Unexpected methodName: %s', methodName); 166 | } 167 | }; 168 | 169 | /** @param {CANNON.Body} body */ 170 | WorkerDriver.prototype.updateBodyProperties = function (body) { 171 | this.worker.postMessage({ 172 | type: Event.UPDATE_BODY_PROPERTIES, 173 | body: protocol.serializeBody(body) 174 | }); 175 | }; 176 | 177 | /****************************************************************************** 178 | * Materials 179 | */ 180 | 181 | /** 182 | * @param {string} name 183 | * @return {CANNON.Material} 184 | */ 185 | WorkerDriver.prototype.getMaterial = function (name) { 186 | // No access to materials here. Eventually we might return the name or ID, if 187 | // multiple materials were selected, but for now there's only one and it's safe 188 | // to assume the worker is already using it. 189 | return undefined; 190 | }; 191 | 192 | /** @param {object} materialConfig */ 193 | WorkerDriver.prototype.addMaterial = function (materialConfig) { 194 | this.worker.postMessage({type: Event.ADD_MATERIAL, materialConfig: materialConfig}); 195 | }; 196 | 197 | /** 198 | * @param {string} matName1 199 | * @param {string} matName2 200 | * @param {object} contactMaterialConfig 201 | */ 202 | WorkerDriver.prototype.addContactMaterial = function (matName1, matName2, contactMaterialConfig) { 203 | this.worker.postMessage({ 204 | type: Event.ADD_CONTACT_MATERIAL, 205 | materialName1: matName1, 206 | materialName2: matName2, 207 | contactMaterialConfig: contactMaterialConfig 208 | }); 209 | }; 210 | 211 | /****************************************************************************** 212 | * Constraints 213 | */ 214 | 215 | /* @param {CANNON.Constraint} constraint */ 216 | WorkerDriver.prototype.addConstraint = function (constraint) { 217 | if (!constraint.type) { 218 | if (constraint instanceof CANNON.LockConstraint) { 219 | constraint.type = 'LockConstraint'; 220 | } else if (constraint instanceof CANNON.DistanceConstraint) { 221 | constraint.type = 'DistanceConstraint'; 222 | } else if (constraint instanceof CANNON.HingeConstraint) { 223 | constraint.type = 'HingeConstraint'; 224 | } else if (constraint instanceof CANNON.ConeTwistConstraint) { 225 | constraint.type = 'ConeTwistConstraint'; 226 | } else if (constraint instanceof CANNON.PointToPointConstraint) { 227 | constraint.type = 'PointToPointConstraint'; 228 | } 229 | } 230 | protocol.assignID('constraint', constraint); 231 | this.worker.postMessage({ 232 | type: Event.ADD_CONSTRAINT, 233 | constraint: protocol.serializeConstraint(constraint) 234 | }); 235 | }; 236 | 237 | /* @param {CANNON.Constraint} constraint */ 238 | WorkerDriver.prototype.removeConstraint = function (constraint) { 239 | this.worker.postMessage({ 240 | type: Event.REMOVE_CONSTRAINT, 241 | constraintID: constraint[ID] 242 | }); 243 | }; 244 | 245 | /****************************************************************************** 246 | * Contacts 247 | */ 248 | 249 | /** @return {Array} */ 250 | WorkerDriver.prototype.getContacts = function () { 251 | // TODO(donmccurdy): There's some wasted memory allocation here. 252 | var bodies = this.bodies; 253 | return this.contacts.map(function (message) { 254 | return protocol.deserializeContact(message, bodies); 255 | }); 256 | }; 257 | -------------------------------------------------------------------------------- /src/drivers/worker.js: -------------------------------------------------------------------------------- 1 | var Event = require('./event'), 2 | LocalDriver = require('./local-driver'), 3 | AmmoDriver = require('./ammo-driver'), 4 | protocol = require('../utils/protocol'); 5 | 6 | var ID = protocol.ID; 7 | 8 | module.exports = function (self) { 9 | var driver = null; 10 | var bodies = {}; 11 | var constraints = {}; 12 | var stepSize; 13 | 14 | self.addEventListener('message', function (event) { 15 | var data = event.data; 16 | 17 | switch (data.type) { 18 | // Lifecycle. 19 | case Event.INIT: 20 | driver = data.engine === 'cannon' 21 | ? new LocalDriver() 22 | : new AmmoDriver(); 23 | driver.init(data.worldConfig); 24 | stepSize = 1 / data.fps; 25 | setInterval(step, 1000 / data.fps); 26 | break; 27 | 28 | // Bodies. 29 | case Event.ADD_BODY: 30 | var body = protocol.deserializeBody(data.body); 31 | body.material = driver.getMaterial( 'defaultMaterial' ); 32 | bodies[body[ID]] = body; 33 | 34 | body.addEventListener('collide', function (evt) { 35 | var message = { 36 | type: Event.COLLIDE, 37 | bodyID: evt.target[ID], // set the target as the body to be identical to the local driver 38 | targetID: evt.body[ID], // set the body as the target to be identical to the local driver 39 | contact: protocol.serializeContact(evt.contact) 40 | } 41 | self.postMessage(message); 42 | }); 43 | driver.addBody(body); 44 | break; 45 | case Event.REMOVE_BODY: 46 | driver.removeBody(bodies[data.bodyID]); 47 | delete bodies[data.bodyID]; 48 | break; 49 | case Event.APPLY_BODY_METHOD: 50 | bodies[data.bodyID][data.methodName]( 51 | protocol.deserializeVec3(data.args[0]), 52 | protocol.deserializeVec3(data.args[1]) 53 | ); 54 | break; 55 | case Event.UPDATE_BODY_PROPERTIES: 56 | protocol.deserializeBodyUpdate(data.body, bodies[data.body.id]); 57 | break; 58 | 59 | // Materials. 60 | case Event.ADD_MATERIAL: 61 | driver.addMaterial(data.materialConfig); 62 | break; 63 | case Event.ADD_CONTACT_MATERIAL: 64 | driver.addContactMaterial( 65 | data.materialName1, 66 | data.materialName2, 67 | data.contactMaterialConfig 68 | ); 69 | break; 70 | 71 | // Constraints. 72 | case Event.ADD_CONSTRAINT: 73 | var constraint = protocol.deserializeConstraint(data.constraint, bodies); 74 | constraints[constraint[ID]] = constraint; 75 | driver.addConstraint(constraint); 76 | break; 77 | case Event.REMOVE_CONSTRAINT: 78 | driver.removeConstraint(constraints[data.constraintID]); 79 | delete constraints[data.constraintID]; 80 | break; 81 | 82 | default: 83 | throw new Error('[Worker] Unexpected event type: %s', data.type); 84 | 85 | } 86 | }); 87 | 88 | function step () { 89 | driver.step(stepSize); 90 | 91 | var bodyMessages = {}; 92 | Object.keys(bodies).forEach(function (id) { 93 | bodyMessages[id] = protocol.serializeBody(bodies[id]); 94 | }); 95 | 96 | self.postMessage({ 97 | type: Event.STEP, 98 | bodies: bodyMessages, 99 | contacts: driver.getContacts().map(protocol.serializeContact) 100 | }); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/system.js: -------------------------------------------------------------------------------- 1 | /* global THREE */ 2 | var CANNON = require('cannon-es'), 3 | CONSTANTS = require('./constants'), 4 | C_GRAV = CONSTANTS.GRAVITY, 5 | C_MAT = CONSTANTS.CONTACT_MATERIAL; 6 | 7 | var LocalDriver = require('./drivers/local-driver'), 8 | WorkerDriver = require('./drivers/worker-driver'), 9 | NetworkDriver = require('./drivers/network-driver'), 10 | AmmoDriver = require('./drivers/ammo-driver'); 11 | 12 | /** 13 | * Physics system. 14 | */ 15 | module.exports = AFRAME.registerSystem('physics', { 16 | schema: { 17 | // CANNON.js driver type 18 | driver: { default: 'local', oneOf: ['local', 'worker', 'network', 'ammo'] }, 19 | networkUrl: { default: '', if: {driver: 'network'} }, 20 | workerFps: { default: 60, if: {driver: 'worker'} }, 21 | workerInterpolate: { default: true, if: {driver: 'worker'} }, 22 | workerInterpBufferSize: { default: 2, if: {driver: 'worker'} }, 23 | workerEngine: { default: 'cannon', if: {driver: 'worker'}, oneOf: ['cannon'] }, 24 | workerDebug: { default: false, if: {driver: 'worker'} }, 25 | 26 | gravity: { default: C_GRAV }, 27 | iterations: { default: CONSTANTS.ITERATIONS }, 28 | friction: { default: C_MAT.friction }, 29 | restitution: { default: C_MAT.restitution }, 30 | contactEquationStiffness: { default: C_MAT.contactEquationStiffness }, 31 | contactEquationRelaxation: { default: C_MAT.contactEquationRelaxation }, 32 | frictionEquationStiffness: { default: C_MAT.frictionEquationStiffness }, 33 | frictionEquationRegularization: { default: C_MAT.frictionEquationRegularization }, 34 | 35 | // Never step more than four frames at once. Effectively pauses the scene 36 | // when out of focus, and prevents weird "jumps" when focus returns. 37 | maxInterval: { default: 4 / 60 }, 38 | 39 | // If true, show wireframes around physics bodies. 40 | debug: { default: false }, 41 | 42 | // If using ammo, set the default rendering mode for debug 43 | debugDrawMode: { default: THREE.AmmoDebugConstants.NoDebug }, 44 | // If using ammo, set the max number of steps per frame 45 | maxSubSteps: { default: 4 }, 46 | // If using ammo, set the framerate of the simulation 47 | fixedTimeStep: { default: 1 / 60 } 48 | }, 49 | 50 | /** 51 | * Initializes the physics system. 52 | */ 53 | async init() { 54 | var data = this.data; 55 | 56 | // If true, show wireframes around physics bodies. 57 | this.debug = data.debug; 58 | 59 | this.callbacks = {beforeStep: [], step: [], afterStep: []}; 60 | 61 | this.listeners = {}; 62 | 63 | this.driver = null; 64 | switch (data.driver) { 65 | case 'local': 66 | this.driver = new LocalDriver(); 67 | break; 68 | 69 | case 'ammo': 70 | this.driver = new AmmoDriver(); 71 | break; 72 | 73 | case 'network': 74 | this.driver = new NetworkDriver(data.networkUrl); 75 | break; 76 | 77 | case 'worker': 78 | this.driver = new WorkerDriver({ 79 | fps: data.workerFps, 80 | engine: data.workerEngine, 81 | interpolate: data.workerInterpolate, 82 | interpolationBufferSize: data.workerInterpBufferSize, 83 | debug: data.workerDebug 84 | }); 85 | break; 86 | 87 | default: 88 | throw new Error('[physics] Driver not recognized: "%s".', data.driver); 89 | } 90 | 91 | if (data.driver !== 'ammo') { 92 | await this.driver.init({ 93 | quatNormalizeSkip: 0, 94 | quatNormalizeFast: false, 95 | solverIterations: data.iterations, 96 | gravity: data.gravity, 97 | }); 98 | this.driver.addMaterial({name: 'defaultMaterial'}); 99 | this.driver.addMaterial({name: 'staticMaterial'}); 100 | this.driver.addContactMaterial('defaultMaterial', 'defaultMaterial', { 101 | friction: data.friction, 102 | restitution: data.restitution, 103 | contactEquationStiffness: data.contactEquationStiffness, 104 | contactEquationRelaxation: data.contactEquationRelaxation, 105 | frictionEquationStiffness: data.frictionEquationStiffness, 106 | frictionEquationRegularization: data.frictionEquationRegularization 107 | }); 108 | this.driver.addContactMaterial('staticMaterial', 'defaultMaterial', { 109 | friction: 1.0, 110 | restitution: 0.0, 111 | contactEquationStiffness: data.contactEquationStiffness, 112 | contactEquationRelaxation: data.contactEquationRelaxation, 113 | frictionEquationStiffness: data.frictionEquationStiffness, 114 | frictionEquationRegularization: data.frictionEquationRegularization 115 | }); 116 | } else { 117 | await this.driver.init({ 118 | gravity: data.gravity, 119 | debugDrawMode: data.debugDrawMode, 120 | solverIterations: data.iterations, 121 | maxSubSteps: data.maxSubSteps, 122 | fixedTimeStep: data.fixedTimeStep 123 | }); 124 | } 125 | 126 | this.initialized = true; 127 | 128 | if (this.debug) { 129 | this.setDebug(true); 130 | } 131 | }, 132 | 133 | /** 134 | * Updates the physics world on each tick of the A-Frame scene. It would be 135 | * entirely possible to separate the two – updating physics more or less 136 | * frequently than the scene – if greater precision or performance were 137 | * necessary. 138 | * @param {number} t 139 | * @param {number} dt 140 | */ 141 | tick: function (t, dt) { 142 | if (!this.initialized || !dt) return; 143 | 144 | var i; 145 | var callbacks = this.callbacks; 146 | 147 | for (i = 0; i < this.callbacks.beforeStep.length; i++) { 148 | this.callbacks.beforeStep[i].beforeStep(t, dt); 149 | } 150 | 151 | this.driver.step(Math.min(dt / 1000, this.data.maxInterval)); 152 | 153 | for (i = 0; i < callbacks.step.length; i++) { 154 | callbacks.step[i].step(t, dt); 155 | } 156 | 157 | for (i = 0; i < callbacks.afterStep.length; i++) { 158 | callbacks.afterStep[i].afterStep(t, dt); 159 | } 160 | }, 161 | 162 | setDebug: function(debug) { 163 | this.debug = debug; 164 | if (this.data.driver === 'ammo' && this.initialized) { 165 | if (debug && !this.debugDrawer) { 166 | this.debugDrawer = this.driver.getDebugDrawer(this.el.object3D); 167 | this.debugDrawer.enable(); 168 | } else if (this.debugDrawer) { 169 | this.debugDrawer.disable(); 170 | this.debugDrawer = null; 171 | } 172 | } 173 | }, 174 | 175 | /** 176 | * Adds a body to the scene, and binds proxied methods to the driver. 177 | * @param {CANNON.Body} body 178 | */ 179 | addBody: function (body, group, mask) { 180 | var driver = this.driver; 181 | 182 | if (this.data.driver === 'local') { 183 | body.__applyImpulse = body.applyImpulse; 184 | body.applyImpulse = function () { 185 | driver.applyBodyMethod(body, 'applyImpulse', arguments); 186 | }; 187 | 188 | body.__applyForce = body.applyForce; 189 | body.applyForce = function () { 190 | driver.applyBodyMethod(body, 'applyForce', arguments); 191 | }; 192 | 193 | body.updateProperties = function () { 194 | driver.updateBodyProperties(body); 195 | }; 196 | 197 | this.listeners[body.id] = function (e) { body.el.emit('collide', e); }; 198 | body.addEventListener('collide', this.listeners[body.id]); 199 | } 200 | 201 | this.driver.addBody(body, group, mask); 202 | }, 203 | 204 | /** 205 | * Removes a body and its proxied methods. 206 | * @param {CANNON.Body} body 207 | */ 208 | removeBody: function (body) { 209 | this.driver.removeBody(body); 210 | 211 | if (this.data.driver === 'local' || this.data.driver === 'worker') { 212 | body.removeEventListener('collide', this.listeners[body.id]); 213 | delete this.listeners[body.id]; 214 | 215 | body.applyImpulse = body.__applyImpulse; 216 | delete body.__applyImpulse; 217 | 218 | body.applyForce = body.__applyForce; 219 | delete body.__applyForce; 220 | 221 | delete body.updateProperties; 222 | } 223 | }, 224 | 225 | /** @param {CANNON.Constraint or Ammo.btTypedConstraint} constraint */ 226 | addConstraint: function (constraint) { 227 | this.driver.addConstraint(constraint); 228 | }, 229 | 230 | /** @param {CANNON.Constraint or Ammo.btTypedConstraint} constraint */ 231 | removeConstraint: function (constraint) { 232 | this.driver.removeConstraint(constraint); 233 | }, 234 | 235 | /** 236 | * Adds a component instance to the system and schedules its update methods to be called 237 | * the given phase. 238 | * @param {Component} component 239 | * @param {string} phase 240 | */ 241 | addComponent: function (component) { 242 | var callbacks = this.callbacks; 243 | if (component.beforeStep) callbacks.beforeStep.push(component); 244 | if (component.step) callbacks.step.push(component); 245 | if (component.afterStep) callbacks.afterStep.push(component); 246 | }, 247 | 248 | /** 249 | * Removes a component instance from the system. 250 | * @param {Component} component 251 | * @param {string} phase 252 | */ 253 | removeComponent: function (component) { 254 | var callbacks = this.callbacks; 255 | if (component.beforeStep) { 256 | callbacks.beforeStep.splice(callbacks.beforeStep.indexOf(component), 1); 257 | } 258 | if (component.step) { 259 | callbacks.step.splice(callbacks.step.indexOf(component), 1); 260 | } 261 | if (component.afterStep) { 262 | callbacks.afterStep.splice(callbacks.afterStep.indexOf(component), 1); 263 | } 264 | }, 265 | 266 | /** @return {Array} */ 267 | getContacts: function () { 268 | return this.driver.getContacts(); 269 | }, 270 | 271 | getMaterial: function (name) { 272 | return this.driver.getMaterial(name); 273 | } 274 | }); 275 | -------------------------------------------------------------------------------- /src/utils/math.js: -------------------------------------------------------------------------------- 1 | module.exports.slerp = function ( a, b, t ) { 2 | if ( t <= 0 ) return a; 3 | if ( t >= 1 ) return b; 4 | 5 | var x = a[0], y = a[1], z = a[2], w = a[3]; 6 | 7 | // http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/ 8 | 9 | var cosHalfTheta = w * b[3] + x * b[0] + y * b[1] + z * b[2]; 10 | 11 | if ( cosHalfTheta < 0 ) { 12 | 13 | a = a.slice(); 14 | 15 | a[3] = - b[3]; 16 | a[0] = - b[0]; 17 | a[1] = - b[1]; 18 | a[2] = - b[2]; 19 | 20 | cosHalfTheta = - cosHalfTheta; 21 | 22 | } else { 23 | 24 | return b; 25 | 26 | } 27 | 28 | if ( cosHalfTheta >= 1.0 ) { 29 | 30 | a[3] = w; 31 | a[0] = x; 32 | a[1] = y; 33 | a[2] = z; 34 | 35 | return this; 36 | 37 | } 38 | 39 | var sinHalfTheta = Math.sqrt( 1.0 - cosHalfTheta * cosHalfTheta ); 40 | 41 | if ( Math.abs( sinHalfTheta ) < 0.001 ) { 42 | 43 | a[3] = 0.5 * ( w + a[3] ); 44 | a[0] = 0.5 * ( x + a[0] ); 45 | a[1] = 0.5 * ( y + a[1] ); 46 | a[2] = 0.5 * ( z + a[2] ); 47 | 48 | return this; 49 | 50 | } 51 | 52 | var halfTheta = Math.atan2( sinHalfTheta, cosHalfTheta ); 53 | var ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta; 54 | var ratioB = Math.sin( t * halfTheta ) / sinHalfTheta; 55 | 56 | a[3] = ( w * ratioA + a[3] * ratioB ); 57 | a[0] = ( x * ratioA + a[0] * ratioB ); 58 | a[1] = ( y * ratioA + a[1] * ratioB ); 59 | a[2] = ( z * ratioA + a[2] * ratioB ); 60 | 61 | return a; 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/protocol.js: -------------------------------------------------------------------------------- 1 | var CANNON = require('cannon-es'); 2 | var mathUtils = require('./math'); 3 | 4 | /****************************************************************************** 5 | * IDs 6 | */ 7 | 8 | var ID = '__id'; 9 | module.exports.ID = ID; 10 | 11 | var nextID = {}; 12 | module.exports.assignID = function (prefix, object) { 13 | if (object[ID]) return; 14 | nextID[prefix] = nextID[prefix] || 1; 15 | object[ID] = prefix + '_' + nextID[prefix]++; 16 | }; 17 | 18 | /****************************************************************************** 19 | * Bodies 20 | */ 21 | 22 | module.exports.serializeBody = function (body) { 23 | var message = { 24 | // Shapes. 25 | shapes: body.shapes.map(serializeShape), 26 | shapeOffsets: body.shapeOffsets.map(serializeVec3), 27 | shapeOrientations: body.shapeOrientations.map(serializeQuaternion), 28 | 29 | // Vectors. 30 | position: serializeVec3(body.position), 31 | quaternion: body.quaternion.toArray(), 32 | velocity: serializeVec3(body.velocity), 33 | angularVelocity: serializeVec3(body.angularVelocity), 34 | 35 | // Properties. 36 | id: body[ID], 37 | mass: body.mass, 38 | linearDamping: body.linearDamping, 39 | angularDamping: body.angularDamping, 40 | fixedRotation: body.fixedRotation, 41 | allowSleep: body.allowSleep, 42 | sleepSpeedLimit: body.sleepSpeedLimit, 43 | sleepTimeLimit: body.sleepTimeLimit 44 | }; 45 | 46 | return message; 47 | }; 48 | 49 | module.exports.deserializeBodyUpdate = function (message, body) { 50 | body.position.set(message.position[0], message.position[1], message.position[2]); 51 | body.quaternion.set(message.quaternion[0], message.quaternion[1], message.quaternion[2], message.quaternion[3]); 52 | body.velocity.set(message.velocity[0], message.velocity[1], message.velocity[2]); 53 | body.angularVelocity.set(message.angularVelocity[0], message.angularVelocity[1], message.angularVelocity[2]); 54 | 55 | body.linearDamping = message.linearDamping; 56 | body.angularDamping = message.angularDamping; 57 | body.fixedRotation = message.fixedRotation; 58 | body.allowSleep = message.allowSleep; 59 | body.sleepSpeedLimit = message.sleepSpeedLimit; 60 | body.sleepTimeLimit = message.sleepTimeLimit; 61 | 62 | if (body.mass !== message.mass) { 63 | body.mass = message.mass; 64 | body.updateMassProperties(); 65 | } 66 | 67 | return body; 68 | }; 69 | 70 | module.exports.deserializeInterpBodyUpdate = function (message1, message2, body, mix) { 71 | var weight1 = 1 - mix; 72 | var weight2 = mix; 73 | 74 | body.position.set( 75 | message1.position[0] * weight1 + message2.position[0] * weight2, 76 | message1.position[1] * weight1 + message2.position[1] * weight2, 77 | message1.position[2] * weight1 + message2.position[2] * weight2 78 | ); 79 | var quaternion = mathUtils.slerp(message1.quaternion, message2.quaternion, mix); 80 | body.quaternion.set(quaternion[0], quaternion[1], quaternion[2], quaternion[3]); 81 | body.velocity.set( 82 | message1.velocity[0] * weight1 + message2.velocity[0] * weight2, 83 | message1.velocity[1] * weight1 + message2.velocity[1] * weight2, 84 | message1.velocity[2] * weight1 + message2.velocity[2] * weight2 85 | ); 86 | body.angularVelocity.set( 87 | message1.angularVelocity[0] * weight1 + message2.angularVelocity[0] * weight2, 88 | message1.angularVelocity[1] * weight1 + message2.angularVelocity[1] * weight2, 89 | message1.angularVelocity[2] * weight1 + message2.angularVelocity[2] * weight2 90 | ); 91 | 92 | body.linearDamping = message2.linearDamping; 93 | body.angularDamping = message2.angularDamping; 94 | body.fixedRotation = message2.fixedRotation; 95 | body.allowSleep = message2.allowSleep; 96 | body.sleepSpeedLimit = message2.sleepSpeedLimit; 97 | body.sleepTimeLimit = message2.sleepTimeLimit; 98 | 99 | if (body.mass !== message2.mass) { 100 | body.mass = message2.mass; 101 | body.updateMassProperties(); 102 | } 103 | 104 | return body; 105 | }; 106 | 107 | module.exports.deserializeBody = function (message) { 108 | var body = new CANNON.Body({ 109 | mass: message.mass, 110 | 111 | position: deserializeVec3(message.position), 112 | quaternion: deserializeQuaternion(message.quaternion), 113 | velocity: deserializeVec3(message.velocity), 114 | angularVelocity: deserializeVec3(message.angularVelocity), 115 | 116 | linearDamping: message.linearDamping, 117 | angularDamping: message.angularDamping, 118 | fixedRotation: message.fixedRotation, 119 | allowSleep: message.allowSleep, 120 | sleepSpeedLimit: message.sleepSpeedLimit, 121 | sleepTimeLimit: message.sleepTimeLimit 122 | }); 123 | 124 | for (var shapeMsg, i = 0; (shapeMsg = message.shapes[i]); i++) { 125 | body.addShape( 126 | deserializeShape(shapeMsg), 127 | deserializeVec3(message.shapeOffsets[i]), 128 | deserializeQuaternion(message.shapeOrientations[i]) 129 | ); 130 | } 131 | 132 | body[ID] = message.id; 133 | 134 | return body; 135 | }; 136 | 137 | /****************************************************************************** 138 | * Shapes 139 | */ 140 | 141 | module.exports.serializeShape = serializeShape; 142 | function serializeShape (shape) { 143 | var shapeMsg = {type: shape.type}; 144 | if (shape.type === CANNON.Shape.types.BOX) { 145 | shapeMsg.halfExtents = serializeVec3(shape.halfExtents); 146 | 147 | } else if (shape.type === CANNON.Shape.types.SPHERE) { 148 | shapeMsg.radius = shape.radius; 149 | 150 | // Patch schteppe/cannon.js#329. 151 | } else if (shape._type === CANNON.Shape.types.CYLINDER) { 152 | shapeMsg.type = CANNON.Shape.types.CYLINDER; 153 | shapeMsg.radiusTop = shape.radiusTop; 154 | shapeMsg.radiusBottom = shape.radiusBottom; 155 | shapeMsg.height = shape.height; 156 | shapeMsg.numSegments = shape.numSegments; 157 | 158 | } else { 159 | // TODO(donmccurdy): Support for other shape types. 160 | throw new Error('Unimplemented shape type: %s', shape.type); 161 | } 162 | return shapeMsg; 163 | } 164 | 165 | module.exports.deserializeShape = deserializeShape; 166 | function deserializeShape (message) { 167 | var shape; 168 | 169 | if (message.type === CANNON.Shape.types.BOX) { 170 | shape = new CANNON.Box(deserializeVec3(message.halfExtents)); 171 | 172 | } else if (message.type === CANNON.Shape.types.SPHERE) { 173 | shape = new CANNON.Sphere(message.radius); 174 | 175 | // Patch schteppe/cannon.js#329. 176 | } else if (message.type === CANNON.Shape.types.CYLINDER) { 177 | shape = new CANNON.Cylinder(message.radiusTop, message.radiusBottom, message.height, message.numSegments); 178 | shape._type = CANNON.Shape.types.CYLINDER; 179 | 180 | } else { 181 | // TODO(donmccurdy): Support for other shape types. 182 | throw new Error('Unimplemented shape type: %s', message.type); 183 | } 184 | 185 | return shape; 186 | } 187 | 188 | /****************************************************************************** 189 | * Constraints 190 | */ 191 | 192 | module.exports.serializeConstraint = function (constraint) { 193 | 194 | var message = { 195 | id: constraint[ID], 196 | type: constraint.type, 197 | maxForce: constraint.maxForce, 198 | bodyA: constraint.bodyA[ID], 199 | bodyB: constraint.bodyB[ID] 200 | }; 201 | 202 | switch (constraint.type) { 203 | case 'LockConstraint': 204 | break; 205 | case 'DistanceConstraint': 206 | message.distance = constraint.distance; 207 | break; 208 | case 'HingeConstraint': 209 | case 'ConeTwistConstraint': 210 | message.axisA = serializeVec3(constraint.axisA); 211 | message.axisB = serializeVec3(constraint.axisB); 212 | message.pivotA = serializeVec3(constraint.pivotA); 213 | message.pivotB = serializeVec3(constraint.pivotB); 214 | break; 215 | case 'PointToPointConstraint': 216 | message.pivotA = serializeVec3(constraint.pivotA); 217 | message.pivotB = serializeVec3(constraint.pivotB); 218 | break; 219 | default: 220 | throw new Error('' 221 | + 'Unexpected constraint type: ' + constraint.type + '. ' 222 | + 'You may need to manually set `constraint.type = "FooConstraint";`.' 223 | ); 224 | } 225 | 226 | return message; 227 | }; 228 | 229 | module.exports.deserializeConstraint = function (message, bodies) { 230 | var TypedConstraint = CANNON[message.type]; 231 | var bodyA = bodies[message.bodyA]; 232 | var bodyB = bodies[message.bodyB]; 233 | 234 | var constraint; 235 | 236 | switch (message.type) { 237 | case 'LockConstraint': 238 | constraint = new CANNON.LockConstraint(bodyA, bodyB, message); 239 | break; 240 | 241 | case 'DistanceConstraint': 242 | constraint = new CANNON.DistanceConstraint( 243 | bodyA, 244 | bodyB, 245 | message.distance, 246 | message.maxForce 247 | ); 248 | break; 249 | 250 | case 'HingeConstraint': 251 | case 'ConeTwistConstraint': 252 | constraint = new TypedConstraint(bodyA, bodyB, { 253 | pivotA: deserializeVec3(message.pivotA), 254 | pivotB: deserializeVec3(message.pivotB), 255 | axisA: deserializeVec3(message.axisA), 256 | axisB: deserializeVec3(message.axisB), 257 | maxForce: message.maxForce 258 | }); 259 | break; 260 | 261 | case 'PointToPointConstraint': 262 | constraint = new CANNON.PointToPointConstraint( 263 | bodyA, 264 | deserializeVec3(message.pivotA), 265 | bodyB, 266 | deserializeVec3(message.pivotB), 267 | message.maxForce 268 | ); 269 | break; 270 | 271 | default: 272 | throw new Error('Unexpected constraint type: ' + message.type); 273 | } 274 | 275 | constraint[ID] = message.id; 276 | return constraint; 277 | }; 278 | 279 | /****************************************************************************** 280 | * Contacts 281 | */ 282 | 283 | module.exports.serializeContact = function (contact) { 284 | return { 285 | bi: contact.bi[ID], 286 | bj: contact.bj[ID], 287 | ni: serializeVec3(contact.ni), 288 | ri: serializeVec3(contact.ri), 289 | rj: serializeVec3(contact.rj) 290 | }; 291 | }; 292 | 293 | module.exports.deserializeContact = function (message, bodies) { 294 | return { 295 | bi: bodies[message.bi], 296 | bj: bodies[message.bj], 297 | ni: deserializeVec3(message.ni), 298 | ri: deserializeVec3(message.ri), 299 | rj: deserializeVec3(message.rj) 300 | }; 301 | }; 302 | 303 | /****************************************************************************** 304 | * Math 305 | */ 306 | 307 | module.exports.serializeVec3 = serializeVec3; 308 | function serializeVec3 (vec3) { 309 | return vec3.toArray(); 310 | } 311 | 312 | module.exports.deserializeVec3 = deserializeVec3; 313 | function deserializeVec3 (message) { 314 | return new CANNON.Vec3(message[0], message[1], message[2]); 315 | } 316 | 317 | module.exports.serializeQuaternion = serializeQuaternion; 318 | function serializeQuaternion (quat) { 319 | return quat.toArray(); 320 | } 321 | 322 | module.exports.deserializeQuaternion = deserializeQuaternion; 323 | function deserializeQuaternion (message) { 324 | return new CANNON.Quaternion(message[0], message[1], message[2], message[3]); 325 | } 326 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals" : { 4 | "afterEach" : false, 5 | "beforeEach" : false, 6 | "describe" : false, 7 | "expect" : false, 8 | "it" : false, 9 | "setup" : false, 10 | "sinon" : false, 11 | "spyOn" : false, 12 | "suite" : false, 13 | "test" : false, 14 | "teardown" : false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * __init.test.js is run before every test case. 3 | */ 4 | window.debug = true; 5 | 6 | var AScene = require('aframe').AScene; 7 | var physics = require('../'); 8 | 9 | setup(function () { 10 | this.sinon = sinon.sandbox.create(); 11 | // Stubs to not create a WebGL context since Travis CI runs headless. 12 | this.sinon.stub(AScene.prototype, 'render'); 13 | this.sinon.stub(AScene.prototype, 'resize'); 14 | this.sinon.stub(AScene.prototype, 'setupRenderer'); 15 | // Mock renderer. 16 | AScene.prototype.renderer = { 17 | vr: { 18 | getDevice: function () { return {requestPresent: function () {}}; }, 19 | setDevice: function () {}, 20 | setPoseTarget: function () {}, 21 | enabled: false 22 | }, 23 | getContext: function () { return undefined; }, 24 | setAnimationLoop: function () {}, 25 | setSize: function () {}, 26 | shadowMap: {} 27 | }; 28 | }); 29 | 30 | teardown(function () { 31 | // Clean up any attached elements. 32 | ['canvas', 'a-assets', 'a-scene'].forEach(function (tagName) { 33 | var els = document.querySelectorAll(tagName); 34 | for (var i = 0; i < els.length; i++) { 35 | els[i].parentNode.removeChild(els[i]); 36 | } 37 | }); 38 | this.sinon.restore(); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper method to create a scene, create an entity, add entity to scene, 3 | * add scene to document. 4 | * 5 | * @returns {object} An `` element. 6 | */ 7 | module.exports.entityFactory = function (opts) { 8 | var scene = document.createElement('a-scene'); 9 | var assets = document.createElement('a-assets'); 10 | var entity = document.createElement('a-entity'); 11 | scene.appendChild(assets); 12 | scene.appendChild(entity); 13 | entity.sceneEl = scene; 14 | 15 | opts = opts || {}; 16 | 17 | if (opts.assets) { 18 | opts.assets.forEach(function (asset) { 19 | assets.appendChild(asset); 20 | }); 21 | } 22 | 23 | document.body.appendChild(scene); 24 | return entity; 25 | }; 26 | 27 | /** 28 | * Creates and attaches a mixin element (and an `` element if necessary). 29 | * 30 | * @param {string} id - ID of mixin. 31 | * @param {object} obj - Map of component names to attribute values. 32 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary. 33 | * @returns {object} An attached `` element. 34 | */ 35 | module.exports.mixinFactory = function (id, obj, scene) { 36 | var mixinEl = document.createElement('a-mixin'); 37 | mixinEl.setAttribute('id', id); 38 | Object.keys(obj).forEach(function (componentName) { 39 | mixinEl.setAttribute(componentName, obj[componentName]); 40 | }); 41 | 42 | var assetsEl = scene ? scene.querySelector('a-assets') : document.querySelector('a-assets'); 43 | assetsEl.appendChild(mixinEl); 44 | 45 | return mixinEl; 46 | }; 47 | 48 | /** 49 | * Test that is only run locally and is skipped on CI. 50 | */ 51 | module.exports.getSkipCISuite = function () { 52 | if (window.__env__.TEST_ENV === 'ci') { 53 | return suite.skip; 54 | } else { 55 | return suite; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '../', 4 | browserify: { 5 | debug: true, 6 | paths: ['src'] 7 | }, 8 | browsers: ['Firefox', 'Chrome'], 9 | client: { 10 | captureConsole: true, 11 | mocha: {'ui': 'tdd'} 12 | }, 13 | envPreprocessor: ['TEST_ENV'], 14 | files: [ 15 | {pattern: 'tests/**/*.test.js'} 16 | ], 17 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'], 18 | preprocessors: {'tests/**/*.js': ['browserify', 'env']}, 19 | reporters: ['mocha'] 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/math/velocity.test.js: -------------------------------------------------------------------------------- 1 | var entityFactory = require('../helpers').entityFactory; 2 | 3 | var EPS = 1e-6; 4 | 5 | suite('velocity', function () { 6 | var el, 7 | component; 8 | 9 | suite('default', function () { 10 | setup(function (done) { 11 | el = this.el = entityFactory(); 12 | el.setAttribute('velocity', ''); 13 | el.addEventListener('loaded', function () { 14 | component = el.components.velocity; 15 | done(); 16 | }); 17 | }); 18 | 19 | test('defaults to 0 0 0', function () { 20 | component.update(); 21 | expect(el.getAttribute('velocity')).to.shallowDeepEqual({x: 0, y: 0, z: 0}); 22 | }); 23 | 24 | test('updates position', function () { 25 | el.setAttribute('velocity', {x: 1, y: 2, z: 3}); 26 | delete component.system; 27 | component.tick(100, 0.1); 28 | var position = el.object3D.position; 29 | expect(position.x).to.be.closeTo(0.0001, EPS); 30 | expect(position.y).to.be.closeTo(0.0002, EPS); 31 | expect(position.z).to.be.closeTo(0.0003, EPS); 32 | }); 33 | }); 34 | 35 | suite('physics', function () { 36 | var el, 37 | component, 38 | physics = { 39 | data: {maxInterval: 0.00005}, 40 | addComponent: function () {}, 41 | removeComponent: function () {} 42 | }; 43 | 44 | setup(function (done) { 45 | el = this.el = entityFactory(); 46 | el.sceneEl.systems.physics = physics; 47 | sinon.spy(physics, 'addComponent'); 48 | sinon.spy(physics, 'removeComponent'); 49 | el.setAttribute('velocity', ''); 50 | el.addEventListener('loaded', function () { 51 | component = el.components.velocity; 52 | done(); 53 | }); 54 | }); 55 | 56 | teardown(function () { 57 | physics.addComponent.restore(); 58 | physics.removeComponent.restore(); 59 | }); 60 | 61 | test('registers with the physics system', function () { 62 | expect(physics.addComponent).to.have.been.calledWith(component); 63 | }); 64 | 65 | test('unregisters with the physics system', function () { 66 | el.removeAttribute('velocity'); 67 | expect(physics.removeComponent).to.have.been.calledWith(component); 68 | }); 69 | 70 | test('defaults to 0 0 0', function () { 71 | component.update(); 72 | expect(el.object3D.position).to.shallowDeepEqual({x: 0, y: 0, z: 0}); 73 | }); 74 | 75 | test('updates position', function () { 76 | el.setAttribute('velocity', {x: 1, y: 2, z: 3}); 77 | component.tick(100, 0.1); 78 | expect(el.object3D.position).to.shallowDeepEqual({x: 0, y: 0, z: 0}); 79 | component.afterStep(100, 0.1 /* overridden by maxInterval */); 80 | var position = el.object3D.position; 81 | expect(position.x).to.be.closeTo(0.00005, EPS); 82 | expect(position.y).to.be.closeTo(0.00010, EPS); 83 | expect(position.z).to.be.closeTo(0.00015, EPS); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/physics/body.test.js: -------------------------------------------------------------------------------- 1 | var entityFactory = require('../helpers').entityFactory; 2 | 3 | var Body = require('../../src/components/body/body').definition, 4 | CustomBody = function () {}; 5 | 6 | AFRAME.utils.extend(CustomBody.prototype, Body); 7 | CustomBody.prototype.constructor = CustomBody; 8 | 9 | suite('body', function () { 10 | var el, 11 | component; 12 | 13 | var body = {type: 'CANNON.Body'}, 14 | physics = { 15 | removeComponent: sinon.spy(), 16 | removeBody: sinon.spy() 17 | }; 18 | 19 | setup(function (done) { 20 | el = body.el = entityFactory(); 21 | el.addEventListener('loaded', function () { 22 | el.sceneEl.systems.physics = physics; 23 | component = new CustomBody(); 24 | sinon.stub(component, 'initBody'); 25 | component.el = el; 26 | done(); 27 | }); 28 | }); 29 | 30 | teardown(function () { 31 | physics.removeComponent.reset(); 32 | physics.removeBody.reset(); 33 | }); 34 | 35 | suite('lifecycle', function () { 36 | test('init', function () { 37 | el.sceneEl.hasLoaded = true; 38 | component.init(); 39 | expect(component.system).to.equal(physics); 40 | expect(component.initBody).to.have.been.calledOnce; 41 | }); 42 | 43 | test.skip('update', function () { 44 | // TODO 45 | }); 46 | 47 | test('remove', function () { 48 | component.wireframe = {type: 'Wireframe'}; 49 | component.body = el.body = body; 50 | component.remove(); 51 | expect(el.body).to.be.undefined; 52 | expect(body.el).to.be.undefined; 53 | expect(component.body).to.be.undefined; 54 | expect(component.wireframe).to.be.undefined; 55 | }); 56 | 57 | test.skip('play', function () { 58 | // TODO 59 | }); 60 | 61 | test('pause', function () { 62 | component.isLoaded = true; 63 | component.system = physics; 64 | component.body = el.body = body; 65 | component.pause(); 66 | expect(physics.removeComponent).to.have.been.calledWith(component); 67 | expect(physics.removeBody).to.have.been.calledWith(body); 68 | }); 69 | }); 70 | 71 | suite('sync', function () { 72 | var eps = Number.EPSILON 73 | teardown(function () { 74 | delete body.quaternion 75 | delete body.position 76 | }); 77 | 78 | test.skip('syncToPhysics', function () { 79 | // TODO 80 | }); 81 | 82 | test.skip('syncFromPhysics', function () { 83 | // TODO 84 | }); 85 | 86 | test('syncFromPhysics nested rotation', function (done) { 87 | var childEl = el.appendChild(document.createElement('a-entity')); 88 | childEl.addEventListener('loaded', function () { 89 | component.el = childEl; 90 | component.body = el.body = body; 91 | // body rotation on x and y 92 | body.quaternion = new THREE.Quaternion() 93 | .setFromEuler(new THREE.Euler(Math.PI / 2, Math.PI / 2, 0)); 94 | body.position = {x: 0, y: 0, z: 0}; 95 | // parent already rotated on x, so child only needs y rotation 96 | el.object3D.quaternion.setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0)); 97 | component.syncFromPhysics(); 98 | var eulerOut = new THREE.Euler(); 99 | eulerOut.setFromQuaternion(childEl.object3D.quaternion); 100 | expect(eulerOut.x).to.be.closeTo(0, eps, 'x rotation'); 101 | expect(eulerOut.y).to.be.closeTo(Math.PI / 2, eps, 'y rotation'); 102 | expect(eulerOut.z).to.be.closeTo(0, eps, 'z rotation'); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/physics/system/physics.test.js: -------------------------------------------------------------------------------- 1 | var Physics = require('../../../src/system'), 2 | CustomPhysics = function () {}; 3 | 4 | AFRAME.utils.extend(CustomPhysics.prototype, Physics); 5 | CustomPhysics.prototype.constructor = CustomPhysics; 6 | 7 | suite('physics', function () { 8 | var system; 9 | 10 | setup(function () { 11 | system = new CustomPhysics(); 12 | }); 13 | 14 | suite('lifecycle', function () { 15 | test('noop', function () { 16 | expect(system).to.be.ok; 17 | }); 18 | 19 | test.skip('init', function () { 20 | // TODO 21 | }); 22 | 23 | test.skip('update', function () { 24 | // TODO 25 | }); 26 | 27 | test.skip('remove', function () { 28 | // TODO 29 | }); 30 | 31 | test.skip('play', function () { 32 | // TODO 33 | }); 34 | 35 | test.skip('pause', function () { 36 | // TODO 37 | }); 38 | }); 39 | }); 40 | --------------------------------------------------------------------------------