Navigate

Search topics across all sections

GitHub
Materials

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.

terminal
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

step-1-color.tsxTSX
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

step-2-normal.tsxTSX
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

step-3-tiling.tsxTSX
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.

1
Color texture looks washed out or grey
Missing the sRGB color space flag
Don't do this
srgb-colorspace.tsxTSX
const colorMap = useTexture('/textures/wood.jpg')
// No colorSpace set — Three.js treats it as linear data
// Colors appear desaturated and wrong

<meshStandardMaterial map={colorMap} />
Color textures are saved in sRGB but PBR math needs linear values. Marking the texture as sRGB lets Three.js handle the conversion. Without it, colors appear washed out because sRGB-encoded values are treated as already-linear.
2
Texture does not tile — edge pixels stretch instead
Using a non-power-of-two texture with RepeatWrapping
Don't do this
npot-textures.tsxTSX
// 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
WebGL requires power-of-two dimensions (256, 512, 1024, etc.) for textures that tile. Non-power-of-two textures cannot use RepeatWrapping or generate mipmaps. Either resize your image or use ClampToEdgeWrapping with linear filtering.
3
GPU memory leak when swapping textures
Old texture never gets freed from GPU memory
Don't do this
texture-dispose.tsxTSX
// Swapping to a new texture without cleanup
texture.image = highResImage;
texture.needsUpdate = true;
// Old GPU allocation is never freed — it leaks
When you change a texture at runtime, always dispose() the old one first. This frees the GPU memory. Without disposal, every texture swap leaves the old data in GPU memory permanently, which can cause crashes on devices with limited VRAM.

Best 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.