Loading textures for 3D models in react-three-fiber

Last time I used a hdri file to render an underwater environment. The next thing I wanted to learn was how to give my submarine model different textures. Looking at the react-three-fiber docs on loading textures I see that I can use TextureLoader with useTexture.

Loading a metal texture for the submarine

I grabbed a free smooth metal texture from AmbientCG and started by refactoring the app a bit. I broke out the <Stage />, <Environment />, and <Submarine /> components into a new Scene.js component.

 import { Environment, Stage } from '@react-three/drei' 
 import { useLoader } from '@react-three/fiber'; 
 import { Submarine } from '../components/Submarine'; 
 import { TextureLoader } from 'three/src/loaders/TextureLoader'; 
  
 export const Scene = ({currentColor}) => { 
   const [colorMap, normalMap, roughnessMap, metalnessMap] = useLoader(TextureLoader, [ 
     './textures/Metal030_1K_Color.jpg', 
     './textures/Metal030_1K_NormalGL.jpg', 
     './textures/Metal030_1K_Roughness.jpg', 
     './textures/Metal030_1K_Metalness.jpg', 
   ]) 
   return ( 
     <Stage intensity={1} contactShadowOpacity={0} > 
       <Environment 
           background={true} 
           files={'UW_1.hdr'} 
           path={'/'} 
         /> 
       <mesh> 
         <Submarine  
           map={colorMap} 
           normalMap={normalMap} 
           roughnessMap={roughnessMap} 
           metalnessMap={metalnessMap}  
           currentColor={currentColor} /> 
       </mesh> 
     </Stage> 
   ) 
 } 
...
  return ( 
     <div> 
       <Menu handleColorChange={handleColorChange} /> 
       <Canvas dpr={[1, 2]} camera={{ fov: 75 }}> 
         <Suspense fallback={null}> 
           <Scene currentColor={currentColor} /> 
         </Suspense> 
         <OrbitControls autoRotate enableZoom={true} enablePan={false} /> 
       </Canvas> 
     </div> 
   ) 
 } 
...

Using TextureLoader with useLoader, it was easy to pull out the texture maps and use them as props for the <Submarine /> component. I needed to update the Submarine.js component for these new props.

 import React, { useLayoutEffect }from 'react'; 
 import { useGLTF } from '@react-three/drei'; 
  
 export const Submarine = ({currentColor, colorMap, normalMap, roughnessMap, metalnessMap}) => { 
   const { scene, nodes, materials } = useGLTF('./models/submarine.gltf'); 
  
   useLayoutEffect(() => { 
     Object.assign(materials.Material, {  
       metalnessMap: metalnessMap, 
       normalMap: normalMap, 
       roughnessMap: roughnessMap, 
       map: colorMap, 
       color: currentColor}) 
   }, [scene, nodes, materials, currentColor, colorMap, normalMap, roughnessMap, metalnessMap]); 
  
   return <primitive object={scene} /> 
 }; 

Here’s what the submarine looks like with the new smooth metal texture applied.

Adding the ability to change to three different textures

I wanted to have a way for the user to change to different types of submarine textures. The first thing I needed to do was download more textures and refactor the app to allow for a currentTexture state.

...
   const smooth = [ 
     './textures/Metal030_1K_Color.jpg', 
     './textures/Metal030_1K_NormalGL.jpg', 
     './textures/Metal030_1K_Roughness.jpg', 
     './textures/Metal030_1K_Metalness.jpg', 
   ];
    
   const rough = [ 
     './textures/Metal040_1K_Color.jpg', 
     './textures/Metal040_1K_NormalGL.jpg', 
     './textures/Metal040_1K_Roughness.jpg', 
     './textures/Metal040_1K_Metalness.jpg', 
   ]; 
  
   const beatup = [ 
     './textures/Metal021_1K_Color.jpg', 
     './textures/Metal021_1K_NormalGL.jpg', 
     './textures/Metal021_1K_Roughness.jpg', 
     './textures/Metal021_1K_Metalness.jpg', 
   ]; 
  
   const [currentTexture, setCurrentTexture] = useState(smooth); 
...

Using the same pattern for changing the color, I made a handleTextureChange function.

...
  const handleTextureChange = (event, texture) => { 
     event.preventDefault(); 
     if (texture === 'smooth') { 
       setCurrentTexture(smooth); 
     } else if (texture === 'rough') { 
       setCurrentTexture(rough); 
     } else if (texture === 'beatup') { 
       setCurrentTexture(beatup); 
     } 
   };
...

I updated the <Menu /> and <Scene /> components with the new function and state.

...
return ( 
     <div> 
       <Menu  
         handleColorChange={handleColorChange}  
         handleTextureChange={handleTextureChange} /> 
       <Canvas dpr={[1, 2]} camera={{ fov: 50 }}> 
         <color attach="background" args={['#253B56']} /> 
         <Suspense fallback={null}> 
           <Scene currentTexture={currentTexture} currentColor={currentColor} /> 
         </Suspense> 
         <OrbitControls autoRotate enableZoom={true} enablePan={true} /> 
       </Canvas> 
     </div> 
   ) 
 }
...

I brought in the new function for use in Menu.js and updated with a new set of menu items for changing texture.

 import React from 'react'; 
  
 export const Menu = ({handleColorChange, handleTextureChange}) => { 
   return ( 
     <nav> 
       <ul> 
         <li> 
           <a href="/red" onClick={event => handleColorChange(event, 'crimson')}>Red</a> 
         </li> 
         <li> 
           <a href="/green" onClick={event => handleColorChange(event, 'teal')}>Green</a> 
         </li> 
         <li> 
           <a href="/blue" onClick={event => handleColorChange(event, 'steelblue')}>Blue</a> 
         </li> 
       </ul> 
       <ul> 
         <li> 
           <a href="/metal1" onClick={event => handleTextureChange(event, 'smooth')}>smooth</a> 
         </li> 
         <li> 
           <a href="/aged" onClick={event => handleTextureChange(event, 'rough')}>rough</a> 
         </li> 
         <li> 
           <a href="/beatup" onClick={event => handleTextureChange(event, 'beatup')}>beatup</a> 
         </li> 
       </ul> 
     </nav> 
   ) 
 }

All that was left to do was update Scene.js and Submarine.js with the new state.


 import { Environment, Stage, useTexture } from '@react-three/drei' 
 import { Submarine } from '../components/Submarine'; 
  
 export const Scene = ({currentColor, currentTexture}) => { 
   const [colorMap, normalMap, roughnessMap, metalnessMap] = useTexture(currentTexture); 
   return ( 
     <Stage intensity={1} > 
       <Environment 
           background={false} 
           files={'UW_1.hdr'} 
           path={'/'} 
         /> 
       <mesh> 
         <Submarine  
           map={colorMap} 
           normalMap={normalMap} 
           roughnessMap={roughnessMap} 
           metalnessMap={metalnessMap}  
           currentColor={currentColor} 
           currentTexture={currentTexture} /> 
       </mesh> 
     </Stage> 
   ) 
 } 
 import React, { useLayoutEffect }from 'react'; 
 import { useGLTF } from '@react-three/drei'; 
  
 export const Submarine = ({currentColor, currentTexture, colorMap, normalMap, roughnessMap, metalnessMap}) => { 
   const { scene, nodes, materials } = useGLTF('./models/submarine.gltf'); 
  
   useLayoutEffect(() => { 
     Object.assign(materials.Material, {  
       metalnessMap: metalnessMap, 
       normalMap: normalMap, 
       roughnessMap: roughnessMap, 
       map: colorMap, 
       color: currentColor}) 
   }, [scene, nodes, materials, currentColor, currentTexture, colorMap, normalMap, roughnessMap, metalnessMap]); 
  
   return <primitive object={scene} /> 
 }; 

I have a branch with this updated code and here’s what it looks like so far: