A Blurred Goat in WebGL

Your browser does not support the canvas tag. This is a static example of what would be seen.

I’d quite like to have a go at building something more closely resembling a modern rendering pipeline out of WebGL and to do that you need to be able to render to targets, and transfer data between them. A simple variant on that theme is a two pass gaussian blur, blurring first horizontally and then taking the result and blurring vertically. That’s what we have here…

The image on the lefts shows the result of feeding a 7×7 texture with a single white dot into the blur filter and the right image shows what it looks like when we feed a goat into the same filter. Note that in the left case the brightness of the blurred image has been boosted 3x so you can see the pixels, otherwise the colours are dark enough the pattern isn’t easily visible.

I won’t go into the detail of what the shaders are doing as I’d rather focus on the new WebGL code I’m adding, but the technique is described elsewhere, for example ‘Efficient Gaussian blur with linear sampling’ for those that are interested. The shader I’m going to use looks like this and implements the same technique described there.

precision highp float;
varying vec4 v_texcoord;
uniform sampler2D s_colourSampler;
uniform vec4 u_blurOffsets;
uniform vec4 u_blurWeights;
void main(void) {
  vec3 rgb1 = texture2D(s_colourSampler, vec2(v_texcoord.xy)).rgb;
  vec3 rgb2 = texture2D(s_colourSampler, vec2(v_texcoord.xy) + u_blurOffsets.xy).rgb;
  vec3 rgb3 = texture2D(s_colourSampler, vec2(v_texcoord.xy) - u_blurOffsets.xy).rgb;
  vec3 rgb4 = texture2D(s_colourSampler, vec2(v_texcoord.xy) + u_blurOffsets.zw).rgb;
  vec3 rgb5 = texture2D(s_colourSampler, vec2(v_texcoord.xy) - u_blurOffsets.zw).rgb;
  vec3 rgb = (
      (rgb1) * u_blurWeights.x
    + (rgb2) * u_blurWeights.y 
    + (rgb3) * u_blurWeights.y
    + (rgb4) * u_blurWeights.z
    + (rgb5) * u_blurWeights.z);
  gl_FragColor = vec4(rgb, 1.0);

What I’m more interested in here is creating and binding targets in WebGL. The following functions builds a fairly standard RGBA colour buffer that can later be bound as the target for rendering operations. The width and height are stored as it can be handy to be able to query them later, say for example if we are matching a viewport to the target dimensions or need to be able to work out the size of a single pixel. The latter part of the function creates a WebGL framebuffer object, which is really just a way of telling WebGL that we want to be able to render to the texture and makes sure that WebGL regards what we’ve built as ‘complete’.

function createRenderTarget(gl, width, height) {

  var rt = {};
  rt.width = width;
  rt.height = height;

  /* create the texture */
  rt.texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, rt.texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  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);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

  /* create the framebuffer object */
  rt.fbo = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, rt.fbo);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rt.texture, 0);
  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
    throw new Error("gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE");

  return rt;
}

I’ve also made some changes to the createTexture function I’ve used in previous posts. It can be helpful to be able to use our render-target and texture objects interchangeably, so I’ve made sure that both have the same member names for common properties like the texture, width and height. After doing this I should be able to pass a render-target to any function that accepts a texture as input.

To demonstrate how to use the render-targets, this is the complete method used to generate the guassian blur shown in the example above. The relevant lines here are the calls to bindFramebuffer where we can choose to either bind a render-target we previously created, or else we can set null which binds to the canvas. In this case we write both the intermediate and target data to render-targets as for the purpose of making a the white dot part of the sample I needed to be able to fill the canvas using point filtering.

function doGuassianBlur5x5(gl, src, tmp_rt, dst_rt) {

  var blurShader = guassianBlur5x5Shader;

  /* *** pass 1 - horizontal *** */
		
  /* set the output buffer */
  gl.bindFramebuffer(gl.FRAMEBUFFER, tmp_rt.fbo);
  gl.viewport(0, 0, tmp_rt.width, tmp_rt.height);
	
  /* setup the blur constants */
  gl.useProgram(blurShader.program);
  gl.uniform4f(blurShader.texCoordScaleBias_loc, 0.5, -0.5, 0.5, 0.5);
  gl.uniform4f(blurShader.blurOffsets_loc, 
    1.3846153846 / src.width, 0.0, 
    3.2307692308 / src.width, 0.0);
  gl.uniform4f(blurShader.blurWeights_loc, 0.2270270270, 0.3162162162, 0.0702702703, 0.0);

  /* bind the source texture */
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, src.texture);
  gl.uniform1i(blurShader.colourSampler_loc, 0);
				
  /* execute the draw call */
  gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenQuadMesh.pos_buffer);
  gl.vertexAttribPointer(blurShader.pos_loc,
       fullscreenQuadMesh.pos_numComponents, 
       fullscreenQuadMesh.pos_type, 
       false,
       fullscreenQuadMesh.pos_stride, 
       0);
  gl.enableVertexAttribArray(blurShader.pos_loc);
  gl.drawArrays(gl.TRIANGLES, 0, 6);	
					
  /* *** pass 2 - vertical *** */

  /* set the output buffer */
  gl.bindFramebuffer(gl.FRAMEBUFFER, dst_rt.fbo);
  gl.viewport(0, 0, dst_rt.width, dst_rt.height);
			
  /* setup the blur constants -  */
  gl.uniform4f(blurShader.texCoordScaleBias_loc, 0.5, 0.5, 0.5, 0.5);
  gl.uniform4f(blurShader.blurOffsets_loc, 
    0.0, 1.3846153846 / src.height, 
    0.0, 3.2307692308 / src.height);
  gl.uniform4f(blurShader.blurWeights_loc, 0.2270270270, 0.3162162162, 0.0702702703, 0.0);

  /* bind the temp blur target as input */
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tmp_rt.texture);
  gl.uniform1i(blurShader.colourSampler_loc, 0);
			
  /* execute the draw call */
  gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenQuadMesh.pos_buffer);
  gl.vertexAttribPointer(blurShader.pos_loc,
       fullscreenQuadMesh.pos_numComponents, 
       fullscreenQuadMesh.pos_type, 
       false,
       fullscreenQuadMesh.pos_stride, 
       0);
  gl.enableVertexAttribArray(blurShader.pos_loc);
  gl.drawArrays(gl.TRIANGLES, 0, 6);

}