16bit Histogram in WebGL2

by Gregg Tavares

HTML

<script id="hist-vs" type="not-js">
#version 300 es

uniform highp usampler2D u_texture;
uniform uvec4 u_colorMult;

void main() {
  const int mipLevel = 0;
  ivec2 size = textureSize(u_texture, mipLevel);
  
  // based on an id (0, 1, 2, 3 ...) compute the pixel x, y for the source image
  ivec2 pixel = ivec2(
      gl_VertexID % size.x, 
      gl_VertexID / size.x);

  // get the pixels but 0 out channels we don't want
  uvec4 color = texelFetch(u_texture, pixel, mipLevel) * u_colorMult;

  // add up all the channels. Since 3 are zeroed out we'll get just one channel
  uint colorSum = color.r + color.g + color.b + color.a;
  
  // set the position to be over a single pixel in the 256x256 destination texture
  uvec2 pos = uvec2(
     colorSum % 256u,
     colorSum / 256u); 

  gl_Position = vec4(((vec2(pos) + 0.5) / 256.0) * 2.0 - 1.0, 0, 1);
  gl_PointSize = 1.0;
}
</script>
<script id="hist-fs" type="not-js">
#version 300 es
precision highp float;

out vec4 color;
void main() {
  color = vec4(1);
}
</script>
<script id="max-fs" type="not-js">
#version 300 es
precision mediump float;

uniform sampler2D u_texture;

out vec4 outColor;

void main() {
  vec4 maxColor = vec4(0);

  // we know the texture is 256x256 so just go over the whole thing
  for (int y = 0; y < 256; ++y) {
    for (int x = 0; x < 256; ++x) {
      ivec2 uv = ivec2(x, y);

      // get max value of pixel
      maxColor = max(maxColor, texelFetch(u_texture, uv, 0));
    }
  }

  outColor = maxColor;
}
</script>
<script id="show-vs" type="not-js">
#version 300 es
in vec4 position;
void main() {
  gl_Position = position;
}
</script>
<script id="show-fs" type="not-js">
#version 300 es
precision mediump float;

uniform sampler2D u_histTexture;
uniform vec2 u_resolution;
uniform sampler2D u_maxTexture;

out vec4 outColor;

void main() {
  // get the max color constants
  vec4 maxColor = texture(u_maxTexture, vec2(0));

  // compute a UV 0 to 1 (only)
  vec2 uv = floor(gl_FragCoord.xy) /...

CSS

img, canvas { border: 1px solid black; margin: 5px; }
img { max-width: 240px; }

JavaScript

"use strict";

/* globals document, alert, twgl */

async function main() {
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 120;
  const m4 = twgl.m4;
  const gl = canvas.getContext("webgl2");
  if (!gl) {
    return log('need WebGL2');
  }
  const ext = gl.getExtension("EXT_color_buffer_float");
  if (!ext) {
     return log("requires EXT_color_buffer_float");
  }

  // required link: https://www.flickr.com/photos/greggman/18414763798/in/album-72157653822314919/
  
	const img = await loadImage("https://i.imgur.com/9WFTldz.jpg");
  
  // make image 16bit
  const ctx = document.createElement('canvas').getContext('2d');
  ctx.canvas.width = img.width;
  ctx.canvas.height = img.height;  
  ctx.drawImage(img, 0, 0);
  /*
  ctx.canvas.width = 1;
  ctx.canvas.height = 1;
  ctx.fillStyle = 'rgb(2, 0, 0)'; ctx.fillRect(0, 0, 1, 1);
  ctx.fillStyle = 'rgb(1, 0, 0)'; ctx.fillRect(1, 0, 1, 1);
  ctx.fillStyle = 'rgb(2, 0, 0)'; ctx.fillRect(2, 0, 1, 1);
  */
  
  // get imgae data at 16bit values
  const data = new Uint16Array(ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data);
  // expand 8bit values to 16 bit valus
  for (let i = 0; i < data.length; ++i) {
    data[i] *= 256;
  }
  
  // upload as 16bit unsigned ints
  const tex = twgl.createTexture(gl, {
    src: data,
    width: ctx.canvas.width,
    internalFormat: gl.RGBA16UI,
    minMag: gl.NEAREST,
    wrap: gl.CLAMP_TO_EDGE,
  });
  
  log("img");
  document.body.appendChild(img);
  log("histogram");
  document.body.appendChild(canvas);  
  
  const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
  const histProgramInfo = twgl.createProgramInfo(gl, ["hist-vs", "hist-fs"]);

  // make a 256x256 RGBA floating point texture and attach to a framebuffer
  const sumFbi = twgl.createFramebufferInfo(gl, [
    { 
      internalFormat: gl.RGBA32F,
      minMag: gl.NEAREST,
      wrap: gl.CLAMP_TO_EDGE,
    },
  ], 256, 256);

  // Render sum of each...