在开始后面的讨论以前,先要弄懂一个问题就是Unity能够在Forward Rendering Path中能够处理哪些以及处理多少光照。这里只提取官方文档中的一些内容加以说明。html
在Forward Rendering中,有三种处理光照(即照亮物体)的方式:逐顶点处理,逐像素处理,球谐函数(Spherical Harmonics,SH)处理。而决定一个灯光是哪一种处理模式取决于它的类型和模式:编程
那在哪里进行光照处理呢?固然是在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光源处理。bash
这里很容易就弄混弄蒙了。咱们先来看官方给的状况,即第一种状况:全部光源都被设置成Auto。这种状况下,Unity会自动为光源选择合适的类型。这时,有一个项目设置很重要就是Pixel Light Count,它决定了逐像素光的最大数目。当Pixel Light Count为4时,就是那张著名的图例状况(来自官方文档):cookie
上面的类型选择过程大概是这样的:首先,前Pixel Light Count(这里是4)个光源会按照逐像素进行处理,而后最多4个逐顶点光源,剩下的就是SH光了。其中,注意每种光源之间会有重叠的状况,这主要是为了防止物体移动时光照产生突变。数据结构
可是,若是光源没有被设置为Auto,而是被指明是Important和Not Important,又会怎样呢?(不要问我有的被设置成Auto,有的设置成Important会怎样,你这人真讨厌本身分析吧。。。)那么,第二种状况:自定义光源类型。首先,记住一点,这时再也不受Pixel Light Count的限制,那么被设置成Important所有会被当成逐像素光源,一个不剩;若是被设置成Not Important,那么最多有4个光源会被当成逐顶点光源,其余就会被当作SH光源进行处理。app
上面听起来很复杂,其实就是个“物竞天择”的过程。咱们能够想象,全部的光源都在争抢更多的计算资源,都想让本身成为最重要的逐像素光,再不济点就逐顶点光,要是实在混的很差就只能当成SH光了。那么挣到了资源又怎么处理呢?对于逐像素光,它有一整个Pass的资源能够挥霍,而这里会涉及到各类光照变量和函数的使用,后面会讲;对于逐顶点光和SH光来讲,很惋惜,Unity并无明确的文档来告诉咱们如何访问它们,咱们只能经过UnityShaderVariables.cginc中的变量声明和Surface Shader的编译结果来“揣测”用法。这也是后面讲的内容。ide
吐槽时间:虽然文档上这么写,但实际过程当中仍是有不少莫名其妙的问题:函数
在UnityShaderVariables.cginc文件中,咱们能够找到Unity提供的和处理光照有关的变量:
CBUFFER_START(UnityLighting) #ifdef USING_DIRECTIONAL_LIGHT uniform fixed4 _WorldSpaceLightPos0; #else uniform float4 _WorldSpaceLightPos0; #endif uniform float4 _LightPositionRange; // xyz = pos, w = 1/range // Built-in uniforms for "vertex lights" float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space float4 unity_4LightAtten0; // scale factors for attenuation with squared distance float4 unity_LightColor[8]; // array of the colors of the 4 light sources float4 unity_LightPosition[8]; // apparently is not always correctly set // x = -1 // y = 1 // z = quadratic attenuation // w = range^2 float4 unity_LightAtten[8]; // apparently is not always correctly set float4 unity_SpotDirection[8]; // SH lighting environment float4 unity_SHAr; float4 unity_SHAg; float4 unity_SHAb; float4 unity_SHBr; float4 unity_SHBg; float4 unity_SHBb; float4 unity_SHC; CBUFFER_END
在UnityCG.cginc能够找到光照处理辅助函数:
// Computes world space light direction inline float3 WorldSpaceLightDir( in float4 v ); // Computes object space light direction inline float3 ObjSpaceLightDir( in float4 v ); // Computes world space view direction inline float3 WorldSpaceViewDir( in float4 v ); // Computes object space view direction inline float3 ObjSpaceViewDir( in float4 v ); float3 Shade4PointLights ( float4 lightPosX, float4 lightPosY, float4 lightPosZ, float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3, float4 lightAttenSq, float3 pos, float3 normal); float3 ShadeVertexLights (float4 vertex, float3 normal); // normal should be normalized, w=1.0 half3 ShadeSH9 (half4 normal);
下面咱们来看下如何在两种Pass中使用上面的变量和函数处理不一样类型的光照。
下面的讨论主要创建在下面的代码下,能够先扫一遍,这里不用细看。它主要计算了漫反射光照和高光反射光照,还示例了逐顶点光源和SH光源的计算等。
Shader "Light Test" { Properties { _Color ("Color", color) = (1.0,1.0,1.0,1.0) } SubShader { Tags { "RenderType"="Opaque"} Pass { Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light) CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" uniform float4 _Color; struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 posWorld : TEXCOORD0; float3 normalDir : TEXCOORD1; float3 lightDir : TEXCOORD2; float3 viewDir : TEXCOORD3; float3 vertexLighting : TEXCOORD4; LIGHTING_COORDS(5, 6) }; vertexOutput vert(vertexInput input) { vertexOutput output; output.pos = mul(UNITY_MATRIX_MVP, input.vertex); output.posWorld = mul(_Object2World, input.vertex); output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz); output.lightDir = WorldSpaceLightDir(input.vertex); output.viewDir = WorldSpaceViewDir(input.vertex); output.vertexLighting = float3(0.0); // SH/ambient and vertex lights #ifdef LIGHTMAP_OFF float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0)); output.vertexLighting = shLight; #ifdef VERTEXLIGHT_ON float3 vertexLight = Shade4PointLights ( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, output.posWorld, output.normalDir); output.vertexLighting += vertexLight; #endif // VERTEXLIGHT_ON #endif // LIGHTMAP_OFF // pass lighting information to pixel shader TRANSFER_VERTEX_TO_FRAGMENT(output); return output; } float4 frag(vertexOutput input):COLOR{ float3 normalDirection = normalize(input.normalDir); float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(_WorldSpaceLightPos0.xyz); } else // point or spot light { float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz; float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos // attenuation = LIGHT_ATTENUATION(input); // Compare to directions computed from vertex // viewDirection = normalize(input.viewDir); // lightDirection = normalize(input.lightDir); // Because SH lights contain ambient, we don't need to add it to the final result float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz; float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2; float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255); } return float4(input.vertexLighting + diffuseReflection + specularReflection, 1.0); } ENDCG } Pass{ Tags { "LightMode"="ForwardAdd"} // pass for additional light sources ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" uniform float4 _Color; struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 posWorld : TEXCOORD0; float3 normalDir : TEXCOORD1; float3 lightDir : TEXCOORD2; float3 viewDir : TEXCOORD3; LIGHTING_COORDS(4, 5) }; vertexOutput vert(vertexInput input) { vertexOutput output; output.pos = mul(UNITY_MATRIX_MVP, input.vertex); output.posWorld = mul(_Object2World, input.vertex); output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz); output.lightDir = WorldSpaceLightDir(input.vertex); output.viewDir = WorldSpaceViewDir(input.vertex); // pass lighting information to pixel shader vertexInput v = input; TRANSFER_VERTEX_TO_FRAGMENT(output); return output; } float4 frag(vertexOutput input):COLOR{ float3 normalDirection = normalize(input.normalDir); float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(_WorldSpaceLightPos0.xyz); } else // point or spot light { float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz; float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos // attenuation = LIGHT_ATTENUATION(input); // Compare to directions computed from vertex // viewDirection = normalize(input.viewDir); // lightDirection = normalize(input.lightDir); float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2; float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255); } return float4(diffuseReflection + specularReflection, 1.0); } ENDCG } } FallBack "Diffuse" }
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文件中。
一个完整的过程以下:
首先咱们必须声明Pass和#pragma,这样才能够保证Unity会正确填充纹理和坐标:
ForwardAdd Pass也是相似的。
Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light) CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdbase
定义光照纹理和阴影纹理的纹理坐标:
即上面的最后一行,LIGHTING_COORDS(5, 6)。5和6指明变量的存储位置。这个宏的定义会根据光源类型、有无cookie发生变化。
struct vertexOutput { float4 pos : SV_POSITION; float4 posWorld : TEXCOORD0; float3 normalDir : TEXCOORD1; float3 lightDir : TEXCOORD2; float3 viewDir : TEXCOORD3; float3 vertexLighting : TEXCOORD4; LIGHTING_COORDS(5, 6) };
上面的代码来自Forward Add中的一段。注意上面从新定义了一个结构v,这是由于TRANSFER_VERTEX_TO_FRAGMENT中,若是该光源非平行光,就须要利用顶点位置来计算衰减(平行光不须要计算衰减),而顶点的访问,宏里直接使用了v,这意味着咱们必须在上下文中提供一个名为v的顶点数据结构。固然,咱们能够直接把vertexInput的命名换成v就不须要这样转换了。。。
而后,须要在vert函数中计算正确的纹理坐标:
vertexOutput vert(vertexInput input) { vertexOutput output; ...... // pass lighting information to pixel shader vertexInput v = input; TRANSFER_VERTEX_TO_FRAGMENT(output); return output; }
最后,咱们在frag函数中请求获得阴影或衰减值:
attenuation = LIGHT_ATTENUATION(input);
Unity就是使用了这三个宏来完成阴影和衰减的计算的。咱们来看一下这三个宏究竟是个什么东东。这里仅以不开启cookie的平行光和点光源为例:
#ifdef POINT #define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2) uniform sampler2D _LightTexture0; uniform float4x4 _LightMatrix0; #define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a) #define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a)) #endif #ifdef DIRECTIONAL #define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1) #define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a) #define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a) #endif #define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex)); #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
能够发现,对于点光源来讲,会计算两种纹理,即光照衰减纹理和阴影纹理,并在最后计算attenuation的时候,就是将两种纹理的采样结果相乘。而对于平行光来讲更加简单,因为平行光没有衰减,所以只须要计算阴影纹理就能够了。
再次强调如下,Forward Rendering来讲,只有Bass Pass中处理的第一个平行光能够有阴影效果。例如,下面左图中的平行光能够投射出阴影,而右图中即使小球在光源和小苹果的中间也不会产生任何阴影:
逐顶点光照
其实逐顶点光照就是一个名字,Unity把这些所谓的“逐顶点光照”的数据存储在一些变量中,咱们彻底能够按逐像素的方式来处理它们。固然,处于性能的考虑,咱们一般仍是会在顶点函数阶段处理它们,所以把它们称为逐顶点光照。
逐顶点光照涉及的变量和函数有两组。这里的组别主要是依靠Unity提供的顶点光照计算函数使用的变量来归类的。
第一组以下:
uniform float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space uniform float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space uniform float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space uniform float4 unity_4LightAtten0; // scale factors for attenuation with squared distance
对应的函数以下:
float3 Shade4PointLights ( float4 lightPosX, float4 lightPosY, float4 lightPosZ, float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3, float4 lightAttenSq, float3 pos, float3 normal) { // to light vectors float4 toLightX = lightPosX - pos.x; float4 toLightY = lightPosY - pos.y; float4 toLightZ = lightPosZ - pos.z; // squared lengths float4 lengthSq = 0; lengthSq += toLightX * toLightX; lengthSq += toLightY * toLightY; lengthSq += toLightZ * toLightZ; // NdotL float4 ndotl = 0; ndotl += toLightX * normal.x; ndotl += toLightY * normal.y; ndotl += toLightZ * normal.z; // correct NdotL float4 corr = rsqrt(lengthSq); ndotl = max (float4(0,0,0,0), ndotl * corr); // attenuation float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq); float4 diff = ndotl * atten; // final color float3 col = 0; col += lightColor0 * diff.x; col += lightColor1 * diff.y; col += lightColor2 * diff.z; col += lightColor3 * diff.w; return col; }
调用的话代码以下:
float3 vertexLight = Shade4PointLights ( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, output.posWorld, output.normalDir);
注意其中顶点位置和法线方向都是指在世界坐标系下的。
第二组变量:
float4 unity_LightPosition[8]; // apparently is not always correctly set // x = -1 // y = 1 // z = quadratic attenuation // w = range^2 float4 unity_LightAtten[8]; // apparently is not always correctly set float4 unity_SpotDirection[8];
函数:
float3 ShadeVertexLights (float4 vertex, float3 normal) { float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz; float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal); float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz; for (int i = 0; i < 4; i++) { float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w; float lengthSq = dot(toLight, toLight); float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z); float diff = max (0, dot (viewN, normalize(toLight))); lightColor += unity_LightColor[i].rgb * (diff * atten); } return lightColor; }
用法:
vertexLight = ShadeVertexLights(input.vertex, input.normal)
注意其中的顶点坐标和法线方向是在对象坐标系下的。并且,其计算结果包含了环境光。。。
这两组函数看起来作了同样的工做,但其实在Forward Rendering咱们只能够选择第一组。下面是官方文档中的解释:
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.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"})中使用的。
还有有一些须要咱们了解的地方:
好啦,说完了理论咱们来看下视觉效果是怎样的。咱们在场景里放了一个小苹果+一个球,而且放了四个不一样颜色的点光源,只输出Shade4PointLights的结果以下(左图为逐顶点光照,右图为逐像素光照):
能够看出来,逐顶点光源从视觉效果上不如逐像素光源,但性能更好。
那么,还有一个问题,即支持计算的逐顶点光源数目最多为4个,定义的存储逐顶点光源信息的变量数组也只有4维。也就是说,若是场景里被设置(或者排序后获得的数目)成逐顶点光源的数目大于4个,那么Unity会对它们进行排序,把其中最重要的4个光源存储到那些变量中。但这种排序方法Unity没有文档进行说明,而从实验结果来看,这个排序结果和光的颜色、密度、距离都有关。例如,若是咱们再加一个蓝色光源,能够发现不会对结果有任何变化:
而若是咱们调整它的颜色、密度、或者位置时,因为排序结果发生变化,就会生成光照突变(左图为改变颜色,右图为改变密度):
那些既不是逐像素光又不是逐顶点光的光源,若是想对物体产生影响,就只能按SH光照进行处理。宫斗失败就是这个结果。Unity里和计算SH光有关的变量和函数以下:
// SH lighting environment float4 unity_SHAr; float4 unity_SHAg; float4 unity_SHAb; float4 unity_SHBr; float4 unity_SHBg; float4 unity_SHBb; float4 unity_SHC; // normal should be normalized, w=1.0 half3 ShadeSH9 (half4 normal) { half3 x1, x2, x3; // Linear + constant polynomial terms x1.r = dot(unity_SHAr,normal); x1.g = dot(unity_SHAg,normal); x1.b = dot(unity_SHAb,normal); // 4 of the quadratic polynomials half4 vB = normal.xyzz * normal.yzzx; x2.r = dot(unity_SHBr,vB); x2.g = dot(unity_SHBg,vB); x2.b = dot(unity_SHBb,vB); // Final quadratic polynomial float vC = normal.x*normal.x - normal.y*normal.y; x3 = unity_SHC.rgb * vC; return x1 + x2 + x3; }
调用代码以下:
float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
关于SH光照的实现细节我没有研究,有兴趣的能够查资料理解下上面函数的含义。以前有网友留言告诉我一篇文章。但太长了我没看。。。还有论坛中的一个帖子,能够看看里面的代码初步了解一下。
咱们以以前的例子为例,看一下只输出SH光照的结果。下面左图中,是只有四个光源的状况,能够看出此时并无任何SH光,这是由于这四个光源此时被当作是逐顶点光照。这里物体颜色非黑是由于unity_SHAr、unity_SHAg、unity_SHAb包含了环境光数据,而非真正的光照形成的,所以理论上只要包含了计算SH光照的代码就不须要在最后结果上添加上面提到的环境光了。右图则是增长了4个新的Not Important光源后的SH光照结果。
咱们将逐顶点光照和SH光照结合在一块儿,代码以下:
// SH/ambient and vertex lights #ifdef LIGHTMAP_OFF float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0)); output.vertexLighting = shLight; #ifdef VERTEXLIGHT_ON float3 vertexLight = Shade4PointLights ( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, output.posWorld, output.normalDir); output.vertexLighting += vertexLight; #endif // VERTEXLIGHT_ON #endif // LIGHTMAP_OFF
其中,须要添加#ifdef这些声明是为了保证,在Unity不提供这些数据时能够不用计算这些光照。
咱们把二者相加的结果输出,能够获得如下的结果:
最后,咱们来谈谈Additional Passes中的逐像素光。咱们须要知道的是,其实在Base Pass中咱们也须要处理逐像素光,但咱们能够明确的知道这个逐像素光只能是第一个平行光。而在Additional Passes中,逐像素光多是平行光、点光源、聚光灯光源(Spot Light)。这里不讨论使用了LightMap或者开启了Cookie的状况。
一样,这里的逐像素光其实也只是一个名字,Unity只是负责把所谓的逐像素光的数据放到一些变量中,可是,没有什么能够阻止咱们是在vert中计算仍是在frag中计算。
注意:想要Additional Passes是叠加在Bass Pass上的话(通常人的目的都是这个),请确保你给Pass添加了合适的混合模式。例如:
Pass{ Tags { "LightMode"="ForwardAdd"} // pass for additional light sources ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending
对于逐像素光照,咱们最长使用的变量和函数以下:
来自UnityShaderVariables.cginc:
uniform float4 _WorldSpaceLightPos0; uniform float3 _WorldSpaceCameraPos;
来自Lighting.cginc:
fixed4 _LightColor0;
来自UnityCG.cginc(文档说明):
// Computes world space light direction inline float3 WorldSpaceLightDir( in float4 v ); // Computes object space light direction inline float3 ObjSpaceLightDir( in float4 v ); // Computes world space view direction inline float3 WorldSpaceViewDir( in float4 v ); // Computes object space view direction inline float3 ObjSpaceViewDir( in float4 v );
能够发现,只有函数给出了明确的文档说明,其余都只能靠Unity内部Shader的结构来揣测了。
咱们先无论这些变量和函数,先来想一想咱们到底想利用逐像素光照来计算什么,在哪里计算。最多见的需求就是计算光源方向和视角方向,而后再进行漫反射和高光反射的计算。在Unity里在哪里计算这些方向彷佛从视觉上没有太大的区别,理论上在vert中计算比在frag中计算更快一点。但计算位置的选择决定了咱们能够如何使用上面的变量和函数。
能够注意到,Unity提供的函数都是在vert函数中的辅助函数,即都是只须要提供顶点位置就能够获得光照方向和视角方向的。也就是说,若是咱们想要在vert函数中就计算各个方向的值,能够这么作:
output.lightDir = WorldSpaceLightDir(input.vertex); output.viewDir = WorldSpaceViewDir(input.vertex);
固然,上面是获得世界坐标系下的用法,咱们也能够获得对象坐标系下的,看需求便可。这些函数其实也是利用了_WorldSpaceLightPos0和_WorldSpaceCameraPos而已。例如WorldSpaceLightDir的定义以下:
// Computes world space light direction inline float3 WorldSpaceLightDir( in float4 v ) { float3 worldPos = mul(_Object2World, v).xyz; #ifndef USING_LIGHT_MULTI_COMPILE return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w; #else #ifndef USING_DIRECTIONAL_LIGHT return _WorldSpaceLightPos0.xyz - worldPos; #else return _WorldSpaceLightPos0.xyz; #endif #endif }
其中,因为平行光的方向不随顶点位置发生变化,所以直接使用_WorldSpaceLightPos0.xyz便可,此时里面存储的其实就是平行光的方向,而非位置。同时,_WorldSpaceLightPos0.w能够代表该光源的类型,若是为0表示是平行光,为1表示是点光源或者聚光灯光源。所以,咱们经常能够看到相似下面的代码:
if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(_WorldSpaceLightPos0.xyz); } else // point or spot light { float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz; lightDirection = normalize(vertexToLightSource); }
实际上是和WorldSpaceLightDir函数的意义是同样的。
_LightColor0就没什么可说的了,就是存储了该逐像素光的颜色。