Unity Shader学习笔记十七-卡通&素描风格渲染

学习教材:《UnityShader入门精要》——冯乐乐

部分计算图例为《UnityShader入门精要》书中截图

代码和实例截图均为个人实际操作得到


卡通风格渲染
卡通风格渲染的游戏画面通常物体颜色分界明显,具有黑色的线条描边。卡通渲染的实现有多种方法,基于色调的着色技术是其中之一,实现过程中通过使用漫反射系数对一维纹理进行采样,控制漫反射色调。之前通过一张渐变纹理来控制漫反射颜色实现过卡通风格的渲染效果。卡通风格的高光效果往往是一块分界明显的色块,而物体边缘通常会有描边。本节中将通过基于模型的方式进行描边,而不是之前的屏幕后处理的方式。

轮廓线渲染
轮廓线的渲染是实时渲染中应用非常广泛的一种效果。目前常用的5种绘制模型轮廓线的方法:

  • 基于观察角度和表面法线的轮廓线渲染
    使用视角方向和表面法线的点乘结果得到轮廓线信息。简单快速,一个Pass可以得到结果,局限性较大,不能得到比较满意的描边效果。
  • 正背面渲染
    使用两个Pass,一个渲染背面,另一个渲染正面面片。快速有效,适用于大多数表面平滑的模型。
  • 基于图像处理的轮廓线
    之前屏幕后处理以及利用深度纹理就是采用的这种方式。可以用于任何模型,但深度和法线变化很小的轮廓无法检测,比如紧贴的薄平面。
  • 基于轮廓边缘的轮廓线检测
    通过计算得到精确的轮廓边,然后直接渲染,渲染出独特的风格。检测一条边是否为轮廓边,只需检测和这条边相邻的三角面片是否满足:

(N0*V>0)!=(N1*V>0)

N0和N1分别是相邻面片的法向,这种方式由于是单帧提取轮廓,当帧数较低时,会出现帧与帧之间的跳跃性。

  • 最后一种是以上的综合渲染方法
    首先找到精确的轮廓边,将模型和轮廓渲染到纹理,再通过图形处理的方式识别轮廓线,在图像空间下进行风格化渲染。

下面使用正背面渲染的方式进行轮廓线的勾勒,之前的正背面渲染中,是直接将顶点在裁剪空间中向裁剪空间下的法线方向进行偏移。这里使用观察空间,在观察空间下对顶点进行观察空间下的法向偏移,区别在于观察空间是一个线性空间,尽管之前的效果也基本达到要求,但线性空间下的处理的结果会更加连贯。为了防止一些内凹的模型在使用正面剔除后发生背面遮挡正面的情况,先对顶点法线的z分量进行定值处理,再将法线归一化后进行扩张。这样可以使扩张后背面更加扁平化,降低遮挡正面面片的可能性。即:

viewNormal.z=-0.5;
viewNormal=normalize(viewNormalize);
viewPos=viewPos+viewNormal*_Outline;

卡通风格的高光通常表现为在模型上是一块块分界明显的色块。为了得到这种效果不再使用之前的高光计算模型。之前Blinn-Phong时,使用法线方向点乘视角和光照方向和的一半,再与_Gloss参数进行指数操作得到系数:

float spec=pow(max(0,dot(normal,halfDir)),_Gloss);     

对于卡通风格的高光反射光照模型,同样需要计算normal和halfDir的点乘结果,然后直接与一个阈值相比较,大于该阈返回1,小于该阈值返回0,以形成分界明显的色块界限:

float spec=dot(normal,halfDir);
spec=step(threshold,spec);

CG的step函数实现和阈值比较返回0,1结果,第一个为参考值,第二个参数大于第一个参数,返回1,否则返回0。
这种直接0,1的取值方式会在高光的边缘区域形成锯齿,因为由0,1突变。为了得到高光边缘叫平滑的效果,可以在边界处的小块区域内进行平滑处理。

float spec=dot(normal,halfDir);
spec=lerp(0,1,smoothstep(-w,w,spec-threshold));      

使用CG的smoothstep函数,w是一个较小的值,当spec-threshold小于-w时,返回0,大于w时,返回1,否则在0,1之间进行插值。这样的效果是在[-w,w]区间,即高光反射边缘,进行0到1的平滑过渡,防止出现锯齿。w的值可以使用CG的fwidth函数得到邻域像素之间的近似导数(像素之间的变化程度)值。

代码实例:

Shader "Custom/Chapter14_ToonShading" {
Properties{
	_MainTex("MainTex",2D)="white"{}
	_Color("Color",Color)=(1,1,1,1)
	_RampTex("Ramp",2D)="white"{}
	_Outline("Outline",Range(0,1))=0.1
	_OutlineColor("OutlineColor",Color)=(0,0,0,1)
	_Specular("SpecularColor",Color)=(1,1,1,1)
	_SpecularScale("Specular Scale",Range(0,0.1))=0.01
}
SubShader{
	Pass{
		NAME "OUTLINE"
		Cull Front
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		fixed _Outline;
		fixed4 _OutlineColor;

		struct a2v{
			float4 vertex:POSITION;
			float3 normal:NORMAL;
		};
		struct v2f{
			float4 pos:SV_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;
		}

		fixed4 frag(v2f i):SV_Target{
			return fixed4(_OutlineColor.rgb,1);
		}
		ENDCG
	}

	Pass{
		Tags{"LightMode"="ForwardBase"}
		Cull Back
		CGPROGRAM
		#pragma vertex   vert
		#pragma fragment  frag
		#pragma multi_compile_fwdbase
		#include "Lighting.cginc"
		#include "UnityCG.cginc"
		#include "AutoLight.cginc" 

		sampler2D _MainTex;
		float4 _MainTex_ST;
		fixed4 _Color;
		sampler2D _RampTex;
		fixed4 _Specular;
		fixed _SpecularScale;

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

		struct v2f{
			float4 pos:SV_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=mul(v.normal,(float3x3)unity_WorldToObject);
			o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;

			TRANSFER_SHADOW(o);

			return o;
		}

		fixed4 frag(v2f i):SV_Target{
			fixed3 worldNormal=normalize(i.worldNormal);
			fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
			fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
			fixed3 worldHalf=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;

			fixed3 diffuse=_LightColor0.rgb*albedo*tex2D(_RampTex,float2(diff,diff)).rgb;

			fixed spec=dot(worldNormal,worldHalf);
			fixed w=fwidth(spec)*2.0;
			fixed3 specular=_Specular.rgb*lerp(0,1,smoothstep(-w,w,spec+_SpecularScale-1))*step(0.0001,_SpecularScale);
			//最后添加的step(0.0001,_SpecularScale);是为了控制当Specular为0时,不出现高光效果
			
			return fixed4(ambient+diffuse+specular,1.0);
		}
		ENDCG
	}
}
FallBack "Diffuse"
//这里的回调需要注意包含能够处理阴影的特殊Pass
}   

实例效果:

《Unity Shader学习笔记十七-卡通&素描风格渲染》

素描风格渲染
素描风格的渲染在非真实渲染中应用也比较流行。目前实时的素描风格渲染是通过使用提前生成的素描纹理来实现的。

《Unity Shader学习笔记十七-卡通&素描风格渲染》

这些纹理组成色调艺术映射,纹理从左到右笔触逐渐增多,用于模拟不同光照效果下的漫反射效果,从上到下对应每张纹理的多级渐远纹理。
下面的过程不考虑多级渐远纹理的生成,直接使用6张纹理进行渲染。首先在顶点着色器计算逐顶点光照,根据光照结果决定纹理的混合权重,然后传递给片元着色器,片元着色器根据权重混合6张纹理的采样结果。

实例代码:

Shader "Custom/Chapter14_Hatching" {
Properties{
	_Color("Color",Color)=(1,1,1,1)
	_TileFactor("Tile Factor",Float)=1
	_Outline("Outline",Range(0,1))=0.1
	_Hatch0("Hatch 0",2D)="white"{}
	_Hatch1("Hatch 1",2D)="white"{}
	_Hatch2("Hatch 2",2D)="white"{}
	_Hatch3("Hatch 3",2D)="white"{}
	_Hatch4("Hatch 4",2D)="white"{}
	_Hatch5("Hatch 5",2D)="white"{}

	//TileFactor为纹理的平铺系数,值越大,素描线条越密集
}
SubShader{
	Tags{"RenderType"="Opaque" "Queue"="Geometry"}
	UsePass "Custom/Chapter14_ToonShading/OUTLINE"  
	//素描风格往往也需要绘制轮廓线,使用之前的渲染轮廓Pass
	Pass{
		Tags{"LightMode"="ForwardBase"}
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#pragma multi_compile_fwdbase

			#include "UnityCG.cginc"
			#include "AutoLight.cginc"

			fixed4 _Color;
			float _TileFactor;
			fixed _Outline;
			sampler2D _Hatch0;
			float4 _Hatch0_ST;
			sampler2D _Hatch1;
			float4 _Hatch1_ST;
			sampler2D _Hatch2;
			float4 _Hatch2_ST;
			sampler2D _Hatch3;
			float4 _Hatch3_ST;
			sampler2D _Hatch4;
			float4 _Hatch4_ST;
			sampler2D _Hatch5;
			float4 _Hatch5_ST;

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

			struct v2f{
				float4 pos:SV_POSITION;
				float2 uv:TEXCOORD0;
				fixed3 hatchWeight0:TEXCOORD1;
				fixed3 hatchWeight1:TEXCOORD2;
				float3 worldPos:TEXCOORD3;

				SHADOW_COORDS(4)

				//6个权重值分别存储在2个float3类型变量中
			};

			v2f vert(a2v v){
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);
				o.uv=v.texcoord.xy*_TileFactor;
				//_TileFactor用来控制素描线条的密集程度(TEX的WrapMode为Repeat)
				
				float3 worldLightDir=normalize(WorldSpaceLightDir(v.vertex));
				float3 worldNormal=UnityObjectToWorldNormal(v.normal);
				float3 diff=max(0,dot(worldLightDir,worldNormal));
				//这里的关键便是通过计算漫反射系数来区分采样权重,并将权重与不同密集程度的TEX相对应

				o.hatchWeight0=fixed3(0,0,0);
				o.hatchWeight1=fixed3(0,0,0);

				//使用世界空间下的光照方向和法线方向得到漫反射系数
				//初始化权重值,*7分为7个区间,并根据hatchFactor的值,为权重赋值
				float hatchFactor=diff*7;
				if(hatchFactor>6){
					//不做任何赋值,保持纯白
				}
				else if(hatchFactor>5.0){
					o.hatchWeight0.x=hatchFactor-5.0;
				}
				else if(hatchFactor>4.0){
					o.hatchWeight0.x=hatchFactor-4.0;
					o.hatchWeight0.y=1.0-o.hatchWeight0.x;
				}
				else if(hatchFactor>3.0){
					o.hatchWeight0.y=hatchFactor-3.0;
					o.hatchWeight0.z=1.0-o.hatchWeight0.y;
				}
				else if(hatchFactor>2.0){
					o.hatchWeight1.x=hatchFactor-2.0;
				}
				else if(hatchFactor>1.0){
					o.hatchWeight1.x=hatchFactor-1.0;
					o.hatchWeight1.y=1.0-o.hatchWeight1.x;
				}
				else{
					o.hatchWeight1.y=hatchFactor;
					o.hatchWeight1.z=1.0-o.hatchWeight1.y;
				}

				o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;

				TRANSFER_SHADOW(o)

				return o;
			}
			fixed4 frag(v2f i):SV_Target{
				fixed4 hatchTex0=tex2D(_Hatch0,i.uv)*i.hatchWeight0.x;
				fixed4 hatchTex1=tex2D(_Hatch1,i.uv)*i.hatchWeight0.y;
				fixed4 hatchTex2=tex2D(_Hatch2,i.uv)*i.hatchWeight0.z;
				fixed4 hatchTex3=tex2D(_Hatch3,i.uv)*i.hatchWeight1.x;
				fixed4 hatchTex4=tex2D(_Hatch4,i.uv)*i.hatchWeight1.y;
				fixed4 hatchTex5=tex2D(_Hatch5,i.uv)*i.hatchWeight1.z;
				//得到6张素描纹理采样结果,并乘以对应的权重
				fixed4 whiteColor=fixed4(1,1,1,1)*(1.0-i.hatchWeight0.x-i.hatchWeight0.y-i.hatchWeight0.z-i.hatchWeight1.x-i.hatchWeight1.y-i.hatchWeight1.z);
				fixed4 hatchColor=hatchTex0+hatchTex1+hatchTex2+hatchTex3+hatchTex4+hatchTex5+whiteColor;
				//计算纯白的占比程度,素描风格中会有留白,并且高光部分也是白色
				UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

				return fixed4(hatchColor.rgb*_Color.rgb*atten,1.0);
				//混合各个颜色,并与衰减和模型颜色相乘得到最终颜色
			}
		ENDCG
	}
}
FallBack "Diffsue"
}      
 

实例效果:

《Unity Shader学习笔记十七-卡通&素描风格渲染》

相关参考

《UnityShader入门精要》 冯乐乐

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注