跳转到内容

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

除非另有说明,本页面上的所有示例源代码均归属公有领域。
华夏公益教科书