React three fiber add texture to obj model
Asked Answered
C

3

5

In my app I want to add texture to the loaded .obj model. I have the same situation with my .fbx loaded models. Below is my example code, but this works only with something like sphereGeometry not with a loaded model.

Thanks in Advance!

import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { useTexture } from '@react-three/drei'

const OBJModel = ({ file }: { file: string }) => {
  const obj = useLoader(OBJLoader, file)
  const texture = useTexture(textureFile)

  return (
    <mesh>
      <primitive object={obj} />
      <meshStandardMaterial map={texture} attach="material" />
    </mesh>
  )
}
Coulometer answered 10/8, 2021 at 10:33 Comment(0)
S
8
  1. primitive is not a subset of mesh. it can be a children of group.
  2. primitive requires both geometry and material as props. Mesh requires both geometry and material as props. it's pretty obvious both cannot be used as subset of each other.
  3. to implement your idea, you need to use only one Mesh or primitive. I'd suggest using Mesh which has abundant documentations. primitive is not documented enough.
  4. the OBJ acquired through useLoader may have complex group inside it. Models usually contain larger sets such as group or scene. Group and Scenes can't have textures. Mesh can.
OBJ(result of useLoader) = scene => group => mesh => geometry, texture

traversing is required to acquire geometry from mesh.

  // I've implemented this with Typescript,
  // but it is not necessary to use 'as' for type conversion.
  const obj = useLoader(OBJLoader, "/rock.obj");
  const texture = useTexture("/guide.png");
  const geometry = useMemo(() => {
    let g;
    obj.traverse((c) => {
      if (c.type === "Mesh") {
        const _c = c as Mesh;
        g = _c.geometry;
      }
    });
    return g;
  }, [obj]);

  // I've used meshPhysicalMaterial because the texture needs lights to be seen properly.
  return (
    <mesh geometry={geometry} scale={0.04}>
      <meshPhysicalMaterial map={texture} />
    </mesh>
  );

I've implemented it in codesandbox. here's the working code:

https://codesandbox.io/s/crazy-dawn-i6vzb?file=/src/components/Rock.tsx:257-550

Scincoid answered 11/8, 2021 at 6:47 Comment(1)
unfortunately the codesandbox example does not work: The editor could not be opened due to an unexpected error: Invalid argumentsEiffel
L
2

i believe obj loader returns a group, or a mesh, it wouldn't make sense to put that into a top-level mesh, and giving the top level a texture wont change the loaded obj mesh.

there are three possible solutions:

  1. use obj.traverse(...) and change the model by mutation, this is what people do in vanilla three and it is problematic because mutation is bad, you are destroying the source data and you won't be able to re-use the model
  2. i would suggest to convert your model to a gltf, then you can use gltfjsx https://github.com/pmndrs/gltfjsx which can lay out the full model declaratively. click the video, this is exactly what you want
  3. if you must use obj files you can create the declarative graph by hand. there is a hook that gives you { nodes, materials } https://docs.pmnd.rs/react-three-fiber/API/hooks#use-graph
Loomis answered 11/8, 2021 at 12:19 Comment(0)
H
2

I just came up with something like this, hopefully would be useful for you also,

const OBJModel = ({ file, textureFile, ...props }) => {
  const obj = useLoader(OBJLoader, file)
  const texture = useTexture(textureFile)

  useEffect(() => {
    obj.traverse(child => {
      if (child.isMesh) child.material.map = texture
    })
  }, [obj])

  return <primitive object={obj} {...props} />
}

Which can be used with something like

<OBJModel file='./a.obj' textureFile='./a.png' position-z={10} />
Heft answered 2/3 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.