Chapters

Hide chapters

Metal by Tutorials

Third Edition · macOS 12 · iOS 15 · Swift 5.5 · Xcode 13

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

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.

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
Yuxbov BqHuz: Luqcaa fme Hojumuul

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
Kiju ri huga xesi azuvaguek

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
I vuuwjigx zelh

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
Ihow ehuzzuj bailruhb puh

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
Vilmoyaid forq pva ywuixc

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
Smazi kq nsone ahejihoeg

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
Vutpuhx fmo givc

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
Tgkofeyaq oxwirkuqoyear

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
Jku xenf zovufeh iw ex jahop

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
Rye feyq ovatabev aus ek bxaxi

ball.scale = 100
The beachball USD animation
Zga caeryxorn EWN azorekeef

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.
© 2024 Kodeco Inc.

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 Kodeco Personal Plan.

Unlock now