├── .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 | [](https://www.npmjs.com/package/aframe-physics-system)
14 | [](https://raw.githubusercontent.com/n5ro/aframe-physics-system/master/LICENSE)
15 | 
16 | 
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