# 23. Animation Written by Marius Horga & Caroline Begbie

Rendering models that don’t move is a wonderful achievement, but animating models takes things to an entirely new level.

To animate means to bring to life. So what better way to play with animation than to render characters with personality and body movement. In this chapter, you’ll find out how to do basic animation using keyframes.

## The Starter Project

➤ In Xcode, open the starter project for this chapter, and build and run the app.

The scene contains a ground plane and a ball. Because there’s no skybox, the renderer will use the forward renderer with PBR shading.

In the Animation group, BallAnimations.swift contains a few pre-built animations. At the moment, the ball animation is a bit unnatural looking — it’s just sitting there embedded into the ground. To liven things up, you’ll start off by making it roll around the scene.

## Animation

Animators like Winsor McCay and Walt Disney brought life to still images by filming a series of hand-drawn pictures one frame at a time.

## Procedural Animation

Procedural animation uses mathematics to calculate transformations over time. In this chapter, you’ll first animate the ball using the sine function, just as you did earlier in Chapter 7, “The Fragment Function”, when you animated a quad with trigonometric functions.

``````struct Beachball {
var ball: Model
var currentTime: Float = 0

init(model: Model) {
self.ball = model
ball.position.y = 1
}

mutating func update(deltaTime: Float) {
currentTime += deltaTime
}
}
``````
``````lazy var beachball = Beachball(model: ball)
``````
``````beachball.update(deltaTime: deltaTime)
``````
``````ball.position.x = sin(currentTime)
``````

## Animation Using Physics

Instead of creating animation by hand using an animation app, you can use physics-based animation, which means that your models can simulate the real world. In this next exercise, you’re going to simulate only gravity and a collision. However, a full physics engine can simulate all sorts of effects, such as fluid dynamics, cloth and soft body (rag doll) dynamics.

``````var ballVelocity: Float = 0
``````
``````ball.position.x = sin(currentTime)
``````
``````let gravity: Float = 9.8 // meter / sec2
let mass: Float = 0.05
let acceleration = gravity / mass
let airFriction: Float = 0.2
let bounciness: Float = 0.9
let timeStep: Float = 1 / 600
``````
``````ballVelocity += (acceleration * timeStep) / airFriction
ball.position.y -= ballVelocity * timeStep

// collision with ground
if ball.position.y <= 0.35 {
ball.position.y = 0.35
ballVelocity = ballVelocity * -1 * bounciness
}
``````
``````ball.position = [0, 3, 0]
``````

### Axis-Aligned Bounding Box

You hard-coded the ball’s radius so that it collides with the ground, but collision systems generally require some kind of bounding box to test whether an object collides with another object.

``````var boundingBox = MDLAxisAlignedBoundingBox()
var size: float3 {
return boundingBox.maxBounds - boundingBox.minBounds
}
``````
``````boundingBox = asset.boundingBox
``````
``````// collision with ground
if ball.position.y <= ball.size.y / 2 {
ball.position.y = ball.size.y / 2
ballVelocity = ballVelocity * -1 * bounciness
}
``````

## Keyframes

Let’s animate the ball getting tossed around by adding some input information about its position over time. For this input, you’ll need an array of positions so that you can extract the correct position for the specified time.

``````mutating func update(deltaTime: Float) {
currentTime += deltaTime
ball.position.y = 1

let fps: Float = 60
let currentFrame =
Int(currentTime * fps) % (ballPositionXArray.count)
ball.position.x = ballPositionXArray[currentFrame]
}
``````

### Interpolation

It’s a lot of work inputting a value for each frame. If you’re just moving an object in a straight line from point A to B, you can interpolate the value. Interpolation is where you calculate a value given a range of values and a current location within the range. When animating, the current location is the current time as a percentage of the animation duration.

``````struct Keyframe<Value> {
var time: Float = 0
var value: Value
}
``````
``````struct Animation {
var translations: [Keyframe<float3>] = []
var repeatAnimation = true
}
``````
``````func getTranslation(at time: Float) -> float3? {
// 1
guard let lastKeyframe = translations.last else {
return nil
}
// 2
var currentTime = time
if let first = translations.first,
first.time >= currentTime {
return first.value
}
// 3
if currentTime >= lastKeyframe.time,
!repeatAnimation {
return lastKeyframe.value
}
}
``````
``````// 1
currentTime = fmod(currentTime, lastKeyframe.time)
// 2
let keyFramePairs = translations.indices.dropFirst().map {
(previous: translations[\$0 - 1], next: translations[\$0])
}
// 3
guard let (previousKey, nextKey) = (keyFramePairs.first {
currentTime < \$0.next.time
})
else { return nil }
// 4
let interpolant =
(currentTime - previousKey.time) /
(nextKey.time - previousKey.time)
// 5
return simd_mix(
previousKey.value,
nextKey.value,
float3(repeating: interpolant))
``````
``````mutating func update(deltaTime: Float) {
currentTime += deltaTime
var animation = Animation()
animation.translations = ballTranslations
ball.position =
animation.getTranslation(at: currentTime) ?? [0, 0, 0]
ball.position.y += ball.size.y
}
``````

### Euler Angle Rotations

Now that you have the ball translating through the air, you probably want to rotate it as well. To express rotation of an object, you currently hold a `float3` with rotation angles on `x`, `y` and `z` axes. These are known as Euler angles after the mathematician Leonhard Euler. Euler is the man behind Euler’s rotation theorem — a theorem which states that any rotation can be described using three rotation angles. This is OK for a single rotation, but interpolating between these three values doesn’t work in a way that you may think.

``````init(rotation angle: float3) {
let rotationX = float4x4(rotationX: angle.x)
let rotationY = float4x4(rotationY: angle.y)
let rotationZ = float4x4(rotationZ: angle.z)
self = rotationX * rotationY * rotationZ
}
``````

## Quaternions

Multiplying x, y and z rotations without compelling a sequence on them is impossible unless you involve the fourth dimension. In 1843, Sir William Rowan Hamilton did just that: he inscribed his fundamental formula for quaternion multiplication on to a stone on a bridge in Dublin.

``````var quaternion = simd_quatf()
``````
``````let rotation = float4x4(quaternion)
``````
``````var rotation: float3 = [0, 0, 0] {
didSet {
let rotationMatrix = float4x4(rotation: rotation)
quaternion = simd_quatf(rotationMatrix)
}
}
``````
``````var quaternion: simd_quatf {
get { transform.quaternion }
set { transform.quaternion = newValue }
}
``````
``````var rotations: [Keyframe<simd_quatf>] = []
``````
``````func getRotation(at time: Float) -> simd_quatf? {
guard let lastKeyframe = rotations.last else {
return nil
}
var currentTime = time
if let first = rotations.first,
first.time >= currentTime {
return first.value
}
if currentTime >= lastKeyframe.time,
!repeatAnimation {
return lastKeyframe.value
}
currentTime = fmod(currentTime, lastKeyframe.time)
let keyFramePairs = rotations.indices.dropFirst().map {
(previous: rotations[\$0 - 1], next: rotations[\$0])
}
guard let (previousKey, nextKey) = (keyFramePairs.first {
currentTime < \$0.next.time
})
else { return nil }
let interpolant =
(currentTime - previousKey.time) /
(nextKey.time - previousKey.time)
return simd_slerp(
previousKey.value,
nextKey.value,
interpolant)
}
``````
``````mutating func update(deltaTime: Float) {
currentTime += deltaTime
var animation = Animation()
animation.translations = ballTranslations
animation.rotations = ballRotations
ball.position =
animation.getTranslation(at: currentTime)
?? float3(repeating: 0)
ball.position.y += ball.size.y / 2
ball.quaternion =
animation.getRotation(at: currentTime)
?? simd_quatf()
}
``````

## USD and USDZ Files

One major problem to overcome is how to import animation from 3D apps. Model I/O can import .obj files, but they only hold static information, not animation. USD is a format devised by Pixar, which can hold massive scenes with textures, animation and lighting information. There are various file extensions:

## Animating Meshes

The file beachball.usda holds translation and rotation animation, and Model I/O can extract this animation. There are several ways to approach initializing this information, and you’ll use the first in this chapter.

``````static var fps: Double = 0
``````
``````Self.fps = Double(metalView.preferredFramesPerSecond)
``````
``````import ModelIO

struct TransformComponent {
let keyTransforms: [float4x4]
let duration: Float
var currentTransform: float4x4 = .identity
}
``````
``````init(
transform: MDLTransformComponent,
object: MDLObject,
startTime: TimeInterval,
endTime: TimeInterval
) {
duration = Float(endTime - startTime)
let timeStride = stride(
from: startTime,
to: endTime,
by: 1 / TimeInterval(GameController.fps))
keyTransforms = Array(timeStride).map { time in
MDLTransform.globalTransform(
with: object,
atTime: time)
}
}
``````
``````mutating func getCurrentTransform(at time: Float) {
guard duration > 0 else {
currentTransform = .identity
return
}
let frame = Int(fmod(time, duration) * Float(GameController.fps))
if frame < keyTransforms.count {
currentTransform = keyTransforms[frame]
} else {
currentTransform = keyTransforms.last ?? .identity
}
}
``````
``````init(
mdlMesh: MDLMesh,
mtkMesh: MTKMesh,
startTime: TimeInterval,
endTime: TimeInterval
) {
self.init(mdlMesh: mdlMesh, mtkMesh: mtkMesh)
}
``````
``````Mesh(
mdlMesh: \$0.0,
mtkMesh: \$0.1,
startTime: asset.startTime,
endTime: asset.endTime)
``````
``````var transform: TransformComponent?
``````
``````if let mdlMeshTransform = mdlMesh.transform {
transform = TransformComponent(
transform: mdlMeshTransform,
object: mdlMesh,
startTime: startTime,
endTime: endTime)
} else {
transform = nil
}
``````
``````var currentTime: Float = 0
``````
``````var meshes: [Mesh]
``````
``````func update(deltaTime: Float) {
currentTime += deltaTime
for i in 0..<meshes.count {
meshes[i].transform?.getCurrentTransform(at: currentTime)
}
}
``````
``````uniforms.modelMatrix = transform.modelMatrix
uniforms.normalMatrix = uniforms.modelMatrix.upperLeft
``````
``````encoder.setVertexBytes(
&uniforms,
length: MemoryLayout<Uniforms>.stride,
index: UniformsBuffer.index)
``````
``````let currentLocalTransform =
mesh.transform?.currentTransform ?? .identity
uniforms.modelMatrix =
transform.modelMatrix * currentLocalTransform
uniforms.normalMatrix = uniforms.modelMatrix.upperLeft
encoder.setVertexBytes(
&uniforms,
length: MemoryLayout<Uniforms>.stride,
index: UniformsBuffer.index)
``````
``````for model in models {
model.update(deltaTime: deltaTime)
}
``````

``````ball.scale = 100
``````

## Key Points

• Animation used to be done using frame-by-frame, but nowadays, animation is created on computers and is usually done using keyframes and interpolation.
• Procedural animation uses physics to compute values at a given time.
• Axis-aligned bounding boxes are useful when calculating collisions between aligned objects.
• Keyframes are generally extreme values between which the computer interpolates. This chapter demonstrates keyframing transformations, but you can animate anything. For example, you can set keyframes for color values over time.
• You can use any formula for interpolation, such as linear, or ease-in / ease-out.
• Interpolating quaternions is preferable to interpolating Euler angles.
• USD files are common throughout the 3D industry because you can keep the entire pipeline stored in the flexible format that USD provides.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.