Passing Uniforms to your custom Babylon.js shader.

I went down the shader rabbit hole recently. It all began when I started playing around with the Node Material Editor, after watching some videos on the Babylon.js youtube channel.

I tried to re-create some basic shading effects described in The Book of Shaders and had achieved some decent results. By doing that work, I realized that it is often quicker and easier to write shader code directly.

So, how do you create custom shaders in Babylon.js? You do it by either implementing <script type="application/fragmentShader"> tags or using the Babylon.Effect.ShaderStore and assigning the shader to a ShaderMaterial.

By default Babylon.js passes the following Uniforms to your shader: "world", "worldView", "worldViewProjection", "view", "projection". I discovered that the canvas's resolution, often available as resolution or u_resolution in other libraries or utilities, was not available as a Uniform. In the Node Material Editor, a node called 'ScreenSize' provides the same information, but that variable didn't work either.

See the comments in this snippet of code.

    //Why is there no Screen Size / resolution / u_resolution
    BABYLON.Effect.ShadersStore["customFragmentShader"]=`
        
        // This doesn't exist
        // uniform vec2 resolution;
        
        // Neither does this
        // uniform vec2 u_resolution;
        
        // ScreenSize exists in NME, but not here... by default
        uniform vec2 screenSize;
        
        void main() {
            vec2 st = gl_FragCoord.xy/screenSize;
            vec3 color = vec3(st.x);
            gl_FragColor = vec4(color,1.0);
        }`

Passing variables and Uniforms to your custom fragment shader

The solution is for you to pass your own values as Uniforms.

First, create a new instance of ShaderMaterial. Then you can use the ShaderMaterial setter methods to pass the type of value you want into the shader. In my case, I wanted to send in a vec2 so, I used the .setVector2 method.

    var shaderMaterial = new BABYLON.ShaderMaterial("shader", scene,{
        vertex: "custom",
        fragment: "custom",
	    },
    );
    
    var engine = scene.getEngine();

    // Use a setter
    shaderMaterial.setVector2(
      // This name becomes the Uniform name you can reference in the shader.
      "screenSize", 
      // This is the value getting passed in.
      new BABYLON.Vector2(
            engine.getRenderWidth(),
            engine.getRenderHeight()
          )
    )
    
    // Finally apply this material to a mesh and see the results.
    var ground = new BABYLON.MeshBuilder.CreateGround("ground", {"width":10, "height":10}, scene);
	  ground.material = shaderMaterial;

What's the takeaway?

  1. Sometime it is just easier to write shader code than to wire up nodes.
  2. Create custom shaders and use the ShaderMaterial.
  3. If you want to have your shader use something outside of itself to calculate the pixel value, you can pass in values as Uniforms.