Materials

The Material Base Class and Multi-Materials

Every material extends Material (alpha, backFaceCulling, wireframe, transparencyMode, freeze()), and PushMaterial adds per-mesh uniform buffers. The built-in families extend it: StandardMaterial, PBRMaterial (with its simpler siblings PBRMetallicRoughnessMaterial and PBRSpecularGlossinessMaterial), NodeMaterial (The Node Material Editor) and OpenPBRMaterial, added in 8.33, a partial implementation of Autodesk and Adobe's OpenPBR standard. ShaderMaterial takes your own GLSL or WGSL. A hundred meshes sharing one material compile it once.

A mesh has one material, but its index buffer can be split into submeshes, and a MultiMaterial gives each its own. A box's 36 indices are six faces of six, so a book can take paper on its edges:

A MultiMaterial splitting a book box into cover and page-edge submeshesJavaScript
const multi = new BABYLON.MultiMaterial('bookMulti', scene);
multi.subMaterials.push(new BABYLON.PBRMaterial('cover', scene),     // index 0
  new BABYLON.StandardMaterial('pages', scene));                     // index 1
book.subMeshes = [];                                         // drop the single default submesh
const total = book.getTotalVertices();
[0, 1, 0, 1, 1, 1].forEach((materialIndex, face) =>       // +z, -z, +x, -x, top, bottom
  new BABYLON.SubMesh(materialIndex, 0, total, face * 6, 6, book));
book.material = multi;

Each submesh is its own draw call even when neighbors share a material (SceneInstrumentation counted six for this book), so give faces that share a material one index range. Mesh.MergeMeshes with multiMultiMaterials set builds a MultiMaterial for you (Merging Meshes and Vertex Data).

A MultiMaterial giving a book PBR covers and paper page edges through six submeshesHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 330px; display: block; touch-action: none; }
  #info { max-width: 600px; padding: 4px 8px; font-family: monospace; }
</style>
<canvas id="view"></canvas>
<div id="info"></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.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('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.5, 1.15, 3.2, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
new BABYLON.DirectionalLight('key', new BABYLON.Vector3(0.6, -0.6, 1), scene).intensity = 0.9;

// Index 0: a glossy PBR cover; index 1: matte paper for the page edges
const coverMat = new BABYLON.PBRMaterial('cover', scene);
coverMat.albedoColor = BABYLON.Color3.FromHexString('#b5452f');
coverMat.metallic = 0;
coverMat.roughness = 0.35;
const pagesMat = new BABYLON.StandardMaterial('pages', scene);
pagesMat.diffuseColor = BABYLON.Color3.FromHexString('#fff3e0');
pagesMat.specularColor = BABYLON.Color3.Black();
// Stripes on the page block, drawn on a DynamicTexture
const stripes = new BABYLON.DynamicTexture('stripes', { width: 64, height: 256 }, scene);
const ctx = stripes.getContext();
for (let y = 0; y < 256; y += 4) { ctx.fillStyle = y % 8 ? '#fff8ee' : '#eadfcb'; ctx.fillRect(0, y, 64, 4); }
stripes.update();
pagesMat.diffuseTexture = stripes;

const multi = new BABYLON.MultiMaterial('bookMulti', scene);
multi.subMaterials.push(coverMat, pagesMat);

const book = BABYLON.MeshBuilder.CreateBox('book', { width: 0.3, height: 1.4, depth: 1 }, scene);
book.subMeshes = [];                                  // drop the single default submesh
const total = book.getTotalVertices();
// A box's 36 indices are six faces of six: +z, -z, +x, -x, top, bottom
[1, 1, 0, 0, 1, 1].forEach((materialIndex, face) =>
  new BABYLON.SubMesh(materialIndex, 0, total, face * 6, 6, book));
book.material = multi;
// The spine: a thin box on the -z face in the cover's material
const spine = BABYLON.MeshBuilder.CreateBox('spine', { width: 0.34, height: 1.42, depth: 0.04 }, scene);
spine.position.z = 0.51;
spine.parent = book;
spine.material = coverMat;

const instrumentation = new BABYLON.SceneInstrumentation(scene);
const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
  info.textContent = `book: ${book.subMeshes.length} submeshes; draw calls this frame: ` +
    `${instrumentation.drawCallsCounter.current} (one per submesh, plus the spine)`;
});
book.rotation.y = -0.9;                               // start with the cover and page edges in view
scene.onBeforeRenderObservable.add(() => { book.rotation.y += engine.getDeltaTime() / 4000; });
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>