Show/Hide Mobile Menu

Physics in RealityKit

31.05.2022

To have a model entity to take part in physics you need to add a collision shape and a physics body:

let size = 0.05
let modelEntity = ModelEntity(mesh: .generateBox(size: size), materials: [SimpleMaterial(color: .red, isMetallic: false)])
modelEntity.collision = CollisionComponent(shapes: [.generateBox(size: size])
modelEntity.physicsBody = PhysicsBodyComponent(massProperties: .default, material: .default, mode: .dynamic)

There are 3 possible physics body modes:

  1. static: The entity doesn't move.
  2. kinematic: You set the velocity of the entity.
  3. dynamic: Forces and collisions affect the entity.

Dynamic Mode

To move an entity in dynamic mode you can apply a force or impulse. A linear force is applied for one frame and is measured in Newtons (N) while a linear impulse is a force applied over time and is measured in Newton Seconds (Ns). To apply a linear force of 0.12N for 1/60 second (assuming a frame rate of 60fps) in the Z direction you would do:

modelEntity.addForce([0, 0, 0.12], relativeTo: modelEntity.parent)

Or for an equivalent linear impulse of 0.002Ns (0.12N / 60):

modelEntity.applyLinearImpulse([0, 0, 0.002], relativeTo: modelEntity.parent)

The argument you pass to relativeTo defines the coordinate system that the vector is in. If you pass the parent the force will always point in the same direction regardless of the direction that the entity is pointing.

Angular force or torque rotates the entity around its origin. The diagram below shows how the torque depends on the force and the length of the lever.

Torque

The direction of the torque vector is worked out using the right hand grip rule. If your right hand is curled around the axis of rotation with your fingers pointing in the direction of the force, then the torque vector points in the the direction of your thumb. So to apply a force of 0.12Nm counter-clockwise around the entity's Y axis for 1/60 second (i.e. 1 frame) you would do the following:

modelEntity.addTorque([0, 0.12, 0], relativeTo: modelEntity)

Or to apply an equivalent angular impulse of 0.002Nms (0.12 / 60):

modelEntity.addAngularImpulse([0, 0.002, 0], relativeTo: modelEntity)

Example App

The example app creates 5 spheres and 1 cuboid inside a container. When you enable physics you need to have a ground or else the objects will fall forever because of gravity. The container was created in Reality Composer. Each entity has Participates checked under Physics and Motion Type set to Fixed. This is the same as setting the physics body mode to static. When you tap on a sphere it will apply a linear impulse in the Z direction. When you tap the cuboid it will apply an angular impulse around the Y axis.

Physics in RealityKit, Reality Composer
@IBAction func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
    guard gestureRecognizer.view != nil else { return }
    
    if gestureRecognizer.state == .ended {
        let screenLocation = gestureRecognizer.location(in: self)
        var hits = hitTest(screenLocation, query: .nearest, mask: sphereCollisionGroup)
        if hits.count > 0 {
            if let modelEntity = hits[0].entity as? ModelEntity {
                pushSphere(modelEntity)
            }
        } else {
            hits = hitTest(screenLocation, query: .nearest, mask: boxCollisionGroup)
            if hits.count > 0, let modelEntity = hits[0].entity as? ModelEntity {
                spinBox(modelEntity)
            }
        }
    }
}

private func pushSphere(_ modelEntity: ModelEntity) {
    modelEntity.applyLinearImpulse([0, 0, 0.002], relativeTo: modelEntity.parent)
}

private func spinBox(_ modelEntity: ModelEntity) {
    modelEntity.applyAngularImpulse([0, 0.0002, 0], relativeTo: modelEntity)
}

You can download the full code for this app here.

Kinematic Mode

To set the linear or angular velocity of an entity, the entity needs to have its physics body mode set to .kinematic. If you leave it in that mode the entity will continue with that velocity forever and friction and other entities will have no effect on it. An entity needs to have a physics motion component as well as a physics body component.

let size = 0.05
let modelEntity = ModelEntity(mesh: .generateBox(size: size), materials: [SimpleMaterial(color: .red, isMetallic: false)])
modelEntity.collision = CollisionComponent(shapes: [.generateBox(size: size])
modelEntity.physicsBody = PhysicsBodyComponent(massProperties: PhysicsMassProperties.default, material: PhysicsMaterialResource.default, mode: .kinematic)

modelEntity.physicsMotion = PhysicsMotionComponent()

Set linear velocity:

modelEntity.physicsMotion?.linearVelocity = [0, 0, 0.1]

To set angular velocity:

modelEntity.physicsMotion?.angularVelocity = [0, 0.1, 0]

Example App

This example app is similar to the previous one. A number of spheres and a cuboid are randomly placed in a container. You can move around a sphere with your finger and when you lift your finger the sphere will keep moving but will slow down due to friction. When the gesture starts the physics body mode of the sphere is set to kinematic and when it finishes the mode is set back to dynamic so that friction and other entities have an effect on it.

@objc private func handleSphereTranslation(_ recognizer: EntityTranslationGestureRecognizer) {
    let sphere = recognizer.entity as! HasPhysics
    
    if recognizer.state == .began {
        sphere.physicsBody?.mode = .kinematic
    } else if recognizer.state == .ended || recognizer.state == .cancelled {
        sphere.physicsBody?.mode = .dynamic
        return
    }
    
    let velocity = recognizer.velocity(in: sphere.parent)
    sphere.physicsMotion?.linearVelocity = [velocity.x, 0, velocity.z]
}

For the green cuboid, the mode is initially set to kinematic and never changed. Swiping on it sets its angular velocity and it never stops spinning.

@objc private func handleBoxTranslation(_ recognizer: EntityTranslationGestureRecognizer) {
    let box = recognizer.entity as! HasPhysics
    let velocity = recognizer.velocity(in: box.parent)
    box.physicsMotion?.angularVelocity = [0, simd_length(velocity) * 15, 0]
}

You can download the full code for this app here.