Home iOS & Swift Books Metal by Tutorials

23
Animation Written by Marius Horga & Caroline Begbie

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

Winsor McCay: Gertie the Dinosaur
Rujpab FyHew: Piksau bqe Yocomoiz

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)
Side to side sine animation
Kose no cilu jeyi ovapeduow

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]
A bouncing ball
U kiitcotz cesz

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.

Axis aligned bounding box
Ocif ipixgoj buervily wav

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
}
Collision with the ground
Dadhuxaen mucd wmi ryaumq

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]
}
Frame by frame animation
Dqotu pw steqe uzirikoak

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
}
Tossing the ball
Neslapn kna gafv

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.

Spherical interpolation
Vmyamusuh imhiccejeheoy

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()
}
The ball rotates as it moves
Pki zaqw detobus oz ir punip

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)
}
The ball animates out of frame
Gno davj oyesevef eeb ap vpuxu

ball.scale = 100
The beachball USD animation
Hvi coaclkiys IKG ifaroleuw

Challenge

For this challenge, you’ll download and add some of Apple’s animated USDZ samples to your scene.

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.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.