As well as writing to data buffers (arrays of numbers) you can also write to textures. Essentially, textures are just arrays of colors. In this example, you'll visualizing the workgoups on the compute shader using a texture pattern. The texture is copied back to the client side as a `buffer'. This buffer is then written to a HTML Canvas so you can visualize the output.
This example helps you understand how the computational load is distributed across threads into workgroups and dispatch sizes, it also gives you some insights into how texture buffers works.
The output for this example is a texture with
16x16
blocks (each block is
8x8
).
The example dispatches
16x16
workgroups, each workgroup consisting of
8x8
blocks (which are represented as filled
8x8
solid color blocks in the image).
The texture is
128x128
(e.g.,
128/16==8
) so the number of dispatched workgroups and the workgroup size match (i.e.,
16x8=128
).
The shader invocation is done with this line:
dispatchWorkgroups(16, 16)
- which defines how many workgroups to launch.
That means that we execute the shader with
16
work groups in the
X
and
Y
dimensions (note the default for the Z dimension is 1 if not specified).
Most importantly, we use the globalId (if we enumerate all work items linearly, the
globalId
would be that of the index) of the current work item.
In this case, these values go from 0 to 128 on each dimension (
16
work groups
x8
work items
=128
shader executions), but the most important thing is the next line: to access the image, texture access function (textureStore) that takes integral values, ranged from
0
to texture size
1
.
Textures are handled different in compute shaders than in graphics shaders (fragment shaders), where texture coordinates are floating point variables and the values are normalized between 0.0 to 1.0. It's done differently on the compute shaders in part to avoid any kind of filterings, roundings, or inaccuracies at the moment of writing into a texture.
Complete Code
Let's go through the complete code to see how it all fits together. The first thing we do is define the wgsl compute shader - which is stored in a string constant called
computeShader
.
The to top of the compute shader defines the storage texture binding (
rgba32float
). The texture can only be used for writing. To write to the texture, you can't just access it like an array using an index, instead you've got to use the builtin function
@compute @workgroup_size(8, 8) fn main(@builtin(global_invocation_id) globalId : vec3<u32>, @builtin(local_invocation_id) localId : vec3<u32>, @builtin(workgroup_id) workgroupId : vec3<u32>, @builtin(num_workgroups) workgroupSize : vec3<u32> ) { var position = vec2<i32>( globalId.xy ); var color = vec4<f32>( vec3<f32>(workgroupId) / vec3<f32>(workgroupSize) , 1.0);
textureStore(myTexture, position, color ); } `;
Each thread is mapped to a pixel. The pixel index is identified in the compute shader using the
global_invocation_id
(which is the
globalId
variable). In addition to the
globalId
, there is a
localId
, which is the index within the smaller group (
0 to 8
in the x-dimension). The workgroup number and workgroup size are also provided which let you know the index of the local group within the larger grid.
Visualize how what the different builtin variables mean.
We add a helper function called
log(..)
which allows us to pring debug information to the main window (instead of just to the debug console).
let div = document.createElement('div'); document.body.appendChild( div ); div.style['font-size'] = '20pt'; function log( s ) { console.log( s ); let args = [...arguments].join(' '); div.innerHTML += args + '<br><br>'; }
Just to kick things off, and to check the log function and the code is running, we send the message `WebGPU Compute Example` to the log function.
Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.