Some surfaces need a second lobe. PBRMaterial groups these as sub-configurations, each off until you set isEnabled, so unused ones cost no shader code. In the sweep's third row, clearCoat lays sharp studio reflections over rough paint (car paint, a laminated cover), sheen gives the middle sphere cloth's wide, grazing-angle glow (meant for rough dielectrics), and anisotropy stretches highlights along the surface tangent, smearing the right sphere's softboxes into brushed-metal streaks:
paint.clearCoat.isEnabled = true;
paint.clearCoat.roughness = 0.05; // default 0: a mirror-smooth coat
cloth.sheen.isEnabled = true;
cloth.sheen.color = new BABYLON.Color3(0.9, 0.85, 1); // tint the rim
brushed.anisotropy.isEnabled = true;
brushed.anisotropy.direction = new BABYLON.Vector2(0, 1); // along the tangent's vThe glTF loader maps KHR_materials_clearcoat, _sheen and _anisotropy onto them. Anisotropy needs a sound tangent space: tessellated geometry with UVs, or exported tangents. subSurface (refraction, translucency, scattering) and iridescence work the same way; each layer adds uniforms and texture fetches.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; }
canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#e9e4dcff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.35, 6, new BABYLON.Vector3(0, 0.1, 0), scene);
camera.attachControl(canvas, true);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
new BABYLON.DirectionalLight('key', new BABYLON.Vector3(0.5, -0.7, 0.6), scene).intensity = 1.6;
// A procedural studio for image-based lighting: a dim dome with three bright softboxes,
// rendered once into a ReflectionProbe's cube texture. (The sandboxed preview cannot download
// an .env or .hdr file, so the environment is built in code.)
function createStudioEnvironment(scene) {
const probe = new BABYLON.ReflectionProbe('studio', 256, scene);
const dome = BABYLON.MeshBuilder.CreateSphere('dome', { diameter: 60, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
dome.material = new BABYLON.StandardMaterial('domeMat', scene);
dome.material.emissiveColor = BABYLON.Color3.FromHexString('#6a6158');
dome.material.disableLighting = true;
const studio = [dome];
[[-7, 5, -7], [8, 3, -5], [0, 9, 5]].forEach(([x, y, z], i) => {
const box = BABYLON.MeshBuilder.CreatePlane('softbox' + i, { width: 9, height: 4 }, scene);
box.position.set(x, y, z);
box.lookAt(BABYLON.Vector3.Zero());
box.rotation.y += Math.PI; // turn the lit face toward the center
box.material = new BABYLON.StandardMaterial('panel' + i, scene);
box.material.emissiveColor = new BABYLON.Color3(3, 3, 3); // brighter than white
box.material.disableLighting = true;
studio.push(box);
});
// The studio is visible only while the probe renders, so the page keeps its light background
studio.forEach((mesh) => { mesh.isVisible = false; probe.renderList.push(mesh); });
probe.cubeTexture.onBeforeRenderObservable.add(() => studio.forEach((m) => { m.isVisible = true; }));
probe.cubeTexture.onAfterRenderObservable.add(() => studio.forEach((m) => { m.isVisible = false; }));
probe.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
scene.environmentTexture = probe.cubeTexture;
return probe;
}
createStudioEnvironment(scene);
const makePBR = (name, hex, roughness, metallic) => {
const m = new BABYLON.PBRMaterial(name, scene);
m.albedoColor = BABYLON.Color3.FromHexString(hex);
Object.assign(m, { roughness, metallic });
return m;
};
// Rough red paint with a mirror-smooth clear coat on top, like a laminated cover
const paint = makePBR('paint', '#b5452f', 0.6, 0);
paint.clearCoat.isEnabled = true;
paint.clearCoat.roughness = 0.05;
// Rough purple cloth with a pale sheen at grazing angles
const cloth = makePBR('cloth', '#5b3f99', 0.9, 0);
cloth.sheen.isEnabled = true;
cloth.sheen.color = new BABYLON.Color3(0.9, 0.85, 1);
cloth.sheen.intensity = 1;
// Brushed metal: highlights stretched along the tangent
const brushed = makePBR('brushed', '#c9c3b8', 0.35, 1);
brushed.anisotropy.isEnabled = true;
brushed.anisotropy.intensity = 1;
brushed.anisotropy.direction = new BABYLON.Vector2(0, 1); // along the tangent's v
[[paint, 'clearCoat'], [cloth, 'sheen'], [brushed, 'anisotropy']].forEach(([material, text], i) => {
// tessellated spheres have the UVs anisotropy needs for its tangent space
const ball = BABYLON.MeshBuilder.CreateSphere(text, { diameter: 1.5, segments: 48 }, scene);
ball.position.x = (i - 1) * 1.8;
ball.material = material;
const label = new BABYLON.GUI.TextBlock('', `${material.name}.${text}`);
Object.assign(label, { color: '#333', fontSize: 13, resizeToFit: true });
ui.addControl(label);
label.linkWithMesh(ball);
label.linkOffsetY = 80;
});
scene.onBeforeRenderObservable.add(() => {
scene.meshes.forEach((m) => { if (m.material instanceof BABYLON.PBRMaterial) m.rotation.y += 0.004; });
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>