Core Philosophy
- A material decides the color of every visible pixel of a geometry via a shader. Three.js ships pre-made shaders as material classes so you rarely write GLSL for standard looks
- One material instance can be shared across many meshes. Tweak the instance once, every mesh updates
- Materials sit on a performance ladder: Basic, Normal, and Matcap are cheap and light-free, Lambert is the cheapest lit material, then Phong, then Standard, with Physical the most expensive. Pick the cheapest one that achieves the look, especially on mobile
- Properties can be passed in the constructor object or set on the instance afterward, both are equivalent
const material = new THREE.MeshBasicMaterial({ map: doorColorTexture })
// same as
const material = new THREE.MeshBasicMaterial()
material.map = doorColorTexture
The material catalog at a glance
- MeshBasicMaterial: flat color or texture, ignores lights entirely
- MeshNormalMaterial: colors by normal direction relative to the camera, great for debugging normals and stylized looks
- MeshMatcapMaterial: fakes lighting by sampling a sphere-render texture, looks lit with zero lights, very performant
- MeshDepthMaterial: white near the camera
near, black nearfar, used internally for depth passes and shadows - MeshLambertMaterial: cheapest light-reactive material, but shows banding artifacts on curved surfaces
- MeshPhongMaterial: Lambert plus specular highlights (
shininess,specular), fewer artifacts, slightly more expensive - MeshToonMaterial: cartoon-style stepped shading, controlled by a
gradientMap - MeshStandardMaterial: PBR with
metalnessandroughness, the realistic default - MeshPhysicalMaterial: Standard plus clearcoat, sheen, iridescence, transmission. The most expensive material in Three.js
- PointsMaterial: for particles. ShaderMaterial / RawShaderMaterial: custom GLSL. Covered elsewhere
Gotcha: Lambert, Phong, Toon, Standard, and Physical all require lights (or an environment map). Swapping to one of them with no lights gives a black screen and no console error. Add an AmbientLight and a PointLight, or an environment map, before assuming the material is broken.
Universal properties (shown on MeshBasicMaterial, apply broadly)
map: applies a texture to the surfacecolor: uniform tint, must be aTHREE.Colorwhen set on the instance. Accepts'#ff0000','#f00','red','rgb(255, 0, 0)',0xff0000. Combined withmapit tints the texturewireframe: true: shows the triangles as 1px lines regardless of distance, the quickest way to see how subdivided a geometry really isopacity: does nothing untiltransparent = truetells Three.js the material supports transparency
material.transparent = true
material.opacity = 0.5
alphaMap: grayscale texture controlling per-pixel transparency, also requirestransparent = trueside:THREE.FrontSide(default),THREE.BackSide, orTHREE.DoubleSide. DoubleSide is needed to see a plane from behind, but it doubles the faces to render, avoid it when the back is never visible
MeshNormalMaterial
- Renders each pixel by its normal's orientation relative to the camera: the purple-blue look. Normals are the per-vertex outward directions used for lighting, reflection, and refraction
- The color stays the same as you orbit because it is camera-relative
- Adds
flatShading: true, which stops interpolating normals between vertices so faces render flat and faceted - Legitimately usable as a final look, not just for debugging
MeshMatcapMaterial
- Supply a texture of a pre-lit sphere via
matcap; the material samples it by normal direction relative to the camera. Result: convincing lighting with no lights in the scene, at near-Basic cost
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture
- Matcap textures are sRGB: set
matcapTexture.colorSpace = THREE.SRGBColorSpacelike a color map - Limitation: the "lighting" is baked into the texture, so it never responds to camera orbit or scene lights
- Sources: the nidorx/matcaps GitHub list (check licenses), render your own sphere in a 3D package, or build one at matcap-studio.vercel.app
Lambert, Phong, Toon
- Lambert: fastest lit material. Fine for performance-critical scenes, but inspect curved geometry for visible banding patterns
- Phong: fewer artifacts plus specular reflection.
shininessraises the highlight tightness,specularcolors it
material.shininess = 100
material.specular = new THREE.Color(0x1188ff)
- Toon: two-tone cartoon shading by default. Add more bands with a tiny gradient texture on
gradientMap
The Toon gradientMap filtering trap
A 3x1 (or 5x1) pixel gradient texture gets blended by default filtering, which smooths the steps and silently kills the cartoon effect. Force nearest sampling and drop mipmaps:
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false
material.gradientMap = gradientTexture
MeshStandardMaterial (the PBR default)
- Physically based rendering with realistic light response. "Standard" because PBR parameters transfer across engines: the same metalness and roughness should look similar everywhere
- The two core dials, both 0 to 1:
material.metalness = 0.45
material.roughness = 0.65
- Wire them to a debug UI (lil-gui) immediately, these values are tuned by eye:
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)
The full map set
material.map = doorColorTexture // sRGB color
material.aoMap = doorAmbientOcclusionTexture // fake crevice shadows
material.aoMapIntensity = 1
material.displacementMap = doorHeightTexture // moves vertices
material.displacementScale = 0.1
material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture
material.normalMap = doorNormalTexture // fake detail, no vertices moved
material.normalScale.set(0.5, 0.5) // a Vector2, not a number
material.transparent = true
material.alphaMap = doorAlphaTexture
Gotchas, each one a classic:
- aoMap only affects
AmbientLight,HemisphereLight, and environment-map lighting. Under a lonePointLightit appears to do nothing - displacementMap looks terrible until the geometry has enough subdivisions (e.g.
PlaneGeometry(1, 1, 100, 100),SphereGeometry(0.5, 64, 64)) and untildisplacementScaleis tamed down from its default of 1 - metalnessMap / roughnessMap multiply with the scalar values. Leaving
metalness = 0.45while a metalnessMap is set skews every texel. Set bothmetalnessandroughnessto1so the maps alone drive the result - normalScale is a
Vector2:material.normalScale.set(0.5, 0.5), not= 0.5
Environment maps
An environment map is an image of the scene's surroundings, used for reflection, refraction, and as a light source in itself:
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
const rgbeLoader = new RGBELoader()
rgbeLoader.load('./textures/environmentMap/2k.hdr', (environmentMap) => {
environmentMap.mapping = THREE.EquirectangularReflectionMapping
scene.background = environmentMap
scene.environment = environmentMap
})
.hdrfiles load viaRGBELoader, notTextureLoader, and the result arrives in a callback (second parameter), unlikeTextureLoader's immediate returnmapping = THREE.EquirectangularReflectionMappingmust be set before usescene.environmentlights every PBR mesh in the scene;scene.backgroundjust displays it. Set either or both- An environment map alone can replace explicit lights entirely: delete the AmbientLight and PointLight and the scene still reads as lit
- Works with Lambert and Phong too, not just Standard/Physical
- Low
roughnessplus highmetalnessis the combination that shows reflections clearly when testing
MeshPhysicalMaterial
Inherits everything from MeshStandardMaterial and adds four effects. Cost warning: this is the worst-performing material in Three.js, do not blanket a scene with it on mobile.
Clearcoat
Simulates a thin varnish or glass layer with its own reflectivity on top of the base material:
material.clearcoat = 1
material.clearcoatRoughness = 0
Sheen
Highlights the surface at grazing angles, the fabric and velvet look, reads as soft:
material.sheen = 1
material.sheenRoughness = 0.25
material.sheenColor.set(1, 1, 1)
Iridescence
Color-shifting artifacts like a fuel puddle or soap bubble, most visible at narrow viewing angles:
material.iridescence = 1
material.iridescenceIOR = 1 // ranges 1 to 2.333
material.iridescenceThicknessRange = [100, 800]
Transmission
Light passes through and refracts, real glass rather than mere opacity fading:
material.transmission = 1
material.ior = 1.5 // index of refraction: glass 1.5, water 1.333, diamond 2.417
material.thickness = 0.5 // fixed value, actual geometry thickness is ignored
- Transmission deforms the image behind the object, which
transparent + opacitynever does - For a clean glass look, strip all maps and set
metalness = 0androughness = 0; raiseroughnessfor frosted glass - IOR values for real substances are on the Wikipedia list of refractive indices
Choosing a material
- No lights, flat texture or color: Basic
- Stylized normal-rainbow or debugging: Normal
- Looks lit, costs nothing, static lighting is fine: Matcap
- Lit and cheap, artifacts acceptable: Lambert
- Lit with specular highlight, still cheap: Phong
- Cartoon: Toon with a nearest-filtered gradientMap
- Realistic PBR: Standard (the default choice for realism)
- Glass, varnish, fabric, soap-bubble effects: Physical, sparingly
Common mistakes
- Switching to Lambert/Phong/Toon/Standard/Physical and getting a black screen: these need lights or an environment map, and there is no error
- Setting
opacityoralphaMapwithouttransparent = true, so nothing changes - Using
THREE.DoubleSideeverywhere: it renders both faces even when one is never seen - Forgetting
colorSpace = THREE.SRGBColorSpaceonmapandmatcaptextures, giving washed-out color - Toon material losing its stepped look because the tiny gradientMap is linearly filtered: set min and mag filters to
NearestFilterand disable mipmaps - Leaving scalar
metalness/roughnessbelow 1 while also using metalnessMap/roughnessMap, which multiplies and distorts the maps - Applying a displacementMap to a two-triangle plane or low-poly sphere and calling the map broken: add subdivisions and lower
displacementScale - Setting
normalScale = 0.5instead ofnormalScale.set(0.5, 0.5): it is a Vector2 - Loading an
.hdrenvironment map withTextureLoaderinstead ofRGBELoader, or forgettingEquirectangularReflectionMapping - Expecting
aoMapto react to a PointLight: it only affects ambient-style light and environment maps - Assigning
material.color = '#ff0000'as a string on an existing instance instead ofnew THREE.Color('#ff0000') - Using MeshPhysicalMaterial on many large on-screen objects and losing the frame rate, especially on mobile