本节笔记摘选自《Unity Shader入门精要》
在实时渲染之中,我们最长使用的是Shadow Map技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。
Unity的处理方法:
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理。这张纹理本质上也是一张深度图。它记录了从光源的位置出发,能看到的场景中距离它的最近的表面位置(深度信息)。
处理位置:
Unity中是使用一个专门的额外Pass来更新光源的阴影映射纹理,这个Pass就是之前提到过的LightModel标签为Shadow Caster的Pass。这个Pass的目标不是帧缓存,而是阴影映射纹理。
工作过程:
Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到的光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果之后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode 为 ShadowCaster的Pass。如果没有,就会继续去Fallback回调的Unity Shader中进行寻找,如果仍然没有找到,该物体就无法向其它物体投射阴影。当找到了一个LightMode为ShadowCaster的Pass之后,Unity会使用该Pass来更新光源的阴影映射纹理。
Unity5之后新增加的处理方法:
Unity5使用了延迟渲染中产生阴影的方法-屏幕空间的阴影映射技术(但并不是所有平台都会使用,因为需要显卡支持MRT)
当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有的阴影区域。如果我们想要一个物体接受来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间转换到屏幕空间之中,然后使用这个坐标对阴影图进行采样即可。
总结:
1.如果我们想要一个物体接受来自其他物体的阴影,就必须在Shader中对阴影映射纹理进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
2.如果我们想要一个物体向其它物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来是实现的。
此外,在写Shader的时候还有一个需要注意的点:当使用SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUTATION时,由于这些宏使用的是上下文变量来进行相关计算,因此,为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。 我们需要保证:a2v结构体的顶点坐标变量必须是vertex,顶点着色器输入结构体a2v必须命名为v,且v2f中的顶点位置变量必须命名为pos。
Shadow Shader:
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/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
// 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 = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// Pass shadow coordinates to pixel shader
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
// 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 position : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.position = 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
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
实际渲染结果:
计算阴影中调用的关键两步:
上方应该生成的就是阴影图(屏幕空间阴影映射技术)
除此之外,还有一些和透明度相关的细节需要注意:
开启了透明度测试的物体使用Transparent/Cutout/VertexLit计算阴影。此时必须要提供_Cutoff的属性。
而开启了透明度混合的物体一般来说是不会渲染阴影的。我们当然可以选择强制开启,但也有一些更高级的方法来形成混合阴影。以后我会尝试做一下。