Published on

Attaching texture to imported models in react-three-fiber.

Authors

Recently I've been playing around with react-three-fiber and learning the basics of the library.

Once you understand the things like the scene, camera, controls, lighting, texture, mesh, material, basic geometry, etc., you eventually want to try to make custom models in a 3D modeling software like Blender and then import them into your project.

Luckily, the people behind the library provide you with a useful useLoader hook along with several loaders depending on the type of model you want to import.

I'm not a 3D artist myself but a good friend of mine is and he kindly offered me some of his models to play around with. The model he gave me was in .obj format therefore I used OBJLoader from the library.

import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'

I was able to successfully load the model however the colors or texture did not apply to it...

The component looked like this:

const Model = () => {
  const obj = useLoader(OBJLoader, './models/model.obj')
  return (
    <>
      <primitive object={obj} />
    </>
  )
}

Then I quickly realized that I forgot to import the texture which was in .png format. You can do that easily with the TextureLoader.

import { TextureLoader } from 'three/src/loaders/TextureLoader'

Alright, so now I have the model and the texture but how do I make the texture apply to the model?

const Model = () => {
  const obj = useLoader(OBJLoader, './models/model.obj')
  const texture = useLoader(TextureLoader, './models/texture.png')
  return (
    <>
      // where to put the texture? :/
      <primitive object={obj} />
    </>
  )
}

Turns out it wasn't as trivial as I thought. After about one hour of research, I realized that models are usually complex and can contain lots of stuff inside like a group or a scene. Groups and scenes can not have textures applied to them but the mesh can.

The solution was to traverse the model to find the geometry inside of it. The next step was to apply it to the mesh and wrap it around a meshPhysicalMaterial component with our texture prop.

const geometry = useMemo(() => {
  let geom
  obj.traverse((item) => {
    if (item.type === 'Mesh') {
      geom = item.geometry
    }
  })
  return geom
}, [obj])

Yay, it worked!

Here's what the working components looked like:
const Model = () => {
  const obj = useLoader(OBJLoader, './models/model.obj')
  const texture = useLoader(TextureLoader, './models/texture.png')
  const geometry = useMemo(() => {
    let geom
    obj.traverse((item) => {
      if (item.type === 'Mesh') {
        geom = item.geometry
      }
    })
    return geom
  }, [obj])
  return (
    <mesh geometry={geometry} scale={0.5}>
      <meshPhysicalMaterial map={texture} />
    </mesh>
  )
}

I'm very happy with the result but there's still a lot of things I need to learn to have a better understanding of how all this works under the hood.

THREE.js is such a fantastic library and react-three-fiber makes it so that we can use this imperative-by-nature library in a more declarative way. Happy New Year!