Exploring WebGPU: Setting Up a Development Environment with Vite and TypeScript
Tags: webgpu, typescript
Published: 2024-04-05 13:42:54

# Introduction

I wanted to try out WebGPU, so I looked into how to set up a development environment.
I managed to run the [Hello Triangle](https://webgpu.github.io/webgpu-samples/?sample=helloTriangle) sample from WebGPU Samples on a local server set up with vite.
Also, I made it possible to develop with TypeScript.

# Setting Up the Development Environment

## Creating a Working Folder

Create a working folder and move into it.

```sh
mkdir /path/to/work-dir
cd /path/to/work-dir
```

## Development Environment

The development environment will be vite.
Please copy the following package.json.

```json
{
  "type": "module"
}
```

Then create package.json.

```sh
pbpaste > package.json
```

Install the necessary modules.

```sh
npm i -D typescript @webgpu/types vite
```

## TypeScript

Please copy the following `tsconfig.json`.
Specifying `@webgpu/types` allows TypeScript to recognize WebGPU.

```json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "types": ["@webgpu/types"]
  },
  "include": ["src"]
}
```

Then create tsconfig.json.

```sh
pbpaste > tsconfig.json
```

## index.html

Please copy the following index.html.

```
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebGPU</title>
  </head>
  <body>
    <canvas></canvas>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

Then create index.html.

```sh
pbpaste > index.html
```

## src Folder

Create a folder to place your code in.

```sh
mkdir src
```

## src/main.ts

Please copy the following code.
Notes are also left, so please check them if you like.

```ts
// shader import requires ?raw
import vertexShader from './vertex.wgsl?raw'
import fragmentShader from './fragment.wgsl?raw'

main()

// Initializing WebGPU requires asynchronous processing
async function main() {
  const canvas: HTMLCanvasElement = document.querySelector('canvas')!

  // Request an adapter to access GPU hardware for WebGPU
  const adapter = await navigator.gpu.requestAdapter()

  if (!adapter) {
    // Handling for environments not supporting WebGPU, part A
    throw new Error()
  }

  // Request a device for executing GPU commands and allocating memory
  // The device will not be nullish (according to type definitions)
  const device = await adapter.requestDevice()

  // Get the WebGPU context
  const context: GPUCanvasContext = canvas.getContext('webgpu')

  if (!context) {
    // Handling for environments not supporting WebGPU, part B
    throw new Error()
  }

  const { devicePixelRatio } = window
  canvas.width = devicePixelRatio * canvas.clientWidth
  canvas.height = devicePixelRatio * canvas.clientHeight

  // Get the canvas color format recommended by WebGPU
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat()

  // Associate the GPU device and context
  context.configure({
    // Specify the GPU device to use
    device,
    // Specify the color format
    format: presentationFormat,
    // Specify the alpha blending mode
    alphaMode: 'premultiplied',
  })

  // Create a render pipeline
  // vertex|fragment shaders are combined, defining the rendering process
  const pipeline = device.createRenderPipeline({
    // Automatically set the pipeline layout
    layout: 'auto',
    // Configuration for vertex shader
    vertex: {
      module: device.createShaderModule({
        code: vertexShader,
      }),
    },
    // Configuration for fragment shader
    fragment: {
      module: device.createShaderModule({
        code: fragmentShader,
      }),
      // Configuration for render targets
      targets: [
        {
          // Specify the color format for render target
          format: presentationFormat,
        },
      ],
    },
    primitive: {
      // Specify the topology of primitives to draw
      topology: 'triangle-list',
    },
  })

  function frame() {
    // Create a GPU command for batch processing multiple GPU commands
    const commandEncoder = device.createCommandEncoder()

    // Get the texture to display rendering results
    const textureView = context.getCurrentTexture().createView()

    const renderPassDescriptor: GPURenderPassDescriptor = {
      colorAttachments: [
        {
          view: textureView,
          // Specify the background
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
          // Specify the load operation
          // 'load': Retains the drawing result from the previous frame
          // 'clear': Initializes with the color specified in
          //    GPURenderPassDescriptor.colorAttachments.clearValue
          //    before drawing a new frame
          loadOp: 'clear',
          // Specify the store operation
          // In this case, it is set to save the rendering result
          storeOp: 'store',
        },
      ],
    }

    // Begin recording GPU commands
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    // Set the render pipeline
    // This specifies the shaders and rendering settings to use
    passEncoder.setPipeline(pipeline)
    // Start rendering
    passEncoder.draw(3)
    // End recording GPU commands
    passEncoder.end()

    // Submit the command to the GPU queue to execute the GPU command
    device.queue.submit([commandEncoder.finish()])

    requestAnimationFrame(frame)
  }

  requestAnimationFrame(frame)
}
```

Then create src/main.ts.

```sh
pbpaste > src/main.ts
```

## src/vertex.wgsl

The WebGPU Shading Language (WGSL) is a shading language for WebGPU.
The code feels somewhat similar to Rust.

Please copy the following code.
Notes are also left, so please check them if you like.

```rust
// The @vertex attribute indicates that this function is a vertex shader
@vertex
fn main(
  // @builtin(vertex_index) is a built-in variable representing the vertex index
  // This is used to identify vertices from the vertex buffer
  @builtin(vertex_index) VertexIndex : u32
  // Returns a 4-dimensional vector representing clip space coordinates
) -> @builtin(position) vec4f {
  // pos is an array containing three 2-dimensional vectors
  // These vectors represent the vertices of a triangle
  var pos = array<vec2f, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );

  // Use the vertex index to retrieve the corresponding vertex coordinates from the array
  // Convert the retrieved 2-dimensional vector to a 4-dimensional vector and return it
  // The z-coordinate is set to 0.0, and the w-coordinate to 1.0
  // This is necessary to place the triangle in clip space
  return vec4f(pos[VertexIndex], 0.0, 1.0);
}
```

Then create src/vertex.wgsl.

```sh
pbpaste > src/vertex.wgsl
```

## src/fragment.wgsl

Please copy the following code.
Notes are also left, so please check them if you like.

```rust
// The @fragment attribute indicates that this function is a fragment shader
@fragment
// Returns a 4-dimensional vector to be written to color attachment index 0
fn main() -> @location(0) vec4f {
  // Red color
  return vec4(1.0, 0.0, 0.0, 1.0);
}
```

Then create src/fragment.wgsl.

```sh
pbpaste > src/fragment.wgsl
```

# Running the Application

```sh
npx vite
```

A red triangle will be displayed in the web browser.

# Conclusion

Although it's simple, we've set up an environment to develop with WebGPU.
Like [Shadertoy](https://www.shadertoy.com/), I plan to deepen my understanding of WebGPU while creating pictures with shaders.

# PR

[VOTE](https://createmyvote.com/) is a web app for voting on binary choices, developed by Blue Inc.
Feel free to play with it on a variety of topics, from technical discussions to everyday choices.