Skip to content

Latest commit

 

History

History
384 lines (287 loc) · 30.6 KB

AmmoDriver.md

File metadata and controls

384 lines (287 loc) · 30.6 KB

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:

  • 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.
  • 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.
  • 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 if you do so. See: Using the Ammo.js API.

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:

  • dynamic: A freely-moving object. Dynamic bodies have mass, collide with other bodies, bounce or slow during collisions, and fall if gravity is enabled.

  • 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.

    • If you do re-position a static object after initialization, you'll need to explicitly update the physics system with the new position. You can do that like this:

      this.el.components['ammo-body'].syncToPhysics();
      
  • 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.

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

  • Primitives
    • Box (box) – Requires halfExtents if using fit: manual.
    • Cylinder (cylinder) – Requires halfExtents if using fit: manual. Use cylinderAxis to change which axis the length of the cylinder is aligned.
    • Sphere (sphere) – Requires sphereRadius if using fit: manual.
    • Capsule (capsule) – Requires halfExtents if using fit: manual. Use cylinderAxis to change which axis the length of the capsule is aligned.
    • Cone (cone) – Requires halfExtents if using fit: manual. Use cylinderAxis to change which axis the point of the cone is aligned.
  • Hull (hull) – Wraps a model in a convex hull, like a shrink-wrap. Not quite as performant as primitives, but still very fast.
  • Hull Approximate Convex Decomposition (hacd) – This is an experimental feature that generates multiple convex hulls to approximate any convex or concave shape.
  • Volumetric Hull Approximate Convex Decomposition (vhacd) – Also experimental, this is hacd with a different algorithm. See: https://kmamou.blogspot.com/2014/11/v-hacd-v20-is-here.html for more information.
  • 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:
    • 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.
    • Avoid very high poly meshes in general and use mesh decimation (simplification) if possible.
  • 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.

Shape Fit

  • fit: all – Requires a mesh to exist on the entity. The specified shape will be created to contain all the vertices of the mesh.
  • 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.

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:

  • Not all classes and properties in Bullet are available. Each class and property has to be 'exposed' via a definition in ammo.idl.
  • 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.
const vector3 = new Ammo.btVector3();
... do stuff
Ammo.destroy(vector3);
  • Exposed properties on classes can be accessed via specially generated get_<property>() and set_<property>() functions. E.g. rayResultCallback.get_m_collisionObject();
  • 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);
  • Conversely, sometimes you need the pointer of an object. Use Ammo.getPointer. E.g. const ptr = Ammo.getPointer(object);.

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.