Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add EnzeD/r3f-skills --skill "r3f-textures"
Install specific skill from multi-skill repository
# Description
React Three Fiber textures - useTexture, texture loading, environment maps, texture configuration. Use when loading images, working with PBR texture sets, cubemaps, HDR environments, or optimizing texture usage.
# SKILL.md
name: r3f-textures
description: React Three Fiber textures - useTexture, texture loading, environment maps, texture configuration. Use when loading images, working with PBR texture sets, cubemaps, HDR environments, or optimizing texture usage.
React Three Fiber Textures
Quick Start
import { Canvas } from '@react-three/fiber'
import { useTexture } from '@react-three/drei'
function TexturedBox() {
const texture = useTexture('/textures/wood.jpg')
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight />
<TexturedBox />
</Canvas>
)
}
useTexture Hook (Drei)
The recommended way to load textures in R3F.
Single Texture
import { useTexture } from '@react-three/drei'
function SingleTexture() {
const texture = useTexture('/textures/color.jpg')
return (
<mesh>
<planeGeometry args={[5, 5]} />
<meshBasicMaterial map={texture} />
</mesh>
)
}
Multiple Textures (Array)
function MultipleTextures() {
const [colorMap, normalMap, roughnessMap] = useTexture([
'/textures/color.jpg',
'/textures/normal.jpg',
'/textures/roughness.jpg',
])
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
map={colorMap}
normalMap={normalMap}
roughnessMap={roughnessMap}
/>
</mesh>
)
}
Named Object (Recommended for PBR)
function PBRTextures() {
// Named object automatically spreads to material
const textures = useTexture({
map: '/textures/color.jpg',
normalMap: '/textures/normal.jpg',
roughnessMap: '/textures/roughness.jpg',
metalnessMap: '/textures/metalness.jpg',
aoMap: '/textures/ao.jpg',
displacementMap: '/textures/displacement.jpg',
})
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
{...textures}
displacementScale={0.1}
/>
</mesh>
)
}
With Texture Configuration
import { useTexture } from '@react-three/drei'
import * as THREE from 'three'
function ConfiguredTextures() {
const textures = useTexture({
map: '/textures/color.jpg',
normalMap: '/textures/normal.jpg',
}, (textures) => {
// Configure textures after loading
Object.values(textures).forEach(texture => {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping
texture.repeat.set(4, 4)
})
})
return (
<mesh>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial {...textures} />
</mesh>
)
}
Preloading
import { useTexture } from '@react-three/drei'
// Preload at module level
useTexture.preload('/textures/hero.jpg')
useTexture.preload(['/tex1.jpg', '/tex2.jpg'])
function Component() {
// Will be instant if preloaded
const texture = useTexture('/textures/hero.jpg')
}
useLoader (Core R3F)
For more control over loading.
import { useLoader } from '@react-three/fiber'
import { TextureLoader } from 'three'
function WithUseLoader() {
const texture = useLoader(TextureLoader, '/textures/color.jpg')
// Multiple textures
const [color, normal] = useLoader(TextureLoader, [
'/textures/color.jpg',
'/textures/normal.jpg',
])
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={color} normalMap={normal} />
</mesh>
)
}
// Preload
useLoader.preload(TextureLoader, '/textures/color.jpg')
Texture Configuration
Wrapping Modes
import * as THREE from 'three'
function ConfigureWrapping() {
const texture = useTexture('/textures/tile.jpg', (tex) => {
// Wrapping
tex.wrapS = THREE.RepeatWrapping // Horizontal: ClampToEdgeWrapping, RepeatWrapping, MirroredRepeatWrapping
tex.wrapT = THREE.RepeatWrapping // Vertical
// Repeat
tex.repeat.set(4, 4) // Tile 4x4
// Offset
tex.offset.set(0.5, 0.5) // Shift UV
// Rotation
tex.rotation = Math.PI / 4 // Rotate 45 degrees
tex.center.set(0.5, 0.5) // Rotation pivot
})
return (
<mesh>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial map={texture} />
</mesh>
)
}
Filtering
function ConfigureFiltering() {
const texture = useTexture('/textures/color.jpg', (tex) => {
// Minification (texture larger than screen pixels)
tex.minFilter = THREE.LinearMipmapLinearFilter // Smooth with mipmaps (default)
tex.minFilter = THREE.NearestFilter // Pixelated
tex.minFilter = THREE.LinearFilter // Smooth, no mipmaps
// Magnification (texture smaller than screen pixels)
tex.magFilter = THREE.LinearFilter // Smooth (default)
tex.magFilter = THREE.NearestFilter // Pixelated (retro style)
// Anisotropic filtering (sharper at angles)
tex.anisotropy = 16 // Usually renderer.capabilities.getMaxAnisotropy()
// Generate mipmaps
tex.generateMipmaps = true // Default
})
}
Color Space
Important for accurate colors.
function ConfigureColorSpace() {
const [colorMap, normalMap, roughnessMap] = useTexture([
'/textures/color.jpg',
'/textures/normal.jpg',
'/textures/roughness.jpg',
], (textures) => {
// Color/albedo textures should use sRGB
textures[0].colorSpace = THREE.SRGBColorSpace
// Data textures (normal, roughness, metalness, ao) use Linear
// This is the default, so usually no action needed
// textures[1].colorSpace = THREE.LinearSRGBColorSpace
// textures[2].colorSpace = THREE.LinearSRGBColorSpace
})
}
Environment Maps
useEnvironment Hook
import { useEnvironment, Environment } from '@react-three/drei'
// Use as texture
function EnvMappedSphere() {
const envMap = useEnvironment({ preset: 'sunset' })
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
metalness={1}
roughness={0}
envMap={envMap}
/>
</mesh>
)
}
// Or use Environment component for scene-wide
function Scene() {
return (
<>
<Environment preset="sunset" background />
<Mesh />
</>
)
}
HDR Environment
import { useEnvironment } from '@react-three/drei'
function HDREnvironment() {
const envMap = useEnvironment({ files: '/hdri/studio.hdr' })
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
metalness={1}
roughness={0}
envMap={envMap}
envMapIntensity={1}
/>
</mesh>
)
}
Cube Map
import { useCubeTexture } from '@react-three/drei'
function CubeMapTexture() {
const envMap = useCubeTexture(
['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
{ path: '/textures/cube/' }
)
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial envMap={envMap} metalness={1} roughness={0} />
</mesh>
)
}
Video Textures
import { useVideoTexture } from '@react-three/drei'
function VideoPlane() {
const texture = useVideoTexture('/videos/sample.mp4', {
start: true,
loop: true,
muted: true,
})
return (
<mesh>
<planeGeometry args={[16, 9].map(x => x * 0.5)} />
<meshBasicMaterial map={texture} toneMapped={false} />
</mesh>
)
}
Canvas Textures
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
function CanvasTexture() {
const meshRef = useRef()
const textureRef = useRef()
useEffect(() => {
const canvas = document.createElement('canvas')
canvas.width = 256
canvas.height = 256
const ctx = canvas.getContext('2d')
// Draw on canvas
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 256, 256)
ctx.fillStyle = 'white'
ctx.font = '48px Arial'
ctx.fillText('Hello', 50, 150)
textureRef.current = new THREE.CanvasTexture(canvas)
}, [])
// Update texture dynamically
useFrame(({ clock }) => {
if (textureRef.current) {
const canvas = textureRef.current.image
const ctx = canvas.getContext('2d')
ctx.fillStyle = `hsl(${clock.elapsedTime * 50}, 100%, 50%)`
ctx.fillRect(0, 0, 256, 256)
textureRef.current.needsUpdate = true
}
})
return (
<mesh ref={meshRef}>
<planeGeometry args={[2, 2]} />
<meshBasicMaterial map={textureRef.current} />
</mesh>
)
}
Data Textures
import { useMemo } from 'react'
import * as THREE from 'three'
function NoiseTexture() {
const texture = useMemo(() => {
const size = 256
const data = new Uint8Array(size * size * 4)
for (let i = 0; i < size * size; i++) {
const value = Math.random() * 255
data[i * 4] = value
data[i * 4 + 1] = value
data[i * 4 + 2] = value
data[i * 4 + 3] = 255
}
const texture = new THREE.DataTexture(data, size, size)
texture.needsUpdate = true
return texture
}, [])
return (
<mesh>
<planeGeometry args={[2, 2]} />
<meshBasicMaterial map={texture} />
</mesh>
)
}
Render Targets
Render to texture.
import { useFBO } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function RenderToTexture() {
const fbo = useFBO(512, 512)
const meshRef = useRef()
const otherSceneRef = useRef()
useFrame(({ gl, camera }) => {
// Render other scene to FBO
gl.setRenderTarget(fbo)
gl.render(otherSceneRef.current, camera)
gl.setRenderTarget(null)
})
return (
<>
{/* Scene to render to texture */}
<group ref={otherSceneRef}>
<mesh position={[0, 0, -5]}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="red" />
</mesh>
</group>
{/* Display the texture */}
<mesh ref={meshRef}>
<planeGeometry args={[4, 4]} />
<meshBasicMaterial map={fbo.texture} />
</mesh>
</>
)
}
Texture Atlas / Sprite Sheet
import { useTexture } from '@react-three/drei'
import { useState } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
function SpriteAnimation() {
const texture = useTexture('/textures/spritesheet.png')
const [frame, setFrame] = useState(0)
// Configure texture
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping
texture.repeat.set(1/4, 1/4) // 4x4 sprite sheet
useFrame(({ clock }) => {
const newFrame = Math.floor(clock.elapsedTime * 10) % 16
if (newFrame !== frame) {
setFrame(newFrame)
const col = newFrame % 4
const row = Math.floor(newFrame / 4)
texture.offset.set(col / 4, 1 - (row + 1) / 4)
}
})
return (
<mesh>
<planeGeometry args={[1, 1]} />
<meshBasicMaterial map={texture} transparent />
</mesh>
)
}
Material Texture Maps Reference
<meshStandardMaterial
// Base color (sRGB)
map={colorTexture}
// Surface detail (Linear)
normalMap={normalTexture}
normalScale={[1, 1]}
// Roughness (Linear, grayscale)
roughnessMap={roughnessTexture}
roughness={1} // Multiplier
// Metalness (Linear, grayscale)
metalnessMap={metalnessTexture}
metalness={1} // Multiplier
// Ambient Occlusion (Linear, requires uv2)
aoMap={aoTexture}
aoMapIntensity={1}
// Self-illumination (sRGB)
emissiveMap={emissiveTexture}
emissive="#ffffff"
emissiveIntensity={1}
// Vertex displacement (Linear)
displacementMap={displacementTexture}
displacementScale={0.1}
displacementBias={0}
// Alpha (Linear)
alphaMap={alphaTexture}
transparent={true}
// Environment reflection
envMap={envTexture}
envMapIntensity={1}
// Lightmap (requires uv2)
lightMap={lightmapTexture}
lightMapIntensity={1}
/>
Second UV Channel (for AO/Lightmaps)
import { useEffect, useRef } from 'react'
function MeshWithUV2() {
const meshRef = useRef()
useEffect(() => {
// Copy uv to uv2 for aoMap/lightMap
const geometry = meshRef.current.geometry
geometry.setAttribute('uv2', geometry.attributes.uv)
}, [])
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial
aoMap={aoTexture}
aoMapIntensity={1}
/>
</mesh>
)
}
Suspense Loading
import { Suspense } from 'react'
import { useTexture } from '@react-three/drei'
function TexturedMesh() {
const texture = useTexture('/textures/large.jpg')
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
)
}
function Fallback() {
return (
<mesh>
<boxGeometry />
<meshBasicMaterial color="gray" wireframe />
</mesh>
)
}
function Scene() {
return (
<Suspense fallback={<Fallback />}>
<TexturedMesh />
</Suspense>
)
}
Performance Tips
- Use power-of-2 dimensions: 256, 512, 1024, 2048
- Compress textures: Use KTX2/Basis for web
- Enable mipmaps: For distant objects
- Limit texture size: 2048 usually sufficient
- Reuse textures: Same texture = better batching
- Preload important textures: Avoid pop-in
// Preload critical textures
useTexture.preload('/textures/hero.jpg')
// Check texture memory
useFrame(({ gl }) => {
console.log('Textures:', gl.info.memory.textures)
})
// Dispose unused textures (R3F usually handles this)
texture.dispose()
See Also
r3f-materials- Applying textures to materialsr3f-loaders- Asset loading patternsr3f-shaders- Custom texture sampling
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.