How to constrain animation?

I’ve imported a GLB from an artist, who originally organised his FBX file using a “multi-take” animation system, with animations sequenced one after the other. I can successfully import the animation, but when I go to play the animation, there is a long delay before and after the animation. I’d like to be able to loop the animation … but I’ve not found the trick to it.

This is what I see:

fish-delay

I’m using @react-three/fiber to import the file:

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
author: Denys Almaral (https://sketchfab.com/denysalmaral)
license: SKETCHFAB Standard (https://sketchfab.com/licenses)
source: https://sketchfab.com/3d-models/low-poly-sea-fishes-with-animations-4dd46cf3a91648469302355cce82e660
title: Low Poly Sea Fishes with Animations
*/

import { useAnimations, useGLTF } from '@react-three/drei'
import { useEffect, useRef } from 'react'
import { AnimationClip } from 'three'
import { GLTF } from 'three-stdlib'

import fishesUrl from '/src/assets/clownfish.glb?url'

interface Props {
  position?: [number, number, number]
}

interface GLTFResult extends GLTF {
  nodes: {
    _rootJoint: any
    Object_7: {
      geometry: any
      skeleton: any
    }
  }
  materials: {
    fishclown: any
  }
  animations: AnimationClip[] & {
    swim: AnimationClip
  }
}

export function Clownfish(props: Props) {
  const group = useRef()
  const { nodes, materials, animations } = useGLTF(fishesUrl) as GLTFResult
  const { actions } = useAnimations(animations, group)

  useEffect(() => {
    // actions.swim?.getMixer().setTime(5.69)
    actions.swim!.play()
  }, [])

  return (
    <group ref={group} {...props} dispose={null}>
      <group name="Scene">
        <group name="Sketchfab_model" rotation={[-Math.PI / 2, 0, 0]}>
          <group
            name="3a338159af63455cbb591f306837a4cefbx"
            rotation={[0, 0, -Math.PI / 2]}
            scale={0.1}
          >
            <group name="Object_2">
              <group name="RootNode">
                <group name="Object_4">
                  <primitive object={nodes._rootJoint} />
                  <group
                    name="fishClown"
                    position={[0, 0.14, -2.21]}
                    rotation={[-Math.PI / 2, 0, 0]}
                  />
                  <group
                    name="Object_6"
                    position={[0, 0.14, -2.21]}
                    rotation={[-Math.PI / 2, 0, 0]}
                  />
                  <skinnedMesh
                    name="Object_7"
                    geometry={nodes.Object_7.geometry}
                    material={materials.fishclown}
                    skeleton={nodes.Object_7.skeleton}
                  />
                </group>
              </group>
            </group>
          </group>
        </group>
      </group>
    </group>
  )
}

useGLTF.preload(fishesUrl)

Inside the useEffect I have access to the clip and the mixer, but I’ve not found how to specify “just animate time between ~5.5s and ~7.5s on a loop” which is what I really want.

Any ideas?

In three you can use sub clip which takes in a start frame, an end frame and your fps of the animation in question, you should be able to use the same here…

Thanks. For future reference for anyone else with this problem, I did this:

import { useFrame } from '@react-three/fiber'
import { useMemo } from 'react'
import { AnimationAction, AnimationClip, AnimationMixer, Group, KeyframeTrack } from 'three'
import { subclip } from 'three/src/animation/AnimationUtils'


interface AnimationActions {
  [id: string]: AnimationAction
}

interface Props {
  animations: AnimationClip[]
  fps?:       number
  padding?:   [number, number]
  root:       Group | null
}

/**
 * Works around a problem I found with animations generated by a designer who used what he describes as a "multi-take"
 * animation approach. I found that animations would have long leading and trailing gaps. To fix that, `makeAction`
 * finds the start and end frames of the animation and creates a new clip to play only those frames.
 */
export function useMultiTakeAnimations(props: Props): AnimationActions {
  const { animations, fps = 30, padding = [0, 0], root } = props

  const info = useMemo(() => {
    return root ? setup(root) : { mixer: null, actions: {} }

    function setup(root: Group): { actions: AnimationActions, mixer: AnimationMixer } {
      const mixer = new AnimationMixer(root)

      const actions: AnimationActions = {}
      animations.forEach((clip) => {
        actions[clip.name] = makeAction({ clip, fps, mixer, padding })
      })

      return { actions, mixer }
    }
  }, [root, fps, padding[0], padding[1]])

  useFrame((_, delta) => {
    const { mixer } = info
    mixer?.update(delta)
  })

  return info.actions
}

interface ActionProps {
  clip:    AnimationClip
  fps:     number
  mixer:   AnimationMixer
  padding: [number, number]
}

function makeAction(props: ActionProps): AnimationAction {
  const { clip, fps, mixer, padding } = props
  const { tracks, name } = clip

  const start = Math.max(0, getStartFrame() + padding[0])
  const end = getEndFrame() + padding[1]
  const sub = subclip(clip, name, start, end)
  return mixer.clipAction(sub)

  function getStartFrame(): number {
    return Math.floor(Math.min(...tracks.map(getTrackStart)) * fps)

    function getTrackStart(track: KeyframeTrack): number {
      return Math.min(...track.times)
    }
  }

  function getEndFrame(): number {
    return Math.ceil(Math.max(...tracks.map(getTrackEnd)) * fps)

    function getTrackEnd(track: KeyframeTrack): number {
      return Math.max(...track.times)
    }
  }
}