卡通材质
卡通着色也叫Non-photorealisticrendering非真实渲染,通常一些3D游戏用来做一些卡通风格的游戏,一般来说特点主要有两点,一是描边,二是风格化着色,表现为明暗渐变过渡为非线性梯度着色。如下图所示:火影忍者和无主之地
先做描边
关于描边网上有不少资料,这里就其中比较常用的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效果:
主要方法是在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;
}
float4 frag(v2f i):COLOR
{
float4 c = _OutlineColor;
return c;
}
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
}
}
}
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效果:
2.RampMap 采样着色
RampMap采样着色是通过映射一张一维纹理(如下图)来实现卡通化着色,相对floor函数,RampMap映射方式比较容易控制卡通效果,美术师只需修改贴图就能修改卡通渐变的过渡等效果。
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
}
}
}