Material Plugins
As of v5.0 Babylon includes a Material Plugin system, which allows customization of an existing material with custom shader code. This adds a lot of flexibility, since one can easily modify the behavior of existing materials with special effects without having to rewrite the entire shader code, in a simple and reusable way.
This is incredibly useful and powerful, since materials can be changed at runtime and have effects that were previously only possible with complex multi-pass renderings or postprocessing.
Let's start with an example: suppose you want an object to be rendered in black and white. All you want is to change the end of its shader, converting the final fragment color to grayscale. To do that you create a material plugin modifying that part of the shader code.
/*** Extend from MaterialPluginBase to create your plugin.*/class BlackAndWhitePluginMaterial extends BABYLON.MaterialPluginBase {constructor(material) {// the second parameter is the name of this plugin.// the third one is a priority, which lets you define the order multiple plugins are run. Lower numbers run first.// the fourth one is a list of defines used in the shader code.super(material, "BlackAndWhite", 200, { BLACKANDWHITE: false });// we need to mark the materialthis.markAllAsDirty = material._dirtyCallbacks[BABYLON.Constants.MATERIAL_AllDirtyFlag];// let's enable it by defaultthis._enable(true);}// Also, you should always associate a define with your plugin because the list of defines (and their values)// is what triggers a recompilation of the shader: a shader is recompiled only if a value of a define changes.prepareDefines(defines, scene, mesh) {defines["BLACKANDWHITE"] = true;}getClassName() {return "BlackAndWhitePluginMaterial";}getCustomCode(shaderType) {if (shaderType === "fragment") {// we're adding this specific code at the end of the main() functionreturn {CUSTOM_FRAGMENT_MAIN_END: `float luma = gl_FragColor.r*0.299 + gl_FragColor.g*0.587 + gl_FragColor.b*0.114;gl_FragColor = vec4(luma, luma, luma, 1.0);`,};}// for other shader types we're not doing anything, return nullreturn null;}}
To actually use this plugin, you need to register it with a factory function. This factory can also associate the plugin instance to the material, so we can easily access it later.
BABYLON.RegisterMaterialPlugin("BlackAndWhite", (material) => {material.blackandwhite = new BlackAndWhitePluginMaterial(material);return material.blackandwhite;});
You can see the final code in action in the PlayGround: Basic material plugin example
More complex plugins
Sometimes your plugins will need to get uniforms. This is also possible with the plugins, which can register defines and uniforms.
Let's take a look at a more involved example, which is not enabled by default but has proper enable/disable controls as well.
class ColorifyPluginMaterial extends BABYLON.MaterialPluginBase {// any local variable definitions of the plugin instance.color = new BABYLON.Color3(1.0, 0.0, 0.0);// we can add an enabled flag to be able to toggle the plugin on and off.get isEnabled() {return this._isEnabled;}set isEnabled(enabled) {if (this._isEnabled === enabled) {return;}this._isEnabled = enabled;// when it's changed, we need to mark the material as dirty so the shader is rebuilt.this.markAllAsDirty();this._enable(this._isEnabled);}_isEnabled = false;constructor(material) {// the fourth parameter is a list of #defines in the GLSL codesuper(material, "Colorify", 200, { COLORIFY: false });this._varColorName = material instanceof BABYLON.PBRBaseMaterial ? "finalColor" : "color";// get our dirty callback from the material, so we can properly invalidate it for rebuilding.this.markAllAsDirty = material._dirtyCallbacks[BABYLON.Constants.MATERIAL_AllDirtyFlag];}// we use the define to enable or disable the plugin.prepareDefines(defines, scene, mesh) {defines.COLORIFY = this._isEnabled;}// here we can define any uniforms to be passed to the shader code.getUniforms() {return {// first, define the UBO with the correct type and size.ubo: [{ name: "myColor", size: 3, type: "vec3" }],// now, on the fragment shader, add the uniform itself.fragment: `#ifdef COLORIFYuniform vec3 myColor;#endif`,};}// whenever a material is bound to a mesh, we need to update the uniforms.// so bind our uniform variable to the actual color we have in the instance.bindForSubMesh(uniformBuffer, scene, engine, subMesh) {if (this._isEnabled) {uniformBuffer.updateColor3("myColor", this.color);}}getClassName() {return "ColorifyPluginMaterial";}getCustomCode(shaderType) {return shaderType === "vertex"? null: {// this is just like before. Multiply the final shader color by// our color. Note that we have access to all shader variables:// we're effectively inserting a piece of code in the shader code.CUSTOM_FRAGMENT_BEFORE_FRAGCOLOR: `#ifdef COLORIFY${this._varColorName}.rgb *= myColor;#endif`,// we can even use regexes to replace arbitrary parts of the code.// if your key starts with '!' it's parsed as a Regex."!diffuseBase\\+=info\\.diffuse\\*shadow;": `diffuseBase += info.diffuse*shadow;diffuseBase += vec3(0., 0.2, 0.8);`,};}}
Applying a plugin to a single material
Even though your plugins can be enabled/disabled as shown above, sometimes you need to apply a plugin to a single material. In this case, instead of calling RegisterMaterialPlugin
, you can instantiate the plugin directly with the material that will be modified:
const myPlugin = new BlackAndWhitePluginMaterial(material);
This is also useful for dynamic loading of plugins.
Material plugin applied to a single materialCaveats
- The code insertion point names are standard over all materials, but not all of them have all insertion points. In general you can expect to have
CUSTOM_VERTEX_DEFINITIONS
,CUSTOM_VERTEX_MAIN_BEGIN
andCUSTOM_VERTEX_MAIN_END
in vertex shaders, andCUSTOM_FRAGMENT_DEFINITIONS
,CUSTOM_FRAGMENT_MAIN_BEGIN
andCUSTOM_FRAGMENT_MAIN_END
in fragment shaders. But other#defines
might not be present. - There's no guarantee that using the shader point names / the regular expressions to update code / the variable names with a material plugin will work across Babylon.js versions. We reserve the possibility to update our shader code in a way that would break backward compatibility with those features if we have to. In particular, the color variable name for
standard.fragment.fx
iscolor
, while forpbrMaterial.fragment.fx
it'sfinalColor
. If you are writing a plugin targeting multiple materials take care in your code to use different variable names according to the plugin and keep track of Babylon potentially changing (and breaking!) things. RegisterMaterialPlugin
only adds the plugin to material instantiated AFTER the registration. So it must be run before you add any meshes or create your materials or they won't have the plugin.- You can register multiple plugins to the same material (or the entire scene). The
priority
field controls the order the plugins will be executed if they are all enabled.
Material Plugin Examples
Here are some other examples of plugins:
Using a class variable to animate a parameter for all instancesPower plant with volumetric fogGrain (solves banding issues)You can also take a look at Babylon's source code. The PBR material includes several complex plugins, such as the Anisotropic plugin, sheen and subsurface, and the detail map plugin applies to PBR and Standard materials.