如果读者朋友找到这篇文章, 多半对 Unity3D 的 Shade4PointLights 感到些许困惑。
首先,这个辅助函数是开启 forward rendering path 时,用在 ForwardBase pass 中,
可以计算最多4盏点光产生的漫反射光照。
四个顶点光源(非重要光源)
要包括Unity支持的所有四个顶点光源,我们必须执行相同的顶点光照计算四次,并将结果加在一起。我们可以使用在UnityCG中定义的Shade4PointLights函数。我们必须给它传递位置矢量、光的颜色、衰减因子再加上顶点位置和法线。
调用方式:
参数解析:
unity_4LightPosX0: float4,Unity内置变量,四个分量分别存储着四个光源位置的X坐标
unity_4LightPosY0: float4,Unity内置变量,四个分量分别存储着四个光源位置的Y坐标
unity_4LightPosZ0: float4,Unity内置变量,四个分量分别存储着四个光源位置的Z坐标
unity_LightColor[0].rgb: Unity内置变量,储存着第一个非重要光源的颜色
unity_LightColor[1·].rgb: Unity内置变量,储存着第二个非重要光源的颜色
unity_LightColor[2].rgb: Unity内置变量,储存着第三个非重要光源的颜色
unity_LightColor[3].rgb: Unity内置变量,储存着第四个非重要光源的颜色
unity_4LightAtten0: float4, Unity内置变量,四个分量分别储存着四个光源的光照衰减因子
i.worldPos:顶点世界坐标
i.normal:顶点法线
Shade4PointLights看起来是什么样子?
开始解读前,先粘上完整代码以供参考:
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;
// don't produce NaNs if some vertex position overlaps with the light
lengthSq = max(lengthSq, 0.000001);
// 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;
}
迷之 lightPos
这个函数最令人困惑的恐怕就是 lightPos[XYZ], 官方的文档中并没有提及数据是如何存储的,代码注释用只说道:
data packed in a special way
通过分析代码,我们可以得知,引擎会把四盏点光的x, y, z坐标,分别存储到
lightPosX, lightPosY, lightPosZ
换句话说,
light0 的位置是 float3(lightPosX[0], lightPosY[0], lightPosZ[0])
light1 的位置是 float3(lightPosX[1], lightPosY[1], lightPosZ[1])
light2 的位置是 float3(lightPosX[2], lightPosY[2], lightPosZ[2])
light3 的位置是 float3(lightPosX[3], lightPosY[3], lightPosZ[3])
了解数据组织后,我们开始分析代码
函数签名
我们来看函数签名
float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal)
返回值:
float3 返回的是漫反射计算结果
参数:
float4 lightPosX 四盏点光的x坐标
float4 lightPosY 四盏点光的y坐标
float4 lightPosZ 四盏点光的z坐标
float3 lightColor0 点光0的颜色
float3 lightColor1 点光1的颜色
float3 lightColor2 点光2的颜色
float3 lightColor3 点光3的颜色
float4 lightAttenSq 灯光衰减
float3 pos 顶点或片段的坐标
float3 normal 顶点或片段的法线
光线向量
函数首先计算了光线向量
// to light vectors
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
toLight[XYZ] 分别存储了四盏点光的光线向量的[XYZ]分量
假设四个光线向量分别为 toLight0, toLight1, toLight2, toLight3,那么
toLight0 = float3(toLightX[0], toLightY[0], toLightZ[0])
toLight1 = float3(toLightX[1], toLightY[1], toLightZ[1])
toLight1 = float3(toLightX[2], toLightY[2], toLightZ[2])
toLight2 = float3(toLightX[3], toLightY[3], toLightZ[3])
灯光距离
// squared lengths
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
// don't produce NaNs if some vertex position overlaps with the light
lengthSq = max(lengthSq, 0.000001);
三位空间中,欧氏距离的计算公式是 sqrt(deltaX ^ 2, delta Y ^ 2, delta Z ^ 2)
原代码中数据的存储布局并不方便直观理解,而它实际的操作就是
- lengthSq 用于存储四盏点光 距离的平方
- lengthSq.x = toLight0.x * toLight0.x + toLight0.y * toLight0.y + toLight0.z * toLight0.z
- lengthSq.y = toLight1.x * toLight1.x + toLight1.y * toLight1.y + toLight1.z * toLight1.z
- lengthSq.z = toLight2.x * toLight2.x + toLight2.y * toLight2.y + toLight2.z * toLight2.z
- lengthSq.w = toLight3.x * toLight3.x + toLight3.y * toLight3.y + toLight3.z * toLight3.z
- lengthSq = max(lengthSq, 0.000001); 防止顶点或片段与某个光源重叠时导致计算结果为NaN
计算 NdotL
// NdotL
float4 ndotl = 0;
ndotl += toLightX * normal.x;
ndotl += toLightY * normal.y;
ndotl += toLightZ * normal.z;
改写过的计算流程如下
- ndotl 的 xyzw 分量分别存储四盏点光的 法线点乘光线 的结果,即dot(Normal, Light)
- ndotl.x = dot (toLight0, normal0)
- ndotl.y = dot (toLight1, normal1)
- ndotl.z = dot (toLight2, normal2)
- ndotl.w = dot (toLight3, normal3)
dot(toLight, normal) ,根据点乘的计算公式等于
toLight.x * normal.x + toLight.y * normal.y + toLight.z * normal.z
以ndotl.x 为例
ndotl.x
= dot (toLight0, normal0)
= toLight0.x * normal0.x + toLight0.y * normal0.z + toLight0.z * normal0.z
改写过的计算结果和原代码的结构是一样的,但是原代码的计算效率更高
Normalize NdotL
// correct NdotL
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
上一步中,由于光线向量 toLight 并没有Normalized.
而 dot(Normal, Light) = | Normal | * | Light | * cos(θ), Normal是单位向量,| Normal | = 1
所以 dot(Normal, Light) = | Light | * cos(θ)
要计算Normalized NdotL, 我们需要将上一步的计算结果除以 | Light |
| Light | = sqrt(lengthSq)
rsqrt(x)函数计算x平方根的倒数,等同于 1 / sqrt(x)
ndotl * corr 的计算结果即为 Normalized NdotL
最后通过 max 让 NdotL的结果非负
计算衰减
// attenuation
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl * atten;
根据距离的平方衰减光线,同时使用 lightAttenSq 作为衰减系数,控制衰减强度
最终 diff 的 x、y、z、w 分别储存了 四盏点光经衰减后的漫反射强度
漫反射颜色
// final color
float3 col = 0;
col += lightColor0 * diff.x;
col += lightColor1 * diff.y;
col += lightColor2 * diff.z;
col += lightColor3 * diff.w;
return col;
最终把 光源颜色lightColor 乘以 漫反射强度diff 获得漫反射颜色。
并把四盏点光的漫反射颜色相加获得最终计算结果返回