Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active December 6, 2025 05:28
Show Gist options
  • Select an option

  • Save greggman/ecdfa822182d90bbfe02df22663451e7 to your computer and use it in GitHub Desktop.

Select an option

Save greggman/ecdfa822182d90bbfe02df22663451e7 to your computer and use it in GitHub Desktop.
WebGPU: 16bit PNG
html, body {
margin: 0; /* remove the default margin */
height: 100%; /* make the html,body fill the page */
}
canvas {
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
<canvas></canvas>
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
import {createTextureFromImage} from 'https://webgpufundamentals.org/3rdparty/webgpu-utils-1.x.module.js';
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
import {mat4} from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// Get a WebGPU context from the canvas and configure it
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({
code: `
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
struct Uniforms {
matrix: mat4x4f,
};
@group(0) @binding(0) var<uniform> uni: Uniforms;
@group(0) @binding(1) var tex: texture_2d<f32>;
@group(0) @binding(2) var smp: sampler;
@vertex fn vs(@builtin(vertex_index) vNdx: u32) -> VSOutput {
let positions = array(
vec2f( 0, 0),
vec2f( 1, 0),
vec2f( 0, 1),
vec2f( 0, 1),
vec2f( 1, 0),
vec2f( 1, 1),
);
let pos = positions[vNdx];
return VSOutput(
uni.matrix * vec4f(pos, 0, 1),
pos,
);
}
@fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(tex, smp, fsInput.texcoord);
}
`,
});
const pipeline = device.createRenderPipeline({
label: 'textured unit quad',
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
targets: [{ format: presentationFormat }],
},
});
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
};
const imageUniformBuffer = device.createBuffer({
size: 4 * 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const imageTexture = await createTextureFromImage(
device,
'https://greggman.github.io/doodles/images/16bit-per-channel-smooth-1024x1024.png',
);
const imageSampler = device.createSampler({});
const imageBindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: imageUniformBuffer } },
{ binding: 1, resource: imageTexture.createView() },
{ binding: 2, resource: imageSampler },
],
});
function render() {
const canvasTexture = context.getCurrentTexture();
renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
// css 'cover'
const canvasAspect = canvas.clientWidth / canvas.clientHeight;
const imageAspect = imageTexture.width / imageTexture.height;
const aspect = canvasAspect / imageAspect;
const aspectScale = aspect > 1 ? [1, aspect, 1] : [1 / aspect, 1, 1];
const matrix = mat4.identity();
mat4.scale(matrix, aspectScale, matrix);
mat4.scale(matrix, [2, 2, 1], matrix);
mat4.translate(matrix, [-0.5, -0.5, 1], matrix);
// Set the uniform values in our JavaScript side Float32Array
device.queue.writeBuffer(imageUniformBuffer, 0, matrix);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, imageBindGroup);
pass.draw(6);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
}
render();
});
observer.observe(canvas);
}
function fail(msg) {
alert(msg);
}
main();
{"name":"WebGPU: 16bit PNG","settings":{},"filenames":["index.html","index.css","index.js"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment