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: