Skip to content

BabylonJS - ShaderMaterial

ShaderMaterial allows you to define a mesh’s appearance using custom shader programs written in GLSL (OpenGL Shading Language). This gives you maximum control over the rendering process, enabling unique visual effects beyond standard materials.

It acts as a bridge, passing data (like mesh vertices, normals, UVs, and custom uniforms) from your JavaScript code to the Vertex and Fragment shaders running on the GPU.

Note: Writing GLSL shaders requires understanding GPU rendering concepts. For many effects, the NodeMaterial editor provides a powerful visual alternative without direct GLSL coding.

You create an instance of BABYLON.ShaderMaterial:

const myShaderMaterial = new BABYLON.ShaderMaterial(
"myShader", // Unique name for the material
scene, // The Babylon scene
{
vertex: "customVertexShaderName", // Identifier for vertex shader code
fragment: "customFragmentShaderName" // Identifier for fragment shader code
// Or use vertexSource/fragmentSource for direct strings
// Or vertexElement/fragmentElement for script tag IDs
// Or provide a base path string like "./shaders/myShader"
// (loads myShader.vertex.glsl and myShader.fragment.glsl)
},
{
attributes: ["position", "normal", "uv"], // Standard attributes Babylon provides
uniforms: ["worldViewProjection", "time", "customColor"], // Uniforms expected by shaders
samplers: ["textureSampler"] // Texture samplers expected by shaders
}
);
  • Name: A unique string identifying this material.
  • Scene: The BabylonJS scene instance.
  • Shader Path/Object: Specifies where to find the GLSL code. Several options:
    • { vertex: "name", fragment: "name" }: Uses shaders stored in BABYLON.Effect.ShadersStore["nameVertexShader"] and BABYLON.Effect.ShadersStore["nameFragmentShader"].
    • { vertexSource: "glsl code string", fragmentSource: "glsl code string" }: Embeds GLSL code directly as strings.
    • { vertexElement: "id", fragmentElement: "id" }: Uses GLSL code embedded within <script> tags in your HTML, identified by their id.
    • "./path/shaderName": (Recommended for organization) Loads shaders from external files named shaderName.vertex.glsl and shaderName.fragment.glsl located relative to your HTML.
  • Options Object: Declares the data your shaders expect:
    • attributes: An array of strings listing the per-vertex attributes required (e.g., position, normal, uv, color). BabylonJS automatically provides standard ones if the mesh has them.
    • uniforms: An array of strings listing the uniform variables required. Uniforms are constant across all vertices/fragments for a single draw call (e.g., transformation matrices like worldViewProjection, time, colors, custom values). BabylonJS provides standard matrices automatically.
    • samplers: An array of strings listing the texture samplers required (e.g., textureSampler).

You’ll need two shader programs:

  1. Vertex Shader: Runs once per vertex. Its primary job is to calculate the final screen position (gl_Position) of the vertex. It can also pass data (like UV coordinates or calculated lighting values) to the fragment shader via varying variables.
  2. Fragment (Pixel) Shader: Runs once per pixel covered by the mesh. Its primary job is to calculate the final color (gl_FragColor) of that pixel, often using data passed from the vertex shader and texture lookups.

Common GLSL variable types:

  • float: Single-precision floating-point number.
  • vec2, vec3, vec4: Vectors of 2, 3, or 4 floats (used for positions, colors, UVs).
  • mat3, mat4: 3x3 or 4x4 matrices (used for transformations).
  • sampler2D: Represents a 2D texture.
  • attribute: Input data per vertex (from JavaScript via attributes option).
  • uniform: Input data constant per draw call (from JavaScript via uniforms option).
  • varying: Used to pass data from the vertex shader to the fragment shader.
  • gl_Position (Vertex Shader Output): The final clip-space position of the vertex.
  • gl_FragColor (Fragment Shader Output): The final color of the pixel.

GLSL shaders typically start with precision highp float; to specify the default precision for floating-point calculations.

Let’s create a simple material that just applies a texture to a mesh using ShaderMaterial. We’ll store the GLSL code in the ShadersStore for this example.

import * as BABYLON from '@babylonjs/core';
const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas, true);
const createScene = async function() {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 5, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
// --- GLSL Shader Code ---
// Vertex Shader: Pass position and UVs through
BABYLON.Effect.ShadersStore["customVertexShader"] = `
precision highp float;
// Attributes (provided by Babylon based on mesh data)
attribute vec3 position;
attribute vec2 uv;
// Uniforms (provided by Babylon)
uniform mat4 worldViewProjection;
// Varying (data to pass to fragment shader)
varying vec2 vUV;
void main(void) {
// Calculate final screen position
gl_Position = worldViewProjection * vec4(position, 1.0);
// Pass UV coordinates to the fragment shader
vUV = uv;
}
`;
// Fragment Shader: Sample texture using UVs
BABYLON.Effect.ShadersStore["customFragmentShader"] = `
precision highp float;
// Varying (received from vertex shader)
varying vec2 vUV;
// Uniforms (texture sampler we'll set from JS)
uniform sampler2D textureSampler;
void main(void) {
// Look up color in the texture at the interpolated UV coordinate
gl_FragColor = texture2D(textureSampler, vUV);
}
`;
// --- Create ShaderMaterial Instance ---
const shaderMaterial = new BABYLON.ShaderMaterial("simpleTextureShader", scene,
{
vertex: "custom", // Corresponds to ShadersStore entry name
fragment: "custom"
},
{
attributes: ["position", "uv"], // We need position and UVs from the mesh
uniforms: ["worldViewProjection"], // Babylon provides this matrix
samplers: ["textureSampler"] // Declare the sampler uniform we'll use
}
);
// --- Set Uniforms ---
// Load the texture
const mainTexture = new BABYLON.Texture("path/to/your/texture.jpg", scene);
// Set the texture sampler uniform
shaderMaterial.setTexture("textureSampler", mainTexture);
// Optional: Disable backface culling if needed
shaderMaterial.backFaceCulling = false;
// --- Apply to Mesh ---
const plane = BABYLON.MeshBuilder.CreatePlane("plane", {size: 2}, scene);
plane.material = shaderMaterial;
return scene;
};
createScene().then(scene => {
engine.runRenderLoop(function() { if (scene) { scene.render(); } });
});
window.addEventListener('resize', function() { engine.resize(); });
  1. GLSL Code: We define basic vertex and fragment shaders. The vertex shader transforms the position using the worldViewProjection matrix (provided by BabylonJS) and passes the uv coordinates to the fragment shader via the vUV varying.
  2. Fragment Shader: It receives the interpolated vUV and uses the built-in texture2D function to look up the color from the textureSampler at those coordinates, setting the result to gl_FragColor.
  3. ShaderMaterial Creation: We instantiate ShaderMaterial, referencing the shaders stored in ShadersStore via the "custom" identifier. We declare the necessary attributes (position, uv), uniforms (worldViewProjection), and samplers (textureSampler).
  4. Setting Uniforms: We create a BABYLON.Texture and use shaderMaterial.setTexture("textureSampler", mainTexture) to bind it to the uniform sampler2D textureSampler variable in the fragment shader.
  5. Application: We assign the created shaderMaterial to the material property of a plane mesh.

You can pass your own data (like time for animations, colors, threshold values) to shaders using uniforms.

// --- Inside createScene ---
// Modify Fragment Shader to use a custom color
BABYLON.Effect.ShadersStore["customColorFragmentShader"] = `
precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
uniform vec4 customColor; // Added uniform
uniform float time; // Added uniform
void main(void) {
vec4 textureColor = texture2D(textureSampler, vUV);
// Modulate texture color by customColor and add a time-based effect
gl_FragColor = textureColor * customColor * (0.75 + sin(time) * 0.25);
}
`;
// Create ShaderMaterial, adding new uniforms
const customShaderMaterial = new BABYLON.ShaderMaterial("customColorShader", scene,
{ vertex: "custom", fragment: "customColor" }, // Use the new fragment shader
{
attributes: ["position", "uv"],
uniforms: ["worldViewProjection", "customColor", "time"], // Added uniforms
samplers: ["textureSampler"]
}
);
// Set the texture
const myTexture = new BABYLON.Texture("path/to/your/texture.jpg", scene);
customShaderMaterial.setTexture("textureSampler", myTexture);
// Set initial custom uniform values
const colorToSend = new BABYLON.Color4(0.5, 1.0, 0.5, 1.0); // Greenish tint
customShaderMaterial.setColor4("customColor", colorToSend);
customShaderMaterial.setFloat("time", 0);
// Apply to a mesh
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 2}, scene);
sphere.material = customShaderMaterial;
// Animate the 'time' uniform in the render loop
let time = 0;
scene.onBeforeRenderObservable.add(() => {
time += engine.getDeltaTime() / 1000.0; // Time in seconds
customShaderMaterial.setFloat("time", time);
});

Here, we added uniform vec4 customColor and uniform float time to the fragment shader. In JavaScript, we declare these in the uniforms array during ShaderMaterial creation. We then use methods like setColor4("uniformName", value) and setFloat("uniformName", value) to pass the data. Other methods include setVector3, setMatrix, etc. Updating the time uniform in the render loop creates an animation effect within the shader.

ShaderMaterial is a powerful tool for advanced rendering. Start with simple examples and gradually increase complexity. Always check the browser’s developer console for GLSL compilation errors if your shader doesn’t work as expected.