www.xbdev.net
xbdev - software development
Thursday May 7, 2026
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU 'Compute'.. Compute, Algorithms, and Code.....
     
 

WebGPU 'Compute'..

Compute, Algorithms, and Code.....

 

Color Adjustments


Adjusting the brightness and contrast of an image by adjusting the intensity of its pixels; in addition to also adjusting the hue, saturation and lighting properties of the image.


Color adjustments.
Color adjustments.


Functions Used: requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()

This example focuses on two image methods:
Brightness and Contrast Adjustment - Modify the brightness and contrast of an image by adjusting the intensity of its pixels.
Hue, Saturation, and Lightness (HSL) Adjustment - Change the hue, saturation, and lightness to alter the color properties of the image.




Two main functions, one for adjusting the brightness and contrast (
adjustBrightnessContrast
function) and the other hue, saturation, and lightness (
adjustHSL
function).

The parameters for adjustint the colors are define by constants at the top of the shader with default values (e.g., BRIGHTNESS, CONTRAST, HUE, SATURATION, and LIGHTNESS).





Complete Code


<?php
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>';
}

log('WebGPU Compute Example');

async function loadTexture(fileName = "https://webgpulab.xbdev.net/var/images/test512.png", 
                           width=512, height=512) {
  console.log('loading image:', fileName);
  // Load image 
  const img = document.createElement("img");
  img.src = fileName;

  await img.decode();

  const originalWidth = img.width;
  const originalHeight = img.height;

  const imageCanvas = document.createElement('canvas');
  imageCanvas.width = width;
  imageCanvas.height = height;
  const imageCanvasContext = imageCanvas.getContext('2d');

  // Draw the image onto the canvas, resizing it to the specified width and height
  imageCanvasContext.drawImage(img, 0, 0, width, height);

  const imageData = imageCanvasContext.getImageData(0, 0, width, height);
  const textureData = imageData.data;
  console.log('textureData.byteLength:', textureData.byteLength);

  const basicTexture = device.createTexture({
    size: [width, height, 1],
    format: "rgba8unorm",
    usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
  });

  await device.queue.writeTexture(
    { texture: basicTexture },
    textureData,
    { bytesPerRow: width * 4 },
    [width, height, 1]
  );

  return { w: width, h: height, t: basicTexture };
}



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();

const imgWidth = 512;
const imgHeight = imgWidth;

// ----------------------------------------------------------

const texture0         = await loadTexture( 'https://webgpulab.xbdev.net/var/images/test512.png', imgWidth );
//const texture1         = await loadTexture( 'https://webgpulab.xbdev.net/var/images/avatar.png', imgWidth);

// ----------------------------------------------------------

// Basic canvas which will be used to display the output from the compute shader

let canvasa = document.createElement('canvas');
document.body.appendChild( canvasa ); canvasa.height = canvasa.width = imgWidth;
const context = canvasa.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 
console.log('presentationFormat:', presentationFormat );

context.configure({ device: device, 
                    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
                    format: "rgba8unorm" /*presentationFormat*/  });

let canvasTexture = context.getCurrentTexture();

// ----------------------------------------------------------

// Output texture - output from the compute shader written to this texture
// Copy this texutre to the 'canvas' - needs to be the same size as the output
// canvas size
const texture1 = device.createTexture({
  size: [imgWidth, imgHeight, 1],
  format: "rgba8unorm",
  usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING
});

// ----------------------------------------------------------


const timerUniformBuffer = device.createBuffer({
  size: 4, 
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

const timestep  = new Float32Array( [0.0] );

device.queue.writeBuffer(timerUniformBuffer,   0, timestep             );


// ----------------------------------------------------------

const GCOMPUTE = GPUShaderStage.COMPUTE;

// Bind group layout and bind group
const bindGroupLayout = device.createBindGroupLayout({
  entries: [ {binding: 0, visibility: GCOMPUTE, texture: { sampleType: "float" }   }, 
             {binding: 1, visibility: GCOMPUTE, buffer:  { type: "uniform"     }   },
             {binding: 2, visibility: GCOMPUTE, storageTexture: {format:"rgba8unorm", access:"write-only", viewDimension:"2d"}   }
           ]
});

const bindGroup = device.createBindGroup({
    layout: bindGroupLayout,
    entries: [  {   binding: 0,  resource: texture0.t.createView()         },
                  {   binding: 1,  resource: { buffer: timerUniformBuffer  } },
                {   binding: 2,  resource: texture1.createView()           }
    ]
});


// Compute shader code
const computeShader = ` 
@group(0) @binding(0) var myTexture0:  texture_2d<f32>; // input texture image
@group(0) @binding(1) var<uniform>     mytimer   : f32; // timer increments each frame
@group(0) @binding(2) var myTexture1:  texture_storage_2d<rgba8unorm, write>; // output image

// Default values as constants
const BRIGHTNESS : f32 = 0.1; // Adjust this value for brightness
const CONTRAST   : f32 = 1.2; // Adjust this value for contrast
const HUE        : f32 = 0.3; // Adjust this value for hue
const SATURATION : f32 = 1.1; // Adjust this value for saturation
const LIGHTNESS  : f32 = 0.1; // Adjust this value for lightness

// Function to adjust brightness and contrast
fn adjustBrightnessContrast(color: vec4<f32>, brightness: f32, contrast: f32) -> vec4<f32> {
    let adjustedColor = (color - vec4<f32>(0.5)) * contrast + vec4<f32>(0.5) + vec4<f32>(brightness);
    return clamp(adjustedColor, vec4<f32>(0.0), vec4<f32>(1.0));
}

// Function to convert RGB to HSL
fn rgbToHsl(color: vec3<f32>) -> vec3<f32> {
    let max = max(max(color.r, color.g), color.b);
    let min = min(min(color.r, color.g), color.b);
    let delta = max - min;

    let l = (max + min) / 2.0;

    if (delta == 0.0) {
        return vec3<f32>(0.0, 0.0, l);
    }

    var s = 0.0;
    if (l < 0.5) { s = delta / (max + min); } else { s = delta / (2.0 - max - min); };

    var h = 0.0;
    if (color.r == max) {
        var d = 0.0;
        if (color.g < color.b) { d=6.0; } else { d=0.0; }
        h = (color.g - color.b) / delta + d;
    } else if (color.g == max) {
       h =  (color.b - color.r) / delta + 2.0;
    } else {
       h = (color.r - color.g) / delta + 4.0;
    };

    return vec3<f32>(h / 6.0, s, l);
}

fn mymod(x:f32, y:f32) -> f32
{
    return ( x - y * floor(x/y) );
}

// Function to convert HSL to RGB
fn hslToRgb(hsl: vec3<f32>) -> vec3<f32> {
    let h = hsl.x;
    let s = hsl.y;
    let l = hsl.z;

    let c = (1.0 - abs(2.0 * l - 1.0)) * s;
    let x = c * (1.0 - abs(mymod(h * 6.0, 2.0 ) - 1.0));
    let m = l - c / 2.0;

    var r = 0.0;
    var g = 0.0;
    var b = 0.0;
    
    if (0.0 <= h && h < 1.0 / 6.0) {
        r=c; g=x; b=0.0;
    } else if (1.0 / 6.0 <= h && h < 1.0 / 3.0) {
        r=x; g=c; b=0.0;
    } else if (1.0 / 3.0 <= h && h < 1.0 / 2.0) {
        r=0.0; g=c; b=x;
    } else if (1.0 / 2.0 <= h && h < 2.0 / 3.0) {
        r=0.0; g=x; b=c;
    } else if (2.0 / 3.0 <= h && h < 5.0 / 6.0) {
        r=x; g=0.0; b=c;
    } else {
        r=c; g=0.0; b=x;
    };

    return vec3<f32>(r + m, g + m, b + m);
}

// Function to adjust HSL
fn adjustHSL(color: vec4<f32>, hue: f32, saturation: f32, lightness: f32) -> vec4<f32> {
    var hsl = rgbToHsl(color.rgb);
    hsl.x = mymod(hsl.x + hue, 1.0);
    hsl.y = clamp(hsl.y * saturation, 0.0, 1.0);
    hsl.z = clamp(hsl.z + lightness, 0.0, 1.0);
    var rgb = hslToRgb(hsl);
    return vec4<f32>(rgb, color.a);
}

@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 imgWidth  = f32( ${imgWidth}  ); 
    var imgHeight = f32( ${imgHeight} ); 

    var coords = vec2<f32>(f32(globalId.x), f32(globalId.y));
    var uv = coords / vec2<f32>(imgWidth, imgHeight); // normalize coordinates to 0.0 - 1.0 range

    // Load the color from the input texture
    var color = textureLoad(myTexture0, vec2<i32>(uv * vec2<f32>(imgWidth, imgHeight)), 0);

    // Adjust Brightness and Contrast
    var adjustedColor = adjustBrightnessContrast(color, BRIGHTNESS, CONTRAST);

    // Adjust Hue, Saturation, and Lightness
    var finalColor = adjustHSL(adjustedColor, HUE, SATURATION, LIGHTNESS);

    // Store the result in the output texture
    textureStore(myTexture1, vec2<i32>(globalId.xy), finalColor);
}
`;
  
// Pipeline setup
const computePipeline = device.createComputePipeline({
    layout :   device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout]}),
    compute: { module    : device.createShaderModule({code:computeShader}),
               entryPoint: "main" }
});

timestep[0] = timestep[0] + 0.1;
device.queue.writeBuffer(timerUniformBuffer,   0, timestep             );

// Commands submission
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups( imgWidth/8, imgWidth/8 );
await passEncoder.end();


canvasTexture = context.getCurrentTexture();

await
commandEncoder.copyTextureToTexture( { texture: texture1 },
                                    { texture: canvasTexture },
                                    { width:imgWidth, height:imgHeight, depthOrArrayLayers:1} );

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





Things to Try


• Try adjusting the colors of different types of images (sky, faces, sunsets, moon, ..).
• Adjust the constant parameters to get a feel for their impact/importance.





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-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.