Skip to content

Three.js Advanced Materials — PBR, Textures, Environment Maps

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Three.js Advanced Materials. We cover key concepts, practical examples, and best practices to help you master this topic.

Three.js advanced materials use physically-based rendering with texture maps to create realistic surfaces through diffuse, normal, roughness, metalness, and environment map inputs.

What You'll Learn

By the end of this guide, you will understand PBR material properties, load and apply multiple texture maps, use environment maps for reflections, configure MeshPhysicalMaterial for realistic surfaces, and optimize material performance.

Why Advanced Materials Matter

Basic materials with solid colors look flat and fake. Real-world surfaces have scratches, bumps, fingerprints, and varying reflectivity. Physically-based rendering (PBR) mimics real light behavior. In Durga Antivirus Pro, the 3D threat dashboard uses PBR materials to render network nodes as glassy orbs with metallic connections — giving security analysts an intuitive, realistic view of network topology.

flowchart LR
    A[Base Color Map] --> E[PBR Material]
    B[Normal Map] --> E
    C[Roughness Map] --> E
    D[Metalness Map] --> E
    F[Ambient Occlusion] --> E
    G[Environment Map] --> E
    E --> H[Final Rendered Surface]

Texture Maps — The Building Blocks

A texture is an image applied to a surface. Different texture channels control different visual properties.

var loader = new THREE.TextureLoader();

var material = new THREE.MeshStandardMaterial({
    map: loader.load('diffuse.jpg'),
    normalMap: loader.load('normal.jpg'),
    roughnessMap: loader.load('roughness.jpg'),
    metalnessMap: loader.load('metalness.jpg'),
    aoMap: loader.load('ao.jpg'),
    aoMapIntensity: 1.0,
    roughness: 0.5,
    metalness: 0.8
});

Expected output: A mesh that appears to have surface detail, bumps, scratches, and realistic lighting response based on the texture maps.

Texture Map Reference

Map Type Color Channel What It Does
map RGB Base color of the surface
normalMap RGB Simulates 3D surface bumps without geometry
roughnessMap Grayscale White = rough, black = smooth
metalnessMap Grayscale White = metal, black = non-metal
aoMap Grayscale Darkens crevices for depth
displacementMap Grayscale Actually moves vertices (needs subdivisions)

Environment Maps — Realistic Reflections

An environment map captures the surrounding scene as a 360-degree image that reflects off metallic or glossy surfaces.

var envMap = loader.load('equirectangular.jpg');
envMap.mapping = THREE.EquirectangularReflectionMapping;

var material = new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    metalness: 1.0,
    roughness: 0.1,
    envMap: envMap,
    envMapIntensity: 1.5
});

Expected output: A chrome-like sphere that reflects the surrounding environment with sharp, glossy reflections.

HDR Environment Maps

For better results, use HDR (high dynamic range) environment maps:

import { RGBELoader } from "three/addons/loaders/RGBELoader.js";

new RGBELoader().load('environment.hdr', function(texture) {
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.environment = texture;
    scene.background = texture;
});

MeshPhysicalMaterial — The Gold Standard

MeshPhysicalMaterial extends MeshStandardMaterial with additional properties for realistic rendering.

var material = new THREE.MeshPhysicalMaterial({
    color: 0x4488ff,
    metalness: 0.0,
    roughness: 0.05,
    clearcoat: 1.0,
    clearcoatRoughness: 0.1,
    clearcoatNormalMap: normalMap,
    reflectivity: 0.5,
    ior: 1.5,
    transmission: 0.9,
    thickness: 1.5
});

Expected output: A glass-like material with clear coat finish, refraction, and transparency.

Property Effect Use Case
clearcoat Adds a clear glossy layer Car paint
ior Index of refraction Glass, water, diamond
transmission Light passes through Windows, bottles
thickness How thick the glass is Realistic refraction
sheen Fabric/fuzz appearance Velvet, carpets

Repeating and Wrapping Textures

var texture = loader.load('brick.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
texture.offset.set(0.5, 0);
texture.rotation = Math.PI / 4;
texture.anisotropy = 4;

Expected output: A brick texture repeated 4 times in each direction, offset right, rotated 45 degrees, with anisotropic filtering for sharp viewing from angles.

Why anisotropy: Without it, textures viewed from a shallow angle become blurry. Anisotropic filtering samples more texels for those angles, keeping the texture sharp.

Common Mistakes

1. Using TextureLoader Before Textures Are Loaded

The TextureLoader returns immediately with an empty texture. The image loads asynchronously. Check with texture.image or use callbacks.

2. Missing UV Coordinates for aoMap and displacementMap

Not all geometries have a second UV set. Use geometry.setAttribute('uv2', geometry.attributes.uv.clone()) to duplicate UVs for aoMap.

3. Normal Map Without Tangent Space

Standard geometries include tangents for normal mapping. Custom BufferGeometry may need computeTangents().

4. HDR Environment Map Without Tone Mapping

HDR textures have values above 1.0. Without tone mapping, colors clip. Enable renderer.toneMapping = THREE.ACESFilmicToneMapping.

5. Texture Memory Overload

4K textures on many objects exhaust GPU memory. Use 1K-2K textures for most objects and mipmapping (enabled by default).

6. Forgetting to Set Texture Mapping Mode

Plain textures apply as equirectangular. For cube maps, set texture.mapping = THREE.CubeReflectionMapping.

Practice Questions

Q1: What is the difference between MeshStandardMaterial and MeshPhysicalMaterial? A: MeshPhysicalMaterial adds clearcoat, transmission, sheen, ior, and more advanced optical properties on top of MeshStandardMaterial's PBR.

Q2: What does a roughness map control? A: It controls how rough each pixel appears. White pixels are rough (diffuse Reflection), black pixels are smooth (specular reflection).

Q3: Why do you need tone mapping for HDR environments? A: HDR images contain values beyond the 0-1 range. Tone mapping compresses these into the displayable range while preserving detail in bright and dark areas.

Q4: What is anisotropy in texture filtering? A: It controls how many texel samples are taken when a surface is viewed at an angle, preventing blurriness. Values are powers of 2 (1, 2, 4, 8, 16).

Q5: How do you create a transparent glass material? A: Use MeshPhysicalMaterial with transmission: 0.9, thickness: 1.5, roughness: 0, ior: 1.5, and transparent: true.

Challenge: Create a scene with three spheres side by side — one matte ceramic, one metallic chrome, and one glass — each with appropriate PBR settings and environment map reflections.

FAQ

What map types does MeshStandardMaterial support?

It supports map (diffuse), normalMap, roughnessMap, metalnessMap, aoMap, displacementMap, emissiveMap, alphaMap, and envMap.

How do I tile a texture?

Set texture.wrapS and wrapT to RepeatWrapping, then set texture.repeat to the desired tile count (e.g., new THREE.Vector2(4, 4)).

What resolution textures should I use?

Use 1K-2K for most objects, 4K for hero objects close to camera. Higher resolutions consume more GPU memory and loading time.

What is the difference between EquirectangularReflectionMapping and CubeReflectionMapping?

Equirectangular uses a single 2:1 image. CubeReflectionMapping uses 6 cube faces. Both produce reflections but cube maps have less distortion.

Can I use video as a texture?

Yes. Create a Video element, then use new THREE.VideoTexture(video). Call video.play() to start playback.

How do I update a material at runtime?

Modify the material properties directly: material.color.setHex(0xff0000). Call material.needsUpdate = true if you change map references.

Try It Yourself — PBR Material Explorer

Build a complete HTML page with a rotating sphere that lets you switch between material presets (ceramic, chrome, glass, rubber) and adjust roughness, metalness, and envMap intensity with sliders.

<!DOCTYPE html>
<html>
<head>
    <title>PBR Material Explorer</title>
    <style>
        body { margin: 0; overflow: hidden; font-family: sans-serif; }
        #ui { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.8); color: white; padding: 15px; border-radius: 8px; width: 220px; }
        #ui label { display: block; margin: 6px 0; font-size: 13px; }
        #ui input[type="range"] { width: 100%; }
        #ui select { width: 100%; padding: 4px; margin: 4px 0; }
        #presets { display: flex; gap: 4px; margin: 8px 0; }
        #presets button { flex: 1; padding: 4px; font-size: 11px; cursor: pointer; }
    </style>
</head>
<body>
<div id="ui">
    <strong>PBR Explorer</strong>
    <label>Roughness <span id="roughVal">0.2</span></label>
    <input type="range" id="roughness" min="0" max="1" step="0.01" value="0.2">
    <label>Metalness <span id="metalVal">0.0</span></label>
    <input type="range" id="metalness" min="0" max="1" step="0.01" value="0">
    <label>Env Intensity <span id="envVal">1.0</span></label>
    <input type="range" id="envIntensity" min="0" max="3" step="0.05" value="1">
    <div id="presets">
        <button onclick="setPreset(0.05, 0, 'Ceramic')">Ceramic</button>
        <button onclick="setPreset(0.1, 1, 'Chrome')">Chrome</button>
        <button onclick="setPreset(0, 0.9, 'Glass')">Glass</button>
        <button onclick="setPreset(0.9, 0, 'Rubber')">Rubber</button>
    </div>
    <div id="presetLabel" style="font-size: 12px; color: #aaa;">Custom</div>
</div>
<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
    }
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(5, 3, 8);

var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);

var controls = new OrbitControls(camera, renderer.domElement);

var ambient = new THREE.AmbientLight(0x222244, 0.5);
scene.add(ambient);
var hemi = new THREE.HemisphereLight(0x88aaff, 0x442200, 0.8);
scene.add(hemi);

var material = new THREE.MeshPhysicalMaterial({
    color: 0x88bbff,
    roughness: 0.2,
    metalness: 0,
    clearcoat: 0,
    envMapIntensity: 1.0
});

var sphere = new THREE.Mesh(new THREE.SphereGeometry(2, 64, 64), material);
sphere.position.y = 1;
scene.add(sphere);

var ground = new THREE.Mesh(
    new THREE.PlaneGeometry(10, 10),
    new THREE.MeshStandardMaterial({ color: 0x222233, roughness: 0.9, metalness: 0 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -1;
scene.add(ground);

document.getElementById("roughness").addEventListener("input", function() {
    material.roughness = parseFloat(this.value);
    document.getElementById("roughVal").textContent = this.value;
    document.getElementById("presetLabel").textContent = "Custom";
});
document.getElementById("metalness").addEventListener("input", function() {
    material.metalness = parseFloat(this.value);
    document.getElementById("metalVal").textContent = this.value;
    document.getElementById("presetLabel").textContent = "Custom";
});
document.getElementById("envIntensity").addEventListener("input", function() {
    material.envMapIntensity = parseFloat(this.value);
    document.getElementById("envVal").textContent = this.value;
});

window.setPreset = function(rough, metal, name) {
    material.roughness = rough;
    material.metalness = metal;
    document.getElementById("roughness").value = rough;
    document.getElementById("metalness").value = metal;
    document.getElementById("roughVal").textContent = rough;
    document.getElementById("metalVal").textContent = metal;
    document.getElementById("presetLabel").textContent = name;
};

function animate() {
    requestAnimationFrame(animate);
    sphere.rotation.y += 0.005;
    controls.update();
    renderer.render(scene, camera);
}
animate();

window.addEventListener("resize", function() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

What's Next

Explore GLSL shaders for complete control over rendering.

Shaders GLSL — Write vertex and fragment shaders. Shader Material — Using ShaderMaterial for custom effects.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro