A-Frame physics using PhysX - based on an original implementation by Zach Capalbo
For examples of usage, see:
There is just one main JS module, physx.js
, which triggers download of a specified additional wasm module.
The URL for the PhysX wasm module is specified on the physx
component schema - but the default settings should work for most cases.
You can either download the module from the dist
directory of this repo and include them like this:
<script src="physx.js"></script>
Or you can download via JSDelivr CDN (specifying the version number you want to use)
<script src="https://cdn.jsdelivr.net/gh/c-frame/physx@latest/dist/physx.min.js"></script>
Not supported yet - if you want this, please raise an issue. PRs also welcome!
Clone this repo, and run
npm install
To run for development purposes, run:
npm run dev
or npm start
Examples can be viewed at /examples
To build (both development & production builds), run:
npm run dist
Implements the a physics system using an emscripten compiled PhysX engine.
If autoLoad
is true
, or when you call startPhysX()
, the physx
system will automatically load and initialize the physics system with reasonable defaults and a ground plane. All you have to do is add physx-body
to the bodies that you want to be part of the simulation. The system will take try to take care of things like collision meshes, position updates, etc automatically. The simplest physics scene looks something like:
<a-scene physx="autoLoad: true">
<a-assets><a-asset-item id="#mymodel" src="..."></a-asset-item></a-assets>
<a-box physx-body="type: static" color="green" position="0 0 -3"></a-box>
<a-sphere physx-body="type: dynamic" position="0.4 2 -3" color="blue"></a-sphere>
<a-entity physx-body="type: dynamic" position="0 5 -3" gltf-model="#mymodel"></a-entity>
</a-scene>
If you want a little more control over how things behave, you can set the physx-material
component on the objects in your simulation, or use physx-joint
s, physx-joint-constraint
s and physx-joint-driver
s to add some complexity to your scene.
If you need more low-level control, the PhysX bindings are exposed through the PhysX
property of the system. So for instance, if you wanted to make use of the PxCapsuleGeometry
in your own component, you would call:
let myGeometry = new this.el.sceneEl.PhysX.PxCapsuleGeometry(1.0, 2.0)
The system uses Zach Capalbo's fork of PhysX, built using the Docker Wrapper. To see what's exposed to JavaScript, see PxWebBindings.cpp
For a complete example of how to use this, you can see the aframe-vartiste-toolkit Physics Playground
It is also helpful to refer to the NVIDIA PhysX documentation
Property | Type | Default | Description |
---|---|---|---|
delay | number | 5000 | Amount of time to wait after loading before starting the physics. Can be useful if there is still some things loading or initializing elsewhere in the scene |
throttle | number | 10 | Throttle for running the physics simulation. On complex scenes, you can increase this to avoid dropping video frames |
autoLoad | boolean | false | If true, the PhysX will automatically be loaded and started. If false, you will have to call startPhysX() manually to load and start the physics engine |
speed | number | 1 | Simulation speed multiplier. Increase or decrease to speed up or slow down simulation time |
wasmUrl | string | https://cdn.jsdelivr.net/gh/c-frame/physx/wasm/physx.release.wasm | URL for the PhysX WASM bundle. |
useDefaultScene | boolean | true | If true, sets up a default scene with a ground plane and bounding cylinder. |
wrapBounds | boolean | false | NYI |
groundCollisionLayers | string | Which collision layers the ground belongs to | |
groundCollisionMask | string | Which collision layers will collide with the ground | |
gravity | vec3 | { x: 0, y: -9.8, z: 0 } | Global gravity vector |
stats | array of strings | 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.- console outputs stats to the console. |
Signature | Description |
---|---|
startPhysX () |
Loads PhysX and starts the simulation |
Controls physics properties for individual shapes or rigid bodies. You can set this either on an entity with the phyx-body
component, or on a shape or model contained in an entity with the physx-body
component. If it's set on a physx-body
, it will be the default material for all shapes in that body. If it's set on an element containing geometry or a model, it will be the material used for that shape only.
For instance, in the following scene fragment:
<a-entity id="bodyA" physx-body physx-material="staticFriction: 0.5">
<a-box id="shape1" physx-material="staticFriction: 1.0"></a-box>
<a-sphere id="shape2"></a-sphere>
</a-entity>
<a-cone id="bodyB" physx-body></a-cone>
shape1
, which is part of the bodyA
rigid body, will have static friction of 1.0, since it has a material set on it. shape2
, which is also part of the bodyA
rigid body, will have a static friction of 0.5, since that is the body default. bodyB
will have the component default of 0.2, since it is a separate body.
Property | Type | Default | Description |
---|---|---|---|
staticFriction | number | 0.2 | Static friction |
dynamicFriction | number | 0.2 | Dynamic friction |
restitution | number | 0.2 | Restitution, or "bounciness" |
density | number | Density for the shape. If densities are specified for all shapes in a rigid body, then the rigid body's mass properties will be automatically calculated based on the different densities. However, if density information is not specified for every shape, then the mass defined in the overarching physx-body will be used instead. |
|
collisionLayers | array | [1] | Which collision layers this shape is present on |
collidesWithLayers | array | [ 1, 2, 3, 4 ] | Array containing all layers that this shape should collide with |
collisionGroup | number | 0 | If collisionGroup is greater than 0, this shape will not collide with any other shape with the same collisionGroup value |
contactOffset | string | If >= 0, this will set the PhysX contact offset, indicating how far away from the shape simulation contact events should begin. | |
restOffset | string | If >= 0, this will set the PhysX rest offset |
Turns an entity into a PhysX rigid body. This is the main component for creating physics objects.
Types
There are 3 types of supported rigid bodies. The type can be set by using the type
proeprty, but once initialized cannot be changed.
dynamic
objects are objects that will have physics simulated on them. The entity's world position, scale, and rotation will be used as the starting condition for the simulation, however once the simulation starts the entity's position and rotation will be replaced each frame with the results of the simulation.static
objects are objects that cannot move. They can be used to create collidable objects fordynamic
objects, or for anchor points for joints.kinematic
objects are objects that can be moved programmatically, but will not be moved by the simulation. They can however, interact with and collide with dynamic objects. Each frame, the entity'sobject3D
will be used to set the position and rotation for the simulation object.
Shapes
When the component is initialized, and on the object3dset
event, all visible meshes that are descendents of this entity will have shapes created for them. Each individual mesh will have its own convex hull automatically generated for it. This means you can have reasonably accurate collision meshes both from building up shapes with a-frame geometry primitives, and from importing 3D models.
Visible meshes can be excluded from this shape generation process by setting the physx-no-collision
attribute on the corresponding a-entity
element. Invisible meshes can be included into this shape generation process by settingt the physx-hidden-collision
attribute on the corresponding a-entity
element. This can be especially useful when using an external tool (like Blender V-HACD) to create a low-poly convex collision mesh for a high-poly or concave mesh. This leads to this pattern for such cases:
<a-entity physx-body="type: dynamic">
<a-entity gltf-model="HighPolyOrConcaveURL.gltf" physx-no-collision=""></a-entity>
<a-entity gltf-model="LowPolyConvexURL.gltf" physx-hidden-collision="" visible="false"></a-entity>
</a-entity>
Note, in such cases that if you are setting material properties on individual shapes, then the property should go on the collision mesh entity
Property | Type | Default | Description |
---|---|---|---|
type | string | dynamic | [dynamic, static, kinematic] Type of the rigid body to create |
mass | number | 1 | Total mass of the body |
angularDamping | number | 0 | If > 0, will set the rigid body's angular damping |
linearDamping | number | 0 | If > 0, will set the rigid body's linear damping |
emitCollisionEvents | boolean | false | If set to true , it will emit contactbegin and contactend events when collisions occur |
highPrecision | boolean | false | If set to true , the object will receive extra attention by the simulation engine (at a performance cost). |
shapeOffset | vec3 | { x: 0, y: 0, z: 0 } |
Signature | Description |
---|---|
toggleGravity () |
Turns gravity on and off |
Creates a driver which exerts force to return the joint to the specified (currently only the initial) position with the given velocity characteristics.
This can only be used on an entity with a physx-joint
component. Currently only supports D6 joint type. E.g.
<a-box physx-body>
<a-entity position="0.2 0.3 0.4" rotation="0 90 0"
physx-joint="type: D6; target: #other-body"
physx-joint-driver="axes: swing, twist; stiffness: 30; angularVelocity: 3 3 0">
</a-entity>
</a-box>
Property | Type | Default | Description |
---|---|---|---|
axes | array | [] | Which axes the joint should operate on. Should be some combination of x , y , z , twist , swing |
stiffness | number | 1 | How stiff the drive should be |
damping | number | 1 | Damping to apply to the drive |
forceLimit | number | 3.4028234663852886e+38 | Maximum amount of force used to get to the target position |
useAcceleration | boolean | true | If true, will operate directly on body acceleration rather than on force |
linearVelocity | vec3 | { x: 0, y: 0, z: 0 } | Target linear velocity relative to the joint |
angularVelocity | vec3 | { x: 0, y: 0, z: 0 } | Targget angular velocity relative to the joint |
lockOtherAxes | boolean | false | If true, will automatically lock axes which are not being driven |
slerpRotation | boolean | true | If true SLERP rotation mode. If false, will use SWING mode. |
Adds a constraint to a physx-joint
. Currently only D6 joints are supported.
Can only be used on an entity with the physx-joint
component. You can set multiple constraints per joint. Note that in order to specify attributes of individual axes, you will need to use multiple constraints. For instance:
<a-box physx-body>
<a-entity physx-joint="type: D6"
physx-joint-constraint__xz="constrainedAxes: x,z; linearLimit: -1 20"
physx-joint-constraint__y="constrainedAxes: y; linearLimit: 0 3; stiffness: 3"
physx-joint-constraint__rotation="lockedAxes: twist; swing"></a-entity>
</a-box>
In the above example, the box will be able to move from -1 to 20 in both the x and z direction. It will be able to move from 0 to 3 in the y direction, but this will be a soft constraint, subject to spring forces if the box goes past in the y direction. All rotation will be locked. (Note that since no target is specified, it will use the scene default target, effectively jointed to joint's initial position in the world)
Property | Type | Default | Description |
---|---|---|---|
lockedAxes | array | [] | Which axes are explicitly locked by this constraint and can't be moved at all. Should be some combination of x , y , z , twist , swing |
constrainedAxes | array | [] | Which axes are constrained by this constraint. These axes can be moved within the set limits. Should be some combination of x , y , z , twist , swing |
freeAxes | array | [] | Which axes are explicitly freed by this constraint. These axes will not obey any limits set here. Should be some combination of x , y , z , twist , swing |
linearLimit | vec2 | Limit on linear movement. Only affects x , y , and z axes. First vector component is the minimum allowed position |
|
limitCone | vec2 | Two angles specifying a cone in which the joint is allowed to swing, like a pendulum. | |
twistLimit | vec2 | Minimum and maximum angles that the joint is allowed to twist | |
damping | number | 0 | Spring damping for soft constraints |
restitution | number | 0 | Spring restitution for soft constraints |
stiffness | number | 0 | If greater than 0, will make this joint a soft constraint, and use a spring force model |
Creates a PhysX joint between an ancestor rigid body and a target rigid body.
The physx-joint
is designed to be used either on or within an entity with the physx-body
component. For instance:
<a-entity physx-body="type: dynamic">
<a-entity physx-joint="target: #other-body" position="1 0 0"></a-entity>
</a-entity>
The position and rotation of the physx-joint
will be used to create the corresponding PhysX joint object. Multiple joints can be created on a body, and multiple joints can target a body.
Stapler Example
Here's a simplified version of the stapler from the physics playground demo
<a-entity id="stapler">
<a-entity id="stapler-top" physx-body="type: dynamic" class="grab-root">
<a-entity class="clickable" propogate-grab="" gltf-part-plus="src: #asset-stapler; part: Top"></a-entity>
<a-entity physx-joint="target: #stapler-bottom; type: Revolute; collideWithTarget: true" position="0 0.0254418 -3.7280"></a-entity>
</a-entity>
<a-entity id="stapler-bottom" gltf-part-plus="src: #asset-stapler; part: Bottom" physx-body="type: dynamic"></a-entity>
</a-entity>
Notice the joint is created between the top part of the stapler (which contains the joint) and the bottom part of the stapler at the position of the physx-joint
component's entity. This will be the pivot point for the stapler's rotation.
Property | Type | Default | Description |
---|---|---|---|
type | string | Spherical | Rigid body joint type to use. See the NVIDIA PhysX joint documentation for details on each type |
target | selector | Target object. If specified, must be an entity having the physx-body component. If no target is specified, a scene default target will be used, essentially joining the joint to its initial position in the world. |
|
breakForce | vec2 | { x: -1, y: -1 } | Force needed to break the constraint. First component is the linear force, second component is angular force. Set both components are >= 0 |
removeElOnBreak | boolean | false | If true, removes the entity containing this component when the joint is broken. |
collideWithTarget | boolean | false | If false, collision will be disabled between the rigid body containing the joint and the target rigid body. |
softFixed | boolean | false | When used with a D6 type, sets up a "soft" fixed joint. E.g., for grabbing things |
If emitCollisionEvents
is set of a physx-body
then contactbegin
and contactend
events are emitted on that entity when it collides with other bodies.
Note the important restriction that SDK state changes are not permitted from an event callback (full details here). This means that you should never add / remove / modify bodies, materials, joints, etc. directly from a collision event callback.
If you need to make such changes in response to a collision event, you can use setTimeout() with a zero timer to delay the update until the physics processing for this tick has completed.
These events include a detail
object with the following properties:
Property | Description |
---|---|
thisShape | A PxShape for the shape within this body involved in the collision. This can be used to look up the physx-body that owns the shape using this.shapeMap on the phsyx component on the scene. For a compound shape, there is currently no way to map this back to a specific object3D that corresponds to the shape in question. |
otherShape | A PxShape for the shape within the other body involved in the collision. See row above for what can be done with this. |
points | The set of contact points, a PxVec3Vector . Read length from points.size() , then access using points.get(index) for each index < size , which returns a JS object with properties x, y & z (like a THREE.Vector3, but not actually one).This will be null on a contactend event. |
impulses | The set of impulses at these contact points, a VectorPxReal . Read length from impulses.size() , then access using impulses.get(index) for each index < size , which returns a number.This will be null on a contactend event. |
otherComponent | The physx-body component for the other object in the collision. |
The following statistics are available from PhysX. Each of these is refreshed every 100 ticks (i.e. every 100 frames).
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. |
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. 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. Note that unlike the physics implementations in aframe-physics-system, PhysX has no significant per-frame processing before invoking the physics engine, so the "after" statistic accounts for all the work done outside the PhysX WASM code. |
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: Engine + After. Reported as Median / 90th / 99th percentiles, as above. |
This repository is based on an original implementation of A-Frame physics using PhysX by Zach Capalbo
Simplification into a standalone codebase by Lee Stemkoski