【Unity Shader学习笔记】Unity光照-阴影
1、原理
1.1、概述
由一个物体向其他物体投射阴影,以及一个物体如何接收其他物体的阴影。
实时渲染中经常使用 Shadow Map 技术。它会首先把摄像机的位置放在与光源重合的位置上, 那么场景中该光源的阴影区域就是那些摄像机看不到的地方。Unity 就是使用的这种技术。
在前向渣染路径中, 如果场景中最重要的平行光开启了阴影, Unity 就会为该光源计算它的阴影映射纹理( shadow map)。 这张阴影映射纹理本质上也是一张深度图, 它记录了从该光源的位置出发、 能看到的场景中距离它最近的表面的深度信息。
1.2、ShadowCaster Pass
Unity 选择使用一个额外的 Pass 来专门更新光源的阴影映射纹理, 这个 Pass 就是 LightMode 标签被设
置为 ShadowCaster 的 Pass。
这个 Pass 的渲染目标为深度纹理。
Unity 首先把摄像机放置到光源的位置上, 然后调用该 Pass, 通过对顶点变换后得到光源空间下的位置, 并据此来输出深度信息到阴影映射纹理中。
1.3、Screenspace Shadow Map
正常情况下,我们在渲染阴影时,会将每个顶点位置变换到光源空间下, 然后使用 xy 分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。 如果该深度值小于该顶点的深度值( z 分量),那么说明该点位于阴影中。
Unity 提出了一种新的采样技术:屏幕空间的阴影映射技术(Screenspace Shadow Map)。
这项技术原本时延迟渲染中产生阴影的一种方法。应用这项技术要求显卡支持 MRT 。
Unity 首先会通过调用 LightMode 为 ShadowCaster 的 Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。
然后, 根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。 如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值, 就说明该表面虽然是可见的, 但是却处于该光源的阴影中。
如果我们想要一个物体接收来自其他物体的阴影, 只需要在 Shader 中把表面坐标从模型空间变换到屏幕空间, 然后使用这个坐标对阴影图进行采样即可。
1.4、总结
一个物体接收来自其他物体的阴影, 以及它向其他物体投射阴影是两个过程。
- 如果我们想要一个物体接收来自其他物体的阴影, 就必须:
- 在 Shader 中对阴影映射纹理( 包括屏幕空间的阴影图) 进行采样, 把采样结果和最后的光照结果相乘来产生阴影效果。
- 再打开模型的 Mesh Render 组件的 Receive Shadows 属性。
- 如果我们想要一个物体向其他物体投射阴影, 就必须:
- 在 Shader 中添加标签为 ShadowCaster 的 Pass 。
- 更改模型的 Mesh Render 组件的 Cast Shadows 属性。
如果使用了屏幕空间的投影映射技术, Unity 还会使用这个 Pass 产生一张摄像机的深度纹理。
如果没有手动添加标签为 ShadowCaster 的 Pass ,Unity 可能会在 Fallback 中找到合适的 Pass 。
2、不透明物体的阴影
2.1、向物体投射阴影
可以直接利用 FallBack 中的代码实现投射阴影。
2.2、让物体接收阴影
2.2.1、计算过程
使用宏来计算阴影。使用 Base Pass 计算阴影。
- 添加
#include "AutoLight.cginc"
- 在 v2f 中添加
SHADOW_COORDS(2)
- 顶点着色器中添加
TRANSFER_SHADOW(o);
- 片元着色器中添加
fixed shadow = SHADOW_ATTENUATION(i);
- 片元着色器的输出修改为:
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
2.2.2、解释
在前向渲染中, 宏 SHADOW_COORDS 实际就是声明了一个名为 _ShadowCoord 的阴影纹理坐标变量。
TRANSFER_SHADOW 的实现会根据平台不同而有所差异。
- 如果当前平台可以使用屏幕空间的阴影映射技术(判断是否定义了 UNITY_NO_SCREENSPACE_SHADOWS 来得到),TRANSFER_SHADOW 会调用内置的 ComputeScreenPos 函数来计算 _ShadowCoord;
- 如果该平台不支持屏幕空间的阴影映射技术, 就会使用传统的阴影映射技术。
TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到 _ShadowCoord 中。
SHADOW_ATTENUATION 负责使用 _ShadowCoord 中的纹理进行采样,得到阴影信息。
由于这些宏中会使用上下文变量来进行相关计算, 例如 TRANSFER_SHADOW 会使用 v.vertex 或 a.pos 来计算坐标, 因此为了能够让这些宏正确工作, 我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。
我们需要保证: a2f 结构体中的顶点坐标变量名必须是 vertex , 顶点着色器的输出结构体 v2f 必须命名为 v, 且 v2f 中的顶点位置变量必须命名为 pos 。
2.3、代码
Shader "Unity Shaders Book/Chapter 9/Shadow" {
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
#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 = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
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;//光照衰减
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}
ENDCG
}
Pass {
// Pass for other pixel lights
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
// Apparently need to add this declaration
#pragma multi_compile_fwdadd
#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;
};
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_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
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);
//判断是否是平行光,处理衰减
#ifdef USING_DIRECTIONAL_LIGHT
fixed 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;
#else
fixed atten = 1.0;
#endif
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
3、统一管理光照衰减与阴影
使用内置宏UNITY_LIGHT_ATTENUATION
,实现同时计算光照衰减因子与阴影。
在上述代码的基础上,修改片元着色器。
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);
//使用宏UNITY_LIGHT_ATTENUATION
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
宏UNITY_LIGHT_ATTENUATION
会帮助我们声明 atten 变量。
该宏输入三个变量:输出参数名、结构体 v2f 、世界空间坐标。
该宏使用结构体 v2f ,调用 14.4 中的 SHADOW_ATTENUATION 宏实现阴影计算。
该宏使用世界空间下的坐标,计算光源空间下的坐标,并对衰减纹理采样得到光照衰减。
如果我们希望可以在 Additional Pass 中添加阴影效果,
使用#pragma multi_compile_fwdadd_fullshadows
指令
代替 Additional Pass 中的#pragma multi_compile_fwdadd
指令。
这样一来, Unity 也会为这些额外的逐像素光源计算阴影, 并传递给 Shader。
4、透明度测试-阴影
FallBack 使用FallBack "Transparent/Cutout/VertexLit"
即可让镂空的部分不投射阴影。
Shader "Unity Shaders Book/Chapter 9/AlphaTestWithShadow"
{
Properties
{
_Color ("Main Tint", Color) = (1,1,1,1)
_MainTex("Main Tex", 2D) = "white" {}
_Cutoff("Alpha CutOff", Range(0, 1)) = 0.5//透明度测试的判断条件
}
SubShader
{
Tags{"Queue" = "AlphaTest" "IgnoreProjector"="True" "RenderType" = "TransparentCutout"}
Pass{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
//属性
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//纹理缩放
fixed _Cutoff;
//输入输出结构体
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
//我们已经占用了三个插值寄存器,因此将其存储在TEXCOORD3上
SHADOW_COORDS(3)
};
v2f vert (a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
//Alpha Test
clip(texColor.a - _Cutoff);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + diffuse * atten, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
5、透明度混合-阴影
生成透明度混合物体的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染。
这会非常消耗性能。因此 Unity 所有内置的透明度混合的文件都没有包含阴影投射的 Pass。
如果一定需要阴影,可以将其当作不透明物体来设置有 ShadowCaster 标签的 Pass 。