写在前面
本系列其他文章:
好久没写博客了,一定是因为课程作业比较多,一定不是因为我懒,恩恩。
三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Shader中实现描边效果的弊端,也就是只对表面平缓的模型有效。这是因为我们是依赖法线和视角的点乘结果来进行描边判断的,因此,对于那些平整的表面,它们的法线通常是一个常量或者会发生突变(例如立方体的每个面),这样就会导致最后的效果并非如我们所愿。如下图所示:
因此,我们有一个更好的方法来实现描边效果,也就是通过两个pass进行渲染——首先渲染对象的背面,用黑色略微向外扩展一点,就是我们的描边效果;然后正常渲染正面即可。而我们应该知道,surface shader是不可以使用pass的。
在这篇里,我们就会学习如何使用Vertex & Fragment Shader来实现上述的过程。很显然,这样的一个过程包含了两个步骤——描边和正常的渲染。
最后的效果如下:
实现描边
在上一篇里,我们使用了边缘高光来实现描边。而这篇里,我们将使用一个单独的pass来得到一个更好的效果。这里说的“更好”指的是以下几个方面:
- 首先是对平整表面的适应性,如上面正方体的例子,这种方法仍可以得到期望的效果;
- 而且这种方法可以不破坏正面模型的逼真度,也就是说正面模型可以完全不受影响。与之产生对比的是上一篇中的方法,使用边缘光照来实现的描边效果会影响到正面模型的表面,即正面模型也会有强烈的描边效果,而这往往不是我们所期望的。
- Cull Front
- hting 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倍后,再转换到投影平面上输出最后的屏幕位置信息。
- Cull Front
- Lighting Off
- ZWrite Off
这样一来,这个pass的结果是不写入深度缓存中的,而后面只要有其他材质要渲染该点的像素就会覆盖它。这样的效果如下:
扁平化背面
- 把顶点位置转换到视角坐标系;
- 把法线转换到视角坐标系;
- 把转换后的法线的z值扁平化,即使其是一个较小的定值,这样所有的背面其实都在一个平面上;
- 按描边的宽度放缩法线,并添加到转换后顶点的位置上,得到新的视角坐标系中的位置;
- 把新的位置转换到投影坐标系中。
- 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 "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;
- }
- 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 = float3(0);
- //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
- }
- }
- FallBack "Diffuse"
- }
写在最后
读者有需要的可以自己添加上视角方向的采样,也就是说在v2f里添加一个新的变量viewDir,然后逐顶点计算后传递给frag函数。如果我后面有时间的话可能会回头添加上。当然,大家还是靠自己比较好。