I'm using useLoader() to load a GLTF object I created in Blender, and I want to place multiple copies of that object on the canvas. However, when I try, only one copy of that object actually gets rendered to the screen.
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { useLoader } from 'react-three-fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
export default function Marker(props) {
const gltf = useLoader(GLTFLoader, 'marker.glb'),
ref = useRef();
const primitiveProps = {
ref,
object: gltf.scene,
position: props.position || [0,0,0],
rotation: props.rotation || [0,0,0],
scale: props.scale || [1,1,1],
castShadow: true,
};
return <primitive {...primitiveProps} />;
}
import React, { Suspense } from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
import Mesh from './Marker';
export default function Marker3d(props) {
return (
<Suspense fallback={<group />}>
<Marker
position={props.position}
rotation={props.rotation}
scale={props.scale}
path={props.path}
/>
</Suspense>
);
}
<Marker3d position={[0,0,0]} />
<Marker3d position={[10,0,0]} />
That first Marker3d does not appear on the canvas. Only the second one does.
Is there a way to add the same loaded object more than once on the canvas?
You can't reuse meshes or put the same object into the scene twice in webgl/threejs, it will just unmount and remount. What you can share is the geometry. Check out the gltfjsx tool I've made, it pulls out the geometry and creates reusable components. You could of course do this by hand, just reach in there and fetch the geometry and material. Alternatively you can deep clone the base object : gltf.scene.clone(true)
Thank you.
const gltf = useLoader(GLTFLoader, 'marker.glb'),
Is this application react ? I have some problem using GLTFLoader! Could you please take a look to see if you can help ?
I asked my question here:
Thank you
The answer was given above, by drcmda. You have to clone the geometry and use that as the basis for your rendered primitive.
Here's some excerpts I have from a working React component:
import { useLoader } from 'react-three-fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
.....
export default function CircularDial(props) {
const dialGltf = useLoader(GLTFLoader, '3d/circular-dial.glb'),
[dialGeometry, setDialGeometry] = useState(),
.....
if (!dialGeometry) {
const dialScene = dialGltf.scene.clone(true);
setDialGeometry(dialScene);
.....
dialProps = {
object: dialGeometry,
position: [0,0,0],
rotation: [0,0,0],
scale: [1,1,1],
},
.....
return <group {...outerGroupProps}>
<group {...innerGroupProps}>
{hashes}
<Label {...labelProps} />
<primitive {...dialProps} />
</group>
</group>;
Here's the full working versions of my original post:
import React, { useEffect, useState } from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
import { useLoader } from 'react-three-fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { degToRad, setCastShadow, setReceiveShadow } from '../../../../Utilities/Functions/3d';
export default function Marker(props) {
const gltf = useLoader(GLTFLoader, '3d/marker.glb'),
[geometry, setGeometry] = useState();
// init
if (!geometry) {
// Scene settings
const scene = gltf.scene.clone(true); // so we can instantiate multiple copies of this geometry
setCastShadow(scene.children, true);
setReceiveShadow(scene.children, true);
setGeometry(scene);
}
const primitiveProps = {
object: geometry,
position: props.position,
rotation: degToRad(props.rotation),
scale: props.scale,
};
return <primitive {...primitiveProps} />;
}
import React, { Suspense } from 'react';
import Marker from './Marker';
function Marker3d(props) {
const rotation = (props.uiConfig && props.uiConfig.rotation) || props.rotation,
position = (props.uiConfig && props.uiConfig.position) || props.position,
scale = (props.uiConfig && props.uiConfig.scale) || props.scale;
return <Suspense fallback={<group />}>
<Marker
position={position}
rotation={rotation}
scale={scale}
/>
</Suspense>;
}
there's a new feature making this easier: https://github.com/react-spring/react-three-fiber#automatic-disposal
I just wanted to comment, if anybody is trying to use the gltf.scene.clone(true) method with a model containing SkinnedMesh etc and is running into issues:
e.g. "Uncaught TypeError: Cannot read property 'frame' of undefined"
I managed to resolve them by utilising Three's SkeletonUtils.clone
https://threejs.org/docs/#examples/en/utils/SkeletonUtils.clone
https://github.com/mrdoob/three.js/issues/5878#issuecomment-476457441
how to change color of gltf file after rendering and can we add onclick and other event in that gltf???
import React, { Suspense, useRef, useState } from "react";
import { Canvas, useLoader, useThree, extend } from "react-three-fiber";
import sample from "./single.glb";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import "./App.css";
import { useFrame } from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
extend({ OrbitControls });
function Box(props) {
const box = useRef();
useFrame(() => {
box.current.rotation.x += 0.01;
box.current.rotation.y += 0.01;
box.current.rotation.z += 0.01;
});
const gltf = useLoader(GLTFLoader, sample);
const [geometry, setGeometry] = useState();
//for cloning gltf file
if (!geometry) {
const scene = gltf.scene.clone(true);
setGeometry(scene);
}
const primitiveProps = {
object: geometry,
position: props.position,
};
return
}
const CameraControls = () => {
const {
camera,
gl: { domElement },
} = useThree();
const controls = useRef();
useFrame((state) => controls.current.update());
return (
ref={controls}
args={[camera, domElement]}
/>
);
};
export default function App() {
return (
);
}
@omnicron96 use gltfjsx, it turns your model into a component, now you can change whatever you want: https://codesandbox.io/s/react-three-fiber-gltf-camera-animation-forked-pecl6?file=/src/Model.js
For future reference, this is how I got it working (with scene.clone())
interface ObjectProps {
url: string;
position: [x: number, y: number, z: number];
}
const Object = ({ url, position, ...props }: ObjectProps) => {
const { scene } = useLoader(GLTFLoader, url)
const copiedScene = useMemo(() => scene.clone(), [scene])
return (
<group>
<primitive object={copiedScene} position={position} />
</group>
);
};
const Objects = () => {
const list = [
{
url:
"https://firebasestorage.googleapis.com/v0/b/zerolens-dev.appspot.com/o/assets%2fgltf%2fmicrophone%2fscene.gltf?alt=media",
},
{
url: // the same as
"https://firebasestorage.googleapis.com/v0/b/zerolens-dev.appspot.com/o/assets%2Fgltf%2Fbox%2Fbox.gltf?alt=media&token=254244e5-52cb-4d8b-81bf-3f064b3f88eb",
},
{
url: // this
"https://firebasestorage.googleapis.com/v0/b/zerolens-dev.appspot.com/o/assets%2Fgltf%2Fbox%2Fbox.gltf?alt=media&token=254244e5-52cb-4d8b-81bf-3f064b3f88eb",
},
];
return list.map(({ url }, i) => {
return <Object key={i} position={[4 * i, 1, 1]} url={url} />;
});
};
function Page() {
return (
<div className="w-screen h-screen">
<Canvas shadowMap camera={{ position: [0, 0, 17], far: 50 }} concurrent>
<ambientLight />
<spotLight
intensity={2}
position={[40, 50, 50]}
shadow-bias={-0.00005}
penumbra={1}
angle={Math.PI / 6}
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
castShadow
/>
<Suspense fallback={null}>
<Objects />
</Suspense>
<OrbitControls />
</Canvas>
</div>
);
}
better do:
const Object = ({ url, position, ...props }: ObjectProps) => {
const { scene } = useLoader(GLTFLoader, url)
const copiedScene = useMemo(() => scene.clone(), [scene])
or else it will clone the scene on every render. react rule #1: no side effects and created objects unmanaged in the render function.
Ahh yes, thank you very much! This is much better! I absolutely love r3f but documentation could still be improved, hopefully I'll have time for a PR.
Most helpful comment
better do:
or else it will clone the scene on every render. react rule #1: no side effects and created objects unmanaged in the render function.