在Unity中为顶点/片段着色器添加阴影
译文
大部分情况下当我们使用Unity的表面着色器的时候,就已经拥有阴影了。但是有时候因为某些原因你不想使用表面着色器,而是想创建你自己的顶点/片段着色器。这样做最大的好处是一切皆由你控制,但是这同时也是它的缺点:你将不得不自己处理Unity在表面着色器里为你处理好的很多东西。其中一件事就是多重光照和阴影。
幸运的是,Unity提供了解决的办法!怎么做?文档中关于这一点所言甚少。我也和很多处在类似处境的人一样对于如何让顶点/片段着色器拥有阴影感到困惑。我Google了一些相关资料,虽然得到的信息不能解决我的问题,却也给了我不少指引。我还查看了一个编译后的表面着色器,试图找出他们是如何添加阴影的。所有这些工作加在一起,再加上一些尝试,最后终于成功了!现在让我来和对此感兴趣的人分享这一切吧。
在开始之前,我得提早指出——当你使用表面着色器的时候,Unity为你做了很多工作;其中一件事当你使用 deffered 或 forward渲染时的内部处理。当你使用自己的顶点/片段着色器的时候,你得把这一点计算在内。实际上,我只需要应付forward渲染这一种情况,并且只对deffered渲染做了简短的测试。虽然在deffered渲染模式下没出什么差错,但是我并不保证它在此模式一切正常。请牢记这点。
我们来看看这个漫反射的shader。它能够投射(和接受)阴影。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
Shader
"Sample/Diffuse"
{
Properties
{
_DiffuseTexture (
"Diffuse Texture"
, 2D) =
"white"
{}
_DiffuseTint (
"Diffuse Tint"
, Color) = (1, 1, 1, 1)
}
SubShader
{
Tags {
"RenderType"
=
"Opaque"
}
pass
{
Tags {
"LightMode"
=
"ForwardBase"
}
CGPROGRAM
#pragma target 3.0
#pragma fragmentoption ARB_precision_hint_fastest
#pragma vertex vertShadow
#pragma fragment fragShadow
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
sampler2D _DiffuseTexture;
float4 _DiffuseTint;
float4 _LightColor0;
struct
v2f
{
float4 pos : SV_POSITION;
float3 lightDir : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
LIGHTING_COORDS(3, 4)
};
v2f vertShadow(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
o.lightDir = normalize(ObjSpaceLightDir(v.vertex));
o.normal = normalize(v.normal).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o);
return
o;
}
float4 fragShadow(v2f i) : COLOR
{
float3 L = normalize(i.lightDir);
float3 N = normalize(i.normal);
float
attenuation = LIGHT_ATTENUATION(i) * 2;
float4 ambient = UNITY_LIGHTMODEL_AMBIENT * 2;
float
NdotL = saturate(dot(N, L));
float4 diffuseTerm = NdotL * _LightColor0 * _DiffuseTint * attenuation;
float4 diffuse = tex2D(_DiffuseTexture, i.uv);
float4 finalColor = (ambient + diffuseTerm) * diffuse;
return
finalColor;
}
ENDCG
}
}
FallBack
"Diffuse"
}
|
如果你曾经写过顶点/片段着色器的话,你应该知道shader中需要注意的就是一些宏。但是让我们看一下要获得阴影,首先应该怎么做。
首先你得定义 pass里的一个Tag : LightMode。
1
|
Tags {
"LightMode"
=
"ForwardBase"
}
|
这会告诉Unity这个通道会使用主光源来投射阴影。Unity对每个光源使用它们自己的通道进行处理。如果我们想要使用多重光照,在另一个通道里面这个值会变成 ForwardAdd。
接下来我们要定义:
1
|
#pragma multi_compile_fwdbase
|
这是为了确保shader为所需的通道执行恰当的编译。有了这个tag,在所有叠加的光源自己的通道里面, fwdbase 就会变成 fwdadd。
为了获得阴影,我们还需要一些额外的代码/宏。所以我们得把AutoLight.cginc 包括进来。
1
|
#include "AutoLight.cginc"
|
因为Unity知道该如何处理光照的一切信息,我们只需要获取相关数据然后让我们的阴影显示出来就好了。这需要做到以下三件事:
- 让Unity生成/包含 所需的参数来对阴影进行采样。
- 使用正确的数据来填充这些参数。
- 获取最终值。
要让Unity“生成”我们所需的值,我们所需做的就是在顶点到片段的结构体中添加 LIGHTING_COORDS 指令。
1
2
3
4
5
6
7
8
|
struct
v2f
{
float4 pos : SV_POSITION;
float3 lightDir : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
LIGHTING_COORDS(3, 4)
};
|
LIGHTING_COORDS指令定义了对阴影贴图和光照贴图采样所需的参数。指令参数的数字说明了所需的TEXCOORD参数的数量。 如果我想要一个视点方向来计算高光反射,结构体如下:
1
2
3
4
5
6
7
8
9
|
struct
v2f
{
float4 pos : SV_POSITION;
float3 lightDir : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
float3 viewDir : TEXCOORD3;
LIGHTING_COORDS(4, 5)
};
|
这有点像是自己定义了这些参数,其实不然。Unity很确定它们使用的是正确的数值,正确的光源,或许还有一个cookie 纹理。如果你好奇到底定义了些什么,请查看 AutoLight.cginc文件。
下一步是顶点着色器。光有这些参数还不够,我们还得提供正确的数据。Untiy提供了另一个指令以便在正确的情况下填充正确的数据 ——TRANSFER_VERTEX_TO_FRAGMENT。这个指令必须在返回 v2f 结构体之前定义。所以顶点着色器如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
v2f vertShadow(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
o.lightDir = normalize(ObjSpaceLightDir(v.vertex));
o.normal = normalize(v.normal).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o);
return
o;
}
|
没有太多要点。只需知道它为你计算不同光源的光照和阴影坐标。
现在我们只剩下创建我们的片段着色器并且使用LIGHT_ATTENUATION指令来返回正确的最终值了。你可以像平时一样使用 attenuation 值。在漫反射shader里我把它用在漫反射计算中了。就像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
float4 fragShadow(v2f i) : COLOR
{
float3 L = normalize(i.lightDir);
float3 N = normalize(i.normal);
float
attenuation = LIGHT_ATTENUATION(i) * 2;
float4 ambient = UNITY_LIGHTMODEL_AMBIENT * 2;
float
NdotL = saturate(dot(N, L));
float4 diffuseTerm = NdotL * _LightColor0 * _DiffuseTint * attenuation;
float4 diffuse = tex2D(_DiffuseTexture, i.uv);
float4 finalColor = (ambient + diffuseTerm) * diffuse;
return
finalColor;
}
|
现在你已经拥有让你的顶点/片段着色器获得阴影所需的一切了。LIGHT_ATTENUATION指令对阴影贴图采样并且返回数据供你使用。再说一次,如果你想知道LIGHT_ATTENUATION指令具体做了些什么,检查 AutoLight.cginc文件。
对了,还有一件小事要处理。如果要让Unity投射/接受阴影,你必须提供一个 shadow receiver 和 shadow caster通道。我这里没有提供这些。我只是加了一个提供了这些通道的 fallback shader。那样我就用不着自己添加这些通道,令shader的体积变大。当然,你还可以把这些代码放到一个 .cginc 文件里,或者干脆放到最底部,眼不见为净。不过现在来讲简简单单的加一个 fallback 就能工作的很好了。
我希望这些内容能对那些试图在自己的着色器里面添加阴影的人有所帮助。有任何问题请给我评论!
原文地址:http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/let-there-be-shadow-r3642
蛮牛发布地址:http://www.manew.com/thread-41638-1-1.html