Shadow Volumes in Alpha

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
m (Text replace - "</javascript>" to "</syntaxhighlight>")
m (Text replace - "<shaderlab>" to "<syntaxhighlight lang="shaderlab">")
Line 161: Line 161:
==ShaderLab - ShadowVolumeExtrusion.shader==
==ShaderLab - ShadowVolumeExtrusion.shader==
<syntaxhighlight lang="shaderlab">
Shader "Hidden/ShadowVolume/Extrusion" {
Shader "Hidden/ShadowVolume/Extrusion" {
Properties {
Properties {

Revision as of 21:55, 17 January 2012

Author: Aras Pranckevicius



Shadow Volume in action

This package implements very basic shadows using shadow volume algorithm. Like all shadow volume based shadows, it's very fillrate hungry, and this script only implements very basic functionality, but on the plus side it should work on pretty much all graphics cards and performance might be enough for simple cases. Requires Unity Pro as it uses GL class.

Note that in Unity 2.0 and up there are easier to use built-in shadows based on shadow mapping algorithm. But this technique still works if you want shadow volumes.


Typical shadow volume setup

The provided unity package contains the example scene inside. In general, the usage is this:

  • Use closed and shadow volume friendly meshes for shadow casters.
    • Meshes should be completely closed and each edge should be between exactly two triangles.
    • Meshes generally should not have hard shading creases, i.e. be smooth. Unless you implement the following point, meshes should be fairly highly tessellated as well.
    • Meshes should be highly tesselated, or each hard edge should actually have two zero-area triangles that join both sides of the hard edge. Imagine that if you move one face away, no holes should appear - the invisible triangles should stretch out and fill the gap.
  • Attach RenderShadowVolume.js to a Camera. Setup shadow casting Objects, shadow casting light, volume extrusion distance and the shader for extrusion (ShadowVolumeExtrusion) in Inspector.
  • Using no ambient lighting is probably a good idea as well.
  • Hit play and enjoy.

This script uses GL class and therefore requires Unity Pro. It should run on about any hardware.

The package

Zipped Unity package for Unity 2.x:

Behind the scenes

The script implements shadow volumes without using the stencil buffer, as described in "Shadow Volumes Revisited" paper by Roettger, Irion and Ertl; 2002.

Shadow is rendered by darkening the screen twice in shadowed regions. The paper describes alternative ways of applying the shadow, this is just one of them.

Shadow volume extrusion is done in a vertex shader, by just moving away vertices that have normals facing away from the light source. This is not 100% correct way of doing the extrusion, but it often works fine for highly tesselated objects. Even with them, sometimes it introduces small 1 pixel shadow artifacts. The extrusion can be improved in two ways:

  1. Do extrusion on the CPU, extruding away faces (not vertices!) that face away from the light, and fill in shadow volume side faces. Or:
  2. Preprocess shadow casting meshes so that all vertices actually contain face normals, and between each non-planar triangle pair there are degenerate triangles included. Pretty much as described in "Shadow Volume Extrusion using a Vertex Shader" article on ShaderX by Brennan; 2002 (can be found on AMD developer site).

As this script uses alpha channel of the screen itself, it does not play nicely with Glow and other effects that also use alpha channel of the screen. A minor change could be made to make it render volumes into alpha channel of a screen-sized render texture.


  • 2008 Mar 11 Initial version

JavaScript - RenderShadowVolume.js

var objects : MeshFilter[];
var shadowLight : Light;
var extrusionDistance = 20.0;
var extrusionShader : Shader;
private var setAlphaMat : Material;
private var extrusionMat : Material;
function Start() {
    if (!shadowLight) {
        Debug.LogWarning ("no shadow casting light set, disabling script");
        enabled = false;
    if (!extrusionShader) {
        Debug.LogWarning ("no shadow casting shader set, disabling script");
        enabled = false;
    if (!camera) {
        Debug.LogWarning ("script must be attached to camera, disabling script");
        enabled = false;
function OnPostRender() {
    if (!enabled)
    if (!setAlphaMat) {
        setAlphaMat = new Material ("Shader \"Hidden/ShadowVolumeAlpha\" {" +
            "SubShader { " +
            " ColorMask A " +
            " ZTest Always Cull Off ZWrite Off" +
            " Pass {" +
            "    Color (0.25,0.25,0.25,0.25)" +
            " } " +
            " Pass {" +
            "    Blend DstColor One" +
            "    Color (1,1,1,1)" +
            " } " +
            " Pass {" +
            "    Blend OneMinusDstColor Zero" +
            "    Color (1,1,1,1)" +
            " } " +
            " Pass {" +
            "    Blend One One" +
            "    Color (0.5,0.5,0.5,0.5)" +
            " } " +
            " Pass {" +
            "    ColorMask RGB" +
            "    Blend Zero DstAlpha" +
            "    Color (1,1,1,1)" +
            " } " +
        setAlphaMat.shader.hideFlags = HideFlags.HideAndDontSave;
        setAlphaMat.hideFlags = HideFlags.HideAndDontSave;
    if (!extrusionMat) {
        extrusionMat = new Material (extrusionShader);
        extrusionMat.hideFlags = HideFlags.HideAndDontSave;
    // clear screen's alpha to 1/4
    GL.PushMatrix ();
    GL.LoadOrtho ();
    setAlphaMat.SetPass (0);
    GL.PopMatrix ();
    // setup extrusion shader properties
    extrusionMat.SetFloat ("_Extrusion", extrusionDistance);
    var lightPos : Vector4;
    if (shadowLight.type == LightType.Directional) {
        var dir = -shadowLight.transform.forward;
        dir = transform.InverseTransformDirection(dir);
        lightPos = Vector4(dir.x,dir.y,-dir.z,0.0);
    } else {
        var pos = shadowLight.transform.position;
        pos = transform.InverseTransformPoint(pos);
        lightPos = Vector4(pos.x,pos.y,-pos.z,1.0);
    extrusionMat.SetVector("_LightPosition", lightPos);
    // render shadow volumes of all objects
    for( var filter : MeshFilter in objects ) {
        var m : Mesh = filter.sharedMesh;
        var tr : Transform = filter.transform;
        Graphics.DrawMesh (m, tr.localToWorldMatrix);
        Graphics.DrawMesh (m, tr.localToWorldMatrix);
    // normalize and apply shadow mask
    GL.PushMatrix ();
    GL.LoadOrtho ();
    setAlphaMat.SetPass (1);
    setAlphaMat.SetPass (2);
    setAlphaMat.SetPass (3);
    setAlphaMat.SetPass (4);
    GL.PopMatrix ();
private static function DrawQuad() : void {
    GL.Begin (GL.QUADS);
    GL.Vertex3 (0, 0, 0.1);
    GL.Vertex3 (1, 0, 0.1);
    GL.Vertex3 (1, 1, 0.1);
    GL.Vertex3 (0, 1, 0.1);
    GL.End ();

ShaderLab - ShadowVolumeExtrusion.shader

Invalid language.

You need to specify a language like this: <source lang="html4strict">...</source>

Supported languages for syntax highlighting:

4cs, 6502acme, 6502kickass, 6502tasm, 68000devpac, abap, actionscript, actionscript3, ada, algol68, apache, applescript, apt_sources, asm, asp, autoconf, autohotkey, autoit, avisynth, awk, bascomavr, bash, basic4gl, bf, bibtex, blitzbasic, bnf, boo, c, c_loadrunner, c_mac, caddcl, cadlisp, cfdg, cfm, chaiscript, cil, clojure, cmake, cobol, coffeescript, cpp, cpp-qt, csharp, css, cuesheet, d, dcs, delphi, diff, div, dos, dot, e, ecmascript, eiffel, email, epc, erlang, euphoria, f1, falcon, fo, fortran, freebasic, fsharp, gambas, gdb, genero, genie, gettext, glsl, gml, gnuplot, go, groovy, gwbasic, haskell, hicest, hq9plus, html4strict, html5, icon, idl, ini, inno, intercal, io, j, java, java5, javascript, jquery, kixtart, klonec, klonecpp, latex, lb, lisp, llvm, locobasic, logtalk, lolcode, lotusformulas, lotusscript, lscript, lsl2, lua, m68k, magiksf, make, mapbasic, matlab, mirc, mmix, modula2, modula3, mpasm, mxml, mysql, newlisp, nsis, oberon2, objc, objeck, ocaml, ocaml-brief, oobas, oracle11, oracle8, oxygene, oz, pascal, pcre, per, perl, perl6, pf, php, php-brief, pic16, pike, pixelbender, pli, plsql, postgresql, povray, powerbuilder, powershell, proftpd, progress, prolog, properties, providex, purebasic, pycon, python, q, qbasic, rails, rebol, reg, robots, rpmspec, rsplus, ruby, sas, scala, scheme, scilab, sdlbasic, smalltalk, smarty, sql, systemverilog, tcl, teraterm, text, thinbasic, tsql, typoscript, unicon, uscript, vala, vb, vbnet, verilog, vhdl, vim, visualfoxpro, visualprolog, whitespace, whois, winbatch, xbasic, xml, xorg_conf, xpp, yaml, z80, zxbasic

Shader "Hidden/ShadowVolume/Extrusion" {
Properties {
    _Extrusion ("Extrusion", Range(0,30)) = 5.0

#pragma vertex vert
#include "UnityCG.cginc"

struct appdata {
    float4 vertex;
    float3 normal;

float _Extrusion;

// camera space light position
// xyz = position, w = 1 for point/spot lights
// xyz = direction, w = 0 for directional lights
float4 _LightPosition;

float4 vert( appdata v ) : POSITION {
    // point to light vector
    float4 objLightPos = mul( _LightPosition, glstate.matrix.invtrans.modelview[0] );
    float3 toLight = normalize( - * objLightPos.w);
    // dot with normal
    float backFactor = dot( toLight, v.normal );
    float extrude = (backFactor < 0.0) ? 1.0 : 0.0; -= toLight * (extrude * _Extrusion);
    return mul( glstate.matrix.mvp, v.vertex );

SubShader {
    Tags { "Queue" = "Transparent+10" }
    ZWrite Off
    ColorMask A
    Offset 1,1
    // Draw front faces, doubling the value in alpha channel
    Pass {
        Cull Back
        Blend DstColor One
        SetTexture[_MainTex] { constantColor(1,1,1,1) combine constant }        
    // Draw back faces, halving the value in alpha channel
    Pass {
        Cull Front
        Blend DstColor Zero
        SetTexture[_MainTex] { constantColor(0.5,0.5,0.5,0.5) combine constant }

FallBack Off
Personal tools