GLSL Programming/Unity/Two-Sided Surfaces

This tutorial covers two-sided per-vertex lighting.

An algebraic surface that includes an oloid in the center. The rendering uses different colors for the two sides of the surface.

It's part of a series of tutorials about basic lighting in Unity. In this tutorial, we extend Section “Specular Highlights” to render two-sided surfaces. If you haven't read Section “Specular Highlights”, this would be a very good time to read it.

The surface normal vector N and the direction to the viewer V are usually on the same side of the surface but there are exceptions; thus, one shouldn't rely on it.

Two-Sided Lighting

edit

As shown by the figure of the algebraic surface, it's sometimes useful to apply different colors to the two sides of a surface. In Section “Cutaways”, we have seen how a fragment shader can use the built-in variable gl_FrontFacing to determine whether a fragment is part of a front-facing or a back-facing triangle. Can a vertex shader also determine whether it is part of a front-facing or a back-facing triangle? The answer is a clear: no! One reason is that the same vertex can be part of a front-facing and a back-facing triangle at the same time; thus, whatever decision is made in the vertex shader, it is potentially wrong for some triangles. If you want a simple rule to remember: “Fragments are either front-facing or back-facing. Vertices are bi.”

Thus, two-sided per-vertex lighting has to let the fragment shader determine, whether the front or the back material color should be applied. For example, with this fragment shader:

         #ifdef FRAGMENT

         varying vec4 frontColor; // color for front face
         varying vec4 backColor; // color for back face

         void main()
         {
            if (gl_FrontFacing) // is the fragment part of a front face?
            {
               gl_FragColor = frontColor;
            }
            else // fragment is part of a back face
            {
               gl_FragColor = backColor;
            }
         }

         #endif

On the other hand, this means that the vertex shader has to compute the surface lighting twice: for a front face and for a back face. Fortunately, this is usually still less work than computing the surface lighting for each fragment.

Shader Code

edit

The shader code for two-sided per-vertex lighting is a straightforward extension of the code in Section “Specular Highlights”. It requires two sets of material parameters (front and back) and deactivates culling. The vertex shader computes two colors, one for front faces and one for back faces using the negated normal vector and the second set of material parameters. Then the fragment shader decides which one to apply.

Shader "GLSL two-sided per-vertex lighting" {
   Properties {
      _Color ("Front Material Diffuse Color", Color) = (1,1,1,1) 
      _SpecColor ("Front Material Specular Color", Color) = (1,1,1,1) 
      _Shininess ("Front Material Shininess", Float) = 10
      _BackColor ("Back Material Diffuse Color", Color) = (1,1,1,1) 
      _BackSpecColor ("Back Material Specular Color", Color) 
         = (1,1,1,1) 
      _BackShininess ("Back Material Shininess", Float) = 10
   }
   SubShader {
      Pass {	
         Tags { "LightMode" = "ForwardBase" } 
            // pass for ambient light and first light source
         Cull Off // render front faces and back faces

         GLSLPROGRAM

         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _BackColor; 
         uniform vec4 _BackSpecColor; 
         uniform float _BackShininess;

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         
         varying vec4 frontColor; 
            // lighting of front faces computed in the vertex shader
         varying vec4 backColor; 
            // lighting of back faces computed in the vertex shader
         
         #ifdef VERTEX
         
         void main()
         {				
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
            
            vec3 normalDirection = normalize(vec3(vec4(gl_Normal, 0.0) 
               * modelMatrixInverse));
            vec3 viewDirection = normalize(vec3(
                vec4(_WorldSpaceCameraPos, 1.0) 
                - modelMatrix * gl_Vertex));
            vec3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0 
                  - modelMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
            
            // Computation of lighting for front faces

            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);

            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
            
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }

            frontColor = vec4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);

            // Computation of lighting for back faces 
            // (uses negative normalDirection and back material colors)

            vec3 backAmbientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_BackColor);

            vec3 backDiffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_BackColor) 
               * max(0.0, dot(-normalDirection, lightDirection));
            vec3 backSpecularReflection;
            if (dot(-normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               backSpecularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               backSpecularReflection = 
                  attenuation * vec3(_LightColor0) 
                  * vec3(_BackSpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, -normalDirection), 
                  viewDirection)), _BackShininess);
            }

            backColor = vec4(backAmbientLighting + 
               backDiffuseReflection + backSpecularReflection, 1.0);

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            if (gl_FrontFacing) 
               // is the fragment part of a front face?
            {
               gl_FragColor = frontColor;
            }
            else // fragment is part of a back face
            {
               gl_FragColor = backColor;
            }
         }
         
         #endif

         ENDGLSL
      }

      Pass {	
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
         Cull Off // render front faces and back faces

         GLSLPROGRAM

         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _BackColor; 
         uniform vec4 _BackSpecColor; 
         uniform float _BackShininess;

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         
         varying vec4 frontColor; 
            // lighting of front faces computed in the vertex shader
         varying vec4 backColor; 
            // lighting of back faces computed in the vertex shader
         
         #ifdef VERTEX
         
         void main()
         {				
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
            
            vec3 normalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            vec3 viewDirection = normalize(vec3(
               vec4(_WorldSpaceCameraPos, 1.0) 
               - modelMatrix * gl_Vertex));
            vec3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0 
                  - modelMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
            
            // Computation of lighting for front faces

            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
            
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }

            frontColor = vec4(diffuseReflection 
               + specularReflection, 1.0);

            // Computation of lighting for back faces 
            // (uses negative normalDirection and back material colors)

            vec3 backDiffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_BackColor) 
               * max(0.0, dot(-normalDirection, lightDirection));
            vec3 backSpecularReflection;
            if (dot(-normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               backSpecularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               backSpecularReflection = 
                  attenuation * vec3(_LightColor0) 
                  * vec3(_BackSpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, -normalDirection), 
                  viewDirection)), _BackShininess);
            }

            backColor = vec4(backDiffuseReflection 
               + backSpecularReflection, 1.0);

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            if (gl_FrontFacing) 
               // is the fragment part of a front face?
            {
               gl_FragColor = frontColor;
            }
            else // fragment is part of a back face
            {
               gl_FragColor = backColor;
            }
         }
         
         #endif

         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

Again, this code consists of two passes, where the second pass is the same as the first apart from the additive blending and the missing ambient color.

Summary

edit

Congratulations, you made it to the end of this short tutorial with a long shader. We have seen:

  • Why a vertex shader cannot distinguish between front-facing and back-facing vertices (because the same vertex might be part of a front-facing and a back-facing triangles).
  • How to compute lighting for front faces and for back faces in the vertex shader.
  • How to let the fragment shader decide which color to apply.

Further Reading

edit

If you still want to know more


< GLSL Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.