aframe-physics-system

Ammo Driver

Ammo.js is an Emscripten port of Bullet, a widely used open-source physics engine.

Contents

Considerations Before Use

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:

Installation

Initial installation is the same as for Cannon.js. See: Scripts, then see Including the Ammo.js Build.

Including the Ammo.js build

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 or NPM and Webpack. The latest WebAssembly build is available either via the Ammo.js github (https://kripken.github.io/ammo.js/builds/ammo.wasm.js) or the Mozilla Reality fork (https://cdn.jsdelivr.net/gh/MozillaReality/ammo.js@8bbc0ea/builds/ammo.wasm.js) created by the Mozilla Hubs team. The latter is especially optimized for use with the Ammo Driver and includes some functionality not yet available in the main repository.

Script Tag

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:

<script src="https://cdn.jsdelivr.net/gh/MozillaReality/ammo.js@8bbc0ea/builds/ammo.wasm.js"></script>
or
<script src="https://kripken.github.io/ammo.js/builds/ammo.wasm.js"></script>

Then, add aframe-physics-system itself, also with a script tag:

<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.js"></script>

NPM and Webpack

For more advanced projects that use npm and webpack, first npm install whichever version of ammo.js desired. npm install github:mozillareality/ammo.js#hubs/master or npm install github:kripken/ammo.js#master 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):

const Ammo = require("ammo.js/builds/ammo.wasm.js");
const AmmoWasm = require("ammo.js/builds/ammo.wasm.wasm");
window.Ammo = Ammo.bind(undefined, {
  locateFile(path) {
    if (path.endsWith(".wasm")) {
      return AmmoWasm;
    }
    return path;
  }
});
require("aframe-physics-system"); //note this require must happen after the above

Finally, add the following rule to your webpack.config.json:

{
    test: /\.(wasm)$/,
    type: "javascript/auto",
    use: {
        loader: "file-loader",
        options: {
            outputPath: "assets/wasm", //set this whatever path you desire
            name: "[name]-[hash].[ext]"
        }
    }
},

See this gist for more information.

Basics

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 for debugDrawMode options.

<a-scene physics=" driver: ammo; debug: true; debugDrawMode: 1;">
  <!-- ... -->
</a-scene>

To create a physics body, both an ammo-body and at least one ammo-shape component should be added to an entity.

<!-- Static box -->
<a-box position="0 0.5 -5" width="3" height="1" depth="1" ammo-body="type: static" ammo-shape="type: box"></a-box>

<!-- Dynamic box -->
<a-box position="5 0.5 0" width="1" height="1" depth="1" ammo-body="type: dynamic" ammo-shape="type: box"></a-box>

See examples/ammo.html for a working sample.

Components

ammo-body

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.

Property Default Description
type dynamic Options: dynamic, static, kinematic. See ammo-body type.
loadedEvent Optional event to wait for before the body attempt to initialize.
mass 1 Simulated mass of the object, >= 0.
gravity undefined undefined undefined Set the gravity for this specific object (if undefined, world gravity will be used - which defaults to 0 -9.8 0)
linearDamping 0.01 Resistance to movement.
angularDamping 0.01 Resistance to rotation.
linearSleepingThreshold 1.6 Minimum movement cutoff before a body can enter activationState: wantsDeactivation
angularSleepingThreshold 2.5 Minimum rotation cutoff before a body can enter activationState: wantsDeactivation
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.
activationState active Options: active, islandSleeping, wantsDeactivation, disableDeactivation, disableSimulation. See Activation States
emitCollisionEvents false Set to true to enable firing of collidestart and collideend events on this entity. See Events.
disableCollision false Set to true to disable object from colliding with all others.
collisionFilterGroup 1 32-bit bitmask to determine what collision “group” this object belongs to. See: Collision Filtering.
collisionFilterMask 1 32-bit bitmask to determine what collision “groups” this object should collide with. See: Collision Filtering.
scaleAutoUpdate true Should the shapes of the object be automatically scaled to match the scale of the entity.
restitution 0 Coefficient of restitution (bounciness). Note that this must be set to a non-zero value on both objects to get bounce from a collision.
This value cannot be changed after initialization of the ammo-body.

ammo-body type

The type of an ammo body can be one of the following:

Activation States

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.

state debug rendering color description
active white Waking state. Bodies will enter this state if collisions with other bodies occur. This is the default state.
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.
wantsDeactivation cyan Intermediary state between active and islandSleeping. Bodies will enter this state if they fall below linearSleepingThreshold and angularSleepingThreshold.
disableDeactivation red Forced active state. Bodies set to this state will never enter islandSleeping or wantsDeactivation.
disableSimulation yellow Bodies in this state will be completely ignored by the physics system.

Collision Filtering

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.

Using collision filtering requires basic understanding of the bitwise OR (a | b) and bitwise AND (a & b) operations.

Example: Imagine 3 groups of objects, A, B, and C. We will say their bit values are as follows:

collisionGroups: {
    A: 1,
    B: 2,
    C: 4
}

Assume all A objects should only collide with other A objects, and only B objects should collide with other B objects.

<!-- All A objects will look like this -->
<a-entity id="alpha" ammo-body="collisionFilterGroup: 1; collisionFilterMask: 1;"></a-entity>
<!-- All B objects will look like this -->
<a-entity id="beta" ammo-body="collisionFilterGroup: 2; collisionFilterMask: 2;"></a-entity>

Now Assume all C objects can collide with either A or B objects.

<!-- All A objects will look like this -->
<a-entity id="alpha" ammo-body="collisionFilterGroup: 1; collisionFilterMask: 5;"></a-entity>
<!-- All B objects will look like this -->
<a-entity id="beta" ammo-body="collisionFilterGroup: 2; collisionFilterMask: 6;"></a-entity>
<!-- All C objects will look like this -->
<a-entity id="gamma" ammo-body="collisionFilterGroup: 4; collisionFilterMask: 7;"></a-entity>

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.

// Object α (alpha) in group A and object β (beta) in group B overlap.

// α checks if it can collide with β. (α's collisionFilterMask AND β's collisionFilterGroup)
(5 & 2) = 0;

// β checks if it can collide with α. (β's collisionFilterMask AND α's collisionFilterGroup)
(6 & 1) = 0;

// Both checks equal 0; α and β do not collide.

// Now, object γ (gamma) in group C is overlapping with object β.

// β checks if it can collide with γ. (β's collisionFilterMask AND γ's collisionFilterGroup)
(6 & 7) = 6;

// γ checks if it can collide with β. (γ's collisionFilterMask AND β's collisionFilterGroup)
(7 & 2) = 2;

// Neither check equals 0; β and γ collide.

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Flags_and_bitmasks for more information about bitmasks.

ammo-shape

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.

Property Dependencies Default Description
type hull Options: box, cylinder, sphere, capsule, cone, hull, hacd, vhacd, mesh, heightfield. see Shape Types.
fit all Options: all, manual. Use manual if defining halfExtents or sphereRadius below. See Shape Fit.
halfExtents fit: manual and type: box, cylinder, capsule, cone 1 1 1 Set the halfExtents to use.
minHalfExtent fit: all and type: box, cylinder, capsule, cone 0 The minimum value for any axis of the halfExtents.
maxHalfExtent fit: all and type: box, cylinder, capsule, cone Number.POSITIVE_INFINITY The maximum value for any axis of the halfExtents.
sphereRadius fit: manual and type: sphere NaN Set the radius for spheres.
cylinderAxis y Options: x, y, z. Override default axis for cylinder, capsule, and cone types.
margin 0.01 The amount of ‘padding’ to add around the shape. Larger values have better performance but reduce collision shape precision.
offset 0 0 0 Where to position the shape relative to the origin of the entity.
heightfieldData fit: manual and type: heightfield [] An array of arrays of float values that represent a height at a fixed interval heightfieldDistance
heightfieldDistance fit: manual and type: heightfield 1 The distance between each height value in both the x and z direction in heightfieldData
includeInvisible fit: all false Should invisible meshes be included when using fit: all

Shape Types

Shape Fit

Note that in general, fit: manual is more performant than fit: all. This is because fit: all iterates over every point in the geometry to determine a suitable bounding volume, whereas fit: manual can just create the shape to the specified parameters. This is particularly important if you are going to be spawning new instances of objects while the physics simulation is ongoing.

Note that there is currently no caching of shapes generated from geometries, so even if you are creating shapes for the same geometry over & over you’ll still pay this performance penalty for each new ammo-shape.

ammo-constraint

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.

Example:

<a-box id="other-box" ammo-body ammo-shape />
<a-box ammo-constraint="target: #other-box;" ammo-body ammo-shape />
Property Dependencies Default Description
type lock Options: lock, fixed, spring, slider, hinge, coneTwist, pointToPoint.
target Selector for a single entity to which current entity should be bound.
pivot type: pointToPoint, coneTwist, hinge 0 0 0 Offset of the hinge or point-to-point constraint, defined locally in this element’s body.
targetPivot type: pointToPoint, coneTwist, hinge 0 0 0 Offset of the hinge or point-to-point constraint, defined locally in the target’s body.
axis type: hinge 0 0 1 An axis that each body can rotate around, defined locally to this element’s body.
targetAxis type: hinge 0 0 1 An axis that each body can rotate around, defined locally to the target’s body.

Using the Ammo.js API

The Ammo.js API lacks any usage documentation. Instead, it is recommended to read the Bullet 2.83 documentation, the Bullet forums and the Emscripten WebIDL Binder documentation. 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.

Some things to note:

const vector3 = new Ammo.btVector3();
... do stuff
Ammo.destroy(vector3);

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:

<a-scene>
  <a-entity id="nyan" dynamic-body="shape: hull" obj-model="obj: url(nyan-cat.obj)"></a-entity>
  <a-plane static-body></a-plane>
</a-scene>
var el = sceneEl.querySelector('#nyan');
const force = new Ammo.btVector3(0, 1, -0);
const pos = new Ammo.btVector3(el.object3D.position.x, el.object3D.position.y, el.object3D.position.z);
el.body.applyForce(force, pos);
Ammo.destroy(force);
Ammo.destroy(pos);

Events

event description
body-loaded Fired when physics body (el.body) has been created.
collidestart Fired when two bodies collide. emitCollisionEvents: true must be set on the ammo-body.
collideend Fired when two bodies stop colliding. emitCollisionEvents: true must be set on the ammo-body.

Collisions

ammo-driver generates events when a collision has started or ended, which are propagated onto the associated A-Frame entity. Example:

var playerEl = document.querySelector("[camera]");
playerEl.addEventListener("collide", function(e) {
  console.log("Player has collided with body #" + e.detail.targetEl.id);
  e.detail.targetEl; // Other entity, which playerEl touched.
});

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.

System Configuration

Property Default Description
driver local [local, worker, ammo]
debug true Whether to show wireframes for debugging.
debugDrawMode 0 See AmmoDebugDrawer
gravity -9.8 Force of gravity (in m/s^2).
iterations 10 The number of solver iterations determines quality of the constraints in the world.
maxSubSteps 4 The max number of physics steps to calculate per tick.
fixedTimeStep 0.01667 The internal framerate of the physics simulation.
stats   Where to output performance stats (if any), panel, console, events (or some combination).
- panel output stats to a panel similar to the A-Frame stats panel.
-events generates physics-tick-timer events, which can be processed externally.
-consoleoutputs stats to the console.

Statistics

The following statistics are available from the Ammo Driver. Each of these is refreshed every 100 ticks (i.e. every 100 frames).

Some statistics are related to the internals of the Ammo Driver, and are not completely understood at this time - but they may nevertheless be helpful in providing an approximate estimate of the complexity involved in a given physics scene.

Statistic Meaning
Static The number of static bodies being handled by the physics engine.
Dynamic The number of dynamic bodies being handled by the physics engine.
Kinematic The number of kinematic bodies being handled by the physics engine.
Manifolds A manifold represents a pair of bodies that are close to each other, but might have zero one or more actual contacts.
Contacts The number of actual contacts between pairs of bodies. There may be zero, one or multiple contacts per manifold (up to four - the physics engine discards any more than this, while always preserving the deepest contact point).
Collisions The number of current collisions between pairs of bodies. This means that the two bodies are in contact with each other (one or more contacts).
One would expect this number too be lower than the number of Manifolds, but that doesn’t seem to consistently be the case. This may indicate a bug, or may just indicate that we need to better understand & explain the exact meanings of these statistics.
Coll Keys An alternative measure of the number of current collisions between pairs of bodies, based on a distinct internal storage mechanism.
This seems to be consistently lower than Collisions, which may indicate a bug, or may just indicate that we need to better understand & explain the exact meanings of these statistics.
Before The number of milliseconds per tick before invoking the physics engine. Typically this is the time taken to synchronize the scene state into the physics engine, e.g. movements of kinematic bodies, or changes to physics shapes.
Median = median value in the last 100 ticks
90th % = 90th percentile value in the last 100 ticks
99th % = maximum recorded value over the last 100 ticks.
After The number of milliseconds per tick after invoking the physics engine. Typically this is the time taken to synchronize the physics engine state into the scene, e.g. movements of dynamic bodies.
Reported as Median / 90th / 99th percentiles, as above.
Engine The number of milliseconds per tick actually running the physics engine.
Reported as Median / 90th / 99th percentiles, as above.
Total The total number of milliseconds of physics processing per tick: Before + Engine + After. Reported as Median / 90th / 99th percentiles, as above.