Shader 中的光源
1、Shader 开发中常用的光源属性
Unity当中一共支持四种光源类型:
- 平行光(Directional)
- 点光源(Point)
- 聚光灯(Spot)
- 面光源(Area)— 面光源仅在烘焙时有用
不管光源类型到底是什么,我们在Shader开发当中经常会使用到的光源相关属性有:
位置、方向、颜色、强度、衰减
也就是说我们在Shader中处理光照效果时,经常会用到这些光的属性参与到计算当中
2、对比平行光、点光源、聚光灯
2.1 平行光
充当角色:太阳
照射范围:无限制
特点:
- 它不存在固定的位置
- 它的重要属性只有方向(可以通过Transform的Rotation属性来改变方向)
- 它到场景中所有点的方向都是一样的
- 由于它没有位置,因此它没有衰减的概念(光的强度不会随着距离而发生变化
2.2 点光源
充当角色:灯泡、烛光 等
照射范围:有限
特点:
- 它的光是由一个点发出的,向四面八方延伸的光
- 它的范围由参数Range来决定
- 它的位置由Transform中的Position来决定
- 它存在衰减,随着物体离点光源距离决定衰减强弱
2.3 聚光灯
充当角色:探照灯、手电筒 等
照射范围:有限
特点:
- 它的光范围由空间中的一块锥形区域定义
- 它的范围由参数Range和Spot Angle 共同决定
- 它的位置由Transform中的Position来决定
- 它存在衰减,随着物体离聚光灯距离决定衰减强弱。
但是它相对点光源衰减计算公式更复杂,因为需要点是否在锥形范围内
3、判断光源类型
Unity中提供了三个重要的宏,分别是:
- _DIRECTIONAL_LIGHT - 平行光
- _POINT_LIGHT - 点光源
- _SPOT_LIGHT - 聚光灯
#if defined(_DIRECTIONAL_LIGHT)
平行光逻辑
#elif defined (_POINT_LIGHT)
点光源逻辑
#elif defined (_SPOT_LIGHT)
聚光灯逻辑
#else
其他逻辑
#endif
4、光照衰减
光照衰减通常指的是在渲染过程中考虑光线在空间中传播时的减弱效应,比如:任何光源的光照亮度会随着物体离光源的距离增加而迅速衰减。一般常见的光照衰减计算方式有
- 线性衰减:光强度与距离成线性关系。即光照衰减与光源到被照射表面的距离成正比。
- 平方衰减:光强度与距离的平方成反比。这种模型更符合现实世界中光照的特性,因为光在空间中的传播过程中通常会遵循平方衰减规律。
4.1 Unity中的光照衰减
Unity中为了提升性能,我们一般不会直接通过数学公式计算光照衰减,而是使用一张纹理作为查找表(LUT, lookup table) 在片元着色器中计算逐像素光照的衰减
Unity Shader中有一个内置的纹理类型的变量 _LightTexture0,该纹理中存储了衰减值相关的数据,Unity 内部预先就计算好了相关数据 并存入到该纹理中,避免重复计算,提升性能表现
其中它的对角线上的纹理颜色值,表明了光源空间中不同位置的点对应的衰减值
纹理中的对角线,起点(0, 0) 位置,表示和光源重合的点的衰减值;终点(1,1) 位置,表示在光源空间中离光源距离最远的点的衰减值
一般我们直接从 _LightTexture0 中进行纹理采样后,利用其中的 UNITY_ATTEN_CHANNEL 宏来得到衰减值所在的分量:
tex2D(_LightTexture0, 对应纹理uv坐标).UNITY_ATTEN_CHANNEL
注意:如果光源存在cookie,也就是灯光遮罩,那么衰减查找纹理便是 _LightTextureB0
4.2 光源空间变换矩阵
Unity Shader中 内置的 光源空间变换矩阵是用于将世界空间下的位置转换到光源空间下(光源位置为原点)
- 老版本:_LightMatrix0
- 新版本:unity_WorldToLight
由于我们需要从 _LightTexture0 光照纹理中取出对应的衰减数据,因此我们需要将顶点位置从世界空间中转换到光源空间中,然后再来从其中取得衰减数据,我们可以通过矩阵运算将世界空间下的点转换到光源空间下
mul(unity_WorldToLight, float4(worldPos, 1));
4.3 点光源的衰减计算
注意:一般点光源不会为其添加cookie光照遮罩,一般想要使用光照遮罩都会在聚光灯中使用
步骤:
(1)将顶点从世界空间转换到光源空间
float3 lightCoord = mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
lightCoord 是光源坐标系下顶点根据光源的范围range规范化后的坐标,相当于是一个模长为0~1之间的向量
(2)利用该光源空间下的坐标来计算离光源的距离,并利用距离参数,从光源纹理中采样
fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
dot(LightCoord, LightCoord).xx 中dot(LightCoord, LightCoord) 是为了通过点乘得到,结果 x² +y² +z² = 离光源距离 distance²
xx是一种特殊写法,目的是构建一个 float2 代表uv坐标,这里的 uv坐标 相当于是(distance², distance²),用distance²做uv坐标,而不是distance
- 为了避免开平方带来性能消耗
- 采用这种平方衰减更符合现实世界中光照的特性
因为人眼对亮部不敏感,而对暗部敏感,这样我们就可以将 衰减值的精度 集中在比较远的地方
(distance是0.5时,distance2是0.25,这样LUT查找表中大部分值都会留给比较远的部分)
4.4 聚光灯衰减计算
灯光组件中有一个Cookie参数,是用来关联光照遮罩图片的,对于平行光和点光源,默认是不会提供任何光照遮罩信息的,但是对于聚光灯来说,Unity会默认为它提供一个Cookie光照遮罩,主要是用于模拟聚光灯的区域性,而此时光照纹理中
- _LightTexture0 存储的是Cookie纹理信息
- _LightTextureB0 存储的是光照纹理信息,里面包含衰减值
因此,获取聚光灯衰减值时,需要从_LightTextureB0中进行采样;获取遮罩范围相关数据时,需要从_LightTexture0中进行采样
(1)将顶点从世界空间转换到光源空间
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
注意:这里我们转换后和点光源不同的是,点光源只会获取其中的xyz,而聚光灯会获取其中的xyzw,这是因为在聚光灯光源空间下的w值有特殊含义,会参与后续的计算
(2)利用光源空间下的坐标信息
我们会通过3个步骤去获取聚光灯的衰减信息
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;
这一步的规则和点光源规则一致,直接根据距离光源原点距离的平方从光照纹理中获取衰减值
需要注意的是
- 聚光灯的光照衰减纹理为_LightTextureB0
- dot函数只会计算xyz,w不会计算
接着我们来分析用于进行范围判断的部分——第一、二步
第一步:(lightCoord.z > 0)
CG语法中没有显示的bool类型,一般情况下 0 表示false,1表示true,也就是说lightCoord.z > 0的返回值,条件满足时为1,条件不满足为0
这里的z代表的其实是 目标点 相对于 聚光灯照射面 距离,如果 lightCoord.z <= 0 证明在聚光灯照射方向的背面,就不应该受到聚光灯的影响, 也就是说这一步的主要作用,是用来决定顶点是否受到聚光灯光照的影响
第二步:tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w
我们以前在进行纹理采样时都会进行一个 先缩放 后 平移 的操作,比如:
uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
而第二步中的 lightCoord.xy / lightCoord.w + 0.5 其实也是在做这样的一个操作,这样做的主要目的是因为:
我们需要把uv坐标映射到0~1的范围内再从纹理中采样,lightCoord.xy / lightCoord.w 进行缩放后 x, y的取值范围是-0.5~0.5之间,再加上0.5后,x,y的取值范围就是0~1之间,便可以进行正确的纹理采样了,而 lightCoord.xy / lightCoord.w 是因为聚光灯有很多横截面,我们需要把各横截面映射到最大的面上进行采样
总结一下:看似复杂的聚光灯光照衰减计算方式,其实就是由 “遮罩衰减” 和 距离衰减 共同决定的
- 第一步:判断是否能有机会照到光 看得到为1,看不到为0:fixed atten = (lightCoord.z > 0)
- 第二步:缩放平移,映射到遮罩纹理采样 根据遮罩纹理的信息决定衰减叠加:tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w *
- 第三步:从光照衰减纹理中取出按距离得到的衰减值:tex2D(_LightTextureB0,dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
5、前向渲染路径中处理多种光源的综合实现
主要步骤:
(1)新建一个Shader文件,删除其中无用代码
(2)复制之前的Blinn - Phong光照模型的逐片元光照
(3)其中已存在的Pass,就是我们的BasePass(基础渲染通道)
我们需要为它加上一个编译指令,#pragma multi_compile_fwdbase
该指令可以保证我们在Shader中使用光照衰减等光照等变量可以被正确赋值,并且会帮助我们编译 BasePass 中所有变体
(4)复制BasePass,基于它来修改我们的Additional Pass(附加渲染通道)
(5)LightMode 改为 ForwardAdd
(6)加入混合命令Blend One One 表示开启混合 线性减淡效果
(7)加入编译指令:#pragma multi_compile_fwdadd
该指令保证我们在附加渲染通道中能访问到正确的光照变量,并且会帮助我们编译Additional Pass中所有变体
(8)修改相关代码,基于不同的光照类型来计算衰减值
8 - 1:光的方向计算方向修改
8 - 2:基于不同光照类型计算衰减值
Shader "ShaderProj/1/MixLightMode"
{
Properties
{
_MainColor("MainColor", Color) = (1,1,1,1)
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
_SpecularNum("SpecularNum", Range(0, 20)) = 1
}
SubShader
{
//Base Pass 基础渲染通道
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
struct v2f
{
float4 pos:SV_POSITION;
float3 wNormal:NORMAL;
float3 wPos:TEXCOORD0;
};
fixed3 getLambertColor(in float3 wNormal)
{
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
return color;
}
fixed3 getSpecularColor(in float3 wPos, in float3 wNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(wPos));
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 halfA = normalize(viewDir + lightDir);
fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
return color;
}
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.wNormal = UnityObjectToWorldNormal(v.normal);
data.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return data;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 lambertColor = getLambertColor(i.wNormal);
fixed3 specularColor = getSpecularColor(i.wPos, i.wNormal);
fixed atten = 1; // 衰减值
//衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算
fixed3 BlinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;
return fixed4(BlinnPhongColor, 1);
}
ENDCG
}
//Additional Pass 附加渲染通道
Pass
{
Tags { "LightMode"="ForwardAdd" }
//线性减淡的效果 进行 光照颜色混合
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
struct v2f
{
float4 pos:SV_POSITION;
float3 wNormal:NORMAL;
float3 wPos:TEXCOORD0;
};
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.wNormal = UnityObjectToWorldNormal(v.normal);
data.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return data;
}
fixed4 frag (v2f i) : SV_Target
{
//兰伯特漫反射
fixed3 worldNormal = normalize(i.wNormal);
#if defined(_DIRECTIONAL_LIGHT) //平行光 光的方向 其实就是它的位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else //点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos);
#endif
fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));
//BlinnPhong高光反射
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);
// 衰减值
#if defined(_DIRECTIONAL_LIGHT)
fixed atten = 1;
#elif defined(_POINT_LIGHT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos ,1)).xyz
//利用这个坐标得到距离的平方 然后再再光源纹理中隐射得到衰减值
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY.ATTEN_CHANNEL;
#elif defined(_SPOT_LIGHT)
//将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
float4 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1));
fixed4 atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
#else
fixed atten = 1;
#endif
//在附加渲染通道中不需要在加上环境光颜色了 因为它只需要计算一次 在基础渲染通道中已经计算了
return fixed4((diffuse + specular)*atten, 1);
}
ENDCG
}
}
}