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)
}
}
}, [])
async
/await
approach? Maybe you don't need to run the whole bunch of functions in parallel – Ferran Buireu