一种具有描边与卡通高光的效果

在阅读《Unity Shader入门精要》时,跟着此书实现了案例shader,今天把第十四章介绍的卡通风格渲染复现了一遍。这是一个简单但被广泛运用的效果。记录一下。感谢乐乐女神~

思路

渲染效果如下图所示:

书中给出的思路是:1.双通道描边;2.对高光反射进行处理,使得出现颜色统一的色块,而不是真实感渲染中的均匀高光反射区域,

描边

在Pass1中将所有顶点沿着外法线方向向外扩大一点,只渲染内表面。值得注意的是,书中源码在摄像机坐标系中扩大顶点。Pass1中将扩大后的内表面渲染成黑色。

在Pass2中正常渲染对象,结合先渲染的Pass1中的扩大版黑色内表面,以形成黑色描边。

Unity以三角形的顶点顺序区分正面还是反面(外面or内面),顺时针为外表面,逆时针为内表面。Cull Off就是正反面都看见,Cull Front就是只看见内表面,Cull Back只看见外表面(这个比较常见)

在Pass1中实现描边。实现代码如下:

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
Pass
{
NAME "OUTLINE"
Cull Front

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include"UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
};
float _Outline;
fixed4 _OutlineColor;

v2f vert (a2v v)
{
v2f o;

float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.5;
normal = normalize(normal);

pos = pos + float4(normal,0)* _Outline;
o.pos = mul(UNITY_MATRIX_P, pos);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = fixed4(_OutlineColor.rgb,1.0);
return col;
}
ENDCG
}

注意内置矩阵UNITY_MATRIX_MV,UNITY_MATRIX_P矩阵的使用。且法线的在坐标系之间的转换与点、向量的转换不一样,需要对矩阵进行逆、转置等变换。UNITY_MATRIX_IT_MV指的是modelview矩阵的逆转矩阵。且对于法线的坐标系间的变换,应使用33的矩阵,4*4的矩阵常用于对点进行变换。在摄像机坐标系中,固定法线的z值,可以使得外轮廓点朝着扁平的方向变化避免出现穿透。

均匀色块

漫反射项与高光项

在Pass2中实现卡通风格的色块,代替真实感渲染的漫反射与高光项。Pass2的代码如下:

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
 Pass{
...
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
};

v2f vert (a2v v) {
v2f o;

o.pos = UnityObjectToClipPos( v.vertex);
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

TRANSFER_SHADOW(o);

return o;
}

float4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);

fixed4 c = tex2D (_MainTex, i.uv);
fixed3 albedo = c.rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

fixed diff = dot(worldNormal, worldLightDir);
diff = (diff * 0.5 + 0.5) * atten;

//利用计算得到的漫反射光照值diff在_Ramp纹理中采样,_Ramp纹理只呈现三种颜色。
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;

fixed spec = dot(worldNormal, worldHalfDir);
fixed w = fwidth(spec) * 2.0;
//对计算得到的高光反射值spec进行处理,使大部分区域其变为非0即1的高光项,接近阈值的区域平滑从0-1过渡
fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
//fixed3 specular = _Specular.rgb * step(1-_SpecularScale, spec) * step(0.0001, _SpecularScale);

return fixed4(ambient + diffuse + specular, 1.0);
}

...
}

记录两个函数,step,smoothstep:

Unity中生成阴影

经典的生成阴影的方法是shadow map方法,Unity已经维护了屏幕空间的阴影映射纹理,作者学会利用Unity的技术的话,就可以非常迅速的得到阴影了……开启光源的shadow type,开启模型的Lighting选项中的阴影选项(receive shadow+cast shadow),注意渲染模型的shader里要能访问到ShadowCaster Pass(更新光源的阴影映射纹理从而更新屏幕的阴影映射纹理)。
在接收shadow的渲染对象的shader中,要想阴影显现,那要在fragment中,申明Unity和阴影相关的宏(如小节“漫反射项与高光项”中的代码)。

参考文献

  1. Unity Shader 入门精要
  2. Built-in shader variables - Unity官方文档
  3. GAMES101课件