what are passes and multiple shader passes and their private variables

S.A.Parkhid picture S.A.Parkhid · Jan 12, 2014 · Viewed 9.4k times · Source

I know that multi pass rendering is about rendering separate parts of the scene and combining them into on image with blending factors applied , this has been done in rendering graphics. but What is a pass and What are multiple passes in shaders. for example below shader is for diffuse lighting with the 1st light :

Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } 
            // make sure that all uniforms are correctly set

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         uniform float4 _Color; // define shader property for shaders

         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;

            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 
               // multiplication with unity_Scale.w is unnecessary 
               // because we normalize transformed vectors

            float3 normalDirection = normalize(float3(
               mul(float4(input.normal, 0.0), modelMatrixInverse)));
            float3 lightDirection = normalize(
               float3(_WorldSpaceLightPos0));

            float3 diffuseReflection = 
               float3(_LightColor0) * float3(_Color)
               * max(0.0, dot(normalDirection, lightDirection));

            output.col = float4(diffuseReflection, 1.0);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

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

And below shader again is for the diffuse lighting with multiple passes and multiple lights.

In the below shader , codes in the both passes are the same , and in the second pass , shader is pointing to _LightColor0 and also in the first pass , shader is using _LightColor0 . So where is multiple lights ? both of passes are pointing to _LightColor0 . I think , both passes are using the first light .

Is it true that a pass has it's private lights array ?

Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } 
           // pass for first light source

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         uniform float4 _Color; // define shader property for shaders

         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;

            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 
               // multiplication with unity_Scale.w is unnecessary 
               // because we normalize transformed vectors

            float3 normalDirection = normalize(float3(
               mul(float4(input.normal, 0.0), modelMatrixInverse)));
            float3 lightDirection = normalize(
               float3(_WorldSpaceLightPos0));

            float3 diffuseReflection = 
               float3(_LightColor0) * float3(_Color)
               * max(0.0, dot(normalDirection, lightDirection));

            output.col = float4(diffuseReflection, 1.0);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         uniform float4 _Color; // define shader property for shaders

         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;

            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 
               // multiplication with unity_Scale.w is unnecessary 
               // because we normalize transformed vectors

            float3 normalDirection = normalize(float3(
               mul(float4(input.normal, 0.0), modelMatrixInverse)));
            float3 lightDirection = normalize(
               float3(_WorldSpaceLightPos0));

            float3 diffuseReflection = 
               float3(_LightColor0) * float3(_Color)
               * max(0.0, dot(normalDirection, lightDirection));

            output.col = float4(diffuseReflection, 1.0);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

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

UPDATE: this shader works with multiple lights , because I have tested that on cube below with three lights with three different colors and here is result : enter image description here Thanks in advance

Answer

Andon M. Coleman picture Andon M. Coleman · Jan 12, 2014

Generally speaking, passes in higher-level shader/material systems (yes, higher than GLSL/Cg/HLSL) like this one are a way of setting up states necessary for multi-pass rendering. If you were dealing directly with GLSL, Cg or HLSL, there is no such thing as a "pass."

In this case, you have two different types of passes because one establishes the base lighting contribution and each successive pass adds to it. In other words, they have different blend functions. The first pass replaces anything in the framebuffer (effectively glBlendFunc (GL_ONE, GL_ZERO) if you are familiar with OpenGL), the second pass adds the computed light to all previous passes (glBlendFunc (GL_ONE, GL_ONE)).

Do not think of _LightColor0 as if it referred to the first light in your scene. Actually, it is the first light in the set of lights (in this case, 1 pass per-light) handled by the lighting pass. If this shader were able to handle multiple lights per-pass, you might see _LightColor0 - _LightColorN and the number of passes required would be something along the lines of: 1 + ceil ((NumLights-1)/(_LightColorN+1)).

This ugly shader requires does lighting per-vertex and requires 1 pass per-light. Even for forward rendering, this is highly inefficient. I could understand requiring multiple passes if shadow maps were being used, but this is about as simple a lighting shader as you can get and it still requires 1 pass per-light. Even ancient fixed-function hardware can do 8 lights per-pass.


Update:

Since there was some confusion regarding how each light is related to a pass in this shader, and you have updated your question to include a diagram, I will explain this using the diagram.

       Lights

In this diagram, there are three light sources. To apply these three lights using this shader requires three passes.

Pass 0: <Yellow Light>
  Blend Function: Framebuffer = (1 * Light) + (0 * Framebuffer)
  Pass Type:      "ForwardBase"

Pass 1: <Red Light>
  Blend Function: Framebuffer = (1 * Light) + (1 * Framebuffer)
  Pass Type:      "ForwardAdd"

Pass 2: <Green Light>
  Blend Function: Framebuffer = (1 * Light) + (1 * Framebuffer)
  Pass Type:      "ForwardAdd"

What this works out to in the end is this:

Final Color = Light0 + Light1 + Light2

If you had any more lights, they would all use the ForwardAdd pass from your shader. By the way, since color values are clamped to 0.0 - 1.0 after blending and each light adds its intensity to all previous lights, it does not take very many lights before lighting becomes pure white. You have to be very careful when using additive lighting unless you use HDR (high dynamic range) to fix this problem.


Regarding the process of drawing the cube multiple times, the GPU has nothing to do with this. The graphics engine itself is what changes the states for each shader pass and re-draws the cube. Since state changes like the blending function cannot be changed per-instance, the engine literally has draw your cube once, change a few states and then draw it again using this shader. This process is known as batching and it gets a lot more complicated when you start drawing more than a simple cube.

Reducing the number of times the cube has to be drawn is very important for achieving high performance. When many lights are used, engines usually switch from forward shading to deferred. This draws the cube one time into multiple textures and each texture stores one or more properties needed to compute lighting such as the position, shininess, albedo, normal, material ID, etc. When it comes time to apply lights, instead of drawing the cube over and over, the properties are looked up from the pre-computed textures (G-Buffers) in a fragment/compute shader.

The thing is, deferred shading is not used for per-vertex lighting and the overall added complexity / memory requirements involved can make it impractical when only 2 or 3 lights are used.