写在前面

自己写过Vertex & Fragment Shader的童鞋,大概都会对Unity的光照痛恨不已。当然,我相信这是因为我们写得少。。。不过这也是由于官方文档对这方面介绍很少的缘故,导致我们无法自如地处理很多常见的光照变量。这篇我们就来讨论下Unity内置的一些光照变量和函数到底怎么用。

以下内容均建立在Forward Rendering Path的基础上。

自己总结的,如果有硬伤一定要告诉我啊!感激不尽~

主要参考:

Forward Rendering Path的渲染细节

在开始后面的讨论之前,先要弄懂一个问题就是Unity可以在Forward Rendering Path中可以处理哪些以及处理多少光照。这里只提取官方文档中的一些内容加以说明。

在Forward Rendering中,有三种处理光照(即照亮物体)的方式:逐顶点处理,逐像素处理,球谐函数(Spherical Harmonics,SH)处理。而决定一个灯光是哪种处理模式取决于它的类型和模式:

  • 场景中最亮的平行光总是逐像素处理的。这意味着,如果场景里只有一个平行光,是否设置它的模式都无关紧要。
  • Render Mode被设置成Not Important的光源,会按逐顶点或者球谐函数处理。经试验,第一点中的平行光不受这点的约束。
  • Render Mode被设置成Important的光源,会按逐像素处理。
  • 如根据以上规则得到的像素光源数量小于设置中的像素光源数量(Pixel Light Count),为了减少亮度,会有更多的光源以逐像素的方式进行渲染。
    • 这一点我没有读懂,按我的实验结果是,如果所有的光源设置成Auto,那么逐像素光源的数目不会超过Pixel Light Count。但如果设置了Render Mode为明确的Not Important或者Important,那么设置Pixel Light Count似乎没有任何影响。

那在哪里进行光照处理呢?当然是在Pass里。Forward Rendering有两种Pass:Base Pass,Additional Passes。这两种Pass的图例说明如下:

注意其中的Per-Vertex Lights/SH Lights前面我标注了可选的,这是说,我们可以选择是否处理这些光源。如果我们没有在Base Pass中写明相关的处理函数,那么这些光源实际上不会对物体产生影响。另一点就是其中橘黄色字表明的代码,其中Tags我就不赘述了,这是基本要求。“#pragma multi_compile_fwdbase”这种在长久的实验中表明最好是写上它们,这会让一些函数和宏可以正确工作,很可惜,现在官方没有给出明确的文档说明,因此我们还是乖乖地每次都加上它们比较好。最后,注意对于Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果

从上面的图中,我们已经知道,由于逐像素的光源是最重要的一种光源,因此Unity会花费一整个Pass来处理它。而对于逐顶点/SH光源来说,它们都将会在Bass Pass中处理(和最重要的平行光一起)。没分量就是这种结果。那么,Base Pass会说,“我这么小就让我做这么多东西,平行光就一个数量少就算了,SH光工作量少也算了,但顶点光也来捣乱我就不干了,不行!我得有条件!”于是Unity规定说,最多只有4个光源会按照逐顶点光源来处理,其他只能按SH光源处理。

这里很容易就弄混弄蒙了。我们先来看官方给的情况,即第一种情况:所有光源都被设置成Auto。这种情况下,Unity会自动为光源选择合适的类型。这时,有一个项目设置很重要就是Pixel Light Count,它决定了逐像素光的最大数目。当Pixel Light Count为4时,就是那张著名的图例情况(来自官方文档):

 

上面的类型选择过程大概是这样的:首先,前Pixel Light Count(这里是4)个光源会按照逐像素进行处理,然后最多4个逐顶点光源,剩下的就是SH光了。其中,注意每种光源之间会有重叠的情况,这主要是为了防止物体移动时光照产生突变。

但是,如果光源没有被设置为Auto,而是被指明是Important和Not Important,又会怎样呢?(不要问我有的被设置成Auto,有的设置成Important会怎样,你这人真讨厌自己分析吧。。。)那么,第二种情况:自定义光源类型。首先,记住一点,这时不再受Pixel Light Count的限制,那么被设置成Important全部会被当成逐像素光源,一个不剩;如果被设置成Not Important,那么最多有4个光源会被当成逐顶点光源,其他就会被当做SH光源进行处理。

上面听起来很复杂,其实就是个“物竞天择”的过程。我们可以想象,所有的光源都在争抢更多的计算资源,都想让自己成为最重要的逐像素光,再不济点就逐顶点光,要是实在混的不好就只能当成SH光了。那么挣到了资源又怎么处理呢?对于逐像素光,它有一整个Pass的资源可以挥霍,而这里会涉及到各种光照变量和函数的使用,后面会讲;对于逐顶点光和SH光来说,很可惜,Unity并没有明确的文档来告诉我们如何访问它们,我们只能通过UnityShaderVariables.cginc中的变量声明和Surface Shader的编译结果来“揣测”用法。这也是后面讲的内容。

吐槽时间:虽然文档上这么写,但实际过程中还是有很多莫名其妙的问题:

  • 奇葩情况一:我在4.6.1版本中,创建一个场景包含了1个平行光+4个点光源,如果使用的Shader没有Additional Passes的定义话,那么4个点光源即便设置成Important,还是会被Unity当成逐顶点光源。
  • 奇葩情况二:如果只定义了Additional Passes,而没有Base Pass的话,就更奇葩了,整个Pass感觉都没有在工作,而得到的结果像是上次缓存之类的东西。总之,请一定要先定义Base Pass再定义Additional Passes。不要任性!
  • 其他更多奇葩等待你发现

光照变量和函数

在UnityShaderVariables.cginc文件中,我们可以找到Unity提供的和处理光照有关的变量:

  1. CBUFFER_START(UnityLighting)
  2.  
  3. #ifdef USING_DIRECTIONAL_LIGHT
  4. uniform fixed4 _WorldSpaceLightPos0;
  5. #else
  6. uniform float4 _WorldSpaceLightPos0;
  7. #endif
  8.  
  9. uniform float4 _LightPositionRange; // xyz = pos, w = 1/range
  10.  
  11. // Built-in uniforms for "vertex lights"
  12. float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space
  13. float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space
  14. float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space
  15. float4 unity_4LightAtten0; // scale factors for attenuation with squared distance
  16.  
  17. float4 unity_LightColor[8]; // array of the colors of the 4 light sources
  18. float4 unity_LightPosition[8]; // apparently is not always correctly set
  19. // x = -1
  20. // y = 1
  21. // z = quadratic attenuation
  22. // w = range^2
  23. float4 unity_LightAtten[8]; // apparently is not always correctly set
  24. float4 unity_SpotDirection[8];
  25.  
  26. // SH lighting environment
  27. float4 unity_SHAr;
  28. float4 unity_SHAg;
  29. float4 unity_SHAb;
  30. float4 unity_SHBr;
  31. float4 unity_SHBg;
  32. float4 unity_SHBb;
  33. float4 unity_SHC;
  34. CBUFFER_END

在UnityCG.cginc可以找到光照处理辅助函数:

  1. // Computes world space light direction
  2. inline float3 WorldSpaceLightDir( in float4 v );
  3.  
  4. // Computes object space light direction
  5. inline float3 ObjSpaceLightDir( in float4 v );
  6.  
  7. // Computes world space view direction
  8. inline float3 WorldSpaceViewDir( in float4 v );
  9.  
  10. // Computes object space view direction
  11. inline float3 ObjSpaceViewDir( in float4 v );
  12.  
  13. float3 Shade4PointLights (
  14. float4 lightPosX, float4 lightPosY, float4 lightPosZ,
  15. float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
  16. float4 lightAttenSq,
  17. float3 pos, float3 normal);
  18.  
  19. float3 ShadeVertexLights (float4 vertex, float3 normal);
  20.  
  21. // normal should be normalized, w=1.0
  22. half3 ShadeSH9 (half4 normal);

下面我们来看下如何在两种Pass中使用上面的变量和函数处理不同类型的光照。

一个基本的Shader

下面的讨论主要建立在下面的代码下,可以先扫一遍,这里不用细看。它主要计算了漫反射光照和高光反射光照,还示例了逐顶点光源和SH光源的计算等。

  1. Shader "Light Test" {
  2. Properties {
  3. _Color ("Color", color) = (1.0,1.0,1.0,1.0)
  4. }
  5. SubShader {
  6. Tags { "RenderType"="Opaque"}
  7.  
  8. Pass {
  9. Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)
  10.  
  11. CGPROGRAM
  12. // Apparently need to add this declaration
  13. #pragma multi_compile_fwdbase
  14.  
  15. #pragma vertex vert
  16. #pragma fragment frag
  17.  
  18. #include "UnityCG.cginc"
  19. #include "Lighting.cginc"
  20. #include "AutoLight.cginc"
  21.  
  22. uniform float4 _Color;
  23.  
  24. struct vertexInput {
  25. float4 vertex : POSITION;
  26. float3 normal : NORMAL;
  27. };
  28. struct vertexOutput {
  29. float4 pos : SV_POSITION;
  30. float4 posWorld : TEXCOORD0;
  31. float3 normalDir : TEXCOORD1;
  32. float3 lightDir : TEXCOORD2;
  33. float3 viewDir : TEXCOORD3;
  34. float3 vertexLighting : TEXCOORD4;
  35. LIGHTING_COORDS(5, 6)
  36. };
  37.  
  38. vertexOutput vert(vertexInput input) {
  39. vertexOutput output;
  40.  
  41. output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
  42. output.posWorld = mul(_Object2World, input.vertex);
  43. output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
  44. output.lightDir = WorldSpaceLightDir(input.vertex);
  45. output.viewDir = WorldSpaceViewDir(input.vertex);
  46. output.vertexLighting = float3(0.0);
  47.  
  48. // SH/ambient and vertex lights
  49. #ifdef LIGHTMAP_OFF
  50. float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
  51. output.vertexLighting = shLight;
  52. #ifdef VERTEXLIGHT_ON
  53. float3 vertexLight = Shade4PointLights (
  54. unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
  55. unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
  56. unity_4LightAtten0, output.posWorld, output.normalDir);
  57. output.vertexLighting += vertexLight;
  58. #endif // VERTEXLIGHT_ON
  59. #endif // LIGHTMAP_OFF
  60.  
  61. // pass lighting information to pixel shader
  62. TRANSFER_VERTEX_TO_FRAGMENT(output);
  63.  
  64. return output;
  65. }
  66.  
  67. float4 frag(vertexOutput input):COLOR{
  68. float3 normalDirection = normalize(input.normalDir);
  69. float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
  70. float3 lightDirection;
  71. float attenuation;
  72.  
  73. if (0.0 == _WorldSpaceLightPos0.w) // directional light?
  74. {
  75. attenuation = 1.0; // no attenuation
  76. lightDirection = normalize(_WorldSpaceLightPos0.xyz);
  77. }
  78. else // point or spot light
  79. {
  80. float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
  81. float distance = length(vertexToLightSource);
  82. attenuation = 1.0 / distance; // linear attenuation
  83. lightDirection = normalize(vertexToLightSource);
  84. }
  85.  
  86. // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
  87. // attenuation = LIGHT_ATTENUATION(input);
  88. // Compare to directions computed from vertex
  89. // viewDirection = normalize(input.viewDir);
  90. // lightDirection = normalize(input.lightDir);
  91.  
  92. // Because SH lights contain ambient, we don't need to add it to the final result
  93. float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz;
  94.  
  95. float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
  96.  
  97. float3 specularReflection;
  98. if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?
  99. {
  100. specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection
  101. }
  102. else // light source on the right side
  103. {
  104. specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
  105. }
  106.  
  107. return float4(input.vertexLighting + diffuseReflection + specularReflection, 1.0);
  108. }
  109. ENDCG
  110. }
  111.  
  112. Pass{
  113. Tags { "LightMode"="ForwardAdd"} // pass for additional light sources
  114. ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending
  115.  
  116. CGPROGRAM
  117. // Apparently need to add this declaration
  118. #pragma multi_compile_fwdadd
  119.  
  120. #pragma vertex vert
  121. #pragma fragment frag
  122.  
  123. #include "UnityCG.cginc"
  124. #include "Lighting.cginc"
  125. #include "AutoLight.cginc"
  126.  
  127. uniform float4 _Color;
  128.  
  129. struct vertexInput {
  130. float4 vertex : POSITION;
  131. float3 normal : NORMAL;
  132. };
  133. struct vertexOutput {
  134. float4 pos : SV_POSITION;
  135. float4 posWorld : TEXCOORD0;
  136. float3 normalDir : TEXCOORD1;
  137. float3 lightDir : TEXCOORD2;
  138. float3 viewDir : TEXCOORD3;
  139. LIGHTING_COORDS(4, 5)
  140. };
  141.  
  142. vertexOutput vert(vertexInput input) {
  143. vertexOutput output;
  144.  
  145. output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
  146. output.posWorld = mul(_Object2World, input.vertex);
  147. output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
  148. output.lightDir = WorldSpaceLightDir(input.vertex);
  149. output.viewDir = WorldSpaceViewDir(input.vertex);
  150.  
  151. // pass lighting information to pixel shader
  152. vertexInput v = input;
  153. TRANSFER_VERTEX_TO_FRAGMENT(output);
  154.  
  155. return output;
  156. }
  157.  
  158. float4 frag(vertexOutput input):COLOR{
  159. float3 normalDirection = normalize(input.normalDir);
  160. float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
  161. float3 lightDirection;
  162. float attenuation;
  163.  
  164. if (0.0 == _WorldSpaceLightPos0.w) // directional light?
  165. {
  166. attenuation = 1.0; // no attenuation
  167. lightDirection = normalize(_WorldSpaceLightPos0.xyz);
  168. }
  169. else // point or spot light
  170. {
  171. float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
  172. float distance = length(vertexToLightSource);
  173. attenuation = 1.0 / distance; // linear attenuation
  174. lightDirection = normalize(vertexToLightSource);
  175. }
  176.  
  177. // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
  178. // attenuation = LIGHT_ATTENUATION(input);
  179. // Compare to directions computed from vertex
  180. // viewDirection = normalize(input.viewDir);
  181. // lightDirection = normalize(input.lightDir);
  182.  
  183. float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
  184.  
  185. float3 specularReflection;
  186. if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?
  187. {
  188. specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection
  189. }
  190. else // light source on the right side
  191. {
  192. specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
  193. }
  194.  
  195. return float4(diffuseReflection + specularReflection, 1.0);
  196. }
  197. ENDCG
  198. }
  199. }
  200. FallBack "Diffuse"
  201. }

Base Pass

回想一下,上面我们说过在Bass Pass中,我们可以处理全部三种光照:处理第一个平行光作为逐像素光处理,处理所有的逐顶点光,处理其他所有SH光。还有很重要的一点就是,我们还要处理环境光、阴影等。一句话,由于Additional Passes只能处理逐像素光,如果你想要其他光照效果,都需要在Bass Pass中处理。

环境光

这里的环境光指的是我们在Edit->Render Setting里面的Ambient Light的值。在Shader中获取它很容易,只需要访问全局变量UNITY_LIGHTMODEL_AMBIENT即可。它是全局变量,因此在在哪个Pass里访问都可以,但环境光只需要加一次即可,因此我们只需要在Bass Pass中叠加到其他颜色上即可。

阴影和光照衰减

Base Pass还有一个非常重要的作用就是添加阴影。上面提到过,对于Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果。也就是说,错过了这里就不会得到阴影信息了。程序中模拟阴影主要是依靠一张Shadow Map,里面记录了从光源出发距离它最近的深度信息。Unity很贴心地提供了这样的一张纹理(_ShadowMapTexture),不用我们自己再编程实现了。

与阴影的实现类似,Unity还提供了一张纹理(_LightTexture0),这张纹理包含了光照衰减(attenuation)。

由于阴影和光照衰减都是对纹理进行采样,然后将结果乘以颜色值,因此Unity把这两步合并到一个宏中,让我们通过一个宏调用就可以解决这两个问题。既然是对纹理采样,那么首先就要知道顶点对应的纹理坐标,Unity同样是通过宏来辅助我们完成的,我们只需要在v2f(vertexOutput)中添加关于宏LIGHTING_COORDS即可。然后,为了计算顶点对应的两张纹理上的坐标,需要在vert函数里面调用一个新的宏:TRANSFER_VERTEX_TO_FRAGMENT。

这个过程中使用的宏定义都在AutoLight.cginc文件中。

一个完整的过程如下:

  1. 首先我们必须声明Pass和#pragma,这样才可以保证Unity会正确填充纹理和坐标:
    1. Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)
    2.  
    3. CGPROGRAM
    4. // Apparently need to add this declaration
    5. #pragma multi_compile_fwdbase

    ForwardAdd Pass也是类似的。

  2. 定义光照纹理和阴影纹理的纹理坐标:
    1. struct vertexOutput {
    2. float4 pos : SV_POSITION;
    3. float4 posWorld : TEXCOORD0;
    4. float3 normalDir : TEXCOORD1;
    5. float3 lightDir : TEXCOORD2;
    6. float3 viewDir : TEXCOORD3;
    7. float3 vertexLighting : TEXCOORD4;
    8. LIGHTING_COORDS(5, 6)
    9. };

    即上面的最后一行,LIGHTING_COORDS(5, 6)。5和6指明变量的存储位置。这个宏的定义会根据光源类型、有无cookie发生变化。

  3. 然后,需要在vert函数中计算正确的纹理坐标:
    1. vertexOutput vert(vertexInput input) {
    2. vertexOutput output;
    3.  
    4. ......
    5.  
    6. // pass lighting information to pixel shader
    7. vertexInput v = input;
    8. TRANSFER_VERTEX_TO_FRAGMENT(output);
    9.  
    10. return output;
    11. }

    上面的代码来自Forward Add中的一段。注意上面重新定义了一个结构v,这是因为TRANSFER_VERTEX_TO_FRAGMENT中,如果该光源非平行光,就需要利用顶点位置来计算衰减(平行光不需要计算衰减),而顶点的访问,宏里直接使用了v,这意味着我们必须在上下文中提供一个名为v的顶点数据结构。当然,我们可以直接把vertexInput的命名换成v就不需要这样转换了。。。

  4. 最后,我们在frag函数中请求得到阴影或衰减值:
    1. attenuation = LIGHT_ATTENUATION(input);
Unity就是使用了这三个宏来完成阴影和衰减的计算的。我们来看一下这三个宏到底是个什么东东。这里仅以不开启cookie的平行光和点光源为例:
  1. #ifdef POINT
  2. #define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
  3. uniform sampler2D _LightTexture0;
  4. uniform float4x4 _LightMatrix0;
  5. #define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)
  6. #define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
  7. #endif
  8.  
  9. #ifdef DIRECTIONAL
  10. #define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)
  11. #define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)
  12. #define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
  13. #endif
  1. #define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1;
  2. #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
  3. #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

可以发现,对于点光源来说,会计算两种纹理,即光照衰减纹理和阴影纹理,并在最后计算attenuation的时候,就是将两种纹理的采样结果相乘。而对于平行光来说更加简单,由于平行光没有衰减,因此只需要计算阴影纹理就可以了。

再次强调以下,Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果。例如,下面左图中的平行光可以投射出阴影,而右图中即便小球在光源和小苹果的中间也不会产生任何阴影:
 

逐顶点光照

其实逐顶点光照就是一个名字,Unity把这些所谓的“逐顶点光照”的数据存储在一些变量中,我们完全可以按逐像素的方式来处理它们。当然,处于性能的考虑,我们通常还是会在顶点函数阶段处理它们,因此把它们称为逐顶点光照。
逐顶点光照涉及的变量和函数有两组。这里的组别主要是依靠Unity提供的顶点光照计算函数使用的变量来归类的。
第一组如下:
  1. uniform float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space
  2. uniform float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space
  3. uniform float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space
  4. uniform float4 unity_4LightAtten0; // scale factors for attenuation with squared distance

对应的函数如下:

  1. float3 Shade4PointLights (
  2. float4 lightPosX, float4 lightPosY, float4 lightPosZ,
  3. float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
  4. float4 lightAttenSq,
  5. float3 pos, float3 normal)
  6. {
  7. // to light vectors
  8. float4 toLightX = lightPosX - pos.x;
  9. float4 toLightY = lightPosY - pos.y;
  10. float4 toLightZ = lightPosZ - pos.z;
  11. // squared lengths
  12. float4 lengthSq = 0;
  13. lengthSq += toLightX * toLightX;
  14. lengthSq += toLightY * toLightY;
  15. lengthSq += toLightZ * toLightZ;
  16. // NdotL
  17. float4 ndotl = 0;
  18. ndotl += toLightX * normal.x;
  19. ndotl += toLightY * normal.y;
  20. ndotl += toLightZ * normal.z;
  21. // correct NdotL
  22. float4 corr = rsqrt(lengthSq);
  23. ndotl = max (float4(0,0,0,0), ndotl * corr);
  24. // attenuation
  25. float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
  26. float4 diff = ndotl * atten;
  27. // final color
  28. float3 col = 0;
  29. col += lightColor0 * diff.x;
  30. col += lightColor1 * diff.y;
  31. col += lightColor2 * diff.z;
  32. col += lightColor3 * diff.w;
  33. return col;
  34. }
调用的话代码如下:
  1. float3 vertexLight = Shade4PointLights (
  2. unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
  3. unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
  4. unity_4LightAtten0, output.posWorld, output.normalDir);

注意其中顶点位置和法线方向都是指在世界坐标系下的。

第二组变量:

  1. float4 unity_LightPosition[8]; // apparently is not always correctly set
  2. // x = -1
  3. // y = 1
  4. // z = quadratic attenuation
  5. // w = range^2
  6. float4 unity_LightAtten[8]; // apparently is not always correctly set
  7. float4 unity_SpotDirection[8];

函数:

  1. float3 ShadeVertexLights (float4 vertex, float3 normal)
  2. {
  3. float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz;
  4. float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal);
  5. float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
  6. for (int i = 0; i < 4; i++) {
  7. float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w;
  8. float lengthSq = dot(toLight, toLight);
  9. float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z);
  10. float diff = max (0, dot (viewN, normalize(toLight)));
  11. lightColor += unity_LightColor[i].rgb * (diff * atten);
  12. }
  13. return lightColor;
  14. }

用法:

  1. vertexLight = ShadeVertexLights(input.vertex, input.normal)

注意其中的顶点坐标和法线方向是在对象坐标系下的。而且,其计算结果包含了环境光。。。

这两组函数看起来做了一样的工作,但其实Forward Rendering我们只可以选择第一组。下面是官方文档中的解释:

Forward rendering helper functions in UnityCG.cginc

These functions are only useful when using forward rendering (ForwardBase or ForwardAdd pass types).

  • float3 Shade4PointLights (...) - computes illumination from four point lights, with light data tightly packed into vectors. Forward rendering uses this to compute per-vertex lighting.

Vertex-lit helper functions in UnityCG.cginc

These functions are only useful when using per-vertex lit shaders (“Vertex” pass type).

  • float3 ShadeVertexLights (float4 vertex, float3 normal) - computes illumination from four per-vertex lights and ambient, given object space position & normal.

文档里说的很清楚,对于Forward Rendering来说,我们应该使用Shade4PointLights来计算最多四个逐顶点光照,而且只能计算Point Lights和Spot Lights,如果一个平行光被设置成逐顶点光源,那么是不会被计算的。换句话说,我们应该使用unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0、unity_4LightAtten0这些数据来访问逐顶点的光源数据。而另一组是在Vertex Pass(e.g. Tags { "LightMode"="Vertex"})中使用的。

还有有一些需要我们了解的地方
  • Unity给出的函数只是为了方便我们提供的一种计算方法,可以看出来Shade4PointLights中,只是按逐顶点的方法(即只需在vert函数中提供顶点位置和法线)计算了漫反射方向的光照,但我们也完全可以自己根据这些光照变量处理逐顶点光源,例如添加高光反射等等。
  • 我们甚至还可以按照逐像素的方式来处理它们,即在frag函数里访问并计算它们。只要你愿意,没有什么可以阻止你这么做。(就是这么任性。)
好啦,说完了理论我们来看下视觉效果是怎样的。我们在场景里放了一个小苹果+一个球,并且放了四个不同颜色的点光源,只输出Shade4PointLights的结果如下(左图为逐顶点光照,右图为逐像素光照):
 
可以看出来,逐顶点光源从视觉效果上不如逐像素光源,但性能更好。
那么,还有一个问题,即支持计算的逐顶点光源数目最多为4个,定义的存储逐顶点光源信息的变量数组也只有4维。也就是说,如果场景里被设置(或者排序后得到的数目)成逐顶点光源的数目大于4个,那么Unity会对它们进行排序,把其中最重要的4个光源存储到那些变量中。但这种排序方法Unity没有文档进行说明,而从实验结果来看,这个排序结果和光的颜色、密度、距离都有关。例如,如果我们再加一个蓝色光源,可以发现不会对结果有任何变化:
而如果我们调整它的颜色、密度、或者位置时,由于排序结果发生变化,就会生成光照突变(左图为改变颜色,右图为改变密度):
  

SH光照

那些既不是逐像素光又不是逐顶点光的光源,如果想对物体产生影响,就只能按SH光照进行处理。宫斗失败就是这个结果。Unity里和计算SH光有关的变量和函数如下:

  1. // SH lighting environment
  2. float4 unity_SHAr;
  3. float4 unity_SHAg;
  4. float4 unity_SHAb;
  5. float4 unity_SHBr;
  6. float4 unity_SHBg;
  7. float4 unity_SHBb;
  8. float4 unity_SHC;
  1. // normal should be normalized, w=1.0
  2. half3 ShadeSH9 (half4 normal)
  3. {
  4. half3 x1, x2, x3;
  5.  
  6. // Linear + constant polynomial terms
  7. x1.r = dot(unity_SHAr,normal);
  8. x1.g = dot(unity_SHAg,normal);
  9. x1.b = dot(unity_SHAb,normal);
  10.  
  11. // 4 of the quadratic polynomials
  12. half4 vB = normal.xyzz * normal.yzzx;
  13. x2.r = dot(unity_SHBr,vB);
  14. x2.g = dot(unity_SHBg,vB);
  15. x2.b = dot(unity_SHBb,vB);
  16.  
  17. // Final quadratic polynomial
  18. float vC = normal.x*normal.x - normal.y*normal.y;
  19. x3 = unity_SHC.rgb * vC;
  20. return x1 + x2 + x3;
  21. }

调用代码如下:

  1. float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

关于SH光照的实现细节我没有研究,有兴趣的可以查资料理解下上面函数的含义。之前有网友留言告诉我一篇文章。但太长了我没看。。。还有论坛中的一个帖子,可以看看里面的代码初步了解一下。

我们以之前的例子为例,看一下只输出SH光照的结果。下面左图中,是只有四个光源的情况,可以看出此时并没有任何SH光,这是因为这四个光源此时被当做是逐顶点光照。这里物体颜色非黑是因为unity_SHAr、unity_SHAg、unity_SHAb包含了环境光数据,而非真正的光照造成的,因此理论上只要包含了计算SH光照的代码就不需要在最后结果上添加上面提到的环境光了。右图则是增加了4个新的Not Important光源后的SH光照结果。

 

我们将逐顶点光照和SH光照结合在一起,代码如下:

  1. // SH/ambient and vertex lights
  2. #ifdef LIGHTMAP_OFF
  3. float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
  4. output.vertexLighting = shLight;
  5. #ifdef VERTEXLIGHT_ON
  6. float3 vertexLight = Shade4PointLights (
  7. unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
  8. unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
  9. unity_4LightAtten0, output.posWorld, output.normalDir);
  10. output.vertexLighting += vertexLight;
  11. #endif // VERTEXLIGHT_ON
  12. #endif // LIGHTMAP_OFF

其中,需要添加#ifdef这些声明是为了保证,在Unity不提供这些数据时可以不用计算这些光照。

我们把两者相加的结果输出,可以得到以下的结果:

Additional Passes

最后,我们来谈谈Additional Passes中的逐像素光。我们需要知道的是,其实在Base Pass中我们也需要处理逐像素光,但我们可以明确的知道这个逐像素光只能是第一个平行光。而在Additional Passes中,逐像素光可能是平行光、点光源、聚光灯光源(Spot Light)。这里不讨论使用了LightMap或者开启了Cookie的情况。

同样,这里的逐像素光其实也只是一个名字,Unity只是负责把所谓的逐像素光的数据放到一些变量中,但是,没有什么可以阻止我们是在vert中计算还是在frag中计算。

注意:想要Additional Passes是叠加在Bass Pass上的话(一般人的目的都是这个),请确保你给Pass添加了合适的混合模式。例如:

  1. Pass{
  2. Tags { "LightMode"="ForwardAdd"} // pass for additional light sources
  3. ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending

对于逐像素光照,我们最长使用的变量和函数如下:

来自UnityShaderVariables.cginc:

  1. uniform float4 _WorldSpaceLightPos0;
  2. uniform float3 _WorldSpaceCameraPos;

来自Lighting.cginc:

  1. fixed4 _LightColor0;

来自UnityCG.cginc(文档说明):

  1. // Computes world space light direction
  2. inline float3 WorldSpaceLightDir( in float4 v );
  3. // Computes object space light direction
  4. inline float3 ObjSpaceLightDir( in float4 v );
  5. // Computes world space view direction
  6. inline float3 WorldSpaceViewDir( in float4 v );
  7. // Computes object space view direction
  8. inline float3 ObjSpaceViewDir( in float4 v );

可以发现,只有函数给出了明确的文档说明,其他都只能靠Unity内部Shader的结构来揣测了。

我们先不管这些变量和函数,先来想想我们到底想利用逐像素光照来计算什么,在哪里计算。最常见的需求就是计算光源方向和视角方向,然后再进行漫反射和高光反射的计算。在Unity里在哪里计算这些方向似乎从视觉上没有太大的区别,理论上在vert中计算比在frag中计算更快一点。但计算位置的选择决定了我们可以如何使用上面的变量和函数。

可以注意到,Unity提供的函数都是在vert函数中的辅助函数,即都是只需要提供顶点位置就可以得到光照方向和视角方向的。也就是说,如果我们想要在vert函数中就计算各个方向的值,可以这么做:

  1. output.lightDir = WorldSpaceLightDir(input.vertex);
  2. output.viewDir = WorldSpaceViewDir(input.vertex);

当然,上面是得到世界坐标系下的用法,我们也可以得到对象坐标系下的,看需求即可。这些函数其实也是利用了_WorldSpaceLightPos0和_WorldSpaceCameraPos而已。例如WorldSpaceLightDir的定义如下:

  1. // Computes world space light direction
  2. inline float3 WorldSpaceLightDir( in float4 v )
  3. {
  4. float3 worldPos = mul(_Object2World, v).xyz;
  5. #ifndef USING_LIGHT_MULTI_COMPILE
  6. return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;
  7. #else
  8. #ifndef USING_DIRECTIONAL_LIGHT
  9. return _WorldSpaceLightPos0.xyz - worldPos;
  10. #else
  11. return _WorldSpaceLightPos0.xyz;
  12. #endif
  13. #endif
  14. }

其中,由于平行光的方向不随顶点位置发生变化,因此直接使用_WorldSpaceLightPos0.xyz即可,此时里面存储的其实就是平行光的方向,而非位置。同时,_WorldSpaceLightPos0.w可以表明该光源的类型,如果为0表示是平行光,为1表示是点光源或者聚光灯光源。因此,我们常常可以看到类似下面的代码:

  1. if (0.0 == _WorldSpaceLightPos0.w) // directional light?
  2. {
  3. attenuation = 1.0; // no attenuation
  4. lightDirection = normalize(_WorldSpaceLightPos0.xyz);
  5. }
  6. else // point or spot light
  7. {
  8. float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
  9. lightDirection = normalize(vertexToLightSource);
  10. }

其实是和WorldSpaceLightDir函数的意义是一样的。

_LightColor0就没什么可说的了,就是存储了该逐像素光的颜色。

写在最后

今天就到这里。

【Unity Shaders】Shader中的光照的更多相关文章

  1. Unity关闭shader中的光照模型以及如何自定义光照模型

    // Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject' // Upgrade NOTE: replaced '_Wor ...

  2. [Unity]在Shader中获取摄像机角度、视线的问题

    又踩了一坑,好在谷歌到了之前的一个人遇到相同的问题,顺利解决. 先说说问题背景,我目前的毕设是体数据渲染,实现的办法是raycast.最基本的一点就是在fragment program里,获取rayc ...

  3. 【Unity Shaders】法线纹理(Normal Mapping)的实现细节

    写在前面 写这篇的目的是为了总结我长期以来的混乱.虽然题目是"法线纹理的实现细节",但其实我想讲的是如何在shader中编程正确使用法线进行光照计算.这里面最让人头大的就是各种矩阵 ...

  4. 【Unity Shaders】Diffuse Shading——在Surface Shader中使用properties

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  5. Unity Shaders Vertex & Fragment Shader入门

    http://blog.csdn.net/candycat1992/article/details/40212735 三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Sha ...

  6. 【Unity Shaders】使用CgInclude让你的Shader模块化——Unity内置的CgInclude文件

    本系列主要參考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同一时候会加上一点个人理解或拓展. 这里是本书全部的插图. 这里是本书所需的代码 ...

  7. 【Unity Shaders】Mobile Shader Adjustment —— 为手机定制Shader

    本系列主要參考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同一时候会加上一点个人理解或拓展. 这里是本书全部的插图.这里是本书所需的代码和 ...

  8. 【Unity Shaders】Vertex & Fragment Shader入门

    写在前面 三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Shader中实现描边效果的弊端,也就是只对表面平缓的模型有效.这是因为我们是依赖法线和视角的点乘结果来进行描边判 ...

  9. 【Unity Shaders】Mobile Shader Adjustment—— 什么是高效的Shader

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

随机推荐

  1. 【bzoj4444 scoi2015】国旗计划

    题目描述 A 国正在开展一项伟大的计划 —— 国旗计划.这项计划的内容是边防战士手举国旗环绕边境线奔袭一圈.这项计划需要多名边防战士以接力的形式共同完成,为此,国土安全局已经挑选了 NN 名优秀的边防 ...

  2. poj3237 树链部分 边权模板

    Tree Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 7384   Accepted: 2001 Description ...

  3. xx学院学员评优评奖管理系统

    [勤拂拭软件,软件开发,毕业设计,程序作业,论文写作指导:q-[1215714557]  加好友请注明:勤拂拭)] 之前帮助一个军校学生做的一个评优评奖管理系统,该系统主要用于学校学生评优评先使用. ...

  4. Spring源码分析(一)--BeanProcessor

    一.何谓BeanProcessor BeanProcessor是SpringFramework里非常重要的核心接口之一,我先贴出一段源代码: /* * Copyright 2002-2015 the ...

  5. spring 自定义事件发布及监听(简单实例)

    前言: Spring的AppilcaitionContext能够发布事件和注册相对应的事件监听器,因此,它有一套完整的事件发布和监听机制. 流程分析: 在一个完整的事件体系中,除了事件和监听器以外,还 ...

  6. jmeter正则表达式书写

    在测试过程中,经常会有以下几种场景,如A接口的返回值,用于B接口中,而且A登陆的账户,每次登陆,这个sid值还是变化的.那么在实际工作中,如何才能A接口中提取参数到B接口中?接下来我们就可以用正则表达 ...

  7. 安装redis 执行make命令时报错解决方法

    一.未安装GCC 解决方法:执行yum install gcc-c++命令安装GCC,完成后再次执行make命令 yum install gcc-c++ Linux无法连接网络 http://www. ...

  8. Android ImgView属性

    ImageView是用于界面上显示图片的控件. 属性 1.为ImageView设置图片 ①android:src="@drawable/img1": src设置图片,默认图片等比例 ...

  9. python笔记十二(匿名函数)

    一.匿名函数 有些情况下,我们需要把函数当做参数传入到另外的函数中,或者是把函数作为某个函数的返回值,此时我们就可以使用匿名函数. 匿名函数的标志是lambda.   >>> f = ...

  10. Go 语言函数闭包

    Go 语言支持匿名函数,可作为闭包.匿名函数是一个"内联"语句或表达式.匿名函数的优越性在于可以直接使用函数内的变量,不必申明. 以下实例中,我们创建了函数 getSequence ...