Diagnosing Black or Incorrectly Lit Textures in Three.js r152+
A textured Three.js mesh renders black or too dark under default ColorManagement. Check texture color spaces and renderer output before adjusting lights.
09 Aug 2025, 14:43 UTC

The symptom
A textured mesh renders black, overly dark, or washed out while untextured geometry and lights appear normal. The issue often appears after moving to Three.js r152 or later, where ColorManagement is enabled by default. ColorManagement controls how the renderer interprets color values and writes them to the canvas.
Useful takeaway: Before changing lights, verify that color textures such as albedo and emissive use THREE.SRGBColorSpace, data textures such as normal, roughness, metalness, and displacement remain on THREE.LinearSRGBColorSpace, and renderer.outputColorSpace matches the intended canvas output, usually THREE.SRGBColorSpace.
Cause and diagnostic table
| Observable symptom | Likely cause | Property to inspect |
|---|---|---|
| Texture looks black or very dark | Color map interpreted as linear instead of sRGB | texture.colorSpace on material.map |
| Colors look washed out or oversaturated | Renderer output color space does not match the canvas | renderer.outputColorSpace |
| Normal or roughness detail looks wrong | Data map incorrectly set to SRGBColorSpace | texture.colorSpace on normalMap, roughnessMap, etc. |
| Texture is missing and appears black | Load failure: 404, CORS, or unsupported format | DevTools Network panel and onLoad callback |
Ordered checks
- Confirm color-map color space. In DevTools, inspect the texture assigned to
material.mapormaterial.emissiveMap. It should equalTHREE.SRGBColorSpace. - Confirm data maps stay linear. Normal, roughness, metalness, displacement, and ao maps should remain on
THREE.LinearSRGBColorSpace. These are data, not color. - Confirm renderer output. Check
renderer.outputColorSpace. For most canvas targets it should beTHREE.SRGBColorSpace. - Confirm the image actually loaded. Open DevTools Network, reload, filter by image, and look for HTTP 200 with no CORS errors. Verify the
TextureLoaderonLoadcallback fired.
Example configuration
Run this in your application source or DevTools console. Replace assets/textures/albedo.jpg with your actual asset path.
import * as THREE from 'three';
const loader = new THREE.TextureLoader();
// Required for cross-origin images; the serving server must also send CORS headers.
loader.setCrossOrigin('anonymous');
const albedo = loader.load('assets/textures/albedo.jpg');
albedo.colorSpace = THREE.SRGBColorSpace;
const normal = loader.load('assets/textures/normal.jpg');
normal.colorSpace = THREE.LinearSRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map: albedo,
normalMap: normal,
});
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.outputColorSpace = THREE.SRGBColorSpace;
Where to run: Browser JavaScript context, typically your application module or DevTools console. Permissions: none beyond normal page access. Expected check: log albedo.colorSpace, normal.colorSpace, and renderer.outputColorSpace; all should match the constants above. Risk: changing colorSpace after a material has rendered may require material.needsUpdate = true.
Fixes tied to findings
- If albedo or emissive is LinearSRGBColorSpace: set
texture.colorSpace = THREE.SRGBColorSpace. - If normal, roughness, metalness, or displacement is SRGBColorSpace: set
texture.colorSpace = THREE.LinearSRGBColorSpace. - If renderer.outputColorSpace is LinearSRGBColorSpace: set
renderer.outputColorSpace = THREE.SRGBColorSpace. - If the texture failed to load: correct the URL, confirm the server returns HTTP 200, and add CORS headers plus
loader.setCrossOrigin('anonymous')before loading.
Escalation criteria
Replace the material temporarily with new THREE.MeshBasicMaterial({ map: albedo }) using the same texture.
- If it still looks wrong, the problem is asset loading or the texture file itself, not lighting.
- If it looks correct, the problem is lighting or color-space configuration. Create a minimal reproducible example with one mesh, one light, and the suspect texture before escalating further.
Limitations and verification
- This guide assumes Three.js r152 or later. Older projects may use deprecated
gammaInput/gammaOutput; do not mix those with ColorManagement. - Cross-origin images require the server to send CORS headers in addition to
setCrossOrigin('anonymous'). - Practical check: reload the page after changes and confirm the console shows no Three.js texture or gamma deprecation warnings. Log the color-space values again and compare them to
THREE.SRGBColorSpace.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.