1
votes

I have a relatively simple three.js scene that I render in an useEffect hook. It uses GLTFLoader() to load a model.

There are also three event listeners on the page fired on load. One is to calculate the browser's inner height, another to track when the user has resized the browser (to resize the three.js scene), and the last to do a simple mouse parallax effect for the three.js model.

The current issue Im facing is low fps / stuttering on page load, but once it's loaded the interactivity of the scene is smooth / 60fps.

What I'm suspecting here as well as having asked professionals online is my code has too much happening at once on render. I have been recommended to do preloading the model then rendering react but Im not sure how to do that in gatbsy.js.

Here is the code. As I have mentioned earlier, the entire three.js scene is rendered inside a useEffect hook after the page has been rendered by react. I have searched online and this seems to be the common way.

const Index = () => {
  const [showreelActive, setShowreelActive] = useState(false)
  const [aboutUsActive, setAboutUsActive] = useState(false)

  const vidRef = useRef()

  useEffect(() => {
    const video = vidRef.current
    video && video.play()
  }, [showreelActive])

  const playShowreel = () => {
    aboutUsActive && setAboutUsActive(false)
    setShowreelActive(true)
    gsap.to(model.scale, {
      x: 1,
      y: 1,
      z: 1,
      ease: "power2.easeOut",
    })
  }

  const closeShowreel = () => setShowreelActive(false)

  const openAboutUs = () => {
    setAboutUsActive(true)
    pauseMouseParallax = true
    isMobile && (controls.enableRotate = false)
    gsap.to(model.scale, {
      x: 2,
      y: 2,
      z: 2,
      ease: "power2.easeOut",
    })
  }

  const closeAboutUs = () => {
    setAboutUsActive(false)
    pauseMouseParallax = false
    isMobile && (controls.enableRotate = true)
    gsap.to(model.scale, {
      x: 1,
      y: 1,
      z: 1,
      ease: "power2.easeOut",
    })
  }

  const mouseOverButtonParallax = () => {
    pauseMouseParallax = true
  }

  const mouseLeaveButtonParallax = () => {
    aboutUsActive ? (pauseMouseParallax = true) : (pauseMouseParallax = false)
  }

  useCalcHeight()

  useEffect(() => {
    const container = document.querySelector("#scene")

    const initScene = () => (scene = new THREE.Scene())

    const initCamera = () => {
      const { fov, aspectRatio, near, far } = {
        fov: 45,
        aspectRatio: container.clientWidth / container.clientHeight,
        near: 0.01,
        far: 180,
      }

      camera = new THREE.PerspectiveCamera(fov, aspectRatio, near, far)

      const { x, y, z } = { x: 0, y: 0, z: 82 }

      camera.position.set(x, y, z)
    }

    const initLight = () => {
      const white = new THREE.Color(0xffffff)
      const gray = new THREE.Color(0xb9b9b9)

      white.convertSRGBToLinear()
      gray.convertSRGBToLinear()

      hemiLight = new THREE.HemisphereLight(white, gray, 2.93)
      hemiLight.position.set(-7, 25, 13)

      scene.add(hemiLight)
    }

    const initRenderer = () => {
      let antiAlias = window.devicePixelRatio > 1 ? false : true
      renderer = new THREE.WebGLRenderer({
        antialias: antiAlias,
        powerPreference: "high-performance",
        alpha: true,
      })

      renderer.physicallyCorrectLights = true
      renderer.outputEncoding = THREE.sRGBEncoding
      renderer.gammaFactor = 2.2

      renderer.setPixelRatio(window.devicePixelRatio > 1 ? 2 : 1)
      renderer.setClearColor(0xffffff, 0)
      renderer.setSize(container.clientWidth, container.clientHeight)

      container.appendChild(renderer.domElement)
    }

    const initControls = () => {
      controls = new OrbitControls(camera, renderer.domElement)

      controls.enableRotate = isMobile && !pauseMouseParallax ? true : false
      controls.enableDamping = true
      controls.dampingFactor = 0.05
      controls.enableZoom = false
      controls.enableKeys = false
      controls.enablePan = false

      controls.minPolarAngle = 0.99
      controls.maxPolarAngle = Math.PI / 1.6
      controls.minAzimuthAngle = -Math.PI / 4
      controls.maxAzimuthAngle = Math.PI / 4
    }

    // load gltf model
    const initModel = () => {
      const loader = new GLTFLoader()

      const modelName = "model.gltf"

      loader.load(
        `models/website_models/${modelName}`,
        gltf => {
          model = gltf.scene.children[0]
          model.position.set(0, 5.5, 0)
          scene.add(model)

          const windmillName = "Roundcube001"
          const findModel = name => {
            return model.children.find(object => object.name == name)
          }
          const windmill = findModel(windmillName)
          const windmillYposition = windmill.position.y
          const windmillTL = gsap
            .timeline({ paused: true })
            .to(windmill.position, {
              y: windmillYposition - windmillYposition * 0.15,
              duration: 1.45,
              repeat: -1,
              repeatDelay: 0.8,
              ease: "power1.inOut",
              yoyo: true,
            })

          const parallaxMouse = e => {
            const { offsetX, offsetY, target } = e
            const { clientWidth, clientHeight } = target

            const xPos = offsetX / clientWidth - 0.5
            const yPos = offsetY / clientHeight - 0.5

            !pauseMouseParallax &&
              gsap.to(model.rotation, {
                x: yPos / 12,
                y: xPos / 9,
                ease: "power1.out",
              })
          }

          const objectMouseParallax = () => {
            window.addEventListener("mousemove", e => parallaxMouse(e))
          }

          gsap
            .timeline({
              defaults: { ease: "power2.easeOut" },
              onComplete: () => {
                !isMobile && objectMouseParallax()
                const loading = document.querySelector(".loading")
                loading.remove()
                windmillTL.play()
              },
            })
            .to(".loading-logo", {
              scale: 0.93,
              opacity: 0,
            })
            .to(".loading", {
              opacity: 0,
              duration: 0.85,
            })
            .from(camera.position, {
              z: 2.5,
              duration: 2.25,
              ease: "power4.inOut",
            })
        },
        () => gsap.set("body", { autoAlpha: 1 }),
        error => {
          alert("Failed to load 3D experience. Please try another browser.")
          console.log(error)
        }
      )
    }

    const init = () => {
      initScene()
      initCamera()
      initLight()
      initRenderer()
      initControls()
      initModel()
    }

    const onWindowResize = () => {
      camera.aspect = container.clientWidth / container.clientHeight
      camera.updateProjectionMatrix()
      renderer.setSize(container.clientWidth, container.clientHeight)
    }

    window.addEventListener("resize", onWindowResize, false)

    const animate = () => {
      renderer.render(scene, camera)
      controls.enableRotate ? controls.update() : null
      requestAnimationFrame(animate)
    }

    if (WEBGL.isWebGLAvailable()) {
      init()
      animate()
    } else {
      const warning = WEBGL.getWebGLErrorMessage()
      alert(
        `This website does not support your browser. Please update. ${warning}`
      )
    }

    return () => {
      if (WEBGL.isWebGLAvailable()) {
        window.removeEventListener("mousemove", e => parallaxMouse(e))
        window.removeEventListener("resize", onWindowResize, false)
      }
    }
  }, [])
1
For a simple attempt, I think you could try initializing the scene inside of a setTimeout of 0 so the browser schedule it in a new taskDerek Nguyen
No discernible difference :(user11834465
Have you tried some async/await approach? Maybe you don't need to run the whole bunch of functions in parallelFerran Buireu
Where would you put the async await?user11834465

1 Answers

1
votes

Seems like I have forgotten about react basics.

Two things that helped / solved the performance issue:

  1. Using conditional rendering. I created a loading state, once gltfloader has loaded model, then loading state is false, then the UI elements are rendered

  2. Used a useeffect with loading state as dependency. Once loading has completed, then trigger the gsap animation. Before I had the gsap animation inside the gltf loader.