Unity中spotlight是如何实现的?

泰课在线
Unity里对spotLight光照区域的限制, 是在哪实现的,我在ForwardAdd怎么没看到这段处理呢
Spot Angle这个参数具体是怎么用的

泰课在线内置shader 貌似是直接用光源位置和顶点求光照方向,没看到有任何限制
关于这个问题,感觉也是我的一个知识漏洞就花时间好好看了下。
-------------------------------------
总体来说,要在Unity Shader里计算光照衰减的代码大概是长下面这样的:
#ifdef USING_DIRECTIONAL_LIGHT
	fixed atten = 1.0;
#else
	#if defined (POINT)
		// 把点坐标转换到点光源的坐标空间中,_LightMatrix0由引擎代码计算后传递到shader中,这里包含了对点光源范围的计算,具体可参考Unity引擎源码。经过_LightMatrix0变换后,在点光源中心处lightCoord为(0, 0, 0),在点光源的范围边缘处lightCoord为1
		float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
		// 使用点到光源中心距离的平方dot(lightCoord, lightCoord)构成二维采样坐标,对衰减纹理_LightTexture0采样。_LightTexture0纹理具体长什么样可以看后面的内容
		// UNITY_ATTEN_CHANNEL是衰减值所在的纹理通道,可以在内置的HLSLSupport.cginc文件中查看。一般PC和主机平台的话UNITY_ATTEN_CHANNEL是r通道,移动平台的话是a通道
		fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
		// 把点坐标转换到聚光灯的坐标空间中,_LightMatrix0由引擎代码计算后传递到shader中,这里面包含了对聚光灯的范围、角度的计算,具体可参考Unity引擎源码。经过_LightMatrix0变换后,在聚光灯光源中心处或聚光灯范围外的lightCoord为(0, 0, 0),在点光源的范围边缘处lightCoord模为1
		float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));
		// 与点光源不同,由于聚光灯有更多的角度等要求,因此为了得到衰减值,除了需要对衰减纹理采样外,还需要对聚光灯的范围、张角和方向进行判断
		// 此时衰减纹理存储到了_LightTextureB0中,这张纹理和点光源中的_LightTexture0是等价的
		// 聚光灯的_LightTexture0存储的不再是基于距离的衰减纹理,而是一张基于张角范围的衰减纹理
		fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
	#else
		fixed atten = 1.0;
	#endif
#endif

上面通过判断keyword条件,保证只在正确的时候执行访问衰减纹理的代码。_LightTexture0和_LightMatrix0只在某些条件下会被定义,例如在开启了POINT、SPOT、POINT_COOKIE、DIRECTIONAL_COOKIE等,具体我们可以在AutoLight.cginc里面找到。

这里面有几个纹理需要再具体说明。

基于距离的衰减纹理

上面提到,在点光源的情况下,基于到点光源中心距离的衰减纹理被存储在了_LightTexture0中,而在聚光灯的情况下,这张纹理被存储在了_LightTextureB0中。对它们的采样都是使用光源空间到点到光源中心的距离平方来作为采样坐标。这张纹理可以摆放一张平面来输出查看(要注意输出通道UNITY_ATTEN_CHANNEL):
泰课在线
可以看出,它相当于一张一维纹理,在坐标0处值为1,在坐标1处值为0,这意味着,当距离光源越近,atten值越接近于1。

基于张角的衰减纹理

前面说过,对于聚光灯来说,衰减不仅仅受距离影响,也受张角影响。再回顾下,聚光灯条件下,总的衰减代码如下:
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

其中,最后的tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL这句不再说明,和上面一样。最前面的(lightCoord.z > 0)是判断方向范围,因为聚光灯的张角范围小于180°,因此如果lightCoord.z <= 0的话它肯定不会被照亮,衰减值就直接是0。

比较难理解的是tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w这句话。总体来讲,这句代码是在基于点的张角来计算衰减,如果和光源空间的中心轴(+z轴)刚好重合,那么这个衰减值应该是1,而在聚光灯的张角边缘处,这个值因为是0。由上面网友提供的Unity 4.3源码,我们可以大概知道聚光灯_LightMatrix0的计算(P.S. 由于下面源码不全,所以并不是完整的_LightMatrix0,从实验结果来看应该还做了一个粗略的范围判断,等下好了源码再补吧):
Light::GetMatrix (const Matrix4x4f* __restrict object2light, Matrix4x4f* __restrict outMatrix) const
{
Matrix4x4f temp1, temp2, temp3;
float scale;
switch (m_AttenuationMode) {
case kSpotCookie:
// we want out.w = 2.0 * in.z / m_CotanHalfSpotAngle
// c = m_CotanHalfSpotAngle
// 1 0 0 0
// 0 1 0 0
// 0 0 1 0
// 0 0 2/c 0
// the "2" will be used to scale .xy for the cookie as in .xy/2 + 0.5 
temp3.SetIdentity();
temp3.Get(3,2) = 2.0f / m_CotanHalfSpotAngle;
temp3.Get(3,3) = 0;

scale = 1.0f / m_Range;
temp1.SetScale (Vector3f(scale,scale,scale));
// temp3 * temp1 * object2Light
MultiplyMatrices4x4 (&temp3, &temp1, &temp2);
MultiplyMatrices4x4 (&temp2, object2light, outMatrix);
break;

看似难懂,但其实上面就是先把点的坐标变换到聚光灯坐标空间下(object2light矩阵),然后在乘以temp3 * temp1。我们把上面的公式拆开,就会发现一个点和上面的矩阵相乘后结果是:

_LightMatrix0 * (x, y, z, 1) = temp3 * temp1 * object2Light * (x, y, z, 1) = (x/scale, y/scale, z/scale, 2z/(scale * m_CotanHalfSpotAngle)

而在shader中,我们靠lightCoord.xy / lightCoord.w + 0.5得到的结果其实是:

uv = lightCoord.xy / lightCoord.w + 0.5 = (x * m_CotanHalfSpotAngle/(2 * z) + 0.5, y * m_CotanHalfSpotAngle/(2 * z) + 0.5)

我们以x分量为例,x * m_CotanHalfSpotAngle/(2 * z) + 0.5其实是把点的tan值除以半张角的tan值(即等于乘以cot值m_CotanHalfSpotAngle),由此得到张角比值,再通过缩放和平移,把张角的判断范围归一到[0, 1],如下图所示:
泰课在线

也就是说,在张角中心坐标值为0.5,在两侧分别为0和1。而这张基于张角的衰减纹理_LightTexture0是长这样的(它的w分量):
泰课在线
由此就不难看出,在张角中心,即坐标0.5处衰减值为1,而在两侧是接近0的。