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:
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.
<!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>