www.xbdev.net
xbdev - software development
Tuesday April 29, 2025
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU 'Compute'.. Compute, Algorithms, and Code.....
     
 

WebGPU 'Compute'..

Compute, Algorithms, and Code.....

 


Fractals (Compute)


The next time you walk down the supermarket or look at your hands - you need to pay attention - fractals are all around you. Fractals are natures patterns.

This example is about using your compute shader to write to a texture which is directly copied to the HTML Canvas. You can use texture buffers, not just numerical ones. Write a fractal patttern to the texture (fractal) and send it to a canvas output.


On the journey to mastering WebGPU compute - the journey should start with something simple - data read/write to the compute!
On the journey to mastering WebGPU compute - the journey should start with something simple - data read/write to the compute!


Functions Used: createElement(), appendChild(), log(), join(), requestAdapter(), requestDevice(), getContext(), getPreferredCanvasFormat(), configure(), getCurrentTexture(), createTexture(), createBindGroupLayout(), createBindGroup(), createView(), createComputePipeline(), createPipelineLayout(), createShaderModule(), createCommandEncoder(), beginComputePass(), setPipeline(), setBindGroup(), dispatchWorkgroups(), end(), copyTextureToTexture(), finish(), submit()

The hello world of fractals is the Mandelbrot set. Named after mathematician Benoit B. Mandelbrot.

The fractal algorithm is built on iterative equation \( z_{n+1} = z_n^2 + c \), where \( z \) and \( c \) are complex numbers and \( n \) is an integer.

A valid Mandelbrot set is for all values of \( c \) for which the sequence remains bounded. Its boundary, when visualized, reveals an beautifully detailed and self-similar pattern with infinite complexity, with each zoom revealing smaller versions of the entire set.

The beauty and complexity of the Mandelbrot set is sure to captivate you while giving you insights into the power of compute shaders.

WARNING
For those who are fairly inexperienced in programming and think jumping into JavaScript and parallal programming - I'd strongly caution you to go back and get a lot of experience writing small test programs - to get a feel for things.


Fractal output for the listing below.
Fractal output for the listing below.



Complete Code


Let's go through the complete fractal program step by step. We start off by creating an output log function - just to record any data/information (send it to the output browser - instead of hiding it away in the console log window). We then check that we have the WebGPU API (and it's enabled).

If successful, we get hold of the adapter and the device.

let div document.createElement('div');
document.body.appendChilddiv );
div.style['font-size'] = '20pt';
function 
log)
{
  
console.log);
  
let args = [...arguments].join(' ');
  
div.innerHTML += args '<br><br>';
}

log('WebGPU Compute Example');

if (!
navigator.gpu) { log("WebGPU is not supported (or is it disabled? flags/settings)"); return; }

const 
adapter await navigator.gpu.requestAdapter();
const 
device  await adapter.requestDevice();


The fractal output will be copied to a HTML canvas. We create a canvas and get a suitable WebGPU context. The output texture from the compute shader will be directly copied to the canvas texture memory. The context needs to have additional flags set in the configuration to allow copying.

In this example, we also force the presentation format to be
rgba8unorm
- so it will be the same as the texture format we create on the compute shader.

let canvas document.createElement('canvas');
document.body.appendChildcanvas ); canvas.height=canvas.width=512;

const 
context canvas.getContext('webgpu');
const 
presentationFormat navigator.gpu.getPreferredCanvasFormat(); 
console.log('presentationFormat:'presentationFormat );

context.configure({ devicedevice
                    
usageGPUTextureUsage.RENDER_ATTACHMENT GPUTextureUsage.COPY_SRC GPUTextureUsage.COPY_DST,
                    
format"rgba8unorm" /*presentationFormat*/  });

let canvasTexture context.getCurrentTexture();


Create a GPU texture - make sure it's the same size and format as the HTML Canvas. Set the flags so it can be copied from and accessed as a storage buffer on the compute shader.

let imgWidth  canvas.width;
let imgHeight imgWidth;

const 
texture device.createTexture({
  
size: [imgWidthimgHeight1],
  
format"rgba8unorm",
  
usageGPUTextureUsage.COPY_DST GPUTextureUsage.COPY_SRC GPUTextureUsage.TEXTURE_BINDING GPUTextureUsage.STORAGE_BINDING
});


The only buffer used by the compute shader will be the storage texture - which we define and configure in the group bindings.

// Bind group layout and bind group
const bindGroupLayout device.createBindGroupLayout({
  
entries: [  {binding0visibilityGPUShaderStage.COMPUTEstorageTexture: {format:"rgba8unorm"
                                                                                
access:"write-only"
                                                                                
viewDimension:"2d"}   }
           ]
});

const 
bindGroup device.createBindGroup({
    
layoutbindGroupLayout,
    
entries: [  {   binding0,  resourcetexture.createView()  }
    ]
});


The compute shader is defined as a string. Instead of passing the image size in a uniform or storage buffer to the shader - it's passed as a constant using a string literal. The definition
${imgWidth}
is replaced by the number in JavaScript before it's passed to the shader compiler.

Compute shaders don't necessarily need to be fixed and hard-coded - they could be generated on-the-fly (just text files). Very easy to generate different compute shaders with different configurations for changing situations and results.

Each pixel of the texture is used to calculate a part of the fractal. The Mandelbrot set iteration loop is run in parallel.

// Compute shader code
const computeShader = ` 
@group(0) @binding(0) var myTexture: texture_storage_2d<rgba8unorm, write>;

@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>
        ) 
{
    
    // Map the globalId to the UV coordinates
    var uv = vec2<f32>(f32(globalId.x), f32(globalId.y)) / f32(
${imgWidth}); // 0 - 1
    uv = uv * 2.0 - 1.0; // -1 to 1.0

    // Mandelbrot set iteration
    var z = vec2<f32>(0.0, 0.0);
    var c = uv + vec2<f32>(-0.5,0.0);// shift x-axis (in middle of the screen)
    var iterations:i32 = 100;
    var i:i32 = 0;
    for (i = 0; i < iterations; i = i + 1) {
        // Mandelbrot formula: z = z^2 + c
        var zTemp = vec2<f32>((z.x * z.x - z.y * z.y), (2.0 * z.x * z.y));
        z = zTemp + c;

        // If magnitude of z exceeds 2, it's not in the Mandelbrot set
        if (length(z) > 2.0) {
            break;
        }
    }

    // Coloring based on iteration count
    var color = vec4(0.0, 0.0, 0.0, 1.0);
    if (length(z) <= 2.0) {
        // Inside the Mandelbrot set, color black
        color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
    } else {
        // Outside the Mandelbrot set, color based on iteration count
        color = vec4<f32>( f32(i) / f32(iterations), 0.0, 0.0, 1.0);
    }

    // Store color in texture
    textureStore(myTexture, vec2<i32>(i32(globalId.x), i32(globalId.y)), color);
}
`;


The pipeline is setup - linked to the compute shader entry point and the binding layouts.

// Pipeline setup
const computePipeline device.createComputePipeline({
    
layout :   device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout]}),
    
compute: { module    device.createShaderModule({code:computeShader}),
               
entryPoint"main" }
});


All ready to go! Just a matter of creating a command encoder - linking it with the pipeline and bindings and dispatching the thread groups. So that each pixel of the texture runs on its own thread, dispatch size is
imageSize/8
- using the function
dispatchWorkgroups( imgWidth/8, imgHeight/8 )
.

// Commands submission
const commandEncoder device.createCommandEncoder();
const 
passEncoder commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0bindGroup);
// workgroup size of 8x8 on the wgsl shader
passEncoder.dispatchWorkgroupsimgWidth/8imgHeight/);
await passEncoder.end();


After the compute shader has completed and the storage texture contains the fractal. The final result is copied to the Canvas output using the
copyTextureToTexture
.

commandEncoder.copyTextureToTexture( { texture:texture },
                                     { 
texturecanvasTexture },
                                     { 
width:imgWidthheight:imgHeightdepthOrArrayLayers:1} );

// Submit GPU commands.
const gpuCommands commandEncoder.finish();
await device.queue.submit([gpuCommands]);


At this point, you should see a black and red Mandelbrot fractal in the output canvas window.



Things to Try


• Try other constants for the fractal parameters (create other shapes and patterns).
• Add colors to the fractal (inside of the fractal is a different color than the outside - filled in).
• Explore implementing other 2D fractals (e.g., Julia Set).
• Run the fractal generation on a single thread and see how much longer it takes to generate.



Resources and Links


• WebGPU Lab Demo [LINK]













































WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Compute graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright wgsl webgpugems shading language cookbook kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.