Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加

Unity制作二次元材质角色


回到目录

大家好,我是阿赵。
这里继续讲二次元角色材质。这次打算讲一下描边和细节的添加。

一、外描边

外描边的做法也不止一种,比如后处理方法的偏导数ddx/ddy之类的,也能整个屏幕的求出边缘。但一般来说单模型渲染常用的描边方式,是写多一个Pass,这个Pass是Cull Front的,也就是说是剔除了正面的,然后给模型的顶点沿着法线方向稍微扩大一点,填充成黑色,最后把正常颜色的模型放在黑色模型的重叠位置,那么黑色模型就变成了描边了效果了。
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第1张图片

这个就是Cull Front的Pass的效果。
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第2张图片

把2个pass一起渲染,就能得到描边的效果。
值得注意的是,沿着法线放大模型这一步,顶点坐标应该在哪个空间里面来做放大呢?
一般来说,使用世界空间肯定是可以的,就是先求出顶点的世界坐标和世界法线方向,然后世界坐标加上世界法线乘以一个控制大小的值。
但更好的做法,是在观察空间里面做这个扩展。这是因为,有时候我们想让这个Cull Front的Pass渲染的黑色模型,可以沿着我们观察的方向的Z轴做偏移,如果在世界空间坐标里面做法线扩展,明显是很难找到一个轴是可以沿着摄像机方向的。如果我们先把顶点坐标和法线都转换到观察空间,那么在观察空间里面的Z坐标,其实就是离我们观察点的远近了。
这里有2个做法:
1、在法线转换成观察空间之后,把法线的z轴直接固定到一个值,这样模型沿着法线方向扩展时,在z轴会沿着我们想要的方向去放大,比如:

v2f vert(appdata 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) * _OutlineLen*0.001;
	o.pos = mul(UNITY_MATRIX_P, pos);
	return o;
}

2、在观察空间扩展完法线之后,用一个值来控制偏移后顶点的z轴偏移,比如:

v2f vert(appdata v)
{
	v2f o;
	float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
	float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
	pos = pos + float4(normalize(normal), 0) * _OutlineLen*0.001;
	pos.z -= v.vertexColor.b;
	o.pos = mul(UNITY_MATRIX_P, pos);
	return o;
}

我这里用了一个顶点颜色的B通道去偏移Z轴,是因为这是一般的习惯做法,通过给顶点颜色指定一个通道,作为描边Z轴偏移的量,这样的做法,可以实现整个模型不同部位的描边显示不一样。
由于我手上的这个模型并没有烘焙顶点色,所以没有顶点通道可以控制,所以我就使用第一种方式来处理。

二、内描边

刚才得到的是外描边。正常来说,如果项目要求不高,也勉强够用了。但如果我们想把效果做得更贴近卡通,那么按道理来说,模型除了外部有描边,模型内部应该也会有一些描边的细节。
回头看看之前说的ILM贴图的A通道:
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第3张图片

可以发现,这张贴图提供了模型里面的描边效果。一般来说,在贴图上面画内描线,会遇到一个问题:
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第4张图片

同样一张贴图上,水平或者垂直的线条,会很清晰,但斜线,会起马赛克,会模糊。
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第5张图片

放大我们的这个角色模型,看一下内描线的效果,发现每一内描线,基本都很清晰,没有出现斜线模糊的情况。这是为什么呢?
接下来看一下这张ILM贴图的A通道,可以发现一些东西:
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第6张图片

会发现,这个内描线,基本上都是水平和垂直的线,并没有出现任何斜线。其实这是一种经验和技巧。在展UV的时候,我们尽量把模型的UV展成这张水平和垂直的方向,这样在画这张线条形的贴图时,线条就能非常的清晰。
这种事情最好是在一开始的时候就规划好。如果说实在做不到,因为漫反射贴图已经画好了,没法改,那么,我们也可以通过展UV2,特别为这个内描线展一张水平垂直的UV。
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第7张图片

加上了内描边之后,整体的感觉就丰富了很多了。

三、细节线条

如果想再进一步添加一些小划痕或者痕迹,这里还可以添加一张细节图:
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第8张图片

看得出来,这张图片并没有像内描边一样,讲究水平垂直的画线,整体比较随意。因为这张贴图添加的是一些像手绘的笔触一样的线条,并不需要非常的整齐和明显。
把这张图也加上,那么线条方面的工作就基本完成了:
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第9张图片

四、贴花

最后才说贴花,是因为这个贴花是独立于之前的角色模型贴图以外的。
Unity制作二次元卡通渲染角色材质——4 、内外描边和细节添加_第10张图片

本来这个模型是分为了身体和武器(吉他)2个部分的,所以刚才说到的所有图,包括BaseMap、SSSMap、ILMMap,都是身体一套,武器一套的。但这一张贴花的图片,却是身体和武器共用的。
为什么不同的模型可以共用一张贴图呢?这是因为,这张贴花贴图,是使用了UV2的,也就是说,把身体和武器需要贴花的部分展成UV2然后合并在一起。至于不需要贴花的部分,UV基本上是缩小到看不见的。
所以这个部分其实也没什么好说的,直接读取模型的UV2,然后赋予贴图就行了。我自己做了一点小改动,因为吉他弦的部分没有地方控制透明通道,所以我给这张贴花图片做了个透明通道,让吉他弦能正常的显示出来。

加上了贴花之后,整个模型的显示基本上就完成了。

五、完整Shader

Shader "azhao/ToonBodyOutline"
{
    Properties
    {
        _BaseMap ("BaseMap", 2D) = "white" {}
		_SSSMap("SSSMap", 2D) = "white" {}
		_ILMMap("ILMMap", 2D) = "white" {}
		_DetailMap("DetailMap",2D) = "white"{}
		_specColor("specColor",Color) = (1,1,1,1)
		_shininess("shininess", Range(1 , 100)) = 1
		_SpecAdd("SpecAdd",float) = 1.0


		_GradationMin("GradationMin",Range(0.0,1.0)) = 0.0
		_GradationMax("GradationMax",Range(0.0,1.0)) = 1.0

		_OutlineColor("OutlineColor",Color) = (0,0,0,1)
		_OutlineLen("_OutlineLen",float) = 2

		_MatCapTex("MatCapTex", 2D) = "white" {}
		_MatCapIntensity("MatCapIntensity",Range(0,2)) = 1
		_MatCapPow("MatCapPow",Range(0,5)) = 1
		_MatCapUVScale("MatCapUVScale",Range(0,1)) = 1

    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#pragma multi_compile_fwdbase
			#include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 normal:NORMAL;
            };

            struct v2f
            {                
                float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 worldPos :TEXCOORD2;
				float3 worldNormal :TEXCOORD3;
            };

            sampler2D _BaseMap;
            float4 _BaseMap_ST;
			sampler2D _SSSMap;
			sampler2D _ILMMap;
			sampler2D _DetailMap;
			float4 _specColor;
			float _shininess;

			float _SpecAdd;
			float _GradationMin;
			float _GradationMax;
			sampler2D _MatCapTex;
			float _MatCapIntensity;
			float _MatCapPow;
			float _MatCapUVScale;

			//获取HalfLambert漫反射值
			float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
			{
				float3 lightDir = UnityWorldSpaceLightDir(worldPos);
				float NDotL = saturate(dot(worldNormal, lightDir));
				float halfVal = NDotL * 0.5 + 0.5;
				return halfVal;
			}

			//获取BlinnPhong高光
			float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
			{
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
				float specDir = max(dot(normalize(worldNormal), halfDir), 0);
				float specVal = pow(specDir, _shininess);
				return specVal;
			}


			float2 GetMatCapUV(float3 normalWorld)
			{
				float3 normalView = mul(UNITY_MATRIX_IT_MV, normalWorld);
				return normalView.xy*0.5 + 0.5;
			}

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
				o.uv2 = TRANSFORM_TEX(v.uv2, _BaseMap);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                // sample the texture
                half4 col = tex2D(_BaseMap, i.uv);
				half4 sssCol = tex2D(_SSSMap, i.uv);
				half4 ilmCol = tex2D(_ILMMap, i.uv);
				half4 detailCol = tex2D(_DetailMap, i.uv2);
				//色阶化
				half halfLambert = GetHalfLambertDiffuse(i.worldPos, i.worldNormal);
				half toonVal = smoothstep(_GradationMin, _GradationMax, halfLambert);


				half specVal = GetBlinnPhongSpec(i.worldPos, i.worldNormal);
				float2 MatCapUV = GetMatCapUV(i.worldNormal)*_MatCapUVScale;
				float4 MatCapCol = tex2D(_MatCapTex, MatCapUV)*_MatCapIntensity;
				MatCapCol = pow(MatCapCol, _MatCapPow);



				half3 finalRGB = col.rgb*toonVal + sssCol  * (1 - toonVal)+_specColor* specVal*ilmCol.r+ _specColor * specVal*ilmCol.b*_SpecAdd;
				finalRGB = finalRGB * (1-ilmCol.b) +MatCapCol.rgb*ilmCol.b;
				finalRGB = finalRGB * ilmCol.a*detailCol.r;
				half alpha = col.a;
                return half4(finalRGB,alpha);
            }
            ENDCG
        }
		
		Pass
		{
			Cull Front
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"
			#pragma multi_compile_fwdbase
			#include "AutoLight.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float3 normal:NORMAL;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 worldPos :TEXCOORD1;
				float3 worldNormal :TEXCOORD2;
			};


			float4 _OutlineColor;
			float _OutlineLen;

			v2f vert(appdata 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) * _OutlineLen*0.001;
				o.pos = mul(UNITY_MATRIX_P, pos);
				return o;
			}

			half4 frag(v2f i) : SV_Target
			{
				return _OutlineColor;
			}
			ENDCG
		}
    }
}

你可能感兴趣的:(Unity引擎Shader效果,unity,材质,游戏引擎,描边,二次元)