跳轉到內容

GLSL 程式設計/Unity/平面陰影

來自華夏公益教科書,自由的教科書
一個帶有投影陰影的卡通人物。

本教程涵蓋將陰影投影到平面上

本教程沒有基於任何特定教程;但是,對“頂點變換”部分有一定的瞭解將會有所幫助。

將硬陰影投影到平面上

[編輯 | 編輯原始碼]

即時計算逼真的陰影很困難。但是,有些情況要容易得多。將硬陰影(即沒有半影的陰影;參見“球體的軟陰影”部分)投影到平面上就是其中一種情況。其思想是透過將投射陰影的物體渲染成陰影的顏色,並將頂點投影到接收陰影的平面正上方來渲染陰影。

在平面的座標系中,將點P在方向L上投影到平面的示意圖。

將物體投影到平面上

[編輯 | 編輯原始碼]

為了渲染投影的陰影,我們必須將物體投影到平面上。為了指定平面,我們將使用預設平面遊戲物件的本地座標系。因此,我們可以透過編輯平面物件輕鬆地修改平面的位置和方向。在該遊戲物件的座標系中,實際的平面只是 平面,它由 軸所跨越。

在頂點著色器中投影物體意味著投影每個頂點。這可以使用類似於“頂點變換”部分中討論的投影矩陣來完成。但是,這些矩陣計算和除錯起來有些困難。因此,我們將採取另一種方法,使用一些向量運算來計算投影。左側的圖示顯示了點P在光方向L上投影到接收陰影的平面。 (注意向量L與通常在光照計算中使用的光向量方向相反。)為了將點P移動到平面上,我們添加了L的縮放版本。縮放因子恰好是P到平面的距離除以L在平面法向量方向上的長度(由於類似三角形,如灰色線所示)。在平面的座標系中,法向量只是 軸,我們也可以使用點P 座標除以向量L的負 座標的比率。

因此,頂點著色器可能如下所示

         GLSLPROGRAM

         // User-specified uniforms
         uniform mat4 _World2Receiver; // transformation from 
            // world coordinates to the coordinate system of the plane

         // The following built-in uniforms  
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object 
         uniform vec4 _WorldSpaceLightPos0; 
            // position or direction of light source
                  
         #ifdef VERTEX
         
         void main()
         {            
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
            modelMatrixInverse[3][3] = 1.0; 
            mat4 viewMatrix = gl_ModelViewMatrix * modelMatrixInverse;

            vec4 lightDirection;
            if (0.0 != _WorldSpaceLightPos0.w) // point or spot light?
            {
               lightDirection = normalize(
                  modelMatrix * gl_Vertex - _WorldSpaceLightPos0);
            } 
            else // directional light
            {
               lightDirection = -normalize(_WorldSpaceLightPos0); 
            }
            
            vec4 vertexInWorldSpace = modelMatrix * gl_Vertex;
            float distanceOfVertex = 
               (_World2Receiver * vertexInWorldSpace).y; 
               // = height over plane 
            float lengthOfLightDirectionInY = 
               (_World2Receiver * lightDirection).y; 
               // = length in y direction

            lightDirection = lightDirection 
               * (distanceOfVertex / (-lengthOfLightDirectionInY));

            gl_Position = gl_ProjectionMatrix * (viewMatrix 
               * (vertexInWorldSpace + lightDirection));
         }
         
         #endif
         ...

統一變數_World2Receiver最好藉助一個小指令碼設定,該指令碼應該附加到投射陰影的物體上

@script ExecuteInEditMode()

public var plane : GameObject;

function Update () 
{
   if (null != plane)
   {
      renderer.sharedMaterial.SetMatrix("_World2Receiver", 
         plane.renderer.worldToLocalMatrix);
   }
}

該指令碼要求使用者指定接收陰影的平面物件,並相應地設定統一變數_World2Receiver

完整的著色器程式碼

[編輯 | 編輯原始碼]

對於完整的著色器程式碼,我們透過注意到 矩陣-向量積的座標只是矩陣的第二行(即從 0 開始的第一行)與向量的點積來提高效能。此外,我們透過在頂點在平面下方或光向上照射時不移動頂點來提高魯棒性。此外,我們嘗試透過以下指令確保陰影位於平面上方

偏移 -1.0,-2.0

這會稍微降低光柵化三角形的深度,以便它們始終遮擋大約相同深度的其他三角形。

著色器的第一遍渲染投射陰影的物體,而第二遍渲染投影的陰影。在實際應用中,第一遍可以用一個或多個遍來計算投射陰影的物體的光照來代替。

Shader "GLSL planar shadow" {
   Properties {
      _Color ("Object's Color", Color) = (0,1,0,1)
      _ShadowColor ("Shadow's Color", Color) = (0,0,0,1)
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // rendering of object
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
 
         #ifdef VERTEX
 
         void main()
         {                                
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            gl_FragColor = _Color;
         }
       
         #endif

         ENDGLSL
      }

      Pass {   
         Tags { "LightMode" = "ForwardBase" } 
            // rendering of projected shadow
         Offset -1.0, -2.0 
            // make sure shadow polygons are on top of shadow receiver
            
         GLSLPROGRAM

         // User-specified uniforms
         uniform vec4 _ShadowColor;
         uniform mat4 _World2Receiver; // set by script

         // The following built-in uniforms ) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object 
         uniform vec4 _WorldSpaceLightPos0; 
            // position or direction of light source
                  
         #ifdef VERTEX
         
         void main()
         {            
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
            modelMatrixInverse[3][3] = 1.0; 
            mat4 viewMatrix = gl_ModelViewMatrix * modelMatrixInverse;

            vec4 lightDirection;
            if (0.0 != _WorldSpaceLightPos0.w) 
            {
               // point or spot light
               lightDirection = normalize(
                  modelMatrix * gl_Vertex - _WorldSpaceLightPos0);
            } 
            else 
            {
               // directional light
               lightDirection = -normalize(_WorldSpaceLightPos0); 
            }
            
            vec4 vertexInWorldSpace = modelMatrix * gl_Vertex;
            vec4 world2ReceiverRow1 = 
               vec4(_World2Receiver[0][1], _World2Receiver[1][1], 
               _World2Receiver[2][1], _World2Receiver[3][1]);
            float distanceOfVertex = 
               dot(world2ReceiverRow1, vertexInWorldSpace); 
               // = (_World2Receiver * vertexInWorldSpace).y 
               // = height over plane 
            float lengthOfLightDirectionInY = 
               dot(world2ReceiverRow1, lightDirection); 
               // = (_World2Receiver * lightDirection).y 
               // = length in y direction

            if (distanceOfVertex > 0.0 && lengthOfLightDirectionInY < 0.0)
            {
               lightDirection = lightDirection 
                  * (distanceOfVertex / (-lengthOfLightDirectionInY));
            }
            else
            {
               lightDirection = vec4(0.0, 0.0, 0.0, 0.0); 
                  // don't move vertex
            }

            gl_Position = gl_ProjectionMatrix * (viewMatrix 
               * (vertexInWorldSpace + lightDirection));
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            gl_FragColor = _ShadowColor;
         }
         
         #endif

         ENDGLSL
      }
   }
}

進一步改進片段著色器

[編輯 | 編輯原始碼]

有一些地方可以改進,特別是在片段著色器中

  • 可以使用在“切除”部分中討論過的discard指令來刪除陰影片段,這些片段位於矩形平面物件之外。
  • 如果平面是紋理化的,則可以透過僅使用區域性頂點座標進行紋理查詢(也在平面物件的著色器中)並將平面的紋理指定為投射陰影的物體的著色器屬性來整合這種紋理化。
  • 可以透過在本著色器中計算平面的光照並根據投射陰影的物體表面法向量與光方向的角度對其進行衰減來模擬軟陰影,類似於“輪廓增強”部分中的方法。

恭喜,本教程到此結束。我們已經瞭解了

  • 如何將頂點沿光方向投影到平面上。
  • 如何實現這種技術將陰影投影到平面上。

進一步閱讀

[編輯 | 編輯原始碼]

如果你想了解更多

  • 關於模型變換、檢視變換和投影變換,你應該閱讀 “頂點變換”章節 的描述。
  • 關於設定投影矩陣來投影陰影,你可以閱讀 Tom McReynolds 組織的 SIGGRAPH '98 課程“使用 OpenGL 的高階圖形程式設計技術”的第 9.4.1 節,該課程可以在 網上 找到。


< GLSL 程式設計/Unity

除非另有說明,本頁面上的所有示例原始碼均歸屬公有領域。
華夏公益教科書