threlte logo

Animation Transitions

Transition seamlessly between GLTF animations.

<script lang="ts">
  import { Pane, Button } from 'svelte-tweakpane-ui'
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { buttonIdle, buttonWalk, buttonRun } from './state'
</script>

<Pane
  title="Transitions"
  position="fixed"
>
  <Button
    title="Idle"
    on:click={() => {
      $buttonIdle = !$buttonIdle
    }}
  />
  <Button
    title="Walk"
    on:click={() => {
      $buttonWalk = !$buttonWalk
    }}
  />
  <Button
    title="Run"
    on:click={() => {
      $buttonRun = !$buttonRun
    }}
  />
</Pane>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    position: relative;
    height: 100%;
    width: 100%;
  }
</style>
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { GLTF, useGltfAnimations } from '@threlte/extras'
  import { buttonIdle, buttonWalk, buttonRun } from './state'

  let currentActionKey = 'idle'

  const { gltf, actions } = useGltfAnimations()

  $: $actions[currentActionKey]?.play()

  const unsub1 = buttonIdle.subscribe(() => {
    console.log('transition to idle')
    transitionTo('idle', 0.3)
  })

  const unsub2 = buttonWalk.subscribe(() => {
    console.log('transition to run')
    transitionTo('walk', 0.3)
  })

  const unsub3 = buttonRun.subscribe(() => {
    console.log('transition to run')
    transitionTo('run', 0.3)
  })

  function transitionTo(nextActionKey: string, duration = 1) {
    const currentAction = $actions[currentActionKey]
    const nextAction = $actions[nextActionKey]
    if (!nextAction || currentAction === nextAction) return
    // Function inspired by: https://github.com/mrdoob/three.js/blob/master/examples/webgl_animation_skinning_blending.html
    nextAction.enabled = true
    if (currentAction) {
      currentAction.crossFadeTo(nextAction, duration, true)
    }
    // Not sure why I need this but the source code does not
    nextAction.play()
    currentActionKey = nextActionKey
  }

  onDestroy(() => {
    // We unsubscribe otherwise we'd have old subscriptions still active
    unsub1()
    unsub2()
    unsub3()
  })
</script>

<GLTF
  bind:gltf={$gltf}
  url="https://threejs.org/examples/models/gltf/Xbot.glb"
/>
<script lang="ts">
  import { T } from '@threlte/core'
  import { injectLookAtPlugin } from '../../plugins/lookAt/lookAtPlugin'
  import Character from './Character.svelte'

  injectLookAtPlugin()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[-0.85, 1.75, 2.46]}
  lookAt={[0, 1, 0]}
/>

<T.AmbientLight />
<T.DirectionalLight position={[10, 5, 5]} />

<Character />

<T.Mesh rotation.x={-90 * (Math.PI / 180)}>
  <T.CircleGeometry args={[3, 72]} />
  <T.MeshStandardMaterial color={'white'} />
</T.Mesh>
import { writable } from 'svelte/store'

export const buttonIdle = writable(false)
export const buttonWalk = writable(false)
export const buttonRun = writable(false)

Explanation

glTF is a comprehensive file format for 3D models, and it supports animations. In this example, we extract the animations from the gltf file, play them, and crossfade between them.

What is the code doing?

  1. Extract the variables gltf and actions;

    const { gltf, actions } = useGltfAnimations()

  2. Bind gltf to the <GLTF> component,

<GLTF
  bind:gltf={$gltf}
  url="https://threejs.org/examples/models/gltf/Xbot.glb"
/>

this causes actions to be populated with an array of the available animations in that gltf file

run console.log(Object.entries($actions)) to see the available action strings, and the shape of the animation object. By doing this, you’ll discover that this example is only using 3 of the 7 available animations attached to this gltf file.

  1. selecting a different animation calls transitionTo function, which crossfades between the two animations