Develop

Physics

Updated: Mar 12, 2026
The Physics feature adds rigid-body simulation to your spatial experience. You can make objects fall under gravity, bounce off surfaces, and connect to each other with constraints like hinges and springs — all through components that integrate with the SDK’s entity-component architecture.
Physics works at two levels:
  • Component-based: Add a Physics component to an entity and the framework handles simulation automatically. This is the recommended approach for most use cases.
  • Programmatic: Use ScenePhysicsObject and ScenePhysicsConstraint directly for advanced scenarios like compound collision shapes or runtime force application.

Register the physics feature

Add PhysicsFeature to your Activity’s feature list. This loads the native physics engine and registers the systems that run the simulation.
class MyActivity : AppSystemActivity() {

    override fun registerFeatures(): List<SpatialFeature> {
        return listOf(
            PhysicsFeature(spatial),
            VRFeature(this),
        )
    }
}
PhysicsFeature accepts optional parameters to configure the physics world:
PhysicsFeature(
    spatial = spatial,
    useGrabbablePhysics = true,
    worldBounds = PhysicsWorldBounds(minY = -10f),
    gravity = Vector3(0f, -10f, 0f)
)
ParameterDefaultDescription
useGrabbablePhysics
true
Automatically switch grabbed objects to KINEMATIC and restore on release
worldBounds
null
Destroy entities that leave these bounds
gravity
(0, -10, 0)
Gravity vector in m/s²

Add physics to an entity

Add a Physics component to make an entity participate in the simulation. Here’s a bouncy ball that falls under gravity:
val ball = Entity.create(
    Mesh(Uri.parse("mesh://sphere")),
    Transform(Pose(Vector3(0f, 2f, -1f))),
    Physics(
        shape = "sphere",
        state = PhysicsState.DYNAMIC,
        dimensions = Vector3(0.1f),
        density = 1.0f,
        restitution = 0.8f
    )
)
And a static floor for it to land on:
val floor = Entity.create(
    Mesh(Uri.parse("mesh://box")),
    Transform(Pose(Vector3(0f, 0f, -1f))),
    Physics(
        shape = "box",
        state = PhysicsState.STATIC,
        dimensions = Vector3(5f, 0.05f, 5f)
    )
)

Physics states

Every physics object has one of three states that determines how it interacts with the simulation:
StateMoves?Affected by forces?Use for
STATIC
No
No
Walls, floors, terrain
DYNAMIC
Yes
Yes
Balls, crates, debris
KINEMATIC
Programmatically
No
Moving platforms, elevators
You can change an entity’s state at runtime:
val physics = entity.getComponent<Physics>()
physics.state = PhysicsState.KINEMATIC
entity.setComponent(physics)
When useGrabbablePhysics is enabled (the default), the SDK handles this transition automatically for entities that have both Grabbable and Physics components. The object switches to KINEMATIC when grabbed and restores its original state on release.

Collision shapes

The shape parameter determines what collision geometry is used for the entity.

Primitive shapes

ShapeValueSize parameter
Box
"box"
dimensions = half-extents (width, height, depth)
Sphere
"sphere"
dimensions.x = radius
Cylinder
"cylinderX", "cylinderY", "cylinderZ"
dimensions.x = radius, dimensions.y = height

Mesh-based shapes

Point shape to a glTF file for collision geometry derived from a 3D model:
Physics(
    shape = "table.glb",
    collisionShapeType = CollisionShapeType.CONVEX_HULL,
    state = PhysicsState.DYNAMIC
)
CollisionShapeTypeBest forNotes
PRIMITIVE
Simple shapes (box, sphere)
Default. Fastest option
TRIANGLE_MESH
Exact collision with environment geometry
STATIC only
CONVEX_HULL
Approximate collision for moving objects
Fast, works for DYNAMIC
CONVEX_DECOMPOSITION
Concave shapes (bowls, L-shapes)
Generates multiple convex hulls. Slower to create
COMPOUND
Multi-part shapes
Built from multiple primitives
Important: TRIANGLE_MESH only works with STATIC objects. If you need accurate collision on a moving object, use CONVEX_HULL or CONVEX_DECOMPOSITION.
You can also load collision meshes from remote URLs. The SDK downloads and caches them automatically:
Physics(
    shape = "https://example.com/models/rock.glb",
    collisionShapeType = CollisionShapeType.CONVEX_HULL,
    state = PhysicsState.DYNAMIC
)

Material properties

Control how objects behave when they collide:
Physics(
    shape = "sphere",
    state = PhysicsState.DYNAMIC,
    density = 1.0f,
    restitution = 0.8f,
    friction = FrictionObject(
        sliding = 0.5f,
        rolling = 0.1f,
        spinning = 0.0f
    )
)
PropertyRangeDescription
density
> 0
Mass density in kg/m³. Mass = density × volume
restitution
0.0 – 1.0
Bounciness. 0 = no bounce, 1 = perfect bounce
friction.sliding
≥ 0
Resistance to sliding motion
friction.rolling
≥ 0
Resistance to rolling motion
friction.spinning
≥ 0
Resistance to spinning in place
You can apply preset materials for common real-world surfaces:
val crate = Physics(shape = "box", state = PhysicsState.DYNAMIC)
    .applyMaterial(PhysicsMaterial.WOOD)

Initial velocities and forces

Set an object’s starting velocity or apply continuous forces:
// Launch a ball upward and forward
Physics(
    shape = "sphere",
    state = PhysicsState.DYNAMIC,
    linearVelocity = Vector3(0f, 3f, -5f),
    angularVelocity = Vector3(10f, 0f, 0f)
)
To apply a continuous force (for example, simulating a fan):
val physics = entity.getComponent<Physics>()
physics.applyForce = Vector3(0f, 0f, -10f)
entity.setComponent(physics)

Collision detection

Register an event listener to respond when physics objects collide:
ball.registerEventListener<PhysicsCollisionCallbackEventArgs>("physicscollision")
{ entity, args ->
    val other = args.collidedEntity
    val position = args.collisionPosition
    val normal = args.collisionNormal
    val impulse = args.impulse

    if (impulse > 5f) {
        playImpactSound(position)
    }
}
The callback provides:
PropertyTypeDescription
collidedEntity
Entity
The other entity involved in the collision
collisionPosition
Vector3
World-space contact point
collisionNormal
Vector3
Surface normal at the contact
impulse
Float
Strength of the impact

Constraints

Constraints connect two physics bodies. The SDK provides six constraint types, each available as a component.

Hinge

Single-axis rotation. Use for doors, levers, and pendulums.
Entity.create(
    HingeConstraint(
        bodyA = doorFrame,
        bodyB = door,
        anchorA = Vector3(0.5f, 0f, 0f),
        anchorB = Vector3(-0.5f, 0f, 0f),
        axis = Vector3(0f, 1f, 0f),
        lowerLimit = 0f,
        upperLimit = 1.57f
    )
)
Hinges support motors that apply continuous torque:
HingeConstraint(
    bodyA = motor,
    bodyB = wheel,
    axis = Vector3(0f, 0f, 1f),
    motorEnabled = true,
    motorTargetVelocity = 3.14f,
    motorMaxForce = 50f
)

Ball socket

Free rotation around a point. Use for pendulums and shoulder-like joints.
Entity.create(
    BallSocketConstraint(
        bodyA = ceiling,
        bodyB = pendulum,
        anchorA = Vector3(0f, -0.1f, 0f),
        anchorB = Vector3(0f, 0.5f, 0f)
    )
)

Fixed

Rigidly connects two bodies so they move as one. Use for attaching props to moving objects.
Entity.create(
    FixedConstraint(
        bodyA = vehicle,
        bodyB = antenna
    )
)

Slider

Linear motion along a single axis. Use for drawers, pistons, and sliding doors.
Entity.create(
    SliderConstraint(
        bodyA = cabinet,
        bodyB = drawer,
        axis = Vector3(0f, 0f, 1f),
        lowerLimit = 0f,
        upperLimit = 0.4f
    )
)

Spring

Elastic connection with configurable stiffness and damping. Use for suspension, bungee cords, and wobbly antennas.
Entity.create(
    SpringConstraint(
        bodyA = anchor,
        bodyB = weight,
        anchorA = Vector3(0f, 0f, 0f),
        anchorB = Vector3(0f, 0f, 0f),
        stiffness = 100f,
        damping = 1f,
        restLength = 0.5f
    )
)

Cone twist

Rotation with cone-shaped limits. Use for rag-doll joints and robotic arms with restricted range of motion.
Entity.create(
    ConeTwistConstraint(
        bodyA = upperArm,
        bodyB = forearm,
        anchorA = Vector3(0f, -0.15f, 0f),
        anchorB = Vector3(0f, 0.15f, 0f),
        twistAxis = Vector3(0f, 1f, 0f),
        swingSpan1 = 1.2f,
        swingSpan2 = 0.8f,
        twistSpan = 0.5f
    )
)

Breakable constraints

Any constraint can break when forces exceed a threshold. Set breakForce and optionally breakTorque, then listen for the break event:
val shelf = Entity.create(
    HingeConstraint(
        bodyA = wall,
        bodyB = shelfBoard,
        axis = Vector3(0f, 0f, 1f),
        breakForce = 100f,
        breakTorque = 50f
    )
)

shelf.registerEventListener<PhysicsConstraintBreakEventArgs>("physics_constraint_break")
{ entity, args ->
    spawnDebrisAt(entity)
}
All constraint components share these common attributes:
AttributeTypeDescription
bodyA
Entity
First connected body
bodyB
Entity
Second connected body
anchorA
Vector3
Connection point on body A (local space)
anchorB
Vector3
Connection point on body B (local space)
breakForce
Float
Force threshold to break (0 = unbreakable)
breakTorque
Float
Torque threshold to break
isBroken
Boolean
Read-only. Whether the constraint has broken

Player avatar physics

The PlayerAvatarPhysics component creates kinematic colliders that follow the player’s head and hands, allowing the player to push dynamic objects in the scene.
Entity.create(
    PlayerAvatarPhysics(
        bodyEnabled = true,
        bodyRadius = 0.25f,
        handsEnabled = true,
        handRadius = 0.08f
    )
)
The body collider is a capsule from floor to head height. Hand colliders are spheres that follow the controllers. All colliders are KINEMATIC — they push dynamic objects but aren’t pushed back.
AttributeDefaultDescription
bodyEnabled
true
Enable the body capsule collider
bodyRadius
0.25f
Capsule radius in meters
handsEnabled
false
Enable hand sphere colliders
handRadius
0.08f
Hand sphere radius in meters
friction
0.5f
Friction for all avatar colliders
restitution
0.0f
Bounciness (0 = no bounce off the player)

Grabbable physics

When useGrabbablePhysics is enabled (the default), entities with both Grabbable and Physics components get automatic state transitions:
  • Player grabs the entity → state switches to KINEMATIC (follows hand)
  • Player releases the entity → state restores to its original value (typically DYNAMIC)
No extra code is needed:
Entity.create(
    Mesh(Uri.parse("ball.glb")),
    Transform(Pose(Vector3(0f, 1f, -1f))),
    Grabbable(),
    Physics(
        shape = "sphere",
        state = PhysicsState.DYNAMIC,
        restitution = 0.6f
    )
)

World configuration

Change gravity

You can change gravity at any time:
val physicsFeature = featureManager.findFeature<PhysicsFeature>()

// Moon gravity
physicsFeature.setGravity(Vector3(0f, -1.6f, 0f))

// Zero gravity
physicsFeature.setGravity(Vector3(0f, 0f, 0f))

Set world bounds

Destroy entities that leave the play area to prevent runaway objects from consuming resources:
PhysicsFeature(
    spatial = spatial,
    worldBounds = PhysicsWorldBounds(
        minY = -20f,
        minX = -50f, maxX = 50f,
        minZ = -50f, maxZ = 50f
    )
)

Debug visualization

Render wireframe collision shapes to troubleshoot physics issues:
val physicsFeature = featureManager.findFeature<PhysicsFeature>()
physicsFeature.enablePhysicsDebugLines(true)

Advanced: Colliders

ScenePhysicsCollider is the low-level API for creating collision shapes directly. The Physics component uses colliders internally, but you can create and manage them yourself for more control — shared shapes, capsules, or multi-part compound geometry.

Collider types

Factory methodShapeParameters
createBox
Box
width, height, depth (half-extents)
createSphere
Sphere
radius
createCylinderX/Y/Z
Cylinder
radius, halfHeight (aligned to X, Y, or Z axis)
createCapsuleX/Y/Z
Capsule
radius, halfHeight (aligned to X, Y, or Z axis)
createConvexHull
Convex hull from glTF
filename
createGLTF
Triangle mesh from glTF
filename
createCompound
Empty compound (add children)
Important: Capsule colliders (createCapsuleX/Y/Z) are only available through the collider API. The Physics component does not expose capsule shapes directly.

Create a standalone collider

All collider factory methods take a Scene reference:
val scene = getScene()

val boxCollider = ScenePhysicsCollider.createBox(scene, 1f, 0.5f, 1f)
val sphereCollider = ScenePhysicsCollider.createSphere(scene, 0.3f)
val capsuleCollider = ScenePhysicsCollider.createCapsuleY(scene, 0.1f, 0.4f)

Share a collider across multiple bodies

A single collider can back multiple physics objects. This saves memory when you have many identically shaped bodies:
val scene = getScene()
val bulletShape = ScenePhysicsCollider.createSphere(scene, 0.02f)

for (entity in bulletEntities) {
    ScenePhysicsObject.createFromCollider(scene, entity, bulletShape, 0.01f)
}

Build compound shapes

Compound colliders combine multiple primitives into one shape. Use them for objects that don’t fit a single box, sphere, or cylinder:
val scene = getScene()
val compound = ScenePhysicsCollider.createCompound(scene)

// T-shape: horizontal bar + vertical bar
compound.addChildBox(0.5f, 0.05f, 0.05f, Pose())                       // horizontal
compound.addChildBox(0.05f, 0.3f, 0.05f, Pose(Vector3(0f, -0.3f, 0f))) // vertical
compound.addChildSphere(0.08f, Vector3(0f, -0.6f, 0f))                 // knob

val physicsObject = ScenePhysicsObject.createFromCollider(scene, entity, compound, 2f)
Compound child methods:
MethodParameters
addChildBox
halfW, halfH, halfD, localPose
addChildSphere
radius, localPos
addChildCylinder
radius, halfHeight, localPose

Register a compound as a custom shape

You can register compound colliders with PhysicsCreationSystem so they work with the Physics component:
val physicsCreationSystem = systemManager.findSystem<PhysicsCreationSystem>()

physicsCreationSystem.physicsCreators["physics://dumbbell"] = { entity ->
    val scene = getScene()
    val compound = ScenePhysicsCollider.createCompound(scene)
    compound.addChildSphere(0.15f, Vector3(-0.4f, 0f, 0f))
    compound.addChildBox(0.4f, 0.04f, 0.04f, Pose())
    compound.addChildSphere(0.15f, Vector3(0.4f, 0f, 0f))

    val physics = entity.getComponent<Physics>()
    val mass = if (physics.state == PhysicsState.DYNAMIC) physics.densityInternal else 0f
    ScenePhysicsObject.createFromCollider(scene, entity, compound, mass)
}
Then reference the shape by name:
Entity.create(
    Transform(Pose(Vector3(0f, 1f, -1f))),
    Physics(
        shape = "physics://dumbbell",
        state = PhysicsState.DYNAMIC,
        density = 7800f
    )
)

Cleanup

Colliders are cleaned up automatically when garbage collected. To release native memory sooner, call destroy():
collider.destroy()

Advanced: Programmatic physics control

For runtime manipulation beyond what components provide, access ScenePhysicsObject directly through the creation system:
val physicsSystem = systemManager.findSystem<PhysicsCreationSystem>()
val physicsObject = physicsSystem.getPhysicsObject(entity)

// Apply an impulse force
physicsObject?.applyForce(Vector3(0f, 500f, 0f))

// Apply force at a specific point on the body (relative to center of mass)
physicsObject?.applyForceAtRelativePosition(
    Vector3(0f, 0f, -10f),    // force direction
    Vector3(0.5f, 0f, 0f)     // offset from center
)

// Apply torque
physicsObject?.applyTorque(0f, 10f, 0f)

// Teleport to a new position
physicsObject?.setPose(Pose(Vector3(0f, 5f, 0f)))

// Read current position
val currentPose = physicsObject?.getPose()

// Set velocity directly
physicsObject?.setLinearVelocity(Vector3(0f, 5f, 0f))
physicsObject?.setAngularVelocity(Vector3(0f, 3.14f, 0f))

// Adjust damping (controls how quickly motion decays)
physicsObject?.setDamping(linearDamping = 0.1f, angularDamping = 0.95f)

// Disable gravity for this object only
physicsObject?.setGravityEnabled(false)

// Change body type at runtime
physicsObject?.setBodyType(PhysicsState.KINEMATIC, 0f)

// Tune sleeping behavior
physicsObject?.setDeactivationTime(2f)
physicsObject?.setSleepingThresholds(linear = 0.8f, angular = 1f)
ScenePhysicsObject method reference:
MethodDescription
applyForce(force)
Apply a force at center of mass
applyForceAtRelativePosition(force, offset)
Apply a force at an offset from center of mass
applyTorque(x, y, z)
Apply rotational force
setPose(pose)
Teleport to a new position and rotation
getPose()
Read current world-space position and rotation
setLinearVelocity(velocity)
Set linear velocity directly
setAngularVelocity(velocity)
Set angular velocity directly
setRestitution(value)
Change bounciness
setFriction(sliding, rolling, spinning)
Change friction values
setDamping(linear, angular)
Control motion decay
setGravityEnabled(enabled)
Toggle gravity for this body
setBodyType(state, mass)
Switch between STATIC, DYNAMIC, KINEMATIC
setDeactivationTime(seconds)
How long until the body sleeps
setSleepingThresholds(linear, angular)
Velocity thresholds for sleep
destroy()
Release native resources

Advanced: Build a custom physics system

The Physics component and its systems handle most use cases. When you need full control — for example, a projectile system with custom velocity logic, or physics objects that don’t use the standard component — you can build your own system using ScenePhysicsObject and ScenePhysicsCollider directly.
Important: You still need PhysicsFeature registered. It initializes the native physics engine and runs the simulation tick. Your custom system replaces the Physics component, not the engine itself.

Step 1: Define a custom component

Create a component schema that carries only the data your system needs:
<?xml version="1.0" ?>
<ComponentSchema packageName="com.example.physics">
  <Component name="Projectile">
    <Vector3Attribute
      name="initialVelocity"
      defaultValue="0.0f, 0.0f, 0.0f"
    />
    <FloatAttribute
      name="radius"
      defaultValue="0.05"
    />
  </Component>
</ComponentSchema>

Step 2: Write the system

Your system watches for entities with your custom component, creates ScenePhysicsObject instances for them, and cleans up when entities are deleted:
class ProjectileSystem : SystemBase() {
    private val physicsObjects = HashMap<Entity, ScenePhysicsObject>()

    override fun execute() {
        val q = Query.where { changed(Projectile.id) and has(Transform.id) }
        for (entity in q.eval()) {
            if (physicsObjects.containsKey(entity)) continue

            val projectile = entity.getComponent<Projectile>()
            val scene = getScene()

            // Create a sphere collider and physics body
            val collider = ScenePhysicsCollider.createSphere(scene, projectile.radius)
            val physicsObject = ScenePhysicsObject.createFromCollider(
                scene, entity, collider, 0.1f // mass in kg
            )

            // Set initial transform and velocity
            physicsObject.setPose(entity.getComponent<Transform>().transform)
            physicsObject.setLinearVelocity(projectile.initialVelocity)
            physicsObject.setRestitution(0.3f)
            physicsObject.setGravityEnabled(true)

            physicsObjects[entity] = physicsObject
        }
    }

    override fun delete(entity: Entity) {
        physicsObjects.remove(entity)?.destroy()
    }
}

Step 3: Register the system

Register your system alongside PhysicsFeature. The feature handles the simulation tick; your system handles creation:
class MyActivity : AppSystemActivity() {

    override fun registerFeatures(): List<SpatialFeature> {
        return listOf(
            PhysicsFeature(spatial),
            VRFeature(this),
        )
    }

    override fun registerSystems(): List<SystemBase> {
        return listOf(
            ProjectileSystem(),
        )
    }

    override fun registerComponents(): List<ComponentRegistration> {
        return listOf(
            ComponentRegistration(Projectile.Companion),
        )
    }
}

Step 4: Spawn entities with the custom component

Use your component instead of Physics:
Entity.create(
    Mesh(Uri.parse("mesh://sphere")),
    Transform(Pose(Vector3(0f, 1.5f, -0.5f))),
    Projectile(
        initialVelocity = Vector3(0f, 5f, -10f),
        radius = 0.05f
    )
)

Create constraints programmatically

When building a custom system, use ScenePhysicsConstraint directly instead of constraint components:
val scene = getScene()

val hinge = ScenePhysicsConstraint.createHinge(
    scene, entityA, entityB,
    anchorA = Vector3(0.5f, 0f, 0f),
    anchorB = Vector3(-0.5f, 0f, 0f),
    axis = Vector3(0f, 1f, 0f)
)

// Configure after creation
hinge.setBreakingThreshold(force = 200f, torque = 100f)
hinge.setMotor(enabled = true, targetVelocity = 3.14f, maxForce = 50f)
hinge.setLimits(lower = 0f, upper = 1.57f)

// Query state at runtime
val impulse = hinge.getAppliedImpulse()
val broken = hinge.isBroken()

// Disable without destroying
hinge.setEnabled(false)
Available constraint factory methods:
MethodParameters
createHinge
entityA, entityB, anchorA, anchorB, axis, lowerLimit, upperLimit
createBallSocket
entityA, entityB, anchorA, anchorB
createFixed
entityA, entityB, anchorA, anchorB
createSlider
entityA, entityB, axis, lowerLimit, upperLimit
createSpring
entityA, entityB, anchorA, anchorB, stiffness, damping, restLength
createConeTwist
entityA, entityB, anchorA, anchorB, twistAxis, swingSpan1, swingSpan2, twistSpan, softness, biasFactor, relaxationFactor

Tips

  • Use primitives when you can. Box and sphere collisions are much faster than mesh-based collision. Use CONVEX_HULL or TRIANGLE_MESH only when the shape matters for gameplay.
  • Keep mass ratios reasonable. A 10,000 kg object interacting with a 0.001 kg object produces jitter. Keep mass ratios under 100:1 for stable simulation.
  • Set world bounds. Objects that fly off-screen still consume CPU. Use PhysicsWorldBounds to clean them up.
  • Enable debug lines when troubleshooting. Mismatched visual and collision geometry is the most common cause of objects that appear to float or clip through surfaces.
  • Create physics objects before constraints. Constraints reference bodies by entity. If the body’s physics object hasn’t been created yet, the constraint fails silently.
  • Use capsules for characters.ScenePhysicsCollider.createCapsuleY gives better collision than a box for upright figures. This shape is only available through the collider API.
  • Share colliders for identical shapes. If you spawn 100 bullets with the same radius, create one ScenePhysicsCollider and pass it to each ScenePhysicsObject.createFromCollider.