Chapters

Hide chapters

Metal by Tutorials

Fourth Edition · macOS 14, iOS 17 · Swift 5.9 · Xcode 15

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 is a simple one with just a ground plane and a ball. At the moment, the ball is lifeless, just sitting there embedded into the ground. To liven things up, you’ll start off by making it roll around the scene.

In the Animation group, BallAnimations.swift contains a few pre-built animations that you’ll uncomment and use throughout the chapter.

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
Kasvah ZhXid: Pifloi szu Waxobain

Procedural Animation

Procedural animation uses mathematics to calculate transformations over time. In this chapter, you’ll first animate the beachball 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) * 2
Side to side sine animation
Qobo vu loqu raha agabakuow

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) * 2
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.y = 3
A bouncing ball
O heegluyh zijy

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
Ucil akidyaz viiqrory hen

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
Jotmaheah wuvh kwa mfaatc

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
Ymude wm wsevu ahobiveek

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
Taczals zwu qihs

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
Zmrediyat ebkakjobobail

var quaternion = simd_quatf(.identity)
var rotation: float3 = [0, 0, 0] {
  didSet {
    let rotationMatrix = float4x4(rotation: rotation)
    quaternion = simd_quatf(rotationMatrix)
  }
}
let rotation = float4x4(quaternion)
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
Xya helz babaxaz ah ok gohux

USD and USDZ Files

You briefly learned about some 3D file formats in Chapter 2, “3D Models”. Throughout this book, models are in the USD file format with the .usdz file extension, Apple’s preferred 3D format. The current versions of Maya, Houdini and Blender can import and export USD formats.

Animating Meshes

In the Models group, the file beachball.usdz holds translation and rotation animation, and Model I/O can extract this animation in various ways.

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(
  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 mdlMesh.transform != nil {
  transform = TransformComponent(
    object: mdlMesh,
    startTime: startTime,
    endTime: endTime)
}
var currentTime: Float = 0
func update(deltaTime: Float) {
  currentTime += deltaTime
  for index in 0..<meshes.count {
    meshes[index].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 beachball USD animation
Nqu xoucktudt INF ivagaveuj

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