How to clone a model and change color respectively?

I made three copies of the model, and want to change the color when the mouse hovers it. But when I hover one of the models, the rest of the two models also change color.

The most stupid way is to create three identical glb model files, I have tried it can work, but I still hope to shared the code.

import { useGLTF } from "@react-three/drei";
import{ Hover } from "modifiers/Hover";

export default function MyScene() {
  const gltf = useGLTF("/example.glb");
  const positions = [
    { x: -4, y: 1.6, z: 3.5, rx: 0, ry: 0, rz: 0 },
    { x: 0, y: 0, z: 3.5, rx: 0, ry: 0, rz: 0 },
    { x: 1.15, y: 1.6, z: 7.7, rx: 0, ry: Math.PI/2, rz: 0 },
  ];

  return (
    <>
      {positions.map((position, index) => {
        const clone = gltf.scene.clone();
        clone.position.set(position.x, position.y, position.z);
        clone.rotation.set(position.rx, position.ry, position.rz);
        return <Hover key={index} > <primitive object={clone} /> </Hover>
      })}
    </>
  );
}

Hover function:

import React, { useRef, useState, ReactNode} from "react";
import { Group, Raycaster, Vector3 } from "three";
import { useFrame } from "@react-three/fiber";

export function Hover(props: HoverProps) {
  const {
    children,
  } = props;
  const hoverRef = useRef<Group>(null);
  const raycaster = new Raycaster();
  const posTemp = new Vector3();
  const [isHover, setHover] = useState(false);
  const [intersectState, setIntersectState] = useState('');
  let intersections;
  useFrame(({ camera }) => {
    if(hoverRef.current) {
      raycaster.set(camera.position, camera.getWorldDirection(posTemp));
      intersections = raycaster.intersectObject(hoverRef.current, true);
      if(intersections && intersections.length > 0 ){ 
        setIntersectState(intersections);
        intersections[0].object.material.emissive.setHex(0xffff00);
      } else {
        if(intersectState && intersectState.length > 0) {
          intersectState[0].object.material.emissive.setHex(0x333333);
          setIntersectState(null);
        }
      }
    }
  });

return (
    <group name="hover" ref={hoverRef}>  
      {children}
    </group>
  );
}

@delongjs

To have different colors, you must have different materials. So, this code will not work:

objA = new THREE.Mesh( geometry, material );
objB = objA.clone(); // objB reuses geometry and material of objA
objB.position.x = 20;
objB.material.color.setRGB(0,1,0);

It will not work, because objA and objB share the same material (they also share the same geometry, but this is irrelevant now). To have a distinct material, it must be explicitly cloned:

objA = new THREE.Mesh( geometry, material );
objB = objA.clone();
objB.position.x = 20;
objB.material = objB.material.clone(); // cloning the material
objB.material.color.setRGB(0,1,0);

In your case, you have to traverse the loaded model (as it may contain many objects and subobjects) and replace all materials that you want to change.

– Pavel

1 Like

the best way to deal with models is gltfjsx. this creates a virtual graph of the model, it can be re-used by default. watch this video and you’ll understand:

here is an example of a shoe model and giving them a different color.

if you do not want to create a jsx graph, either just call clone like pavel said.

function Model({ url, ...props }) {
  const { scene } = useGLTF(url)
  const cloned = useMemo(() => scene.clone(), [scene])
  return <primitive object={cloned} {...props} />

or, easier, there’s a drei abstraction

import { Clone } from '@react-three/drei'

function Model({ url, ...props }) {
  const { scene } = useGLTF(url)
  return <Clone object={scene} {...props} />

<Clone> has some benefits over scene.clone, it has shortcuts for castShadow/receiveShadow, can inject materials, but it also works with skinned models whereas scene.clone() would now fail.

3 Likes

Thanks for the answer, I used gltfjsxgltf.pmnd originally, but I still get stuck. I’m not sure my question is clear enough, I have three same tsx models file, Map1、Map2、Map3, and they have the same color, when I hover Map1 it should change color(Map2、Map3 doesn’t change), when the mouse leaves Map1, the color will become original, just like this example three.js examples

// Map1.tsx

type GLTFResult = GLTF & {
  nodes: {
    meshExample01: THREE.Mesh;
    meshExample02: THREE.Mesh;
    .....
  };
  materials: {
    materialExample01: THREE.MeshPhysicalMaterial;
    materialExample02: THREE.MeshPhysicalMaterial;
    ....
  };
};

export default function Map1(props: BoardProps) {
  const group = useRef<THREE.Group>(null);
  const { nodes, materials } = useGLTF("example.glb") as GLTFResult;
  return (
    <group ref={group} position={[0,0,0]} rotation={[0, Math.PI/2, 0]} dispose={null}>
      <group name="Scene">
        <Hover >
          <mesh
            name="meshExample01"
            geometry={nodes.meshExample01.geometry}
            material={materials.materialExample01}
            position={[0.02, 0.17, 0.02]}
            rotation={[Math.PI / 2, 0, Math.PI / 2]}
          />
          <mesh
            name="meshExample02"
            geometry={nodes.meshExample02.geometry}
            material={materials["materialExample01"]}
            position={[0.24, 0.1, 0.02]}
            rotation={[Math.PI / 2, 0.2, Math.PI / 2]}
          />
          .................etc
        </Hover>
      </group>
    </group>
  );
}

useGLTF.preload("example.glb");
// Map2.tsx

same with Map1.tsx
// Map3.tsx

same with Map1.tsx

if you mount Map multiple times, they will all share the same material. if you want each to have its own hover color you just need to give it a distinct material.

gltfjsx gives you something like this:

<mesh
  geometry={nodes.meshExample01.geometry}
  material={materials.materialExample01}
  ...>

make it

<mesh geometry={nodes.meshExample01.geometry}>
  <meshStandardMaterial color={materials.materialExample01.color} />

look into that shoe example above once more. the reason it can have a distinct color is because it makes its own material.

1 Like

Thanks for the very clear explanation, I still have one more question to ask from the shoe example, How can I extract the mesh material from material={materials.mesh} to <meshStandardMaterial … />? Thanks.

material={materials.mesh}
<meshStandardMaterial
  aoMap={materials.mesh.aoMap}
  normalMap={materials.mesh.normalMap}
  normalMap-encoding={THREE.LinearEncoding}
  roughnessMap={materials.mesh.roughnessMap}
  metalnessMap={materials.mesh.metalnessMap}
  envMapIntensity={0.8}
/>

yes, like that, if you need it to have a distinct material but you want to copy over something things that’s how you’d do it.