Listen up. If you're building modern applications, understanding Textures & Mapping in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1Why Textures Matter for Realism
So far, our objects have only had solid colors. To make them look like real brick, wood, or metal, we need Textures.
A flat solid color, no matter how well-lit, always reads as an obviously artificial 3D primitive ā it's missing the fine surface detail (grain, grime, scratches, pores) that real-world materials have. Textures close that gap by mapping a 2D image onto a mesh's surface using UV coordinates baked into the geometry.
This lesson covers the full pipeline: loading image files, assigning them to material properties, and eventually combining several texture maps together for physically based rendering that responds correctly to your scene's lighting.
// š¼ļø Time to add some detail3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2Loading Images with TextureLoader
A Texture is essentially a 2D image wrapped around a 3D object. You load textures using the TextureLoader.
TextureLoader.load() is asynchronous under the hood ā it returns a THREE.Texture object immediately (so you can assign it to a material right away) and fills in the actual pixel data once the image finishes downloading, updating the GPU texture automatically when it does. This is why you don't need to await anything to keep your code working, though the mesh will briefly render untextured on a slow connection.
For production apps, it's common to reuse a single TextureLoader instance (or use THREE.LoadingManager) across multiple loads rather than creating a new loader per texture, which lets you track combined load progress.
const textureLoader = new THREE.TextureLoader();
const colorTexture = textureLoader.load('/brick.jpg');3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3Assigning a Texture via the map Property
Once loaded, you assign the texture to a property on your material. The most common is the map property, which defines the base colors.
The map slot specifically drives albedo/diffuse color ā what the surface's raw color looks like before lighting is applied. Because colorTexture is passed straight into the material constructor here, MeshStandardMaterial multiplies each texel's color against the material's own color property (white by default), so if you've set a tinted color, it will blend with the texture rather than being ignored.
A subtlety worth remembering: color textures like this one should be authored (and loaded) in sRGB color space, whereas data textures like normal or roughness maps should not ā Three.js handles this via texture.colorSpace, which defaults correctly for map but must be set explicitly in some edge cases.
const material = new THREE.MeshStandardMaterial({
map: colorTexture
});3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4Loading Textures with useTexture in R3F
In React Three Fiber, we use the incredibly powerful useTexture hook from the @react-three/drei library to load textures.
Under the hood, useTexture wraps Three.js's loaders in React's Suspense system ā it throws a promise while the image loads, so wrapping your scene in <Suspense fallback={...}> gives you a loading state for free instead of manually tracking an isLoaded boolean. It also caches loaded textures by URL, so calling useTexture('/rock-color.jpg') twice in different components reuses the same GPU texture rather than downloading and uploading it again.
This hook can also load multiple textures at once by passing an array or object of URLs, which is the common pattern once you start combining several PBR maps on one material.
import { useTexture } from '@react-three/drei';
const colorMap = useTexture('/rock-color.jpg');
<meshStandardMaterial map={colorMap} />3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5PBR Texture Maps Beyond Color
But PBR (Physically Based Rendering) uses MORE than just the color map. It uses other textures to define bumps, reflections, and shadows.
Each map targets a different physical property that MeshStandardMaterial's lighting math reads independently: roughnessMap controls how blurry versus sharp specular highlights are per-pixel, metalnessMap marks which areas behave like bare metal versus dielectric material, and aoMap (ambient occlusion) darkens crevices that would naturally receive less indirect light.
These maps are typically authored together in tools like Substance Painter or Blender and exported as a matched set (sometimes packed into a single texture's RGB channels for efficiency), since they all need to align to the exact same UV coordinates to look correct together.
// Types of textures:
// - Color Map (Base Color)
// - Normal Map (Bumps and dents)
// - Roughness Map (Shiny vs dull areas)3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6Normal Maps: Faking Depth with Light
A Normal Map is a special purple/blue image that tells the renderer how light should bounce off the surface, creating the illusion of deep 3D details on a perfectly flat surface.
The purple/blue tint comes from encoding a per-pixel surface normal vector directly into the RGB channels ā the dominant blue channel represents the Z axis (pointing outward from the surface), while red and green encode X/Y tilt. When the lighting shader reads this map, it perturbs the surface normal used in its lighting calculations pixel-by-pixel, so light and shadow fall as if there were real bumps and dents, even though the underlying geometry is still a flat plane or low-poly sphere.
Because it's purely a lighting trick, a normal map never actually changes the silhouette of an object ā viewed from a grazing angle at the object's edge, the illusion breaks down, which is the main visual giveaway that distinguishes it from real displaced geometry.
const normalTexture = useTexture('/rock-normal.jpg');
<meshStandardMaterial
map={colorTexture}
normalMap={normalTexture}
/>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7Live Preview: Texture Wrapping on a Sphere
Watch this live 3D preview! We use a checkerboard texture for the map. Notice how the texture wraps perfectly around the sphere.
This example builds the texture entirely in JavaScript rather than loading an image file ā it draws a black-and-white checker pattern onto an HTML <canvas> element with the 2D drawing API, then wraps that canvas in a THREE.CanvasTexture so it can be used exactly like any loaded image. This is a handy technique for procedural patterns, debug grids, or dynamically generated UI textures that don't need a separate asset file.
Because the sphere's UV coordinates are generated automatically by SphereGeometry, the checker squares curve smoothly around the surface without manual UV editing ā the texture's repeat.set(4, 4) call (covered next) also compounds with those built-in UVs to control how many checker tiles appear.
const App = () => {
// Creating a simple checkerboard texture programmatically
const canvas = document.createElement('canvas');
canvas.width = 128; canvas.height = 128;
const context = canvas.getContext('2d');
context.fillStyle = 'white'; context.fillRect(0,0,128,128);
context.fillStyle = 'black'; context.fillRect(0,0,64,64);
context.fillRect(64,64,64,64);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
return (
<Canvas camera={{ position: [0, 0, 3] }}>
<ambientLight intensity={1} />
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial map={texture} />
</mesh>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8Tiling Textures with repeat
Did you notice texture.repeat.set(4, 4)? You can tile textures so they repeat across a surface, rather than stretching one giant image across the whole object.
The repeat property scales UV coordinates before they're sampled ā repeat.set(4, 4) tells the GPU to read the texture as if it appeared 4 times horizontally and 4 times vertically across the same UV range, which is exactly what turns one small checkerboard image into a dense tiled pattern instead of one giant smear across the whole sphere.
Tiling only works cleanly if wrapS and wrapT are set to THREE.RepeatWrapping first ā by default Three.js uses ClampToEdgeWrapping, which stretches the texture's edge pixels outward instead of repeating, so skipping that step is a common reason tiling appears to silently do nothing.
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Wrap Modes: RepeatWrapping vs ClampToEdgeWrapping
To allow a texture to repeat, its wrapping properties wrapS (horizontal) and wrapT (vertical) must both be set to THREE.RepeatWrapping.
Three.js supports three wrap modes: ClampToEdgeWrapping (the default, stretches edge pixels), RepeatWrapping (tiles the texture seamlessly), and MirroredRepeatWrapping (tiles but flips every other repeat, which hides visible seams on non-tileable images by mirroring instead of repeating them directly).
A texture image dimension of a power of two (256, 512, 1024, etc.) used to be a hard requirement for repeating textures in WebGL1; modern WebGL2-based Three.js is more forgiving, but power-of-two textures are still recommended for predictable mipmapping and tiling behavior across devices.
texture.wrapS = THREE.???;3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10Textures Recap and What's Next
Awesome! You can now load realistic textures and maps. We are ready to move on to importing fully-built 3D models from Blender!
You've now covered the full texturing toolkit: loading images with TextureLoader or useTexture, assigning them to material slots like map, roughnessMap, and normalMap, and controlling how they tile with repeat, wrapS, and wrapT. Together these techniques take a plain gray primitive to something that reads as a believable, detailed surface.
The next natural step is importing actual 3D models ā meshes authored in Blender or similar tools already come with their own UV-mapped textures baked in, so everything you've learned here about how materials read texture maps applies directly once those models are loaded via GLTFLoader.
// š¼ļø Textures mapped!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Step-by-Step Breakdown
So far, our objects have only had solid colors. To make them look like real brick, wood, or metal, we need Textures.
A Texture is essentially a 2D image wrapped around a 3D object. You load textures using the TextureLoader.
Which Three.js utility class is used to load image files (like .jpg or .png) to be used as textures?
- āImageLoader
- āTextureLoader
- āMaterialLoader
Once loaded, you assign the texture to a property on your material. The most common is the map property, which defines the base colors.
In React Three Fiber, we use the incredibly powerful useTexture hook from the @react-three/drei library to load textures.
Which property on a MeshStandardMaterial is used to assign the primary color image texture?
- āimage
- āmap
- ātexture
But PBR (Physically Based Rendering) uses MORE than just the color map. It uses other textures to define bumps, reflections, and shadows.
A Normal Map is a special purple/blue image that tells the renderer how light should bounce off the surface, creating the illusion of deep 3D details on a perfectly flat surface.
Which map type creates the illusion of 3D depth, bumps, and dents without actually adding any polygons to the geometry?
- ābump
- ānormal
- ādepth
Watch this live 3D preview! We use a checkerboard texture for the map. Notice how the texture wraps perfectly around the sphere.
Did you notice texture.repeat.set(4, 4)? You can tile textures so they repeat across a surface, rather than stretching one giant image across the whole object.
To allow a texture to repeat, what must its wrapping properties (wrapS and wrapT) be set to?
- āClampToEdgeWrapping
- āRepeatWrapping
- āTileWrapping
Awesome! You can now load realistic textures and maps. We are ready to move on to importing fully-built 3D models from Blender!
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Don't Rely on Texture Detail Alone to Convey Information
A material's texture (label text baked into a color map, a warning pattern, a diagram drawn onto a 3D surface) is completely invisible to screen readers and to any user who can't resolve fine visual detail on a WebGL canvas. Any information conveyed purely through a texture ā a product's material name, a diagram's labels ā needs an accessible text equivalent outside the canvas.
<p className="sr-only">Material: brushed steel with a matte clear-coat finish</p>SEO Implications
- 1
Texture Assets Are Not Indexed as Page Content
Image files loaded via TextureLoader or useTexture are fetched and rendered onto WebGL geometry, not inserted as `<img>` tags ā search engines don't parse their pixel content, alt text, or file names as part of the page. If the material shown (e.g. 'oak wood', 'brushed aluminum') is relevant to what users search for, describe it in real page copy rather than relying on the texture file alone.
Best Practices
Compress and Resize Texture Images Before Shipping
Large, uncompressed source textures (4K PNGs exported straight from a design tool) bloat both network transfer and GPU memory, since Three.js uploads the full decoded image to the GPU regardless of how large the mesh actually appears on screen. Resize to the smallest resolution that still looks sharp at your target viewport, and prefer compressed formats like KTX2/Basis for production.
Set Wrap Mode and Repeat Before the Texture Renders
wrapS, wrapT, and repeat only take effect on the next texture upload after texture.needsUpdate is triggered (which setting these properties does automatically in most cases) ā configure them immediately after loading rather than mutating them sporadically later, and always pair RepeatWrapping with repeat.set() since one without the other produces no visible tiling.
Frequent Bugs
A texture appears stretched into a single edge-color smear instead of tiling, even after calling texture.repeat.set().
RepeatWrapping must be set on both wrapS and wrapT ā Three.js defaults to ClampToEdgeWrapping, which stretches the texture's outer edge pixels outward rather than repeating the image, so repeat.set() alone has no visible effect until the wrap mode is changed.
Real-World Examples
Configurable Material Swatches in a Furniture Viewer
3D furniture and product configurators let users click swatches to instantly swap a sofa's fabric or a table's wood finish ā implemented by preloading a small set of color/roughness/normal texture sets with useTexture and swapping the material's map references on click rather than reloading the whole model.
const fabrics = useTexture({ map: '/velvet-color.jpg', roughnessMap: '/velvet-rough.jpg' });
<meshStandardMaterial {...fabrics} />