WebGL Deferred Shading

We recently saw this great post about using the WEBGLdrawbuffers extension for deferred shading and wanted to share our current approach.

The first pass in deferred shading writes a bunch of properties to a g-buffer. Our g-buffer uses a floating point texture with the OEStexturefloat extension and has 5 components: color, normals, gloss, metallic, and depth.

As a reminder, here's a screenshot of the final output:

2014 02 composited Final output, interactive demo here

Here are screenshots of just the individual components of the same scene:

2014 02 gbuffer diffuse Color component

2014 02 gbuffer normals Normal component

2014 02 gbuffer gloss Gloss component

2014 02 gbuffer metallic Metallic component

2014 02 gbuffer depth Depth component

This is the shader code we use to encode and decode the gbuffer:

{% highlight glsl %} vec4 encodeGBuffer(const in gBufferComponents components, const in vec2 texCoords, const in vec2 resolution, const in float clipFar) { vec4 res; vec3 diffuseYcocg = rgbToYcocg(components.diffuse); vec2 diffuseYc = getCheckerboard(texCoords, resolution) > 0.0 ? diffuseYcocg.xy : diffuseYcocg.xz; diffuseYc.y += 0.5; //Divide by clip far * 2.0 to sneak distance into a 0 to 1 range float depth = components.depth / (clipFar*2.0); //Scale normal from -1 to 1 to 0 to 1 range vec3 normal = components.normal * 0.5 + 0.5; //Scale down float components to 0 to 0.5 res = vec4(normal, depth) * 0.5; res.xyz += floor(vec3(diffuseYc, components.gloss) * 255.0); res.w *= components.metallic; return res; }

gBufferComponents decodeGBuffer(const in sampler2D gBufferSampler, const in vec2 texCoords, const in vec2 resolution, const in float clipFar) { gBufferComponents res; vec4 encodedGBuffer = texture2D(gBufferSampler, texCoords).rgba; vec3 floatComponents = fract(encodedGBuffer.xyz) * 2.0; res.normal = floatComponents.xyz * 2.0 - 1.0; res.depth = abs(encodedGBuffer.w) * 4.0 * clipFar; res.metallic = sign(encodedGBuffer.w); const float byteToFloat = 1.0/255.0; vec3 byteComponents = floor(encodedGBuffer.xyz) * byteToFloat; res.gloss = byteComponents.z; vec3 diffuseYcocg; diffuseYcocg.x = byteComponents.x; float pixelOffsetCoordsX = 1.0 / resolution.x; float offsetDir = getCheckerboard(texCoords, resolution); vec2 diffuseChroma; diffuseChroma.x = byteComponents.y - 0.5; diffuseChroma.y = texture2D(gBufferSampler, texCoords + vec2(pixelOffsetCoordsX*offsetDir, 0.0)).y; diffuseChroma.y = floor(diffuseChroma.y) * byteToFloat - 0.5; diffuseYcocg.yz = offsetDir > 0.0 ? diffuseChroma.xy : diffuseChroma.yx; res.diffuse = ycocgToRgb(diffuseYcocg); return res; } {% endhighlight %}

After the gbuffer pass, we follow up with several passes that add shadows, direct lighting, and other visual effects.

This approach is very memory and bandwith intensive and we're always looking for ways to improve it. Got ideas? We'd love to hear them.