【Unity Shader实战】卡通风格的Shader(二)

2015年03月14日 16:18 0 点赞 0 评论 更新于 2017-05-08 20:26

在之前一篇关于卡通风格Shader的文章结尾,我们提到了在Surface Shader中实现描边效果存在弊端,即该方法仅对表面平缓的模型有效。这是因为我们依赖法线和视角的点乘结果来进行描边判断,对于平整的表面,其法线通常是常量或者会发生突变(例如立方体的每个面),导致最终效果与预期不符。

为了实现更好的描边效果,我们可以采用通过两个pass进行渲染的方法:首先渲染对象的背面,将其用黑色略微向外扩展,从而得到描边效果;然后正常渲染正面。需要注意的是,Surface Shader无法使用pass,因此在本文中,我们将学习如何使用Vertex & Fragment Shader来实现上述过程,该过程主要包含描边和正常渲染两个步骤。最终效果如下(此处可插入对应效果图)。

实现描边

在上一篇文章中,我们使用边缘高光来实现描边。而在本文中,我们将使用一个单独的pass来获得更好的效果,这里的“更好”主要体现在以下两个方面:

  • 对平整表面的适应性:以正方体为例,该方法仍能达到预期的描边效果。
  • 不破坏正面模型的逼真度:上一篇文章中使用边缘光照实现的描边效果会影响正面模型的表面,使正面模型也出现强烈的描边效果,这通常不是我们所期望的。而本文的方法可以避免这种情况,正面模型能保持不受影响。

这个pass的第一个步骤是剔除正面部分,代码如下:

Cull Front
Lighting Off

接下来,我们先看frag函数,其功能非常简单,就是输出黑色。如果不想要黑色的描边,可以在此处进行修改:

float4 frag(v2f i) : COLOR
{
return float4(0, 0, 0, 1);
}

然后,我们来计算vert函数部分。为了模拟描边效果,我们需要沿着顶点的法线方向向外扩张该点。因此,我们需要在下面的结构体中声明position和normal属性:

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : POSITION;
};

接着,我们定义一个范围在0到1之间的_Outline变量来控制描边的宽度。vert函数如下:

float _Outline;
v2f vert (a2v v)
{
v2f o;
o.pos = mul( UNITY_MATRIX_MVP, v.vertex + (float4(v.normal,0) * _Outline));
return o;
}

上述代码的含义是:将原先的顶点位置v.vertex沿着v.normal的方向扩展_Outline倍,然后转换到投影平面上,输出最后的屏幕位置信息。效果如下(此处可插入对应效果图)。

但当我们把描边的宽度调得比较高时,会发现眼睛和嘴巴的地方出现很大的黑色色块。这是因为眼睛和嘴巴是独立于身体之外的两个网格,它们各自使用了一个新的材质,且在深度关系上,眼睛和嘴巴在身体的后面(被身体的皮肤包裹)。因此,在渲染时,身体的渲染输出像素会覆盖眼睛和嘴巴的部分,包括身体的描边部分,这并非我们所期望的结果。

一种暴力的解决方法是直接关闭该pass的深度信息,代码如下:

Cull Front
Lighting Off
ZWrite Off

这样,这个pass的结果将不会写入深度缓存,后续只要有其他材质要渲染该点的像素就会覆盖它。效果如下(此处可插入对应效果图)。

然而,新的问题又出现了:眼睛和嘴巴部分的显示虽然正常了,但小怪物的先后关系却混乱了,后面小怪物的身体挡住了前面小怪物的描边。要解决这个问题,就需要写入深度缓存,这似乎陷入了一个死循环。

实际上,这表明我们生成描边的方法需要改进。我们回顾出现眼睛和嘴巴错误的原因,是因为描边宽度调得太大。我们这么做(这里是故意调大的),是因为有时相邻顶点的法线指向差异很大,为了得到理想的描边宽度,不得不将其调大。而上述过程的实质是将背光面的模型放大,可理解为创建了一个新的黑色模型。当放大过度时,就会出现穿透和遮挡的问题。

正确的方法是将顶点当成轮廓处理,而不是一个真正的模型。也就是说,当我们观察背面的某一个顶点时,要将其Z方向的值扁平化,这样描边的结果将主要受X和Y方向的影响。

扁平化背面

首先,我们要在视角坐标系中处理描边,因为描边效果是基于我们的观察角度而定的。所以,我们需要将所需的变量(顶点的位置和顶点的法线)都转换到视角坐标系下处理。其中,顶点位置的转换使用UNITY_MATRIX_MV即可,而法线的转换相对麻烦一些,因为法线并非真正定义在模型坐标系中,而是与它正交,我们需要使用ModelView转换矩阵的转置矩阵来将法线转换到视角坐标系中(具体原理可参考相关资料)。

具体步骤如下:

  1. 将顶点位置转换到视角坐标系。
  2. 将法线转换到视角坐标系。
  3. 将转换后的法线的z值扁平化,使其为一个较小的定值,这样所有的背面实际上都在一个平面上。
  4. 按描边的宽度放缩法线,并添加到转换后顶点的位置上,得到新的视角坐标系中的位置。
  5. 将新的位置转换到投影坐标系中。代码如下:
    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.4;
    pos = pos + float4(normalize(normal),0) * _Outline;
    o.pos = mul(UNITY_MATRIX_P, pos);
    return o;
    }
    

卡通化

这部分内容与上一篇文章的方法一致,同样使用简化颜色和渐变纹理来模拟卡通效果,在此不再赘述。

弊端

与上一篇文章的方法相比,本文的方法解决了之前的两个弊端:一是轮廓宽度无法精确保证,二是对法线突变的模型适应性较差。但它也存在自身的问题,即无法为模型内部的褶皱添加轮廓。例如,上述小怪兽模型只有最外层的边界有轮廓线,其内部的肥肉褶皱无法体现。要解决这个问题,可以依靠第三种更高级的Shader来实现,具体内容请参见《卡通风格的Shader(三)》。

代码

相信大家最关心的还是代码,下面为大家提供完整的代码。代码分为两种,一种使用了法线纹理,一种没有使用法线纹理,每种代码都包含三个pass:第一个pass处理背面进行描边,第二个pass处理正面的forwardbase,第三个pass处理正面的forwardadd。

在Vertex & Fragment中处理法线和光照是一件非常具有挑战性的事情(作者感慨:VF虐我千百遍,我却待她如初恋),后续如果有时间,我会写一篇关于这方面的文章。

没有使用法线纹理的代码

Shader "MyToon/Toon-Fragment" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
_Outline ("Outline", Range(0,1)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200

Pass {
Tags { "LightMode"="ForwardBase" }
Cull Front
Lighting Off
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
float _Outline;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : POSITION;
};

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;
pos = pos + float4(normalize(normal),0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}

float4 frag(v2f i) : COLOR {
return float4(0, 0, 0, 1);
}
ENDCG
}

Pass {
Tags { "LightMode"="ForwardBase" }
Cull Back
Lighting On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
sampler2D _MainTex;
sampler2D _Ramp;
float4 _MainTex_ST;
float _Tooniness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
LIGHTING_COORDS(2,3)
};

v2f vert (a2v v) {
v2f o;
//Transform the vertex to projection space
o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
o.normal= mul((float3x3)_Object2World, SCALED_NORMAL);
//Get the UV coordinates
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}

float4 frag(v2f i) : COLOR {
//Get the color of the pixel from the texture
float4 c = tex2D (_MainTex, i.uv);
//Merge the colours
c.rgb = (floor(c.rgb*_Tooniness)/_Tooniness);
//Based on the ambient light
float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
//Work out this distance of the light
float atten = LIGHT_ATTENUATION(i);
//Angle to the light
float diff = dot (normalize(i.normal), normalize(_WorldSpaceLightPos0.xyz));
diff = diff * 0.5 + 0.5;
//Perform our toon light mapping
diff = tex2D(_Ramp, float2(diff, 0.5));
//Update the colour
lightColor += _LightColor0.rgb * (diff * atten);
//Product the final color
c.rgb = lightColor * c.rgb * 2;
return c;
}
ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }
Cull Back
Lighting On
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
sampler2D _MainTex;
sampler2D _Ramp;
float4 _MainTex_ST;
float _Tooniness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
half3 lightDir : TEXCOORD2;
LIGHTING_COORDS(3,4)
};

v2f vert (a2v v) {
v2f o;
//Transform the vertex to projection space
o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
o.normal= mul((float3x3)_Object2World, SCALED_NORMAL);
o.lightDir = WorldSpaceLightDir( v.vertex );
//Get the UV coordinates
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}

float4 frag(v2f i) : COLOR {
//Get the color of the pixel from the texture
float4 c = tex2D (_MainTex, i.uv);
//Merge the colours
c.rgb = (floor(c.rgb*_Tooniness)/_Tooniness);
//Based on the ambient light
float3 lightColor = float3(0);
//Work out this distance of the light
float atten = LIGHT_ATTENUATION(i);
//Angle to the light
float diff = dot (normalize(i.normal), normalize(i.lightDir));
diff = diff * 0.5 + 0.5;
//Perform our toon light mapping
diff = tex2D(_Ramp, float2(diff, 0.5));
//Update the colour
lightColor += _LightColor0.rgb * (diff * atten);
//Product the final color
c.rgb = lightColor * c.rgb * 2;
return c;
}
ENDCG
}
}
FallBack "Diffuse"
}

使用了法线纹理的Shader

Shader "MyToon/Toon-Fragment_Normal" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bump ("Bump", 2D) = "bump" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
_Outline ("Outline", Range(0,1)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200

Pass {
Tags { "LightMode"="ForwardBase" }
Cull Front
Lighting Off
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
float _Outline;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : POSITION;
};

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;
pos = pos + float4(normalize(normal),0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}

float4 frag(v2f i) : COLOR {
return float4(0, 0, 0, 1);
}
ENDCG
}

Pass {
Tags { "LightMode"="ForwardBase" }
Cull Back
Lighting On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
sampler2D _MainTex;
sampler2D _Bump;
sampler2D _Ramp;
float4 _MainTex_ST;
float4 _Bump_ST;
float _Tooniness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float2 uv2 : TEXCOORD1;
float3 lightDirection : TEXCOORD2;
LIGHTING_COORDS(3,4)
};

v2f vert (a2v v) {
v2f o;
//Create a rotation matrix for tangent space
TANGENT_SPACE_ROTATION;
//Store the light’s direction in tangent space
o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
//Transform the vertex to projection space
o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
//Get the UV coordinates
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.uv2 = TRANSFORM_TEX (v.texcoord, _Bump);
// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}

float4 frag(v2f i) : COLOR {
//Get the color of the pixel from the texture
float4 c = tex2D (_MainTex, i.uv);
//Merge the colours
c.rgb = (floor(c.rgb*_Tooniness)/_Tooniness);
//Get the normal from the bump map
float3 n = UnpackNormal(tex2D (_Bump, i.uv2));
//Based on the ambient light
float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
//Work out this distance of the light
float atten = LIGHT_ATTENUATION(i);
//Angle to the light
float diff = saturate (dot (n, normalize(i.lightDirection)));
//Perform our toon light mapping
diff = tex2D(_Ramp, float2(diff, 0.5));
//Update the colour
lightColor += _LightColor0.rgb * (diff * atten);
//Product the final color
c.rgb = lightColor * c.rgb * 2;
return c;
}
ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }
Cull Back
Lighting On
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
sampler2D _MainTex;
sampler2D _Bump;
sampler2D _Ramp;
float4 _MainTex_ST;
float4 _Bump_ST;
float _Tooniness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float2 uv2 : TEXCOORD1;
float3 lightDirection : TEXCOORD2;
LIGHTING_COORDS(3,4)
};

v2f vert (a2v v) {
v2f o;
//Create a rotation matrix for tangent space
TANGENT_SPACE_ROTATION;
//Store the light’s direction in tangent space
o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
//Transform the vertex to projection space
o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
//Get the UV coordinates
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.uv2 = TRANSFORM_TEX (v.texcoord, _Bump);
// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}

// 此处原代码未完整,可根据实际情况补充完整
// ...
}
}
FallBack "Diffuse"
}

作者信息

boke

boke

共发布了 1025 篇文章