Closed OneHatRepo closed 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)
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 <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 (
); }
@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.
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.
this should be inbuilt into gltfjsx ootb imo. and would be an easy change, anyone wants to try a pr?
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 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
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!
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
Is there a way to achieve this with OBJ models?
you can use useLoader and useGraph with obj files no problem.
Anybody knows how to use this clone with animations or any example?
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]);
`
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");
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.
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?