Physics in RealityKit
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:
static
: The entity doesn't move.kinematic
: You set the velocity of the entity.dynamic
: Forces and collisions affect the entity.
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.
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)
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.
@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.
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()
To set linear velocity:
modelEntity.physicsMotion?.linearVelocity = [0, 0, 0.1]
And to set angular velocity:
modelEntity.physicsMotion?.angularVelocity = [0, 0.1, 0]
This example app is similar to the previous one. A number of spheres and a cuboid are randomly placed in a container. You can drag a sphere around 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 changes. 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.