pmndrs / drei

🥉 useful helpers for react-three-fiber
https://docs.pmnd.rs/drei
MIT License
8.22k stars 673 forks source link

Canvas view is skewed when using ScrollControls #2081

Open mirker21 opened 2 weeks ago

mirker21 commented 2 weeks ago

Hello!

I am currently using "@react-three/drei": "^9.111.3", "@react-three/fiber": "^8.17.5", "three": "^0.167.1"

I am having an issue with the canvas in r3f. When I implemented ScrollControls on the camera, the environment and all the items in the scene appear to be stretched out vertically, and would return to normal as soon as I scrolled to the bottom. Also, the Environment takes up the whole canvas when PerspectiveCamera is used, unlike Orthographic camera, but I originally intended to use OrthographicCamera.

Here is my example of the problem I am encountering. I would like the skewed effect to not happen, and for the camera’s animation to happen instead. If you know what might be causing this issue, please let me know!

Thank you!

mirker21 commented 2 weeks ago

Update: as mentioned in this post, adding a useLayoutEffect helps a little bit. However, I'm not able to control the aspect that well so that cameraRef.current remains consistently unstretched. If I set the aspect to a decimal, it doesn't look stretched, but it stretches when I scroll closer to the bottom. Also the useLayoutEffect only seems to trigger whenever I resize the window. I was expecting scroll.offset to trigger this aspect change, but it doesn't.

mirker21 commented 2 weeks ago

Update: I found a hacky sort of solution, I created a state that was changed to the scrollTop value using the onWheel event handler on the , and I passed this value onto the camera where I had the state be a dependency in useLayoutEffect.

For the equation for calculating the correct aspect, I wrote down x and y values (x = scrollTop, y=aspect) starting from the top and plugged them into here, which gave me an equation to use in useLayoutEffect to update the aspect more accurately.

The onWheel event listener doesn't detect every minute wheel movement though, so there are small moments here and there that the aspect has a slight stretch effect before it corrects itself, but at least it is easier to look at now.

Here is my code:

ScrollControls Canvas Component:

import { ScrollControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import Camera from "./3D_models/3D_intro_components/Camera"
import { Suspense, useRef, useState } from "react";

export default function WebpageContentIntro3D(): JSX.Element {
    const [scrollTop, setScrollTop] = useState(0);
    const scrollRef = useRef<any>('div#webpage-intro-3d')

    function handleScroll() {
        if (scrollRef.current !== null) {
            setScrollTop(scrollRef.current.children[0].children[0].children[1].scrollTop)
        }
    }

    return (
        <div id="webpage-intro-3d" ref={scrollRef} onWheel={() => handleScroll()}>
            <Canvas id="webpage-intro-3d-canvas">
                <ambientLight intensity={4} color={0xFFFFFF} />

                <mesh position={[0,0,0]}>
                    <meshStandardMaterial color='red' />
                    <boxGeometry args={[1,1,1]} />
                </mesh>

                <Suspense fallback={null}>
                    <ScrollControls pages={20} damping={0.2} maxSpeed={0.25}>
                        <Camera scrollTop={scrollTop}/>
                    </ScrollControls>
                </Suspense>
            </Canvas>
        </div>
    )
}

Scroll Camera Component:

import * as THREE from 'three'
import React, { useLayoutEffect, useRef } from 'react'
import { useEffect } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { useGLTF, useAnimations, useScroll, PerspectiveCamera, Scroll, OrthographicCamera } from '@react-three/drei'
import { GLTF } from 'three-stdlib'

type ActionName = 'Animation'

interface GLTFAction extends THREE.AnimationClip {
  name: ActionName
}

type GLTFResult = GLTF & {
  nodes: {}
  materials: {}
  animations: GLTFAction[]
}

export default function Camera({ scrollTop }: { scrollTop: number }) {
  const group = React.useRef<THREE.Group>(null!)
  const cameraRef = useRef<any>(null!);
  const { nodes, materials, animations } = useGLTF('/3D_Assets/3D_Intro_Component_Models/camera-transformed.glb') as GLTFResult
  const { actions } = useAnimations(animations, group)
  const {viewport} = useThree()
  const scroll = useScroll()
  useEffect(() => void (actions.Animation!.reset().play().paused = true), [])
  useFrame(() => {
    actions.Animation!.time = actions.Animation!.getClip().duration * scroll.offset;
  })

  useLayoutEffect(() => {
    cameraRef.current.aspect = 1.5 * scroll.offset + 0.15;
    cameraRef.current.updateProjectionMatrix()
  }, [viewport, scrollTop])
  return (
    <Scroll>
      <group ref={group} dispose={null}>
        <group name="Scene">
          <PerspectiveCamera makeDefault ref={cameraRef} name="Camera" far={50000} near={0.001} position={[-1.154, 17.732, 15.619]} scale={[0.007, .007, 0.001]} />
        </group>
      </group>
    </Scroll>
  )
}

useGLTF.preload('/3D_Assets/3D_Intro_Component_Models/camera-transformed.glb')