API

Writing shaders for WebGPU in WGSL

Currently, most of the main shaders used by Babylon.js are written in WGSL, the only shader language that WebGPU supports. That being said, we actively maintain compatibility with GLSL.

If you decide to write a shader with GLSL, Babylon.js will detect it and use internal tools to compile it to WGSL. This takes some time and downloads a WASM library to do the conversion.

It is recommended to write your shaders directly in WGSL for faster startup time and smaller download sizes.

For some specific materials like CustomMaterial or PBRCustomMaterial, to protect backward compatibility, you will need to inject your custom shader code in GLSL.

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 a few 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 example:

import { ShaderLanguage } from "@babylonjs/core/Materials/shaderLanguage";
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
const mat = new ShaderMaterial("shader", scene, {
vertex: "myShader",
fragment: "myShader",
}, {
attributes: ["position", "uv", "normal"],
uniformBuffers: ["Scene", "Mesh"],
shaderLanguage: 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 example:

import { ShaderStore } from "@babylonjs/core/Engines/shaderStore";
ShaderStore.ShadersStoreWGSL["myShaderVertexShader"] = `
#include<sceneUboDeclaration>
#include<meshUboDeclaration>
...
`;
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:

import { ShaderLanguage } from "@babylonjs/core/Materials/shaderLanguage";
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
const mat = new ShaderMaterial("shader", scene, {
vertex: "myShader",
fragment: "myShader",
}, {
attributes: ["position", "uv", "normal"],
uniformBuffers: ["Scene", "Mesh"],
shaderLanguage: 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 compute 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 internally as separate global variables, but are defined in structures managed by the engine for you. However, this 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 through uniforms.varName, not simply varName. Variables declared this 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 WGSL syntax, not GLSL! For example: varying vUV : vec2<f32>;
  • You must NOT add the @group(X) @binding(Y) decoration! The system will add it 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 for these variables:

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

Important difference from GLSL

The normalized device coordinates (NDC) in WebGPU are slightly different from those in WebGL. In GLSL, NDC for the z-axis lies in the -1.0 ≤ z ≤ 1.0 range. In WGSL, NDC for the z-axis lies in the 0.0 ≤ z ≤ 1.0 range. If you are using NDC in your shader, please ensure you make the necessary adjustments for the z-axis. There are no differences for the xy axes.

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 baked vertex animation feature introduced in 5.0, as well as clip planes. See: Using BVA and clip planes in WGSL

WebGPU uses a special texture object for fast video playback, called an "external texture". Our VideoTexture class takes advantage of this, but if you want to do it yourself, this playground shows how to use the ShaderMaterial class to implement video playback with texture_external: Video playing with the ShaderMaterial class