React-three-fiber: useLoader to load one object, use it multiple times

Created on 16 Nov 2019  路  12Comments  路  Source: pmndrs/react-three-fiber

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?

Most helpful comment

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.

All 12 comments

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:

https://stackoverflow.com/questions/59912539/error-loading-module-exports-requirethree-examples-jsm-loaders-gltfloader

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 (
enableZoom={false}
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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

UltimateForm picture UltimateForm  路  4Comments

regalstreak picture regalstreak  路  5Comments

flxsu picture flxsu  路  4Comments

francopetra picture francopetra  路  3Comments

Darkensses picture Darkensses  路  6Comments