The Material Plugin System

Since version 5.0, a material plugin injects your own shader code into StandardMaterial or PBRMaterial (and their subclasses) at named hook points, so you keep lighting, shadows, fog and fallbacks instead of rewriting them in a ShaderMaterial. A plugin extends MaterialPluginBase, declares a #define (changing a define is what triggers a recompile) and returns code for hooks such as CUSTOM_FRAGMENT_MAIN_END, in GLSL for Engine or WGSL for WebGPUEngine. BookNest grays out books that are out of stock:

bookstore.js: a plugin that grays out a sold-out book in GLSL or WGSLJavaScript
const GRAY = {                                      // Section 7.5.7: luma, 35% toward white
  GLSL: `{ float luma = dot(gl_FragColor.rgb, vec3(0.299, 0.587, 0.114));
    gl_FragColor.rgb = mix(vec3(luma), vec3(0.93), 0.35); }`,
  WGSL: `{ let luma = dot(fragmentOutputs.color.rgb, vec3f(0.299, 0.587, 0.114));
    fragmentOutputs.color = vec4f(mix(vec3f(luma), vec3f(0.93), 0.35),
      fragmentOutputs.color.a); }`,
};
class SoldOutPlugin extends BABYLON.MaterialPluginBase {  // Section 7.5.7: out of stock
  constructor(material) {
    super(material, 'SoldOut', 200, { SOLD_OUT: false });  // name, priority, defines
    this._enable(true);                                    // active from the start
  }
  prepareDefines(defines) { defines.SOLD_OUT = true; }     // a define change recompiles
  getClassName() { return 'SoldOutPlugin'; }
  isCompatible() { return true; }                          // both languages below
  getCustomCode(shaderType, language) {
    if (shaderType !== 'fragment') return null;
    const code = language === BABYLON.ShaderLanguage.WGSL ? GRAY.WGSL : GRAY.GLSL;
    return { CUSTOM_FRAGMENT_MAIN_END: `#ifdef SOLD_OUT\n${code}\n#endif` };
  }
}

new BookNestStore.SoldOutPlugin(material) attaches it to one material (Cover Textures's figure shows it on a PBR cover); BABYLON.RegisterMaterialPlugin(name, factory) attaches a plugin to every later material. Plugins can also declare uniforms (getUniforms), samplers and attributes. The braces keep luma from colliding with the host shader's variables.

A material plugin that grays out the one out-of-stock book, injected into StandardMaterialHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; }
  canvas { width: 100%; max-width: 600px; height: 330px; 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 GRAY = {                                      // luma, 35% toward white
  GLSL: `{ float luma = dot(gl_FragColor.rgb, vec3(0.299, 0.587, 0.114));
    gl_FragColor.rgb = mix(vec3(luma), vec3(0.93), 0.35); }`,
  WGSL: `{ let luma = dot(fragmentOutputs.color.rgb, vec3f(0.299, 0.587, 0.114));
    fragmentOutputs.color = vec4f(mix(vec3f(luma), vec3f(0.93), 0.35),
      fragmentOutputs.color.a); }`,
};
class SoldOutPlugin extends BABYLON.MaterialPluginBase {
  constructor(material) {
    super(material, 'SoldOut', 200, { SOLD_OUT: false });  // name, priority, defines
    this._enable(true);                                    // active from the start
  }
  prepareDefines(defines) { defines.SOLD_OUT = true; }     // a define change recompiles
  getClassName() { return 'SoldOutPlugin'; }
  isCompatible() { return true; }                          // both shader languages below
  getCustomCode(shaderType, language) {
    if (shaderType !== 'fragment') return null;
    const code = language === BABYLON.ShaderLanguage.WGSL ? GRAY.WGSL : GRAY.GLSL;
    // The braces keep luma from colliding with the host shader's variables
    return { CUSTOM_FRAGMENT_MAIN_END: `#ifdef SOLD_OUT\n${code}\n#endif` };
  }
}

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.2, 1.3, 5.2, new BABYLON.Vector3(0, 0.9, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);
new BABYLON.PointLight('lamp', new BABYLON.Vector3(0, 3, -2), scene).intensity = 0.5;
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');

const CATALOG = [['The Quiet Harbor', '#1f5f8b', true], ['Patterns of the Deep Web', '#5b3f99', true],
  ['Salt and Saffron', '#e09a10', false], ['Small Steps to Big Summits', '#3f7d3a', true],
  ["The Clockmaker's Paradox", '#b5452f', true], ['Gardens in Glass', '#2a9d8f', true]];
const shelf = BABYLON.MeshBuilder.CreateBox('shelf', { width: 4.2, height: 0.08, depth: 0.6 }, scene);
shelf.material = new BABYLON.StandardMaterial('walnut', scene);
shelf.material.diffuseColor = BABYLON.Color3.FromHexString('#6d4c33');
CATALOG.forEach(([title, hex, inStock], i) => {
  const book = BABYLON.MeshBuilder.CreateBox(title, { width: 0.55, height: 1.5, depth: 0.14 }, scene);
  book.position.set((i - 2.5) * 0.66, 0.79, 0);
  book.rotation.y = 0.25;
  const material = new BABYLON.StandardMaterial(title, scene);
  material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  material.specularColor = new BABYLON.Color3(0.3, 0.3, 0.3);
  book.material = material;
  if (!inStock) {
    new SoldOutPlugin(material);                    // keeps lighting, only regrades the result
    const tag = new BABYLON.GUI.TextBlock('', 'sold out');
    Object.assign(tag, { color: '#b5452f', fontSize: 14, fontWeight: 'bold', resizeToFit: true });
    ui.addControl(tag);
    tag.linkWithMesh(book);
    tag.linkOffsetY = -118;
  }
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>