Shade4PointLights

如果读者朋友找到这篇文章, 多半对 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)

原代码中数据的存储布局并不方便直观理解,而它实际的操作就是

  1. lengthSq 用于存储四盏点光 距离的平方
  2. lengthSq.x = toLight0.x * toLight0.x + toLight0.y * toLight0.y + toLight0.z * toLight0.z
  3. lengthSq.y = toLight1.x * toLight1.x + toLight1.y * toLight1.y + toLight1.z * toLight1.z
  4. lengthSq.z = toLight2.x * toLight2.x + toLight2.y * toLight2.y + toLight2.z * toLight2.z
  5. lengthSq.w = toLight3.x * toLight3.x + toLight3.y * toLight3.y + toLight3.z * toLight3.z
  6. lengthSq = max(lengthSq, 0.000001); 防止顶点或片段与某个光源重叠时导致计算结果为NaN

 

计算 NdotL

 // NdotL
    float4 ndotl = 0;
    ndotl += toLightX * normal.x;
    ndotl += toLightY * normal.y;
    ndotl += toLightZ * normal.z;

改写过的计算流程如下

  1. ndotl 的 xyzw 分量分别存储四盏点光的 法线点乘光线 的结果,即dot(Normal, Light)
  2. ndotl.x = dot (toLight0, normal0)
  3. ndotl.y = dot (toLight1, normal1)
  4. ndotl.z = dot (toLight2, normal2)
  5. 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 获得漫反射颜色。

并把四盏点光的漫反射颜色相加获得最终计算结果返回

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值