Interactive Shader Effects Series
<li><strong>Lighting</strong>
<ul>
<li><a href="/posts/the-basics-of-3d-lighting">Basics of Lighting</a></li>
<li>Bump Mapping</li>
<li>Cel Shading</li>
<li>Comparison of Reflectance Models</li>
<li><a href="/posts/shader-effects-gamma-correction">Gamma Correction</a></li>
<li>High Dynamic Range (HDR)</li>
<li>Screen Space Ambient Occlusion</li>
<li><a href="/posts/shader-effects-shadow-mapping">Shadow Mapping</a></li>
</ul>
</li>
<li><strong>Materials</strong>
<ul>
<li>Skin</li>
<li>Snow and Ice</li>
<li>Water</li>
</ul>
</li>
<li><strong>Miscellaneous</strong>
<ul>
<li>Transition Effects</li>
</ul>
</li>
<li><strong>Procedural</strong>
<ul>
<li>2D, 3D, 4D Noise</li>
<li>Sky / Cloud Generation</li>
<li>Terrain</li>
</ul>
</li>
<p>In order to run this demo, you must meet the following requirements.</p>
<ul class="UlList">
<li>You are running the latest version of Mozilla Firefox, Google Chrome, or Safari.</li>
<li>You have a WebGL compatible video card with the latest drivers.</li>
<li>Your video card is not blacklisted. You can check the current blacklist on <a href="http://www.khronos.org/webgl/wiki/BlacklistsAndWhitelists">Khronos</a>.</li>
</ul>
<p>Some browsers may require additional configuration in order to get WebGL to run. If you are having problems running this demo, visit the following sites.</p>
<ul class="UlList">
<li><a href="https://wiki.mozilla.org/Blocklisting/Blocked_Graphics_Drivers#How_to_force-enable_blocked_graphics_features">Firefox: How to force-enable blocked graphics features</a>.</li>
<li><a href="http://www.borfast.com/blog/how-enable-webgl-google-chrome-linux-blacklisted-graphics-card">Chrome: How to enable WebGL with a blacklisted graphics card.</a></li>
<li><a href="https://discussions.apple.com/thread/3300585?start=0&tstart=0">Safari: How to enable WebGL</a>.</li>
</ul>
</div>
<div id="WebGLDemo">
<!-- WebGL Canvas -->
<canvas id="Canvas" width="490" height="367">
</canvas>
<!-- Loading Progress -->
<div id="DivLoading">
<span>Loading </span><span id="TxtLoadingProgress"></span><span>%</span>
</div>
<!-- Controls -->
<div id="DivControls">
<div>
<h4>Source Texture</h4>
<select id="CBoxSource" autocomplete="off">
<option>Colour</option>
<option selected="selected">RCMP</option>
<option>Face</option>
</select>
</div>
<div id="DivSrcColour">
<h4>Source Colour</h4>
<div id="SrcRedSlider" class="RedSlider"><div id="SrcRedSliderTxt" class="SliderTextValue"></div></div>
<div id="SrcGreenSlider" class="GreenSlider"><div id="SrcGreenSliderTxt" class="SliderTextValue"></div></div>
<div id="SrcBlueSlider" class="BlueSlider"><div id="SrcBlueSliderTxt" class="SliderTextValue"></div></div>
</div>
<br />
<div>
<h4>Dest Texture</h4>
<select id="CBoxDestination" autocomplete="off">
<option>Colour</option>
<option selected="selected">RCMP</option>
<option>Face</option>
</select>
</div>
<div id="DivDstColour">
<h4>Dest Colour</h4>
<div id="DstRedSlider" class="RedSlider"><div id="DstRedSliderTxt" class="SliderTextValue"></div></div>
<div id="DstGreenSlider" class="GreenSlider"><div id="DstGreenSliderTxt" class="SliderTextValue"></div></div>
<div id="DstBlueSlider" class="BlueSlider"><div id="DstBlueSliderTxt" class="SliderTextValue"></div></div>
</div>
<br />
<div>
<h4>Blend Op</h4>
<select id="CBoxBlendMode" autocomplete="off">
<option>Add</option>
<option>Subtract</option>
<option>Multiply</option>
<option>Darken</option>
<option>Colour Burn</option>
<option>Linear Burn</option>
<option>Lighten</option>
<option>Screen</option>
<option>Colour Dodge</option>
<option>Linear Dodge</option>
<option>Overlay</option>
<option>Soft Light</option>
<option>Hard Light</option>
<option>Vivid Light</option>
<option>Linear Light</option>
<option>Pin Light</option>
<option>Difference</option>
<option>Exclusion</option>
</select>
</div>
</div>
</div>
</div>
<div id="tabs-2">
<div id="vertex-tabs">
<ul>
<li><a href="#vertex-tabs-1">Image</a></li>
</ul>
<div id="vertex-tabs-1">
<pre class="prettyprint">
<code class="language-cpp">
/// <summary>
/// Vertex shader for rendering a 2D plane on the screen. The plane should be sized
/// from -1.0 to 1.0 in the x and y axis. This shader can be shared amongst multiple
/// post-processing fragment shaders.
/// </summary>
/// <summary>
/// Attributes.
/// <summary>
attribute vec3 Vertex;
attribute vec2 Uv;
/// <summary>
/// Varying variables.
/// <summary>
varying vec2 vUv;
/// <summary>
/// Vertex shader entry.
/// <summary>
void main ()
{
gl_Position = vec4(Vertex, 1.0);
// Flip y-axis
vUv = vec2(Uv.x, 1.0 - Uv.y);
}
<div id="fragment-tabs-1">
<pre class="prettyprint">
<code class="language-cpp">
/// <summary>
/// Fragment shader for blending 2 images.
/// </summary>
ifdef GL_ES
precision highp float;
endif
/// <summary>
/// This uber-shader uses this pre-processor directive to specify which blend operation
/// will be performed at runtime. This is preferred over writing a dozen separate fragment
/// shaders.
/// <summary>
define {BLEND_MODE}
/// <summary>
/// Uniform variables.
/// <summary>
uniform vec4 DstColour; // Colour (tint) applied to destination texture.
uniform vec4 SrcColour; // Colour (tint) applied to source texture
uniform sampler2D Sample0; // Background layer (AKA: Destination)
uniform sampler2D Sample1; // Foreground layer (AKA: Source)
/// <summary>
/// Varying variables.
/// <summary>
varying vec2 vUv;
/// <summary>
/// Blend the source and destination pixels.
/// This function does not precompute alpha channels. To learn more about the equations that
/// factor in alpha blending, see http://www.w3.org/TR/2009/WD-SVGCompositing-20090430/.
/// <summary>
/// <param name="src">Source (foreground) pixel.</param>
/// <param name="dst">Destiantion (background) pixel.</param>
/// <returns>The blended pixel.</returns>
vec3 blend (vec3 src, vec3 dst)
{
ifdef ADD
return src + dst;
endif
ifdef SUBTRACT
return src - dst;
endif
ifdef MULTIPLY
return src * dst;
endif
ifdef DARKEN
return min(src, dst);
endif
ifdef COLOUR_BURN
return vec3((src.x == 0.0) ? 0.0 : (1.0 - ((1.0 - dst.x) / src.x)),
(src.y == 0.0) ? 0.0 : (1.0 - ((1.0 - dst.y) / src.y)),
(src.z == 0.0) ? 0.0 : (1.0 - ((1.0 - dst.z) / src.z)));
endif
ifdef LINEAR_BURN
return (src + dst) - 1.0;
endif
ifdef LIGHTEN
return max(src, dst);
endif
ifdef SCREEN
return (src + dst) - (src * dst);
endif
ifdef COLOUR_DODGE
return vec3((src.x == 1.0) ? 1.0 : min(1.0, dst.x / (1.0 - src.x)),
(src.y == 1.0) ? 1.0 : min(1.0, dst.y / (1.0 - src.y)),
(src.z == 1.0) ? 1.0 : min(1.0, dst.z / (1.0 - src.z)));
endif
ifdef LINEAR_DODGE
return src + dst;
endif
ifdef OVERLAY
return vec3((dst.x <= 0.5) ? (2.0 * src.x * dst.x) : (1.0 - 2.0 * (1.0 - dst.x) * (1.0 - src.x)),
(dst.y <= 0.5) ? (2.0 * src.y * dst.y) : (1.0 - 2.0 * (1.0 - dst.y) * (1.0 - src.y)),
(dst.z <= 0.5) ? (2.0 * src.z * dst.z) : (1.0 - 2.0 * (1.0 - dst.z) * (1.0 - src.z)));
endif
ifdef SOFT_LIGHT
return vec3((src.x <= 0.5) ? (dst.x - (1.0 - 2.0 * src.x) * dst.x * (1.0 - dst.x)) : (((src.x > 0.5) && (dst.x <= 0.25)) ? (dst.x + (2.0 * src.x - 1.0) * (4.0 * dst.x * (4.0 * dst.x + 1.0) * (dst.x - 1.0) + 7.0 * dst.x)) : (dst.x + (2.0 * src.x - 1.0) * (sqrt(dst.x) - dst.x))),
(src.y <= 0.5) ? (dst.y - (1.0 - 2.0 * src.y) * dst.y * (1.0 - dst.y)) : (((src.y > 0.5) && (dst.y <= 0.25)) ? (dst.y + (2.0 * src.y - 1.0) * (4.0 * dst.y * (4.0 * dst.y + 1.0) * (dst.y - 1.0) + 7.0 * dst.y)) : (dst.y + (2.0 * src.y - 1.0) * (sqrt(dst.y) - dst.y))),
(src.z <= 0.5) ? (dst.z - (1.0 - 2.0 * src.z) * dst.z * (1.0 - dst.z)) : (((src.z > 0.5) && (dst.z <= 0.25)) ? (dst.z + (2.0 * src.z - 1.0) * (4.0 * dst.z * (4.0 * dst.z + 1.0) * (dst.z - 1.0) + 7.0 * dst.z)) : (dst.z + (2.0 * src.z - 1.0) * (sqrt(dst.z) - dst.z))));
endif
ifdef HARD_LIGHT
return vec3((src.x <= 0.5) ? (2.0 * src.x * dst.x) : (1.0 - 2.0 * (1.0 - src.x) * (1.0 - dst.x)),
(src.y <= 0.5) ? (2.0 * src.y * dst.y) : (1.0 - 2.0 * (1.0 - src.y) * (1.0 - dst.y)),
(src.z <= 0.5) ? (2.0 * src.z * dst.z) : (1.0 - 2.0 * (1.0 - src.z) * (1.0 - dst.z)));
endif
ifdef VIVID_LIGHT
return vec3((src.x <= 0.5) ? (1.0 - (1.0 - dst.x) / (2.0 * src.x)) : (dst.x / (2.0 * (1.0 - src.x))),
(src.y <= 0.5) ? (1.0 - (1.0 - dst.y) / (2.0 * src.y)) : (dst.y / (2.0 * (1.0 - src.y))),
(src.z <= 0.5) ? (1.0 - (1.0 - dst.z) / (2.0 * src.z)) : (dst.z / (2.0 * (1.0 - src.z))));
endif
ifdef LINEAR_LIGHT
return 2.0 * src + dst - 1.0;
endif
ifdef PIN_LIGHT
return vec3((src.x > 0.5) ? max(dst.x, 2.0 * (src.x - 0.5)) : min(dst.x, 2.0 * src.x),
(src.x > 0.5) ? max(dst.y, 2.0 * (src.y - 0.5)) : min(dst.y, 2.0 * src.y),
(src.z > 0.5) ? max(dst.z, 2.0 * (src.z - 0.5)) : min(dst.z, 2.0 * src.z));
endif
ifdef DIFFERENCE
return abs(dst - src);
endif
ifdef EXCLUSION
return src + dst - 2.0 * src * dst;
endif
}
/// <summary>
/// Fragment shader entry.
/// <summary>
void main ()
{
// Get samples from both layers
vec4 dst = texture2D(Sample0, vUv) DstColour;
vec4 src = texture2D(Sample1, vUv) SrcColour;
// Apply blend operation
vec3 colour = clamp(blend(src.xyz, dst.xyz), 0.0, 1.0);
// Set fragment
gl_FragColor.xyz = colour;
gl_FragColor.w = 1.0;
}
Blend Modes
In the image at the top of the post: lightcycle from Tron Legacy demonstrating a bloom effect. A technique achieved by blending a glowmap with the underlying image.
Introduction
When compositing two or more images, you mix them together using some sort of blend operation. The common blending operation is to mix the foreground onto the background taking into account the alpha channels between the two. This can be handled for you in the fixed function pipeline, but most other useful blending operations must be calculated manually in your shaders. This is primarily a mathematical exercise, so each blending operation will be listed below along with its formula.
Add
pixel = src + dst
OpenGL equivalent: glBlendFunc(GL_ONE, GL_ONE)
Uses: Particles, glows (bloom), lens flare, bright sources.
Subtract
pixel = src - dst
OpenGL equivalent: glBlendEquation(GL_FUNC_SUBTRACT)
Uses: Lens filters (sunglasses).
Multiply
dst
Uses: Grayscale colour tinting.
Darken
pixel = min(src, dst)
OpenGL equivalent: glBlendEquation(GL_MIN)
Uses: Colour filtering, maintain shadows, adjust contrast.
Colour Burn
[pixel = \left{
\begin{matrix}
0.0 & src = 0.0 \
1.0 - ((1.0 - dst) / src) & src \ne 0.0
\end{matrix}
\right} ]
Uses: Strengthen shadows, colour saturation.
Linear Burn
pixel = (src + dst) - 1.0
Uses: Stronger variant to multiply, strengthen shadows.
Lighten
pixel = max(src, dst)
OpenGL equivalent: glBlendEquation(GL_MAX)
Uses: Colour filtering, maintain highlights, adjust contrast.
Screen
pixel = (src + dst) - (src
dst)
Uses: Increase brightness, used frequently with bloom.
Colour Dodge
[pixel = \left{
\begin{matrix}
1.0 & src = 1.0 \
min(1.0, dst / (1.0 - src)) & src \ne 1.0
\end{matrix}
\right} ]
Uses: Overexposures, increase vividness.
Linear Dodge
pixel = src + dst
Same as addition, different name.
Overlay
[pixel = \left{
\begin{matrix}
2.0
src dst & dst \le 0.5 \
1.0 - 2.0
(1.0 - dst) (1.0 - src) & dst \gt 0.5
\end{matrix}
\right} ]
Uses: Sepia effect, duotones, colour grading, tint effect.
Soft Light
[pixel = \left{
\begin{matrix}
dst - (1.0 - 2.0
src) dst
(1.0 - dst) & src \le 0.5 \
dst + (2.0 src - 1.0)
(4.0 dst
(4.0 dst + 1.0)
(dst - 1.0) + 7.0 dst) & src \gt 0.5, dst \le 0.25 \
dst + (2.0
src - 1.0) (\sqrt{dst} - dst) & src \gt 0.5, dst \gt 0.25
\end{matrix}
\right} ]
Uses: Modifying contrast, exposing shadows and highlights, vivid bloom.
Hard Light
[pixel = \left{
\begin{matrix}
2.0
src dst & src \le 0.5 \
1.0 - 2.0
(1.0 - dst) (1.0 - src) & src \gt 0.5
\end{matrix}
\right} ]
Uses: Identical to overlay except src and dst are swapped. Creates an overlapping colour.
Vivid Light
[pixel = \left{
\begin{matrix}
1.0 - (1.0 - dst) / (2.0
(src - 0.5)) & src \gt 0.5 \
dst / (1.0 - 2.0 src) & src \le 0.5
\end{matrix}
\right} ]
Uses: Combines colour dodge and colour burn based on pixel intensity. Used for special effects.
Linear Light
[pixel = \left{
\begin{matrix}
dst + 2.0
(src - 0.5) & src \gt 0.5 \
dst + 2.0 src - 1.0 & src \le 0.5
\end{matrix}
\right} ]
Uses: Combines linear dodge and linear burn based on pixel intensity. Used for special effects.
Pin Light
[pixel = \left{
\begin{matrix}
max(dst, 2.0
(src - 0.5)) & src \gt 0.5 \
min(dst, 2.0 src) & src \le 0.5
\end{matrix}
\right} ]
Uses: Combines lighten and darken based on pixel intensity. Used for special effects.
References
-
W3C SVG Working Group (2009-04-30). “SVG Compositing Specification”. Wikipedia. Retrieved 2012-06-26.
-
Wikipedia Editors (2012-05-31). “Blend modes”. Wikipedia. Retrieved 2012-06-26.
The source code for this project is made freely available for download. The ZIP package below contains both the HTML and JavaScript files to replicate this WebGL demo.
The source code utilizes the Nutty Open WebGL Framework, which is an open sourced, simplified version of the closed source Nutty WebGL Framework. It is released under a modified MIT license, so you are free to use if for personal and commercial purposes.
Download Source