Good Evening/Morning Everyone,
Hoping that someone can help.
I’m relatively new to the threeJS, react-fiber and 3d modelling world. I managed to get a 3D model loaded on a webpage I’m building. I got the model animations playing smoothly and got some interactivity with the model moving its head to follow the cursor - all great so far. However, I encountered a memory leak that for the life of me I can’t work out.
The model has no issue whatsoever when the website loads initially, however, if I scroll further down the page when the Canvas begins to leave the visible window, all of a sudden the memory usage begins to spike. I’ve done quite a bit of research but I can’t find a solution so I’m hoping the resident geniuses here will be able to help.
I attempted to create a codesandbox but had some difficulties with dependencies - if necessary I can retry however. What I have included instead is the relevant code and a recording showing the memory-heap spike:
The Canvas
import { Canvas } from "@react-three/fiber"
import { ReactNode, Suspense, useEffect, useRef} from "react"
import {EnvironmentProps, Stage } from "@react-three/drei";
import useMediaQuery from "@/utils/hooks/useMediaQuery";
interface ModelViewerProps {
environment?: EnvironmentProps,
children: ReactNode,
}
const ModelViewer = ({environment, children}:ModelViewerProps) => {
const isMobile = useMediaQuery('mobile');
return (
<Canvas
className="modelViewer"
shadows
camera={{
position: [.2, 0, 1],
}}
resize={{scroll:false}}
>
<Suspense fallback={null}>
<Stage
intensity={0.5}
preset="rembrandt"
shadows={{
type: 'contact',
color: '#408FCE',
offset: -0.38,
scale: 4,
blur: 4,
}}
environment="city"
adjustCamera={isMobile ? 1 : 0}
center={{disableX: !isMobile}}
>
<mesh>
{children}
</mesh>
</Stage>
</Suspense>
</Canvas>
)
}
The Model
import { getMouseDegrees } from "@/utils/functions/getMouseDegrees";
import useMediaQuery from "@/utils/hooks/useMediaQuery";
import { useAnimations, useGLTF } from "@react-three/drei";
import { dispose, useFrame } from "@react-three/fiber";
import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react";
import { MathUtils, SkinnedMesh } from "three";
export interface MouseType {
x: number,
y: number
}
type CustomBoneType = THREE.Bone & {
rotation: {
xD: number,
yD: number
}
}
const moveHead = (mouse:MouseType, bone: CustomBoneType, degreeLimit = 40) => {
const degrees = getMouseDegrees(mouse.x,mouse.y,degreeLimit);
bone.rotation.xD = MathUtils.lerp(bone.rotation.xD || 0, degrees.y + 18, 0.1)
bone.rotation.yD = MathUtils.lerp(bone.rotation.yD || 0, degrees.x, 0.1)
bone.rotation.x = MathUtils.degToRad(bone.rotation.xD)
bone.rotation.y = MathUtils.degToRad(bone.rotation.yD)
}
const Azura3DModelNew = ({...props}) => {
const group = useRef(null);
const { nodes, materials, animations } = useGLTF('/3dModels/azura3dModel-meshopt.glb');
const { actions, names } = useAnimations(animations, group);
const isDesktop = useMediaQuery('desktop');
const mouse = useRef({x:0,y:0});
const bonesToModify = [
[nodes['spine009'],5],
[nodes['spine010'],10],
[nodes['spine011'],30] ,
]
useFrame((state, delta) => {
if (isDesktop) {
mouse.current = {x: state.size.width / 2 + (state.mouse.x * state.size.width) / 2, y: state.size.height / 2 + (-state.mouse.y * state.size.height) / 2}
bonesToModify.forEach((bone) => moveHead(mouse.current,bone[0] as CustomBoneType,bone[1] as number));
}
})
useEffect(() => {
actions['foxSitIdle']?.reset().fadeIn(0.5).play();
// I assume the below isn't needed but left here to highlight that I had tried this also.
// return () => {
// dispose(materials);
// dispose(nodes);
// useGLTF.clear('/3dModels/azura3dModel-meshopt.glb');
}
}, [actions])
return (
<group
ref={group}
dispose={null}
position={[.3,0,0]}
scale={[1,1,1]}
rotation={[0,-.3,0]}
key={nanoid()}
>
<group name="metarig" scale={0.7} dispose={null}>
<primitive object={nodes.master}/>
<primitive object={nodes.floorConst} />
<group name="Azura" dispose={null}>
<skinnedMesh
name="Plane"
geometry={(nodes.Plane as SkinnedMesh).geometry}
material={materials["Material.005"]}
skeleton={(nodes.Plane as SkinnedMesh).skeleton}
// receiveShadow castShadow
dispose={null}
/>
<skinnedMesh
name="Plane_1"
geometry={(nodes.Plane_1 as SkinnedMesh).geometry}
material={materials["Material.002"]}
skeleton={(nodes.Plane_1 as SkinnedMesh).skeleton}
// receiveShadow castShadow
dispose={null}
/>
<skinnedMesh
name="Plane_2"
geometry={(nodes.Plane_2 as SkinnedMesh).geometry}
material={materials["Material.003"]}
skeleton={(nodes.Plane_2 as SkinnedMesh).skeleton}
// receiveShadow castShadow
dispose={null}
/>
</group>
</group>
</group>
)
}
export default Azura3DModelNew
I tried to use the React Profiler and Chrome Dev tools in an attempt to diagnose the issue but without success.
In the video below you will see that after loading, I simply scroll to the bottom of the currently empty page (I removed all other elements for simplicity). Very soon after the memory begins to spike. I can only assume that there is some kind of issue with disposing the model as needed.
Video Recording Showing Memory Spike:
2023-02-26 01-37-19.mkv (2.9 MB)
Thank you very much in advance to anyone that may be able to help.
Kind Regards,
David