Writing shaders for WebGPU in WGSL

Currently, all shaders used by Babylon.js are written in GLSL and are converted to WGSL (the only shader language that WebGPU knows about) by some special tools.

So, even in WebGPU, if you use a CustomMaterial or a PBRCustomMaterial to inject some custom shader code, you must write it in GLSL.

If you want to write shader code in the WGSL language, you can either write a compute shader or use the ShaderMaterial class to wrap a vertex/fragment shader. The latter is the subject of this page.

Using ShaderMaterial to write WGSL code

You can use the ShaderMaterial class to write WGSL code in much the same way you use it to write GLSL but with some small differences.

Note: if you use the "color" attribute in your shader code, don't add it to the attributes property passed to the ShaderMaterial constructor! This attribute will be automatically added if a vertex buffer named "color" is attached to the mesh. If you add "color" to the attributes array, you will get an error like "Attribute shader location (1) is used more than once".

Setting the right shader language

You must set the shaderLanguage property to BABYLON.ShaderLanguage.WGSL in the options parameter you pass to the constructor. For eg:

const mat = new BABYLON.ShaderMaterial("shader", scene, {
vertex: "myShader",
fragment: "myShader",
},
{
attributes: ["position", "uv", "normal"],
uniformBuffers: ["Scene", "Mesh"],
shaderLanguage: BABYLON.ShaderLanguage.WGSL,
}
);

Using the WGSL shader store

If using the shader store, you must put your WGSL code in BABYLON.ShaderStore.ShadersStoreWGSL instead of BABYLON.ShaderStore.ShadersStore.

For eg:

BABYLON.ShaderStore.ShadersStoreWGSL["myShaderVertexShader"]=`
#include<sceneUboDeclaration>
#include<meshUboDeclaration>
...
`;
BABYLON.ShaderStore.ShadersStoreWGSL["myShaderFragmentShader"]=`
varying vPositionW : vec3<f32>;
varying vUV : vec2<f32>;
...
`;

Declaration of the entry points

You must also declare the entry point for the vertex and fragment shader in a special way.

Vertex:

@vertex
fn main(input : VertexInputs) -> FragmentInputs {
...
}

Fragment:

@fragment
fn main(input : FragmentInputs) -> FragmentOutputs {
...
}

Using pre-defined uniforms

To use the pre-defined uniforms of the scene (view, viewProjection, projection, vEyePosition) and mesh (world, visibility), you must include the appropriate file(s) in the shader code:

#include<sceneUboDeclaration>
#include<meshUboDeclaration>

and add the uniform buffer name(s) to the uniformBuffers option of the ShaderMaterial class constructor:

const mat = new BABYLON.ShaderMaterial("shader", scene, {
vertex: "myShader",
fragment: "myShader",
},
{
attributes: ["position", "uv", "normal"],
uniformBuffers: ["Scene", "Mesh"],
shaderLanguage: BABYLON.ShaderLanguage.WGSL,
}
);

In the WGSL code, you access a uniform by prefixing its name by scene. or mesh. for the scene or mesh uniforms, respectively:

@vertex
fn main(input : VertexInputs) -> FragmentInputs {
vertexOutputs.position = scene.viewProjection * mesh.world * vec4<f32>(vertexInputs.position, 1.0);
}

Special syntax used in WGSL code

Unlike computational shaders that use ordinary WGSL code, the shader code you write for ShaderMaterial must use special syntax to work with the existing workflow. To make it easier for developers, the declaration of variables is the same as that used in GLSL:

  • declaring a varying variable:
varying varName : varType;
  • declaring an attribute variable:
attribute varName : varType;
  • declaring a uniform variable:
uniform varName : varType;

Contrary to GLSL, the inputs and outputs of the vertex/fragment shader are not declared as separate global variables internally, but are defined in some structures which are managed by the engine for you. However, it means that you need a special syntax to access these variables. Here is the mapping between the GLSL syntax and the WGSL syntax:

  • In the vertex shader:
    • an attribute must be referenced by vertexInputs.attributeName
    • gl_VertexID => vertexInputs.vertexIndex
    • gl_InstanceID => vertexInputs.instanceIndex
    • a varying must be referenced by vertexOutputs.varName
    • gl_Position => vertexOutputs.position
  • In the fragment shader:
    • a varying must be referenced by fragmentInputs.varName
    • gl_FragCoord => fragmentInputs.position
    • gl_FrontFacing => fragmentInputs.frontFacing
    • gl_FragColor => fragmentOutputs.color
    • gl_FragDepth => fragmentOutputs.fragDepth

Notes:

  • When using the uniform varName : varType syntax, you access the variable by doing uniforms.varName, not simply varName. The variables declared that way can be set from the javascript code by using the regular methods of the ShaderMaterial class (setFloat, setInt, etc) as with GLSL
  • varType must use a WGSL syntax, not GLSL! For eg: varying vUV : vec2<f32>;
  • you must NOT add the @group(X) @binding(Y) decoration! The system will add them automatically

Using new objects available in WGSL

You can use the standard WGSL syntax to declare:

  • custom uniform buffers:
struct MyUBO {
scale: f32,
};
var<uniform> myUBO: MyUBO;
  • storage textures:
var storageTexture : texture_storage_2d<rgba8unorm,write>;
  • storage buffers:
struct Buffer {
items: array<f32>,
};
var<storage,read_write> storageBuffer : Buffer;
  • external textures:
var videoTexture : texture_external;

Again, you must NOT add the @group(X) @binding(Y) decoration! The system will add them automatically.

On the javascript side, you have the corresponding methods to set a value to these variables:

  • uniform buffer: setUniformBuffer(name, buffer)
  • storage texture: same method than for regular textures (setTexture(name, texture))
  • storage buffer: setStorageBuffer(name, buffer)
  • external texture: setExternalTexture(name, buffer)

Examples

This playground is a basic example of using WGSL in a ShaderMaterial: Basic example of WGSL with ShaderMaterial

As when using GLSL, ShaderMaterial supports morphs, bones and instancing in WGSL. You will need to add the appropriate includes in your code to support these features. See how it is done in this playground (this example also demonstrates how to use a storage texture and a storage buffer): Advanced usage of the ShaderMaterial class

You can also use the new in 5.0 baked vertex animation feature as well as clip planes. See: Using BVA and clip planes in WGSL

Playing videos with the regular VideoTexture is slow in WebGPU because there are a lot of texture copies that occur behind the scene in the browser. The texture_external type object is meant for fast video playing in WebGPU. This playground shows how to use the ShaderMaterial class to implement video playing with texture_external: Video playing with the ShaderMaterial class