Computing shaders


Computing Shaders

Today I started with WebGPU Fundamentals again, going further to Computing shaders.

The first thing that is different from yesterdays fragment and vertex shaders is that we need storage variable:

      @group(0) @binding(0) var<storage, read_write> data: array<f32>;

The thing that still confuses me a bit is locations in WGSL shaders: “We tell it we’re going to specify this array on binding location 0 (the binding(0)) in bindGroup 0 (the @group(0)).”

Things get more interesting when we need to create separate buffers to store the data in and to read the data from:

  //data for compute shader
  const input = new Float32Array([1, 3, 5]);

  //For WebGPU to use it, we need to make a buffer that exists on the GPU and copy the data to the buffer.

  // create a buffer on the GPU to hold our computation
  // input and output
  const workBuffer = device.createBuffer({
    label: 'work buffer',
    size: input.byteLength,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
  });
  // Copy our input data to that buffer
  device.queue.writeBuffer(workBuffer, 0, input);

  // create a buffer on the GPU to get a copy of the results
  const resultBuffer = device.createBuffer({
    label: 'result buffer',
    size: input.byteLength,
    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
  });

  // Setup a bindGroup to tell the shader which
  // buffer to use for the computation
  const bindGroup = device.createBindGroup({
    label: 'bindGroup for work buffer',
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: { buffer: workBuffer } },
    ],
  });

WebGPU Inter-stage Variables

Moving on to WebGPU Inter-stage Variables, in next chapter I learned how to pass structures (of which I think kind of like js classes) between 2 shaders:

      struct OurVertexShaderOutput {
      @builtin(position) position: vec4f,
      @location(0) color: vec4f,
    };
    @vertex fn vs(
      @builtin(vertex_index) vertexIndex : u32
   ) -> OurVertexShaderOutput {
      let pos = array(
        vec2f( 0.0,  0.5),  // top center
        vec2f(-0.5, -0.5),  // bottom left
        vec2f( 0.5, -0.5)   // bottom right
      );

      let color = array(
        vec4f(1, 0, 0, 1), // red
        vec4f(0, 1, 0, 1), // green
        vec4f(0, 0, 1, 1), // blue
      );

      var vsOutput: OurVertexShaderOutput;
      vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
      vsOutput.color = color[vertexIndex];
      return vsOutput;
    }

     @fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
      let red = vec4f(1, 0, 0, 1);
      let colored = fsInput.color;

      let grid = vec2u(fsInput.position.xy) / 16;
      let checker = (grid.x + grid.y) % 2 == 1;

      return select(red, colored, checker);
    }

Here I combined 2 examples to both pass color from vertex shader to frag shader, and to add condition to use the passed color or red in frag shader. This is the outcome: vertex-to-frag

Uniforms

Uniforms look just like in GLSL with some differences: to create a Uniform, first we need to descripe its’ “class” aka struct:

    struct OurStruct {
        color: vec4f,
        scale: vec2f,
        offset: vec2f,
      };

Then, we need to declare a uniform with type of our struct:

@group(0) @binding(0) var<uniform> ourStruct: OurStruct;

and after this we can use uniforms in our shader code. To be able to se the from Javascript, we also need to create a buffer first, and to calculate it’s size:

const uniformBufferSize =
    4 * 4 + // color is 4 32bit floats (4bytes each)
    2 * 4 + // scale is 2 32bit floats (4bytes each)
    2 * 4;  // offset is 2 32bit floats (4bytes each)
  const uniformBuffer = device.createBuffer({
    label: 'uniforms for triangle',
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });

after playing with setting uniforms a bit, I got this beautiful rotating triangle:

rotating-trinagle