《Unity Shader入门精要》学习笔记三(复杂的光照)
1、渲染路径
渲染路径决定了光照是如何应用到Unity Shader中的。
只有为Shader正确地选择和设置了需要地渲染路径,该Shader的光照才能被正确执行。
LightMode标签支持的渲染路径设置选项:
1)什么是渲染路径
渲染路径是指引擎在绘制3D场景时所采用的光照与着色计算流程。不同的渲染路径决定了光源如何影响物体、光照数据如何传递给着色器(Shader),以及最终画面的视觉质量和性能表现。
可以理解为:Unity提供了几种“标准渲染流程”,开发者需要明确告诉引擎,我这个物体希望走哪一套流程。
2)为什么要指定渲染路径
Unity支持的渲染路径有:
- 前向渲染(Forward Rendering)
- 延迟渲染(Deferred Rendering)
- 仅顶点光照(Vertex Lit)
每种路径在底层处理光照的方式完全不同。为了确保着色器能够正确获取光照信息(如光源颜色、方向、强度等),必须通过Shader的Pass标签(Tags)明确声明:我这个Pass是为哪种渲染路径准备的。
Tags { "LightMode" = "ForwardBase" }
3)不指定会怎么样?
如果不指定,Unity将无法判断你希望使用哪种光照流程。此时:
- 引擎可能将其当作最基础的 顶点光照(Vertex Lit)来处理
- 多光源、逐像素光照、阴影等高级效果将无法正常工作
- 内置光照变量(如unity_LightColor、_WorldSpaceLightPos0)可能不会被正确赋值
- 最终导致材质显示异常,比如变黑、缺少高光、光影错乱等。
4)如何工作?
当你在 Project Settings 中设置了项目的默认渲染路径(如 Forward),Unity 会在渲染每一帧时:
- 根据当前路径,组织和准备相应的光照数据;
- 遍历场景中的物体,查找其 Shader 中符合当前路径的 Pass(通过
LightMode
标签识别); - 调用该 Pass 进行渲染,并自动填充对应的光照变量;
- 实现正确的光照计算。
因此,指定渲染路径是连接你编写的 Shader 与 Unity 渲染系统之间的“桥梁”。只有桥搭好了,光照信息才能顺利传递,画面才能正确呈现。
2、前向渲染原理
1)概念
前向渲染=画画+叠滤镜
想象你在画画:
- 你有一张画布(这就是帧缓冲区,里面存着最终画面的颜色)
- 你面前有一堆3D模型(比如一个球、一个盒子),你要把它们一个一个画到画布上
- 画的时候,你不是随便画的,你还得考虑光照——哪个地方亮、哪个地方暗、有没有影子
这个怎么画+怎么打光的流程,就叫前向渲染。
2)前向渲染怎么一步步画的
第一步,从前往后,一个物体一个物体地画
我要画这个物体了,先把它拆成一个个小三角形(图元),一个一个处理
第二步,每个三角形,又分成无数小点(像素)来画
这个三角形盖住了屏幕上很多像素点,我得一个一个去看看要不要画它
第三步,先看“谁在前面”,挡住地就不画
Unity有个“深度记录本”(深度缓冲区),记着每个像素点当前最靠近镜头的是谁。
比如,你先画一个球,离镜头近;后面还有一个盒子,离得远。
当你画盒子的时候,发现某个像素点以及该被球占了,盒子再这儿就被挡住了,那这个点就直接扔掉,不画了。这样就能保证,近的物体挡住远的,不会穿模。
第四步,能画的点,开始打光
这个点没被挡住,是可见的,那就根据光照来算它的颜色。
比如:
- 这个点有没有被阳光照到
- 是亮面还是暗面
- 是不是有灯照它
把这些光照效果算出来,得出一个颜色,然后写到画布上(颜色缓冲区)。
重点是:一个光源,就要画一遍。上面这一整套流程,是针对一个光源的。
如果有第二个光源,得完整走一遍上面得的流程:画三角形 -> 判断遮挡 -> 算光照 -> 上色,然后把新算出的光照颜色叠加上去(混合)。
性能优化策略:每个物体最多只认真算2~3个最强的光源,其他的光就糊弄一下(比如按顶点光算,或者直接忽略)。
3)前向渲染方式
逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理。
决定一个光源使用哪种处理模式取决于它的类型和渲染模式。
光源类型指的是该光源是平行光还是其他类型的光源。
光源的渲染模式指的是该光源是否是重要的。
渲染路径的设置用于告诉Unity该Pass在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如_LightColor0等),如何使用这些内置变量进行计算完全取决于开发者的选择。例如,我们完全可以利用Unity提供的内置变量在Base Pass中只进行逐顶点光照;同样,我们也完全可以在Additional Pass中按逐顶点的方式进行光照计算,不进行任何逐像素光照计算。
4)内部变量
内置光照变量:
内置光照函数:
3、顶点照明渲染路径
它是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持哪些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。
顶点照明渲染路径中可以使用的内置变量:
顶点照明渲染路径中可以使用的内置函数:
4、延迟渲染路径
当场景中包含大量实时光源时,前向渲染的性能会急速下降。
每执行一个Pass都需要重新渲染一遍物体,很多计算实际上是重复的。
延迟渲染,除了前向渲染中使用的颜色缓冲和深度缓冲外,还会利用额外的缓冲区(称为G缓冲,G:Geometry)。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。
延迟渲染=先拍照存原始数据,再统一P图打光。
它把“画物体”和“算光照”分开,所以哪怕场景里有100个灯,也只需要画一遍物体,大大提升了效率。缺点是费内存,而且对透明东西不太友好。
两个步骤:
步骤 | 干了啥 | 官方叫法 |
---|---|---|
Pass 1 | 不算光照,只存数据:颜色、法线、深度等 | 几何Pass / G-Buffer Pass |
Pass 2 | 读取数据,统一打光,生成最终画面 | 光照Pass / Lighting Pass |
延迟渲染和前向渲染 的区别:
前向渲染:一个物体,被5个灯照,那就画5遍,每遍算一个灯。
延迟渲染:不管几个灯,我都只画一遍物体,把信息存好;然后,有多少灯就算多少次光照,但不用再画物体了。
5、光源类型
Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源。面光源仅在烘培时才可发挥作用。
1)平行光
平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。
特点:
- 没有唯一的位置,可以放在场景中的任意位置
- 几何属性只有方向,可以通过调整平行光的Transform组件中的Rotation属性来改变它的光源方向
- 平行光到场景中所有点的方向都是一样的
- 没有衰减的改变,光照强度不会随着距离而发生改变
2)点光源
点光源的照亮空间是有限的,它是由空间中的一个球体定义的。
点光源可以表示由一个点发出的、向所有方向延伸的光。
点光源是有位置属性的,它是由点光源的Transform组件中的Position属性定义的。
点光源是会衰减的。
3)聚光灯
照亮空间是有限的,是空间中的一块锥形区域定义的。
聚光灯可以表示由一个特定位置出发、向特定方向延伸的光。
(6)属性访问
在前向渲染路径上,访问位置、方向、颜色、强度以及衰减。
放置2个光源,1个是已有的平行光,另一个是点光源(光是绿色的)。
完整代码:
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/ForwardRendering"
{Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)_Specular("Specular", Color) = (1,1,1,1)_Gloss("Gloss", Range(8.0, 256)) = 20}SubShader{Tags{"RenderType"="Opaque"}Pass{Tags{"LightMode" = "ForwardBase"}CGPROGRAM#pragma multi_compile_fwdbase#pragma vertex vert #pragma fragment frag#include "Lighting.cginc"fixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i .worldPos.xyz);fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);fixed atten = 1.0;return fixed4(ambient + (diffuse + specular) * atten, 1.0);}ENDCG}Pass{Tags{"LightMode" = "ForwardAdd"}Blend One OneCGPROGRAM#pragma multi_compile_fwdadd#pragma vertex vert #pragma fragment frag #include "Lighting.cginc"#include "AutoLight.cginc"fixed4 _Diffuse;fixed4 _Specular;fixed _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);#ifdef USING_DIRECTIONAL_LIGHTfixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);#elsefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);#endiffixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);#ifdef USING_DIRECTIONAL_LIGHTfixed atten = 1.0;#else#if defined(POINT)float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;#elif defined(SPOT)float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;#elsefixed atten = 1.0;#endif#endifreturn fixed4((diffuse + specular) * atten, 1.0);}ENDCG}}FallBack "Specular"
}
最终效果如下:
代码解读:
Tags { "RenderType"="Opaque" }
告诉Unity这个是不透明物体,不要半透明处理。
Pass {Tags { "LightMode"="ForwardBase" }
第一个Pass:处理基础光(环境光+第一个光源)
第一个最重要的光源通常是太阳。
#include "Lighting.cginc"
引入Unity的光照数据库,比如_LightColor0(当前主光源的颜色)、_WorldSpaceLightPos0(主光源方向)等。
struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;
};
每个顶点告诉Shader:
- 它的顶点位置(vertex)
- 它的法线方向(normal)————用于判断光照角度
struct v2f {float4 pos : SV_POSITION; // 屏幕上的位置float3 worldNormal : TEXCOORD0; // 世界中的法线方向float3 worldPos : TEXCOORD1; // 世界中的位置
};
顶点着色器(vert)把物体从“模型坐标”转到“世界坐标”和"屏幕坐标"。
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
环境光—— 即使没有光照也有点亮
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
漫反射光—— 物体基础颜色受光照影响
dot(worldNormal, worldLightDir):计算光照角度(正面照最亮,侧面照暗)
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
镜面高光—— 反光亮点
- viewDir:摄像机看这个点的方向
- halfDir:光线和视线的中间方向
- pow(xx, _Gloss):控制高光大小,_Gloss越大,亮点越小越锐利
fixed atten = 1.0;
光照衰减—— 距离越远光越弱
在ForwardBase中,主光源通常是平行光(太阳),所以衰减是1.0(不随距离变暗)。
第二个Pass:处理“额外的光源“(ForwardAdd)
Pass {Tags { "LightMode"="ForwardAdd" }Blend One One
Blend One One:叠加,把新光源的颜色加到之前的结果上。
#include "AutoLight.cginc"
用于处理点光源、聚光灯的衰减和阴影。
#ifdef USING_DIRECTIONAL_LIGHTfixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#elsefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
如果是平行光(太阳),方向是固定的。
如果是点光源(灯泡),方向是从灯泡指向当前像素。
添加if判断,是因为目前未限制第二个光源的类型。
_WorldSpaceLightPos0.xyz 在不同光源下含义不同,方向光存储的是光照方向,点光/聚光存储的是光源在世界中的位置。
#ifdef USING_DIRECTIONAL_LIGHTfixed atten = 1.0;
#else#if defined (POINT)float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;#elif defined (SPOT)float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;#elsefixed atten = 1.0;#endif
#endif
复杂的衰减。
点光源(灯泡):光越远越暗,用纹理查衰减值。
聚光灯(手电筒):只有锥形范围内有光,还要查纹理判断是否在光锥内。
其他:默认不衰减。
6、光照衰减
光照衰减的含义:想象你手里有个手电筒,照得越远的地方,光线就越暗。这种“离光源越远,光越暗”的现象,就叫光照衰减。
在 Unity 里,它不是靠复杂的数学公式实时算出来的,而是用一张“提前画好的图”来查这个“有多暗”的值。这张图,就是所谓的 纹理(Texture)。
Unity 用一张叫 _LightTexture0 的纹理图来查每个点的光照衰减值。
你可以把这张图想象成一个“亮度查询表”,横着、竖着都有坐标(像地图一样),但 Unity 只关心这张图从左下角到右上角的对角线上的点。
- 图上
(0,0)
的位置:表示就在光源正中心的地方,光是最亮的(衰减最小)。 - 图上
(1,1)
的位置:表示你能看到的最远距离,光是最暗的(衰减最大)。
所以,只要知道一个点“离光源有多远”,就能在这张图的对角线上找个位置,查出它应该有多亮。
怎么知道一个点离光源有多远?还得有个“光源坐标系”。Unity 要知道这个点在“光源自己的视角下”的位置,就像“以手电筒为中心”来看这个点在哪。
Unity 提供了一个叫 _LightMatrix0 的“转换器”,它的作用就是:把一个在世界中的点(比如你的角色脑袋),转换成“在光源眼里它在哪”。
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
把世界中的点,用 _LightMatrix0 变换一下,得到它在光源空间里的位置(lightCoord)。
用“距离的平方”去查表,为啥不用“距离”? 不用开根号比较快,而且那张 _LightTexture0 图,本来就是按“距离平方”来画的,所以完全匹配。
用距离平方去纹理图上查衰减值:
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
dot(lightCoord, lightCoord)
:算出距离的平方。.rr
:把一个数变成一个二维坐标,比如 0.5 变成 (0.5, 0.5),正好落在纹理图的“对角线”上。tex2D(...)
:拿着这个坐标去_LightTexture0
图上查颜色。.UNITY_ATTEN_CHANNEL
:查出来的颜色有 R、G、B、A 四个分量,Unity 只取其中一个(比如 R 分量)作为衰减值,这个宏确保跨平台正确。
总结一下:
- Unity 用一张叫
_LightTexture0
的图来查“光有多暗”。 - 先用
_LightMatrix0
把世界中的点转成“在光源眼里”的位置。 - 算这个位置到光源中心的距离的平方(不用开方,省时间)。
- 用这个平方值当坐标,去
_LightTexture0
的对角线上查亮度。 - 得到的结果就是光照衰减值,用来让远处的物体变暗。
7、Unity阴影
1)为什么要加“阴影”
你站在阳光下,前面有个电线杆,这时候地上会出现一个“黑影”,这就是电线杆挡住阳光形成的阴影。
Unity就要模拟这种“谁挡住了光,谁就再地上投个影子”的效果。
2)Unity怎么知道“哪里有影子”
使用Shadow Map(阴影贴图)的技术,原理是:从光源的眼睛看过去,能看到的地方就有光,看不到的地方就是影子。
Unity怎么记录“光源能看到哪些地方”呢?它会先拍一张“光源视角的照片”,但这张照片不记录颜色,只记录每个点离光源有多远。这张距离图就叫 阴影映射纹理(shadowmap)。它就是一张“深度图”,告诉Unity:从光源出发,最近的物体在哪儿。
3)生成阴影图(谁可以投影子)
要生成这张shadowmap,Unity要做一件事:把摄像机移到光源的位置(比如移到太阳那里),然后看看它能看到什么。我们只关心深度信息(哪个物体最近),不需要算光照、颜色、反光这些复杂的东西。所以Unity不用普通的渲染流程,而是专门用一个叫ShadowCaster Pass的通道来快速画出这张图。
ShadowCaster Pass:它是一个特殊的渲染指令,写在Shader里的。
标记是:LightMode="ShadowCaster"
它的作用是告诉Unity,我能投影子,然后把自己的轮廓“印”到shadowmap上。shadowmap上记录了最近物体的深度。
如果你的模型(物体)有这个Pass,它就能挡住光,向别的物体投影子。
4)使用阴影图(谁接收影子)
有了shadowmap,它记录了“从光源看,哪里被挡住了”。
接下来,当Unity渲染地面、墙壁、人物时,它会问:“我现在这个点,是在光源看得见的地方,还是被挡住了”。
怎么判断?拿这个点的位置换算到“光源的视角”下,去shadowmap里查一下。如果shadowmap说:这里最近的物体更近(比如电线杆),而你现在的位置更远,说明你被挡住了,你在阴影里面。如果shadowmap说,这里就是你,没被档,说明你在光照下。
然后Unity把这个“是否在阴影里”的结果,乘到最终的颜色上:
在光里?正常亮。
在影子里?变暗。
这样影子就出来了。
5)Unity5后的黑科技:屏幕空间阴影(Screen Space Shadow)
先生成一张“全屏的阴影图”,叫做屏幕空间阴影图,等到渲染角色、地板时,只需要算出这个点在屏幕上的位置,去这张“阴影图”里查一下:这个位置有没有影子。
好处:查一次就行,速度快。只有部分设备支持这种技术。
步骤:
- 先从光源视角渲染一遍场景,生成 Shadow Map(记录“光源能看到的最近物体”)。
- 再从摄像机视角渲染一遍,得到当前画面所有像素的深度信息(叫“深度纹理”)。
- 最后把这两张图对比,算出“哪些像素被挡住了光” → 得到一张“全屏阴影图”。
- 渲染物体时,直接查这张“全屏阴影图”就知道要不要变暗。
当物体动了之后:
情况 | Unity 怎么处理 |
---|---|
物体移动了 | → 重新参与 Shadow Map 的渲染(通过 ShadowCaster Pass) |
光源移动了 | → 重新从光源视角渲染 Shadow Map |
摄像机移动了 | → 重新生成摄像机的深度纹理(因为视角变了) |
静态物体 + 静态光源 | → Unity 会缓存阴影,不用每帧重算(叫“静态光照”或“烘焙阴影”) |
所以:
- 动态物体(会动的):每帧都参与阴影计算。
- 静态物体(不会动的):Unity 会提前算好,不拖慢每帧性能。
Fallback "Specular"会触发产生阴影。虽然Specular本身也没有包含ShadowCaster的Pass,但是由于它的Fallback调用了VertexLit,它会继续回调,并最终回调到内置的VertexLit,这其中包含了ShadowCaster的Pass。
6)接收阴影的示例
Shader "Custom/ReceiveShadowShader"
{Properties{_Color ("Color", Color) = (0, 1, 0, 1) // 绿色}SubShader{Tags { "RenderType"="Opaque" }LOD 200Pass{Tags { "LightMode" = "ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdbase // 必须加!支持光照和阴影#include "UnityCG.cginc"fixed4 _Color;// ✨ 第一剑:SHADOW_COORDS —— 我要准备接影子了!struct v2f{float4 pos : SV_POSITION;SHADOW_COORDS(1) // 声明:这个结构体里会存一个“影子坐标”};// ✨ 第二剑:TRANSFER_SHADOW —— 把影子坐标从顶点传给像素v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex); // 正常渲染位置// 把“如何查影子”这个信息,从顶点计算出来,传给像素TRANSFER_SHADOW(o);return o;}// ✨ 第三剑:SHADOW_ATTENUATION —— 真正去查“有没有影子”fixed4 frag (v2f i) : SV_Target{fixed4 color = _Color;// 去“影子图”里查一下:这个点有没有被挡住光?fixed shadow = SHADOW_ATTENUATION(i);// 把影子结果乘上去:有影子就变暗,没影子就正常亮color *= shadow;return color;}ENDCG}}Fallback "Diffuse" // 如果这个Shader不支持阴影,用默认的
}
代码解读:
SHADOW_COORDS、TRANSFER_SHADOW和SHADOW_ATTENUATION是计算阴影时的“三剑客”。
SHADOW_COORDS(1)
意思:在v2f这个结构体里,要留一个地方,专门用来存“怎么查影子”的信息。
(1)表示用哪个“纹理坐标槽”来存这个信息。
TRANSFER_SHADOW(o);
意思:在顶点着色器里,计算出“查影子需要的坐标”,并传给像素着色器。
Unity会根据当前是“普通阴影”还是“屏幕空间阴影”,自动决定怎么算这个坐标。
它会把顶点从“模型位置”变换到“光源视角”或“屏幕空间”,为查影子做准备。
fixed shadow = SHADOW_ATTENUATION(i);
意思:现在到了像素着色器,拿着刚才传来的坐标,去“影子图”里查一下,我有没有被挡住光。
如果查出来是1.0:没有影子,正常亮。
如果查出来是0.3:在影子里,变暗。
这个值就是光照要乘的系数。
总结:
宏 | 角色 | 干啥的 |
---|---|---|
SHADOW_COORDS | 准备者 | “我需要一个地方存影子坐标” |
TRANSFER_SHADOW | 传递者 | “顶点算好坐标,传给像素” |
SHADOW_ATTENUATION | 执行者 | “去影子图查,然后决定多暗” |
8、统一管理光照衰减和阴影
在Base Pass中,平行光的衰减因子总是等于1,而在Additional Pass中,我们需要判断该Pass处理的光源类型,再使用内置变量和宏计算衰减因子。
实际上,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的,我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。
我们可以通过UNITY_LIGHT_ATTENUATION宏来同时计算这两个值。
代码示例:
Shader "Unity Shaders Book/Chapter 9/Attenuation And Shadow Use Build-in Functions" {Properties {_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)_Specular ("Specular", Color) = (1, 1, 1, 1)_Gloss ("Gloss", Range(8.0, 256)) = 20}SubShader {Tags { "RenderType"="Opaque" }Pass {// Pass for ambient light & first pixel light (directional light)Tags { "LightMode"="ForwardBase" }CGPROGRAM// Apparently need to add this declaration#pragma multi_compile_fwdbase #pragma vertex vert#pragma fragment frag// Need these files to get built-in macros#include "Lighting.cginc"#include "AutoLight.cginc"fixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;SHADOW_COORDS(2)};v2f vert(a2v v) {v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(_Object2World, v.vertex).xyz;// Pass shadow coordinates to pixel shaderTRANSFER_SHADOW(o);return o;}fixed4 frag(v2f i) : SV_Target {fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infosUNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);return fixed4(ambient + (diffuse + specular) * atten, 1.0);}ENDCG}Pass {// Pass for other pixel lightsTags { "LightMode"="ForwardAdd" }Blend One OneCGPROGRAM// Apparently need to add this declaration#pragma multi_compile_fwdadd// Use the line below to add shadows for point and spot lights
// #pragma multi_compile_fwdadd_fullshadows#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"#include "AutoLight.cginc"fixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;SHADOW_COORDS(2)};v2f vert(a2v v) {v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(_Object2World, v.vertex).xyz;// Pass shadow coordinates to pixel shaderTRANSFER_SHADOW(o);return o;}fixed4 frag(v2f i) : SV_Target {fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infosUNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);return fixed4((diffuse + specular) * atten, 1.0);}ENDCG}}FallBack "Specular"
}
UNITY_LIGHT_ATTENUATION 是 Unity 提供的一个 内置宏(macro),定义在 AutoLight.cginc 中。它的作用是:计算光照衰减(attenuation)和阴影(shadowing)的综合影响因子。
UNITY_LIGHT_ATTENUATION 是一个宏,它会“展开”成真正的代码,并在展开时“声明”atten 变量。atten:用来接收衰减+阴影结果的变量名。