UnityShader实例05:Toon(卡通)材质

卡通材质

卡通着色也叫Non-photorealisticrendering非真实渲染,通常一些3D游戏用来做一些卡通风格的游戏,一般来说特点主要有两点,一是描边,二是风格化着色,表现为明暗渐变过渡为非线性梯度着色。如下图所示:火影忍者和无主之地

UnityShader实例05:Toon(卡通)材质_第1张图片UnityShader实例05:Toon(卡通)材质_第2张图片

先做描边

关于描边网上有不少资料,这里就其中比较常用的Rim lighting和法线外拓来实现;

1.Rim lighting

原理在之前XRAY材质有提到过,通过视线和物体法线夹角的点积来获得轮廓。只不过这里我需要做一个线条比较硬一点的描边,因此需要对获取到的轮廓做个取整:

i.color=floor(i.color*2);
另外外轮廓的颜色需要做lerp处理而不是乘法叠加,否则黑色叠上去是看不到的;
col=lerp(col,i.color*_OutlineColor,i.color);



VF版本代码01:

Shader "PengLu/Toon/OutlineOnePassVF" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_OutlineColor("OutlineColor",Color) = (0,1,1,1)
		_RimPower ("Rim Power", Range(0.1,8.0)) = 2

	}
	SubShader {
		Tags { "Queue"="Geometry" "RenderType"="Opaque" }
		LOD 200
		
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			float4 _OutlineColor;
			float _RimPower;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
				float4 color:COLOR;
				float4 normal:NORMAL;
			};

			struct v2f {
				float4  pos : SV_POSITION;
				float2 texcoord : TEXCOORD0;
				float4	color:COLOR;
			} ;
			v2f vert (appdata_t v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				float3 viewDir = normalize(ObjSpaceViewDir(v.vertex));
                float rim = 1 - saturate(dot(viewDir,v.normal ));
                o.color = pow(rim,_RimPower);
				return o;
			}
			float4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.texcoord);
				i.color=floor(i.color*2);
				col=lerp(col,i.color*_OutlineColor,i.color);
				return col; 
			}
			ENDCG
		}
		
	} 
	FallBack "Diffuse"
}
VF版本代码01效果:

UnityShader实例05:Toon(卡通)材质_第3张图片

总结

Rim Lighting实现简单,只需一个Pass对渲染效率增加负担极小。而且边缘可根据需要做出渐变。但是这种方法 只适用于法线较均匀过度的模型。而不适用于棱角分明的低面物体,如立方体模型。


2.顶点外拓

主要方法是在shader里面用两个pass渲染物体两次,第一个pass顶点外拓,用来打底作为描边,第二个pass正常渲染物体。下面这个例子做了一些改良:分别从顶点和法线方向挤出,然后做判断进行插值。

第一个pass作如下处理:

Cull Front 裁剪了物体的前面(对着相机的),把背面挤出
ZWrite On 像素的深度写入深度缓冲,如果关闭的话,物体与物体交叠处将不会被描边,因为此处无z值后渲染的物体会把此处挤出的描边“盖住”


在处理顶点的函数vert中把点挤出

float3 dir=normalize(v.vertex.xyz);
建立一个float3方向变量dir,把该点的位置作为距离几何中心的方向的单位向量


float3 dir2=v.normal;
dir2为法线方向

D=dot(dir,dir2);
D为计算该点位置朝向和法线方向的点积,通过正负值可以确定是指向还是背离几何中心的,正为背离,负为指向

dir=dir*sign(D);
乘上正负值,真正的方向值

dir=dir*_Factor+dir2*(1-_Factor);
把该点位置朝向与法线方向按外部变量_Factor的比重混合,来控制挤出多远

v.vertex.xyz+=dir*_Outline;
把物体背面的点向外挤出

v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
//			dir=dir*_Factor+dir2*(1-_Factor);
			dir=lerp(dir,dir2,_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}

接下来在frag里面给描边上色

float4 frag(v2f i):COLOR
		{
			float4 c = _OutlineColor;
			return c;
		}

第二个pass正常渲染不做修改

VF版本代码02:

//one directional light
Shader "PengLu/Toon/ToonOneLightVF" {
	Properties {
		_Color("Main Color",color)=(1,1,1,1)
		_OutlineColor("Outline Color",color)=(0.1,0.1,0.2,1)
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Outline("Thick of Outline",range(0,0.1))=0.01
		_Factor("Factor",range(0,1))=0.5
		_ToonEffect("Toon Effect",range(0,1))=0.5
		_Steps("Steps of toon",range(0,9))=3
	}
	SubShader {
	
		pass{
		Tags{"LightMode"="Always"}
		Cull Front
		ZWrite On
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		float _Outline;
		float _Factor;
		float4 _Color,_OutlineColor;
		
		struct v2f {
			float4 pos:SV_POSITION;
		};

		v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
//			dir=dir*_Factor+dir2*(1-_Factor);
			dir=lerp(dir,dir2,_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c = _OutlineColor;
			return c;
		}
		ENDCG
		}
		pass{
		Tags{"LightMode"="ForwardBase"}
		Cull Back
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _LightColor0;
		float4 _Color;
		float _Steps;
		float _ToonEffect;
		sampler2D _MainTex;
		float4 _MainTex_ST;

		struct v2f {
			float4 pos:SV_POSITION;
			float3 lightDir:TEXCOORD0;
			float3 viewDir:TEXCOORD1;
			float3 normal:TEXCOORD2;
			float2 texcoord:TEXCOORD3;
		};
		v2f vert (appdata_full v) {
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			o.normal=v.normal;
			o.lightDir=ObjSpaceLightDir(v.vertex);
			o.viewDir=ObjSpaceViewDir(v.vertex);
			o.texcoord=TRANSFORM_TEX(v.texcoord, _MainTex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);			
			float diff=max(0,dot(N,i.lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			float toon=floor(diff*_Steps)/_Steps;
			diff=lerp(diff,toon,_ToonEffect);
			c=_Color*_LightColor0*(diff);
			c*=tex2D(_MainTex, i.texcoord);
			return c;
		}
		ENDCG
		}
	} 
}

VF版本代码02效果

UnityShader实例05:Toon(卡通)材质_第4张图片

总结

顶点外拓实现的效果比较好,线条粗细比较均匀,适用范围比较广,本例子做了些改良之后,硬边位置的描边接缝也减少了;但是由于使用了两个pass, draw call消耗是正常的两倍消耗了。

开始着色

描边之后就是着色了,卡通着色最主要的特点就是亮部到暗部的过度不像是普通着色那样平滑渐变,而是梯度渐变以区别于写实风格。下面也介绍两种卡通着色的方法,一种是通过floor函数取整离散着色;一种是ramp贴图采样着色。下面分别介绍这两种方法的实现,为方便考虑下面的例子只处理一个直射光。

1.floor函数去整离散着色

卡通着色主要是frag函数里面处理

要做梯度着色,需要有渐变,因此需要先实现漫反射颜色

float3 N=normalize(i.normal);			
float diff=max(0,dot(N,i.lightDir));
求出正常的漫反射颜色

diff=(diff+1)/2;

因为只有一个直射光没考虑环境光和其他光的,因此需要做亮化处理把暗部提亮。

diff=smoothstep(0,1,diff);

使颜色平滑的在[0,1]范围之内

float toon=floor(diff*_Steps)/_Steps;
把颜色做离散化处理,把diffuse颜色限制在_Steps种(_Steps阶颜色),简化颜色,这样的处理使色阶间能平滑的显示

diff=lerp(diff,toon,_ToonEffect);
通过控制卡通化程度值_ToonEffect,调节卡通与写实的比重


VF版本代码03:

Shader "PengLu/Toon/ToonOnePassVF" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_OutlineColor("OutlineColor",Color) = (0,1,1,1)
		_RimPower ("Rim Power", Range(0.1,8.0)) = 2
		_ToonEffect("Toon Effect",range(0,1))=0.5
		_Steps("Steps of toon",range(0,9))=3
	}
	SubShader {
		Tags { "Queue"="Geometry" "RenderType"="Opaque" }
		LOD 200
		
		Pass
		{
			Tags{"LightMode"="ForwardBase"}
			Cull Back


			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			float4 _OutlineColor;
			float _RimPower;
			float4 _LightColor0;
			float _Steps;
			float _ToonEffect;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			struct v2f {
				float4  pos : SV_POSITION;
				float3 lightDir:TEXCOORD0;
				float3 viewDir:TEXCOORD1;
				float3 normal:TEXCOORD2;
				float2 texcoord:TEXCOORD3;
				float4	color:COLOR;
			} ;
			v2f vert (appdata_full v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.normal=v.normal;
				o.lightDir=normalize(ObjSpaceLightDir(v.vertex));
				o.viewDir=normalize(ObjSpaceViewDir(v.vertex));
                float rim = 1 - saturate(dot(o.viewDir,v.normal ));
                o.color = pow(rim,_RimPower);
				return o;
			}
			float4 frag (v2f i) : COLOR
			{
				fixed4 col = tex2D(_MainTex, i.texcoord);
//				i.color=smoothstep(0,1,i.color*2);
				i.color=floor(i.color*2);
				col=lerp(col,i.color*_OutlineColor,i.color);
				float3 N=normalize(i.normal);			
				float diff=max(0,dot(N,i.lightDir));
				diff=(diff+1)/2;
				diff=smoothstep(0,1,diff);
				float toon=floor(diff*_Steps)/_Steps;
				diff=lerp(diff,toon,_ToonEffect);
				col*=_LightColor0*(diff);
				return col; 
			}
			ENDCG
		}
		
	} 
	FallBack "Diffuse"
}
VF版本代码03效果:

UnityShader实例05:Toon(卡通)材质_第5张图片


2.RampMap 采样着色

RampMap采样着色是通过映射一张一维纹理(如下图)来实现卡通化着色,相对floor函数,RampMap映射方式比较容易控制卡通效果,美术师只需修改贴图就能修改卡通渐变的过渡等效果。

UnityShader实例05:Toon(卡通)材质_第6张图片

RampMap映射的代码基本和floor函数方式相同,只有离散方式不同,关键代码如下:

float toon=tex2D(_ToonMap,float2(diff,0.5)).r;
将diff作为uv坐标在贴图上u方向查询获得对应的颜色。

VF版本代码04:

//one directional light

Shader "PengLu/Toon/ToonRampMapVF" {
	Properties {
		_Color("Main Color",color)=(1,1,1,1)
		_OutlineColor("Outline Color",color)=(0.1,0.1,0.2,1)
		_MainTex ("BaseTex", 2D) = "white" {}
		_ToonMap("Ramp Map",2D)="white"{}
		_Outline("Thick of Outline",range(0,0.1))=0.02
		_Factor("Factor",range(0,1))=0.5
		_ToonEffect("Toon Effect",range(0,1))=0.5
	}
	SubShader {
		pass{
		Tags{"LightMode"="Always"}
		Cull Front
		ZWrite On
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		float _Outline;
		float _Factor;
		float4 _OutlineColor;
		
		struct v2f {
			float4 pos:SV_POSITION;
		};

		v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
			dir=dir*_Factor+dir2*(1-_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=_OutlineColor;
			return c;
		}
		ENDCG
		}//end of pass
		
		pass{
		Tags{"LightMode"="ForwardBase"}
		Cull Back
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		sampler2D _ToonMap;
		sampler2D _MainTex;
		float4 _MainTex_ST;
		float4 _LightColor0;
		float4 _Color;
		float _ToonEffect;

		struct v2f {
			float4 pos:SV_POSITION;
			float3 lightDir:TEXCOORD0;
			float3 normal:TEXCOORD1;
			float2 texcoord:TEXCOORD2;
		};

		v2f vert (appdata_full v) {
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			o.normal=v.normal;
			o.lightDir=ObjSpaceLightDir(v.vertex);
			o.texcoord=TRANSFORM_TEX(v.texcoord, _MainTex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			float toon=tex2D(_ToonMap,float2(diff,0.5)).r;
			diff=lerp(diff,toon,_ToonEffect);
			c=_Color*_LightColor0*(diff);
			c*=tex2D(_MainTex,i.texcoord);
			return c;
		}
		ENDCG
		}
	} 
}

VF版本代码04效果:

UnityShader实例05:Toon(卡通)材质_第7张图片

你可能感兴趣的:(unity3D,shader实例笔记)