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.
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.