Navigate
Search topics across all sections
Textures
Textures are images that get wrapped around your 3D objects to add detail — wood grain, brick patterns, scratches, and more — without needing millions of extra polygons.
You load a beautiful wood texture and apply it to your mesh, but the colors look flat and washed out — almost grey. You double-check the image file and it looks perfect. The problem? You forgot to tell Three.js that this is a color texture stored in sRGB, so it is treating the pixel values as raw linear data.
Texture colors appear desaturated or unnaturally dark on PBR materials.
Real-world
Have you ever wrapped a birthday present? You take a flat sheet of wrapping paper and fold it around a 3D box.
That is exactly what textures do — they take a flat 2D image and wrap it around a 3D object.
The tricky part? UV coordinates are the folding instructions. U goes left-to-right (like X), V goes bottom-to-top (like Y). They tell the computer which part of the image goes on which part of the shape.
Built-in shapes like boxes and spheres already come with UV coordinates, so the wrapping happens automatically. But for custom models, you might need to set up those folding instructions yourself in a 3D modeling tool like Blender.
How a Texture Gets to Your Screen
From image file to rendered surface, here is the journey every texture takes.
Load image
useTexture or TextureLoader
Configure
Color space, wrapping, filtering
Attach to material
map, normalMap, roughnessMap, etc.
GPU renders it
UV coordinates map image to mesh
Guided Walkthrough
Let us go from a bare white box to a fully textured brick wall in three steps.
Step 1 — Load and apply a color texture
import { useTexture } from '@react-three/drei'
const colorMap = useTexture('/textures/brick.jpg')
colorMap.colorSpace = THREE.SRGBColorSpace
<mesh>
<boxGeometry />
<meshStandardMaterial map={colorMap} />
</mesh>The useTexture hook loads and caches the image. We mark it as sRGB because it is a color texture. The map prop wraps the image around the box using its built-in UV coordinates.
Step 2 — Add a normal map for surface detail
const [color, normal] = useTexture([
'/textures/brick_color.jpg',
'/textures/brick_normal.jpg',
])
color.colorSpace = THREE.SRGBColorSpace
<meshStandardMaterial
map={color}
normalMap={normal}
/>A normal map fakes bumpy surface detail without adding real geometry. The bricks now look like they have depth and grooves catching the light. Notice we do NOT set sRGB on the normal map — it stores angle data, not colors.
Step 3 — Tile the texture to cover a larger surface
const tex = useTexture('/textures/brick.jpg')
tex.wrapS = THREE.RepeatWrapping
tex.wrapT = THREE.RepeatWrapping
tex.repeat.set(4, 4)
<mesh>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial map={tex} />
</mesh>RepeatWrapping tells the texture to tile instead of stretching. The repeat.set(4, 4) means the image repeats 4 times in each direction. This is how you cover a large wall with a small brick image.
What you just learned
Textures wrap 2D images onto 3D surfaces using UV coordinates as folding instructions
Color textures need SRGBColorSpace — data textures like normals stay linear
RepeatWrapping tiles a texture, but requires power-of-two dimensions (512, 1024, etc.)
Normal maps fake bumpy surface detail without adding extra geometry
Question
If a 1024x1024 solid white texture uses the exact same GPU memory as a 1024x1024 detailed photograph, what does that tell you about how textures are stored on the GPU? And how should that change the way you choose texture sizes for your project?
Think about it...
You have a 600x400 brick texture and you want it to tile 3x3 across a wall. What will happen?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set tileCount to 4 — how does the checkerboard change?
Try This!
Intermediate
Switch wrapping to Mirrored — spot the difference
Try This!
Beginner
Change color tint to red
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
const colorMap = useTexture('/textures/wood.jpg')
// No colorSpace set — Three.js treats it as linear data
// Colors appear desaturated and wrong
<meshStandardMaterial map={colorMap} />// 300x500 texture (not power of two)
const tex = useTexture('/textures/photo_300x500.jpg')
tex.wrapS = THREE.RepeatWrapping
tex.wrapT = THREE.RepeatWrapping
tex.repeat.set(4, 4)
// Console warnings, no tiling, stretched edges// Swapping to a new texture without cleanup
texture.image = highResImage;
texture.needsUpdate = true;
// Old GPU allocation is never freed — it leaksBest Practices
Use power-of-two dimensions
Always use 256, 512, 1024, or 2048 pixel sizes. This enables mipmaps, repeat wrapping, and avoids console warnings. It is the single most important texture rule.
Dispose textures when done
Call texture.dispose() to free GPU memory when a texture is no longer needed. This is critical for apps that load and unload scenes or swap textures at runtime.
Use the smallest size that looks good
A 4096x4096 texture uses roughly 64 MB of GPU memory. On mobile, that can cause crashes. Start small (512 or 1024) and only go bigger if you see visible quality loss up close.
Enable anisotropic filtering for floors
Surfaces viewed at steep angles (floors, roads) shimmer without anisotropic filtering. Set texture.anisotropy to the renderer maximum for crisp results at any viewing angle.