pmndrs / react-three-fiber

🇨🇭 A React renderer for Three.js
https://docs.pmnd.rs/react-three-fiber
MIT License
27.06k stars 1.54k forks source link

useLoader to load one object, use it multiple times #245

Closed OneHatRepo closed 4 years ago

OneHatRepo commented 4 years ago

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?

drcmda commented 4 years ago

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)

OneHatRepo commented 4 years ago

Thank you.

afsanefdaa commented 4 years ago

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

OneHatRepo commented 4 years ago

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>;
OneHatRepo commented 4 years ago

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>;
}
drcmda commented 4 years ago

there's a new feature making this easier: https://github.com/react-spring/react-three-fiber#automatic-disposal

simonghales commented 4 years ago

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

omnicron96 commented 3 years ago

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 <primitive ref={box} {...primitiveProps} />; }

const CameraControls = () => { const { camera, gl: { domElement }, } = useThree(); const controls = useRef(); useFrame((state) => controls.current.update()); return ( <orbitControls enableZoom={false} ref={controls} args={[camera, domElement]} /> ); };

export default function App() { return (

{/* */}

); }

drcmda commented 3 years ago

@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

agcty commented 3 years ago

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>
  );
}
drcmda commented 3 years ago

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.

agcty commented 3 years ago

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.

Ctrlmonster commented 2 years ago

For any future soul aiming to do the same with a SkinnedMesh generated by gltfjsx.


const Object = ({url, position, ...props}) => {
  const {scene, animations} = useGLTF(url);
  const copiedScene = useMemo(() => SkeletonUtils.clone(scene), [scene]); // use instead of scene.clone()
  const {nodes} = useGraph(copiedScene); // nodes need to be copied as well 

  return (
    <primitive object={copiedScene} position={position}>
       <skinnedMesh name="Wings" geometry={nodes.Wings.geometry} skeleton={nodes.Wings.skeleton}/>
       <skinnedMesh name="Body" geometry={nodes.Body.geometry} skeleton={nodes.Body.skeleton}/>
    </primitive>
  );
}

Maybe a hint how to do this (manually changing a few lines in the generated jsx), could be added to the gltfjsx docs.

drcmda commented 2 years ago

this should be inbuilt into gltfjsx ootb imo. and would be an easy change, anyone wants to try a pr?

Ctrlmonster commented 2 years ago

I made a custom hook for my project:


export function useSkinnedMeshClone(path) {
  const {scene, materials, animations} = useGLTF(path);
  const clonedScene = useMemo(() => SkeletonUtils.clone(scene), [scene]);
  const {nodes} = useGraph(clonedScene);

  return {scene: clonedScene, materials, animations, nodes};
}

Maybe something like this could be added to Drei. Users might want this functionality for any SkinnedMesh, regardless of whether it came from gltfjsx or was loaded directly.

The gltfjsx result could then in turn use that hook in place of useGLTF if the user provided the option.

// const {scene, materials, animations, nodes} = useGLTF(path);  default
const {scene, materials, animations, nodes} = useSkinnedMeshClone(path); // with 'clonable' option
adamistheanswer commented 1 year ago

I made a custom hook for my project:

export function useSkinnedMeshClone(path) {
  const {scene, materials, animations} = useGLTF(path);
  const clonedScene = useMemo(() => SkeletonUtils.clone(scene), [scene]);
  const {nodes} = useGraph(clonedScene);

  return {scene: clonedScene, materials, animations, nodes};
}

Maybe something like this could be added to Drei. Users might want this functionality for any SkinnedMesh, regardless of whether it came from gltfjsx or was loaded directly.

The gltfjsx result could then in turn use that hook in place of useGLTF if the user provided the option.

// const {scene, materials, animations, nodes} = useGLTF(path);  default
const {scene, materials, animations, nodes} = useSkinnedMeshClone(path); // with 'clonable' option

I've been struggling with this all morning, I can't thank you enough! I don't think the docs for gltf2jsx are clear for the enough for this exception of skinned meshes

federicovezzoli commented 1 year ago

What if I have to clone the materials as well? I have this exact same problem, and cloning the scene solved half of my problems, because I'm also overriding the materials coming from the gltf in a sort of configurator. Is there a way to clone the materials as well and apply them to the primitive?

Thanks!

barbarossusuz commented 1 year ago

I made a custom hook for my project:

export function useSkinnedMeshClone(path) {
  const {scene, materials, animations} = useGLTF(path);
  const clonedScene = useMemo(() => SkeletonUtils.clone(scene), [scene]);
  const {nodes} = useGraph(clonedScene);

  return {scene: clonedScene, materials, animations, nodes};
}

Maybe something like this could be added to Drei. Users might want this functionality for any SkinnedMesh, regardless of whether it came from gltfjsx or was loaded directly.

The gltfjsx result could then in turn use that hook in place of useGLTF if the user provided the option.

// const {scene, materials, animations, nodes} = useGLTF(path);  default
const {scene, materials, animations, nodes} = useSkinnedMeshClone(path); // with 'clonable' option

const copiedScene = useMemo(() => scene.clone(), [scene]) this did not work

You are life saver

datteroandrea commented 1 year ago

Is there a way to achieve this with OBJ models?

drcmda commented 1 year ago

you can use useLoader and useGraph with obj files no problem.

3dyMedios commented 1 year ago

Anybody knows how to use this clone with animations or any example?

barbarossusuz commented 1 year ago

Anybody knows how to use this clone with animations or any example?

My object has animations and i am using like this. Hope this helps.

`

const {scene, materials, animations, nodes} = useMeshCloneForGLTF(Model);
const {ref, actions, names} = useAnimations(animations)
scene.animations = ["idle"]

useEffect(() => {
    actions.idle.play()
    actions.walk.reset().fadeIn(0.5).play()
}, [actions]);

`

tommyCUB commented 1 year ago

Sorry , it doesn't works for me , I'm trying to use the same model multiple times in a scenes and only shows once.

export function Timmy(props) {
  const group = useRef();

  const { scene, materials, animations, nodes } = useSkinnedMeshClone(
    "/models/players/Timmy-transformed.glb"
  );

  const { actions } = useAnimations(animations, group);
  const position = props.position;
  return (
    <group
      ref={group}
      {...props}
      dispose={null}
      position={[position.y, 0, position.x]}
    >
      <group name="Scene">
        <group name="Armature" rotation={[Math.PI / 2, 0, 0]} scale={0.01}>
          <primitive object={nodes.mixamorig6Hips} />
        </group>
        <skinnedMesh
          name="Ch09"
          geometry={nodes.Ch09.geometry}
          material={materials.Ch09_body}
          skeleton={nodes.Ch09.skeleton}
          rotation={[Math.PI / 2, 0, 0]}
          scale={0.01}
        />
      </group>
    </group>
  );
}

useGLTF.preload("/models/players/Timmy-transformed.glb");