Skip to content

Writing shaders

Due to the need for cross-graphic API compatibility, we dynamically generate GLSL or WGSL code based on the device type using native JavaScript.

Here is an example of generating a Shader:

javascript

// Creating shader via javascript
const program = device.buildRenderProgram({
  vertex(pb) {
    this.$inputs.pos = pb.vec3().attrib('position');
    this.$inputs.uv = pb.vec2().attrib('texCoord0');
    this.mvpMatrix = pb.mat4().uniform(0);
    pb.main(function(){
      this.$builtins.position = pb.mul(this.mvpMatrix, pb.vec4(this.$inputs.pos, 1));
      this.$outputs.outUV = this.$inputs.uv;
    });
  },
  fragment(pb) {
    this.$outputs.color = pb.vec4();
    this.tex = pb.tex2D().uniform(0);
    this.factor = pb.vec4().uniform(0);
    pb.main(function(){
      this.sampleColor = pb.textureSample(this.tex, this.$inputs.outUV);
      this.$outputs.color = pb.mul(this.sampleColor, this.factor);
    });
  }
});

// The generated vertex shader code
console.log(program.getShaderSource('vertex'));
// The generated fragment shader code
console.log(program.getShaderSource('fragment'));

We use the method buildRenderProgram() to create shaders for rendering. The parameters vertex() and fragment() are the generation functions for vertex shader and fragment shader, respectively.

The two generation functions accept a parameter pb (ProgramBuilder), which is the main interface object for generating Shaders. It provides definitions for data types in Shaders, process control, as well as definitions for built-in variables and functions within Shaders. The generation functions cannot be arrow functions, as their implicit 'this' refers to the global scope of the Shader.

Scope

In the context of shader generation functions, the concept of scope is similar to that in shader code. Within a scope, you can define constants and variables. In the global scope, you can define the inputs and outputs of the shader, as well as uniform constants. Within the function scope, you can perform branching and looping, which are also implemented through generation functions and have their own scopes. When accessing variables or constants within a scope, the system will search upwards along the scope chain.

Entry Function

The global scope calls pb.main() to create the entry function for the Shader, with the parameter being a generator function (where 'this' points to the scope).

javascript

const program = device.buildRenderProgram({
  vertex(pb){
    // THIS here refers to the global scope

    // VertexShader entry point
    pb.main(function(){
      // THIS here refers to the function scope
    });
  },
  fragment(pb){
    // FragmentShader entry point
    pb.main(function(){
    });
  }
});

Variables

Global scope allows for the definition of global variables, while other scopes can define local variables within blocks.

javascript

vertex(pb) {
  // Declare a global variable of type vec4
  this.x = pb.vec4();
  // Declare and initialize global variables
  this.y = pb.vec4(1, 2, 3, 4);
}

The above code generates GLSL:

vec4 x;
vec4 y = vec4(1.0,2.0,3.0,4.0);

The above code generates WGSL:

var<private> x: vec4<f32>;
var<private> y: vec4<f32> = vec4<f32>(1.0,2.0,3.0,4.0);

The code below defines a local variable within the function scope.

javascript

vertex(pb) {
  pb.main(function(){
    // THIS is the scope of the entry function
    this.x = pb.vec4(1);
  });
}

If there is a variable with the same name in the parent scope, the statement becomes an assignment to the variable with the same name in the parent scope, as shown in the following example:

javascript

vertex(pb) {  
  this.x = pb.vec4();
  pb.main(function(){
    // Variable x already exists in the parent scope, and the following is the assignment statement
    this.x = pb.vec4(1);
  });
}

Generated GLSL:

vec4 a;
void main() {
  a = vec4(1.0);
}

If we indeed need to declare local variables, regardless of whether there are variables with the same name in the parent scope, we need to explicitly specify the current scope, as shown in the following example:

javascript

vertex(pb) {  
  this.x = pb.vec4();
  pb.main(function(){
    // Call $l to get the current scope
    this.$l.x = pb.vec4(1);
    // At this point, the variable x already exists in the current scope, which becomes the assignment
    this.$l.x = pb.vec4(2);
  });
}

Generated GLSL:

vec4 a;
void main() {
  vec4 a = vec4(1.0);
  a = vec4(2.0);
}

We support the following Shader data types

typeconstructorWebGL/WebGL2WebGPU
floatthis.x = pb.float()float x;var x: f32;
vec2this.x = pb.vec2()vec2 x;var x: vec2<f32>;
vec3this.x = pb.vec3()vec3 x;var x: vec3<f32>;
vec4this.x = pb.vec4()vec4 x;var x: vec4<f32>;
intthis.x = pb.int()int x;var x: i32;
ivec2this.x = pb.ivec2()ivec2 x;var x: vec2<i32>;
ivec3this.x = pb.ivec3()ivec3 x;var x: vec3<i32>;
ivec4this.x = pb.ivec4()ivec4 x;var x: vec4<i32>;
uintthis.x = pb.uint()uint x;var x: u32;
uvec2this.x = pb.uvec2()uvec2 x;var x: vec2<u32>;
uvec3this.x = pb.uvec3()uvec3 x;var x: vec3<u32>;
uvec4this.x = pb.uvec4()uvec4 x;var x: vec4<u32>;
boolthis.x = pb.bool()bool x;var x: bool;
bvec2this.x = pb.bvec2()bvec2 x;var x: vec2<bool>;
bvec3this.x = pb.bvec3()bvec3 x;var x: vec3<bool>;
bvec4this.x = pb.bvec4()bvec4 x;var x: vec4<bool>;
mat2this.x = pb.mat2()mat2 x;var x: mat2x2<f32>;
mat2x3this.x = pb.mat2x3()mat2x3 x;var x: mat2x3<f32>;
mat2x4this.x = pb.mat2x4()mat2x4 x;var x: mat2x4<f32>;
mat3x2this.x = pb.mat3x2()mat3x2 x;var x: mat3x2<f32>;
mat3this.x = pb.mat3()mat3 x;var x: mat3x3<f32>;
mat3x4this.x = pb.mat3x4()mat3x4 x;var x: mat3x4<f32>;
mat4x2this.x = pb.mat4x2()mat4x2 x;var x: mat4x2<f32>;
mat4x3this.x = pb.mat4x3()mat4x3 x;var x: mat4x3<f32>;
mat4this.x = pb.mat4()mat4 x;var x: mat4x4<f32>;
tex2Dthis.x = pb.tex2D()sampler2D x;var x: texture_2d<f32>;
itex2Dthis.x = pb.itex2D()isampler2D x;var x: texture_2d<i32>;
utex2Dthis.x = pb.utex2D()usampler2D x;var x: texture_2d<u32>;
tex2DShadowthis.x = pb.tex2DShadow()sampler2DShadow x;var x: texture_depth_2d;
tex2DArraythis.x = pb.tex2DArray()sampler2DArray x;var x: texture_2d_array<f32>;
itex2DArraythis.x = pb.itex2DArray()isampler2DArray x;var x: texture_2d_array<i32>;
utex2DArraythis.x = pb.utex2DArray()usampler2DArray x;var x: texture_2d_array<u32>;
tex2DArrayShadowthis.x = pb.tex2DArrayShadow()sampler2DArrayShadow x;var x: texture_depth_2d_array;
tex3Dthis.x = pb.tex3D()sampler3D x;var x: texture_3d<f32>;
itex3Dthis.x = pb.itex3D()isampler3D x;var x: texture_3d<i32>;
utex3Dthis.x = pb.utex3D()usampler3D x;var x: texture_3d<u32>;
texCubethis.x = pb.texCube()samplerCube x;var x: texture_cube<f32>;
itexCubethis.x = pb.itexCube()isamplerCube x;var x: texture_cube<i32>;
utexCubethis.x = pb.utexCube()usamplerCube x;var x: texture_cube<u32>;
texCubeShadowthis.x = pb.texCubeShadow()samplerCubeShadow x;var x: texture_depth_cube;
texStorage2Dthis.x = pb.texStorage2D.rgba8unorm()N/Avar x: texture_storage_2d<rgba8unorm, write>;
texStorage2DArraythis.x = pb.texStorage2DArray.rgba8unorm()N/Avar x: texture_storage_2d_array<rgba8unorm, write>;
texStorage3Dthis.x = pb.texStorage3D.rgba8unorm()N/Avar x: texture_storage_3d<rgba8unorm, write>;

For variables that are not texture types, we can also define array types.

javascript

vertex(pb){
  // Define a vec4 array of length 2
  this.x = pb.vec4[2]();
  // Define and initialize (not supported on WebGL1 device)
  this.y = pb.vec4[2](pb.vec4(1), pb.vec4(2));
}

Generated GLSL:

vec4 x[2];
vec4 y[2] = vec4[2](vec4(1.0),vec4(2.0));

Generated WGSL:

var<private> x: array<vec4<f32>, 2>;
var<private> y: array<vec4<f32>, 2> = array<vec4<f32>, 2>(vec4<f32>(1.0),vec4<f32>(2.0));

Two-dimensional arrays and variable-length array types are only supported on WebGPU devices.

javascript

// Define a two-dimensional array of type vec4
this.x = pb.vec4[2][2]();

// Define a variable-length array
this.y = pb.vec4[0]();

Generated WGSL:

var<private> a: array<array<vec4<f32>, 2>, 2>;

We also support structured types. First, you need to define the constructor of the structure using the pb.defineStruct() function, and then use this constructor to create instances.

javascript

vertex(pb){
  // Define a structure that contains two members,,{ vec4 a, vec3 b }
  const MyStruct = pb.defineStruct([pb.vec4('a'), pb.vec3('b')]);
  pb.main(function(){
    // Creates an instance of this structure type
    this.k = MyStruct();
    // Assign values to structure members
    this.k.a = pb.vec4(1);
    this.k.b.x = 0;
    this.k.b.y = 1;
    this.k.b.z = 2;
    // Construct and initialize the struct
    this.t = MyStruct(pb.vec4(1), pb.vec3(2));
  });
}

Built-in scope

The built-in variable scope encompasses all the built-in variables within a Shader, which can be used directly without declaration.

javascript

vertex(pb){
  // Obtain the built-in scope through the $builtins property.
  // vertexIndex corresponds to the gl_VertexID in GLSL or the @builtin@(vertex_index) in WebGPU
  this.$l.vertexId = this.$builtins.vertexIndex;
},

The table below contains all the built-in variables we currently support.

Builtin variableWebGLWebGL2WGSLStage
$builtins.positiongl_Positiongl_Position$builtin(position)vertex
$builtins.pointSizegl_PointSizegl_PointSizeN/Avertex
$builtins.fragCoordgl_FragCoordgl_FragCoord$builtin(position)fragment
$builtins.frontFacinggl_FrontFacinggl_FrontFacing@builtin(front_facing)fragment
$builtins.fragDepthgl_FragDepthEXTgl_FragDepth@builtin(frag_depth)fragment
$builtins.vertexIndexN/Agl_VertexID@builtin(vertex_index)vertex
$builtins.instanceIndexN/Agl_InstanceID@builtin(instance_index)vertex
$builtins.localInvocationIdN/AN/A@builtin(local_invocation_id)compute
$builtins.globalInvocationIdN/AN/A@builtin(global_invocation_id)compute
$builtins.workGroupIdN/AN/A@builtin(workgroup_id)compute
$builtins.numWorkGroupsN/AN/A@builtin(num_workgroups)compute
$builtins.sampleMaskInN/AN/A@builtin(sample_mask_in)fragment
$builtins.sampleMaskOutN/AN/A@builtin(sample_mask_out)fragment
$builtins.sampleIndexN/AN/A@builtin(sample_index)fragment

Input/Output scope

Inputs and outputs are two special scopes. In the context of VertexShader, the input scope exclusively comprises vertex inputs, while the output scope includes the Varying variables passed from the VertexShader to the FragmentShader. For the FragmentShader, its input scope is automatically generated from the VertexShader's output scope, eliminating the need for manual declaration. The output scope of the FragmentShader is solely dedicated to color outputs.

javascript

vertex(pb){
  // The input scope of the VertexShader defines the vertex stream, which must call the attrib() method to specify the vertex semantic.
  this.$inputs.pos = pb.vec3().attrib('position');
  this.$inputs.color = pb.vec4().attrib('diffuse');
  // Define the Varying output variable
  this.$outputs.outColor = pb.vec4();
  pb.main(function(){
    // Homogeneous space vertex position
    this.$builtins.position = pb.vec4(this.$inputs.pos, 1);
    // Varying output variable
    this.$outputs.outColor = this.$inputs.color;
  });
},
fragment(pb){
  // Color output
  this.$outputs.color = pb.vec4();
  pb.main(function(){
    // The vertex output automatically becomes the fragment input
    this.$outputs.color = this.$inputs.outColor;
  });
}

Operators

Since JavaScript does not support operator overloading, various mathematical operations must be performed by calling functions.

javascript

vertex(pb){
  pb.main(function(){
    // x = 1.0
    this.x = pb.float(1);
    // y = 2.0
    this.y = pb.float(2);
    // z = (x + y) * 2.0;
    this.z = pb.mul(pb.add(this.x, this.y), 2);
  });
}

We support the following operators:

operatorWebGLWebGPU
Arithmetic operations
pb.add(this.a, this.b)a + ba + b
pb.sub(this.a, this.b)a - ba - b
pb.mul(this.a, this.b)a * ba * b
pb.div(this.a, this.b)a / ba / b
pb.compAnd(this.a, this.b)a & ba & b
pb.compXor(this.a, this.b)a ^ ba ^ b
pb.compOr(this.a, this.b)a | ba | b
pb.neg(this.a)-a-a
pb.sal(this.a, this.b)a << ba << b
pb.sar(this.a, this.b)a >> ba >> b
Logical operations
pb.and(this.a, this.b)a && ba && b
pb.or(this.a, this.b)a || ba || b
pb.not(this.a)!a!a
Relational operations
pb.equal(this.a, this.b)a == ball(a == b)
pb.notEqual(this.a, this.b)a != bany(a != b)
pb.lessThan(this.a, this.b)lessThan(a, b)a < b
pb.lessThanEqual(this.a, this.b)lessThanEqual(a, b)a <= b
pb.greaterThan(this.a, this.b)greaterThan(a, b)a > b
pb.greaterThanEqual(this.a, this.b)greaterThanEqual(a, b)a >= b
pb.compEqual(this.a, this.b)equal(a, b)a == b
pb.compNotEqual(this.a, this.b)notEqual(a, b)a != b

Branching

The if branch is implemented by the $if method of the scope, which takes a Boolean expression and a scope-generating function as parameters. The dollar sign is added before the method name to avoid confusion with scope variables.

javascript

vertex(pb){
  this.x = pb.float(0);
  pb.main(function(){
    // if branch
    this.$if(pb.greaterThan(this.x, 0), function(){
      // if x > 0
    });
    // if-else
    this.$if(pb.greaterThan(this.x, 0), function(){
      // if x > 0
    }).$else(function(){
      // else
    });
    // if-elseif
    this.$if(pb.greaterThan(this.x, 0), function(){
      // if x >0
    }).$elseif(pb.equal(this.x, 0), function(){
      // else if x == 0
    }).$else(function(){
      // else
    });
  })
}

Looping

We support simplified for loops, do-while loops (not supported on WebGL1 devices), and while loops (not supported on WebGL1 devices).

  • FOR loop

The implementation is achieved through the scope's $for method, which takes the loop variable, initial value, ending value, and a scope generation function as parameters. It only supports the pattern of for(loop variable = initial value; loop variable < ending value; loop variable++).

for (TYPE VAR = START; VAR < END; VAR++)

javascript

pb.main(function(){
  this.x = pb.int(10);
  this.y = pb.int(0);
  /* 
  Equivalent to the following GLSL
  for(int i = 1; i < x; i++) {
    y = y + i;
  }
  */
  this.$for(pb.int('i'), 1, this.x, function(){
    this.y = pb.add(this.y, this.i);
  });
});
  • DO-WHILE loop

The do-while loop is implemented using $do method.

javascript

pb.main(function(){
  this.x = pb.int(10);
  this.y = pb.int(0);
  /*
  Equivalent to the following GLSL
  do {
    y = y + x;
    x = x - 1;
  } while(x > 0);
  */
  this.$do(function(){
    // Loop body
    this.y = pb.add(this.y, this.x);
    this.x = pb.sub(this.x, 1);
  }).$while(pb.greaterThan(this.x, 0));
});
  • WHILE loop

The while loop is implemented using $while method, with a boolean expression as its parameter.

javascript

pb.main(function(){
  this.x = pb.int(10);
  this.y = pb.int(0);
  /*
  Equivalent to the following GLSL
  while (x > 0) {
    y = y + x;
    x = x - 1;
  }
  */
  this.$while(pb.greaterThan(this.x, 0), function(){
    // Loop body
    this.y = pb.add(this.y, this.x);
    this.x = pb.sub(this.x, 1);
  });
});
  • Exit loop

You can exit the loop by calling $break or $continue methods.

Function

You can create functions using pb.func method.

javascript

vertex(pb){
  // Create a function called addTwoNumbers that takes two float arguments and returns their sum
  pb.func('addTwoNumbers', [pb.float('a'), pb.float('b')], function(){
    // $return method is used to return a value from the function
    this.$return(pb.add(this.a, this.b));
  });
  // Entry point
  pb.main(function(){
    this.x = pb.float(1);
    // Call the function we just defined
    this.y = this.addTwoNumbers(this.x, 2);
    // ... ...
  });
}

To create a function, you need to specify the function name and a list of named parameters. The return type is inferred from the function body, eliminating the need for explicit declaration. If a function with the same name is created, an overloaded version of the function will be automatically generated.

Function parameters can be input or output.

javascript

vertex(pb){
  // Create a function that takes arguments of type two floats and swaps their values
  pb.func('swapTwoNumbers', [pb.float('a').inout(), pb.float('b').inout()], function(){
    this.$l.tmp = this.a;
    this.a = this.b;
    this.b = this.tmp;
  });
  // Entry point
  pb.main(function(){
    this.x = pb.float(1);
    this.y = pb.float(2):
    this.swapTwoNumbers(this.x, this.y);
    // ... ...
  });
}

Builtin functions

We support almost all GLSL and WGSL built-in functions

javascript

pb.func('foo', [pb.vec3('v')], function(){
  // Call the normalize built-in function
  this.$return(pb.normalize(this.v));
});

The following table contains the built-in functions that we support.

FunctionGLSLWGSL
pb.radians(this.x)radians(x)radians(x)
pb.degrees(this.x)degrees(x)degrees(x)
pb.sin(this.x)sin(x)sin(x)
pb.cos(this.x)cos(x)cos(x)
pb.tan(this.x)tan(x)tan(x)
pb.asin(this.x)asin(x)asin(x)
pb.acos(this.x)acos(x)acos(x)
pb.atan(this.x)atan(x)atan(x)
pb.atan2(this.x, this.y)atan(x, y)atan2(x, y)
pb.sinh(this.x)sinh(x)sinh(x)
pb.cosh(this.x)cosh(x)cosh(x)
pb.tanh(this.x)tanh(x)tanh(x)
pb.asinh(this.x)asinh(x)asinh(x)
pb.acosh(this.x)acosh(x)acosh(x)
pb.atanh(this.x)atanh(x)atanh(x)
pb.pow(this.x, this.y)pow(x, y)pow(x, y)
pb.exp(this.x)exp(x)exp(x)
pb.exp2(this.x)exp2(x)exp2(x)
pb.log(this.x)log(x)log(x)
pb.log2(this.x)log2(x)log2(x)
pb.sqrt(this.x)sqrt(x)sqrt(x)
pb.inverseSqrt(this.x)inversesqrt(x)inverseSqrt(x)
pb.abs(this.x)abs(x)abs(x)
pb.sign(this.x)sign(x)sign(x)
pb.floor(this.x)floor(x)floor(x)
pb.ceil(this.x)ceil(x)ceil(x)
pb.round(this.x)sinh(x)sinh(x)
pb.fract(this.x)fract(x)fract(x)
pb.mod(this.x)mod(x)mod(x)
pb.sinh(this.x)sinh(x)sinh(x)
pb.fma(this.x, this.y, this.z)x * y + zfma(x, y, z)
pb.min(this.x, this.y)min(x, y)min(x, y)
pb.max(this.x, this.y)max(x, y)max(x, y)
pb.clamp(this.x, this.y, this.z)clamp(x, y, z)clamp(x, y, z)
pb.mix(this.x, this.y, this.z)mix(x, y, z)mix(x, y, z)
pb.step(this.x, this.y)step(x, y)step(x, y)
pb.smoothStep(this.x, this.y, this.z)smoothstep(x)smoothstep(x)
pb.length(this.x)length(x)length(x)
pb.distance(this.x, this.y)distance(x, y)distance(x, y)
pb.dot(this.x, this.y)dot(x, y)dot(x, y)
pb.cross(this.x, this.y)cross(x, y)cross(x, y)
pb.normalize(this.x)normalize(x)normalize(x)
pb.faceForward(this.x, this.y, this.z)N/AfaceForward(x, y, z)
pb.reflect(this.x, this.y)reflect(x, y)reflect(x, y)
pb.refract(this.x, this.y, this.z)refract(x, y, z)refract(x, y, z)
pb.frexp(this.x)N/Afrexp(x)
pb.transpose(this.x)transpose(x)transpose(x)
pb.determinant(this.x)determinant(x)determinant(x)
pb.arrayLength(this.x)N/AarrayLength(x)
pb.select(this.x, this.y, this.z)N/Aselect(x, y, z)
pb.floatBitsToInt(this.x)floatBitsToInt(x)N/A
pb.floatBitsToUint(this.x)floatBitsToUint(x)N/A
pb.intBitsToFloat(this.x)intBitsToFloat(x)N/A
pb.uintBitsToFloat(this.x)uintBitsToFloat(x)N/A
pb.pack4x8snorm(this.x)N/Apack4x8snorm(x)
pb.unpack4x8snorm(this.x)N/Aunpack4x8snorm(x)
pb.pack4x8unorm(this.x)N/Apack4x8unorm(x)
pb.unpack4x8unorm(this.x)N/Aunpack4x8unorm(x)
pb.pack2x16snorm(this.x)N/Apack2x16snorm(x)
pb.unpack2x16snorm(this.x)N/Aunpack2x16snorm(x)
pb.pack2x16unorm(this.x)N/Apack2x16unorm(x)
pb.unpack2x16unorm(this.x)N/Aunpack2x16unorm(x)
pb.pack2x16float(this.x)N/Apack2x16float(x)
pb.unpack2x16float(this.x)N/Aunpack2x16float(x)
pb.dpdx(this.x)dFdx(x)dpdx(x)
pb.dpdy(this.x)dFdy(x)dpdy(x)
pb.fwidth(this.x)fwidth(x)fwidth(x)
pb.dpdxCoarse(this.x)dFdx(x)dpdxCoarse(x)
pb.dpdxFine(this.x)dFdx(x)dpdxFine(x)
pb.dpdyCoarse(this.x)dFdy(x)dpdyCoarse(x)
pb.dpdyFine(this.x)dFdy(x)dpdyFine(x)
pb.round(this.x)N/Around(x)
pb.trunc(this.x)N/Atrunc(x)
pb.textureDimensions(this.tex, this.level)textureSize(tex, level)textureDimensions(tex, level)
pb.textureGather(this.tex, this.sampler, this.coord)N/AtextureGater(tex, sampler, coord)
pb.textureGather(this.comp, this.tex, this.sampler, this.coord)N/AtextureGater(comp, tex, sampler, coord)
pb.textureArrayGather(this.tex, this.sampler, this.coord, this.arrayIndex)N/AtextureGater(tex, sampler, coord, arrayIndex)
pb.textureArrayGather(this.comp, this.tex, this.sampler, this.coord, this.arrayIndex)N/AtextureGater(comp, tex, sampler, coord, arrayIndex)
pb.textureGatherCompare(this.x, this.sampler, this.coord, this.depthRef)N/AtextureGaterCompare(x, sampler, coord, depthRef)
pb.textureArrayGatherCompare(this.x, this.sampler, this.coord, this.arrayIndex, this.depthRef)N/AtextureGaterCompare(x, sampler, coord, arrayIndexdepthRef)
pb.textureLoad(this.tex, this.coord, this.level)texelFetch(tex, coord, level)textureLoad(tex, coord, level)
pb.textureArrayLoad(this.tex, this.coord, this.arrayIndex, this.level)texelFetch(tex, coord, level)textureLoad(tex, coord, arrayIndex, level)
pb.textureStore(this.tex, this.coord, this.value)N/AtextureStore(tex, coord, value)
pb.textureArrayStore(this.tex, this.coord, this.arrayIndex, this.value)N/AtextureStore(tex, coord, arrayIndex, value)
pb.textureLoad(this.tex, this.coord, this.level)texelFetch(tex, coord, level)textureLoad(tex, coord, level)
pb.textureNumLayers(this.tex)N/AtextureNumLayers(tex)
pb.textureNumLevels(this.tex)N/AtextureNumLevels(tex)
pb.textureNumSamples(this.tex)N/AtextureNumSamples(tex)
pb.textureSample(this.tex, this.coord)texture(tex, coord)textureSample(tex, coord)
pb.textureArraySample(this.tex, this.coord, this.arrayIndex)texture(tex, coord)textureSample(tex, coord, arrayIndex)
pb.textureSampleBias(this.tex, this.coord, this.bias)texture(tex, coord, bias)textureSampleBias(tex, coord, bias)
pb.textureArraySampleBias(this.tex, this.coord, this.arrayIndex, this.bias)texture(tex, coord, bias)textureSampleBias(tex, coord, arrayIndex, bias)
pb.textureSampleCompare(this.tex, this.coord, this.depthRef)texture(tex, coord)textureSampleCompare(tex, coord, depthRef)
pb.textureArraySampleCompare(this.tex, this.coord, this.arrayIndex, this.depthRef)texture(tex, coord)textureSampleCompare(tex, coord, this.arrayIndex, depthRef)
pb.textureSampleLevel(this.tex, this.coord, this.level)textureLod(tex, coord, level)textureSampleLevel(tex, coord, level)
pb.textureArraySampleLevel(this.tex, this.coord, this.arrayIndex, this.level)textureLod(tex, coord, level)textureSampleLevel(tex, coord, arrayIndex, level)
pb.textureSampleCompareLevel(this.tex, this.coord, this.depthRef)texture(tex, coord)textureSampleCompareLevel(tex, coord, depthRef)
pb.textureArraySampleCompareLevel(this.tex, this.coord, this.arrayIndex, this.depthRef)texture(tex, coord)textureSampleCompareLevel(tex, coord, arrayIndex, depthRef)
pb.textureSampleGrad(this.tex, this.coord, this.ddx, this.ddy)textureGrad(tex, coord, ddx, ddy)textureSampleGrad(tex, coord, ddx, ddy)
pb.textureArraySampleGrad(this.tex, this.coord, this.arrayIndex, this.ddx, this.ddy)textureGrad(tex, coord, ddx, ddy)textureSampleGrad(tex, coord, arrayIndex, ddx, ddy)
pb.storageBarrier()N/AstorageBarrier()
pb.workgroupBarrier()N/AworkgroupBarrier()
pb.atomicLoad(this.ptr)N/AatomicLoad(ptr)
pb.atomicStore(this.ptr, this.value)N/AatomicStore(ptr, value)
pb.atomicAdd(this.ptr, this.value)N/AatomicAdd(ptr, value)
pb.atomicSub(this.ptr, this.value)N/AatomicSub(ptr, value)
pb.atomicMax(this.ptr, this.value)N/AatomicMax(ptr, value)
pb.atomicMin(this.ptr, this.value)N/AatomicMin(ptr, value)
pb.atomicAnd(this.ptr, this.value)N/AatomicAnd(ptr, value)
pb.atomicOr(this.ptr, this.value)N/AatomicOr(ptr, value)
pb.atomicXor(this.ptr, this.value)N/AatomicXor(ptr, value)

Uniform

Uniform variables needs to be defined in the global scope as follows:

javascript

vertex(pb){
  // Vertex inputs
  this.$inputs.pos = pb.vec3().attrib('position');
  this.$inputs.color = pb.vec4().attrib('diffuse');
  // Varying outputs
  this.$outputs.outColor = pb.vec4();
  // Defines an uniform
  this.mvpMatrix = pb.mat4().uniform(0);
  pb.main(function(){
    // Transforms position into homogeneous space
    this.$builtins.position = pb.mul(this.mvpMatrix, pb.vec4(this.$inputs.pos, 1));
    // Varying output
    this.$outputs.outColor = this.$inputs.color;
  });
},
fragment(pb){
  // color output
  this.$outputs.color = pb.vec4();
  pb.main(function(){
    this.$outputs.color = this.$inputs.outColor;
  });
}

To use a variable as a uniform, you must invoke the uniform() method to declare that the variable is a uniform. The method requires the number of the BindGroup to which the uniform belongs as its parameter. The concept of a BindGroup here aligns with the BindGroup concept in the WebGPU standard. Just like referencing global variables, uniform variables can be referenced in the same manner; however, they cannot be assigned new values. In both WebGL2 and WebGPU, all uniform constants are packaged into a UniformBuffer, although it's also possible to declare a UniformBuffer independently.

javascript

vertex(pb){
  // 60 vec4 elements in a single uniform buffer
  this.values = pb.vec4[60]().uniformBuffer(0);
}

Bind Group

After a Shader is created, it requires setting Uniform constants to be used for rendering. We follow the WebGPU specification to pass parameters to the Shader through resource binding groups.

javascript

const program = device.buildRenderProgram({
  vertex(pb) {
    this.$inputs.pos = pb.vec3().attrib('position');
    this.$inputs.uv = pb.vec2().attrib('texCoord0');
    this.mvpMatrix = pb.mat4().uniform(0);
    pb.main(function(){
      this.$builtins.position = pb.mul(this.mvpMatrix, pb.vec4(this.$inputs.pos, 1));
      this.$outputs.outUV = this.$inputs.uv;
    });
  },
  fragment(pb) {
    this.$outputs.color = pb.vec4();
    this.tex = pb.tex2D().uniform(1);
    this.factor = pb.vec4().uniform(1);
    pb.main(function(){
      this.sampleColor = pb.textureSample(this.tex, this.$inputs.outUV);
      this.$outputs.color = pb.mul(this.sampleColor, this.factor);
    });
  }
});

The shader mentioned above requires three uniform constants: mvpMatrix, which belongs to binding group 0, and tex and factor, which are part of binding group 1. To pass parameters, we need to create these two binding groups.

javascript

// Create a binding group with the binding group layout descriptor at index 0
const bindgroup0 = device.createBindGroup(program.bindGroupLayouts[0]);
// Set uniform values
const mvpMatrix = new Matrix4x4();
bindgroup0.setValue('mvpMatrix', mvpMatrix);

// Create a binding group with the binding group layout descriptor at index 1
const bindgroup1 = device.createBindGroup(program.bindGroupLayouts[1]);
// Set uniform values
bindgroup1.setValue('factor', new Vector4(1, 1, 1, 1));
bindgroup1.setTexture('tex', texture);

When rendering, it's necessary to set the device's current Shader and the related binding groups.

javascript

// Set current shader
device.setProgram(program);
// Set bind groups
device.setBindGroup(0, bindgroup0);
device.setBindGroup(1, bindgroup1);
// Draw call

Compute Shader

Compute Shaders are primarily utilized for executing a vast number of parallel computations on the GPU and are available on WebGPU devices. The Device.buildComputeProgram() method can be used to create a Compute Shader.

javascript

const computeProgram = device.buildComputeProgram({
  // Specify how many compute cores, or threads, need to be applied for in the three dimensions
  workgroupSize: [64, 1, 1],
  compute(pb) {
    // Storage buffer for reading
    this.readbuffer = pb.vec4[0]().storageBuffer(0);
    // Storage buffer for writing
    this.writebuffer = pb.vec4[0]().storageBuffer(0);
    pb.main(function(){
      // current index
      this.index = this.$builtins.globalInvocationId.x;
      // Write value
      this.writebuffer.setAt(this.index, this.readbuffer.at(this.index));
    });
  }
});

Released under the MIT License.