<body>
  <div>
	  <canvas id="canvas"/>
  </div>
  <div id="app">
    <pre id="results"></pre>
  </div>
</body>
// Determines whether to render to the canvas framebuffer and display results, or to render to a texture and readout the results
const showCanvas = true;
const numRuns = 10;
// Image size
const width = 640;
const height = 480;
// Filter parameters. Sigma defines the width of the Gaussian; maskSize defines the number of samples to use in each direction.
// Increasing maskSize will result in a more accurate filter. A sensible minimum value for maskSize is be 2 * sigma + 1.
// DS9 uses a "smoothing factor" R, with sigma=r/2 and maskSize = 2*r+1. Defaults to match this
const smoothingFactor = 20;
const sigma = smoothingFactor / 2.0;
const maskSize = Math.floor(smoothingFactor * 2 + 1);

let canvas;
if (showCanvas) {
    canvas = document.getElementById("canvas");
} else {
    canvas = document.createElement("canvas");
}

canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl", {antialias: false, alpha: false});
gl.getExtension("OES_texture_float");

gl.viewport.height = width;
gl.viewport.height = height;


const targetTextureWidth = width;
const targetTextureHeight = height;

// create input texture
const textureIn = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textureIn);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, targetTextureWidth, targetTextureHeight, 0, gl.LUMINANCE, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

// create intermediate texture
const intermediateTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, intermediateTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

// create output texture
const targetTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, targetTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

// Create and bind the framebuffer
const frameBufferIntermediate = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBufferIntermediate);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, intermediateTexture, 0);

let frameBufferFinal = gl.createFramebuffer();

if (!showCanvas) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBufferFinal);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetTexture, 0);
}

const dataIn = new Float32Array(width * height);
const dataOut = new Float32Array(width * height);
const dataOutUint8 = new Uint8Array(dataOut.buffer);

function getShaderFromString(shaderScript, type) {
    if (
        !shaderScript ||
        !(
            type === WebGLRenderingContext.VERTEX_SHADER ||
            type === WebGLRenderingContext.FRAGMENT_SHADER
        )
    ) {
        return null;
    }

    let shader = gl.createShader(type);
    gl.shaderSource(shader, shaderScript);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, WebGLRenderingContext.COMPILE_STATUS)) {
        console.log(gl.getShaderInfoLog(shader));
        return null;
    }
    return shader;
}

const vertexShaderString = `
precision highp float;

// xy = vertex position in normalized device coordinates ([-1,+1] range).
attribute vec2 vertexPositionNDC;

varying vec2 vTexCoords;

const vec2 scale = vec2(0.5, 0.5);

void main()
{
    vTexCoords  = vertexPositionNDC * scale + scale; // scale vertex attribute to [0,1] range
    gl_Position = vec4(vertexPositionNDC, 0.0, 1.0);
}
`;

const fragmentShaderString = `
precision highp float;
uniform sampler2D colorMap;
uniform vec2 iResolution;
uniform bool bEncodeFloats;
uniform bool bVerticalBlur;
uniform float fSigma;
uniform int iMaskSize;
varying vec2 vTexCoords;

float normpdf(in float x, in float sigma)
{
	return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma;
}


float shift_right(float v, float amt) {
  v = floor(v) + 0.5;
  return floor(v / exp2(amt));
}
float shift_left(float v, float amt) {
  return floor(v * exp2(amt) + 0.5);
}

float mask_last(float v, float bits) {
  return mod(v, shift_left(1.0, bits));
}
float extract_bits(float num, float from, float to) {
  from = floor(from + 0.5);
  to = floor(to + 0.5);
  return mask_last(shift_right(num, from), to - from);
}

vec4 encode_float(float val) {
  if (val == 0.0)
    return vec4(0, 0, 0, 0);
  float sign = val > 0.0 ? 0.0 : 1.0;
  val = abs(val);
  float exponent = floor(log2(val));
  float biased_exponent = exponent + 127.0;
  float fraction = ((val / exp2(exponent)) - 1.0) * 8388608.0;
  
  float t = biased_exponent / 2.0;
  float last_bit_of_biased_exponent = fract(t) * 2.0;
  float remaining_bits_of_biased_exponent = floor(t);
  
  float byte4 = extract_bits(fraction, 0.0, 8.0) / 255.0;
  float byte3 = extract_bits(fraction, 8.0, 16.0) / 255.0;
  float byte2 = (last_bit_of_biased_exponent * 128.0 + extract_bits(fraction, 16.0, 23.0)) / 255.0;
  float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;
  return vec4(byte4, byte3, byte2, byte1);
}

void main()
{
  vec2 fragCoords = vTexCoords.xy * iResolution.xy;
  float c = texture2D(colorMap, fragCoords.xy / iResolution.xy).r;
  //declare stuff
  const int mSize = ${maskSize.toFixed(0)};
  const int kSize = (mSize-1)/2;
  float kernel[mSize];
  float final_colour = 0.0;
  
  //create the 1-D kernel  
  float Z = 0.0;
  for (int j = 0; j <= kSize; ++j)
  {
    kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), fSigma);
  }
  
  //get the normalization factor (as the gaussian has been clamped)
  for (int j = 0; j < mSize; ++j)
  {
    Z += kernel[j];
  }
  
  //read out the texels
  int i = 0;
 
    for (int j=-kSize; j <= kSize; ++j)
    {
      vec2 offset = bVerticalBlur ? vec2(float(i), float(j)) : vec2(float(j), float(i));      
      final_colour += kernel[kSize+j]*texture2D(colorMap, (fragCoords.xy+offset) / iResolution.xy).r;

    }
  
  
  float outVal = final_colour/(Z);
  float inVal = texture2D(colorMap, vTexCoords).r;
  float result = outVal;
  if (bEncodeFloats) {
    gl_FragColor = encode_float(result);    
  }
  else {
    gl_FragColor = vec4(result, result, result, 1.0);
  }
  
  
}
`;

const vertexShader = getShaderFromString(
    vertexShaderString,
    WebGLRenderingContext.VERTEX_SHADER
);
const fragmentShader = getShaderFromString(
    fragmentShaderString,
    WebGLRenderingContext.FRAGMENT_SHADER
);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
const vertexPositionAttribute = gl.getAttribLocation(
    shaderProgram,
    "vertexPositionNDC"
);

const resolutionLocation = gl.getUniformLocation(shaderProgram, "iResolution");
gl.uniform2f(resolutionLocation, width, height);
const encodeFloatsLocation = gl.getUniformLocation(shaderProgram, "bEncodeFloats");
gl.uniform1i(encodeFloatsLocation, !showCanvas);
const sigmaLocation = gl.getUniformLocation(shaderProgram, "fSigma");
gl.uniform1f(sigmaLocation, sigma);
const verticalBlurLocation = gl.getUniformLocation(shaderProgram, "bVerticalBlur");
gl.uniform1i(verticalBlurLocation, 0);

function drawFullScreenQuad(shaderProgram) {
    if (!shaderProgram) {
        return;
    }

    var verts = [
        // First triangle:
        1.0,
        1.0,
        -1.0,
        1.0,
        -1.0,
        -1.0,
        // Second triangle:
        -1.0,
        -1.0,
        1.0,
        -1.0,
        1.0,
        1.0
    ];
    const screenQuadVBO = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, screenQuadVBO);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);

    // Bind:
    gl.bindBuffer(gl.ARRAY_BUFFER, screenQuadVBO);
    gl.enableVertexAttribArray(vertexPositionAttribute);
    gl.vertexAttribPointer(vertexPositionAttribute, 2, gl.FLOAT, false, 0, 0);

    // Draw 6 vertexes => 2 triangles:
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // Cleanup:
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
}

// Fill Array

function fillBlock(xOffset, yOffset, w, h, val) {
    for (let i = 0; i < w; i++) {
        for (let j = 0; j < h; j++) {
            const x = xOffset + i;
            const y = yOffset + j;
            dataIn[y * width + x] = val;
        }
    }
}

function fillLineDiag(xOffset, yOffset, l, stripe, val) {
    for (let i = 0; i < l; i++) {
        const x = xOffset + i;
        const y = height - yOffset - i;
        const penDown = (i / stripe) % 2 < 1.0;
        dataIn[y * width + x] = penDown ? val : 0;
    }
}

for (let i = 0; i < dataIn.length; i++) {
    dataIn[i] = 0;
}

fillBlock(100, 100, 100, 100, 5);
fillBlock(200, 200, 100, 100, 4);
fillBlock(300, 300, 100, 100, 3);
fillBlock(100, 300, 100, 100, 2);
fillBlock(300, 100, 100, 100, 1);

fillBlock(500, 500, 50, 50, 1);
fillBlock(550, 550, 50, 50, 2);
fillBlock(600, 600, 50, 50, 3);
fillBlock(500, 600, 50, 50, 4);
fillBlock(600, 500, 50, 50, 5);

fillBlock(200, 700, 500, 10, 2);

fillLineDiag(10, 10, 800, 10, 3);
fillLineDiag(90, 10, 800, 10, 4);
fillLineDiag(170, 10, 800, 10, 5);

let uploadTime = 0;
let downloadTime = 0;
let totalTime = 0;

for (let i = 0; i < numRuns; i++) {
    // Upload data
    let startTimeUpload = performance.now();
    gl.bindTexture(gl.TEXTURE_2D, textureIn);
    gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.LUMINANCE, gl.FLOAT, dataIn);    
    gl.finish();
    let endTime = performance.now();
    uploadTime += endTime - startTimeUpload;

    // Horizontal blur first pass
    gl.uniform1i(encodeFloatsLocation, false);
    gl.uniform1i(verticalBlurLocation, false);
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBufferIntermediate);    
    drawFullScreenQuad(shaderProgram);
    
   if (!showCanvas) {
        gl.uniform1i(encodeFloatsLocation, true);
        gl.bindFramebuffer(gl.FRAMEBUFFER, frameBufferFinal);        
    }
    else {
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.uniform1i(verticalBlurLocation, false);
    }
  
    gl.bindTexture(gl.TEXTURE_2D, intermediateTexture);
    gl.uniform1i(verticalBlurLocation, true);
  
    drawFullScreenQuad(shaderProgram);
    gl.finish();
    // Download data
    let startTimeDownload = performance.now();
    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, dataOutUint8);
    endTime = performance.now();
    downloadTime += endTime - startTimeDownload;
    totalTime += endTime - startTimeUpload;
}
uploadTime /= numRuns;
downloadTime /= numRuns;
totalTime /= numRuns;

const x = 100;
const y = 150;
let dataString = "Data readout disabled (Set showCanvas to false to enable) ";
if (!showCanvas) {
    dataString = `[${dataOut[y * width + x].toFixed(4)}, ${dataOut[y * width + x + 1].toFixed(4)}]`;
}

const sizeMB = dataIn.byteLength * 1e-6;
const uploadSpeed = sizeMB / uploadTime * 1e3;
const downloadSpeed = sizeMB / downloadTime * 1e3;

const uploadString = `Uploaded ${sizeMB.toFixed(2)} MB in ${uploadTime.toFixed(
    2
)} ms (${uploadSpeed.toFixed(2)} MB/s)`;
const downloadString = `Downloaded ${sizeMB.toFixed(
    2
)} MB in ${downloadTime.toFixed(2)} ms (${downloadSpeed.toFixed(2)} MB/s)`;

document.getElementById("results").innerHTML =
    `Average over ${numRuns} runs: sigma=${sigma}, M=${maskSize}\n${dataString}\n${uploadString}\n${downloadString}\nShader time ${(totalTime - uploadTime - downloadTime).toFixed(2)} ms\nTotal time ${totalTime.toFixed(2)} ms`;

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.