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?
- Sometime it is just easier to write shader code than to wire up nodes.
- Create custom shaders and use the ShaderMaterial.
- If you want to have your shader use something outside of itself to calculate the pixel value, you can pass in values as Uniforms.