Change camera transforms on top of gsap scrolltrigger controls

I have a gltf scroll animation (a camera moving downwards) set up via a gsap scrolltrigger. The animation itself works like a charm - what I’m struggling with is:
How do I tie in a custom animate() function (to maniuplate the camera on top of the animation)

Any alterations to the camera, i.e. through the camera shake effect from ‘@react-three/postprocessing’, leads to the camera orientation being “offset”, even with all camera shake parameters set to 0.

Ideally, I’d like to implement subtle camera movements according to the mouse input, but I’m at a loss how to make both the scrolltrigger and a custom animate() function play along nicely.

Here’s what I have so far:

useLayoutEffect(() => {
    const animationDuration = animations[0].duration;
    const clip = anim.actions[anim.names];
    clip.clampWhenFinished = true;
    clip.loop = THREE.LoopOnce;;
    const mixer = clip.getMixer();

    let ctx = gsap.context(() => {
      let proxy = {
        get time() {
          return mixer.time;
        set time(value) {
          clip.paused = false;
          clip.paused = true;
      proxy.time = 0;

      // Initializes timeline for scroll animation.
          scrollTrigger: {
            trigger: document.getElementById('scroll-wrapper'),
            pin: false,
            scrub: true,
            start: 'top top',
            end: 'bottom bottom',
            markers: false
            time: 0
            time: animationDuration,
            ease: 'none',
            duration: 3
    }, group);

    return () => ctx.revert();

Here’s how I tried to implement the custom mouse movement function. It works okay (except for the camera offset) when I don’t scroll. As soon as I scroll the scrolltrigger takes over. Is there a way to combine the two?

// Camera Move
  const { camera, scene, gl } = useThree();

  const mouse = new THREE.Vector2();
  const target = new THREE.Vector2();
  const windowHalf = new THREE.Vector2(
    window.innerWidth / 2,
    window.innerHeight / 2

  document.addEventListener('mousemove', onMouseMove, false);
  function onMouseMove(event) {
    mouse.x = event.clientX - windowHalf.x;
    mouse.y = event.clientY - windowHalf.x;

  function animate() {
    target.x = (1 - mouse.x) * 0.0001;
    target.y = (1 - mouse.y) * 0.0001;

    camera.rotation.x += 0.05 * (target.y - camera.rotation.x);
    camera.rotation.y += 0.05 * (target.x - camera.rotation.y);

    // gl.render(scene, camera);

Here’s a video to demonstrate the problem.

I think the problem lies in the fact that the camera rotation is hardcoded in the gltf animation. I think I’d have to use useFrame({camera}) to get the current rotation and add on top of that. I’ll investigate further.

Curious thing: When I simply add a CameraShakeEffect from drei, it somehow works - except that the initial rotation of the Camera from the gltf-file is ignored. If I comment out the CameraShake, save, bring it back in and save, the effect works with the inital camera rotation.

Instead of animating the camera… make another node like
let anchor = new THREE.Group()

Then animate “anchor” with gsap, and you can then control the orientation of the camera independently.

Hey Manthrax,
thanks a lot for your reply. I had tried an empty group before, but had messed up the camera rotation. Now it works like a charm.

Here’s how I set it up in Blender:

  • Added an Empty with Track To Constraint (like previously on my camera)
  • Copied all keyframes from Camera to the Empty.
  • Cleaned the keyframes on the Camera, set it’s rotation to x 90 and parented it to the Empty.

==> Now, in the animation the Empty controls the camera, which itself can be further modified. Genius!

1 Like

Awesome! Glad you got it sorted it out :slight_smile: