塞尔达风之杖技术分析-角色渲染和面部表情

简介

《塞尔达传说 风之杖》是一款由任天堂开发的任天堂GameCube平台上的动作冒险游戏,一经发布就广受好评,之后任天堂在WiiU上还发布了HD版本。其别具一格的卡通美术风格现如今看来都不算过时,甚至给最近火的一塌糊涂的《塞尔达传说 荒野之息》带来了很大的影响。

虽说gamebox上的版本马赛克比较重,但是link的表情表现的非常的到位,从简单的眨眼,到各种神情的变化,都特别的到位,比如下面这个他被用木桶投掷到岛上的过场

非常带感。


今天要分析的是游戏中的角色渲染和表情系统,并且在Unity中进行实现。

工具

Unity3D 5.3.5

Maya 2014

模型处理

原始的模型可以从这里下载到。最原始的模型是obj的模型,要放到Unity里的话,首先要将其转化成fbx。

打开Maya,选择导入模型

导入之后的mesh是一个完整的mesh,先把它打撒。选中模型,然后 网格->分离,Mesh就被打散了


看下面数,不到3000


衣服的贴图尺寸180*96,怎么”挤” 进去的?


由于衣服大部分的地方都是大块的纯色,不同于常规的展UV的方法,纹理也是一些很简单的颜色,uv就直接被放到对应的色块上。

在看最重要的面部。

嘴巴部分是专门用了一块mesh来处理,这样只要在材质中切换贴图就能够达到换表情的作用。

嘴巴部分由于是切割的mesh,所以在导入的时候生成法线会出问题


嘴巴部分的的法线和面部的法线不是连续的,这样会导致后面处理光照的时候会很奇怪。

处理方法是用Maya的法线工具


会生成一个合并好的mesh,


这下正常了。再看眼睛,眼睛的表现分为三个部分。眉毛,眼白,眼睛。这里的处理方法不是用面部的网格切一块下来了,而是在眼睛出罩一些片mesh

每一边的眼睛都覆盖了三个mesh片,分别是眉毛,眼白,瞳孔(为了看清楚将它们拉开了一些)。

实际上是往外拉了一点点,防止Z-Fighting


接下来可以导出模型了,文件->导出,选fbx,默认设置。


角色渲染

角色渲染的话就是最简单的卡通渲染,根据direction light的方向和法线的角度得到一个shadow值,和纹理的颜色相乘。


Shader "Custom/celshading"
{
	Properties
	{
		[NoScaleOffset] _MainTex("Texture", 2D) = "white" {}
	}
		SubShader
	{
		Pass
		{
			Tags{ "LightMode" = "ForwardBase" }
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			

			struct v2f
			{
				float2 uv : TEXCOORD0; // texture coordinate
				float4 vertex : SV_POSITION; // clip space position
				half3 worldNormal : TEXCOORD1;
			};

			// vertex shader
			v2f vert(appdata_base v)
			{
				v2f o;
				// transform position to clip space
				// (multiply with model*view*projection matrix)
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				// just pass the texture coordinate
				o.uv = v.texcoord;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				return o;
			}

			// texture we will sample
			sampler2D _MainTex;

			// pixel shader; returns low precision ("fixed4" type)
			fixed4 frag(v2f i) : SV_Target
			{
				// sample texture and return it
				fixed4 col = tex2D(_MainTex, i.uv);
				float shadow = dot(i.worldNormal, normalize(_WorldSpaceLightPos0.xyz));
				
				col *= smoothstep(0.0, 0.1, shadow) * 0.4 + 0.6;
				//fixed4 col = 0;
				//col.rgb = i.worldNormal*0.5 + 0.5;
				//col.rgb = _WorldSpaceLightPos0.xyz;
				return col;
			}
			ENDCG
		}
	}
}


不加光照

加上之后得到结果


用到了一个内置函数

smoothstep - interpolate smoothly between two input values based on a third

目的是为了让明暗交接的地方不要太锋利,过度的平滑一些。

加上阴影

// shadow caster rendering pass, implemented manually
// using macros from UnityCG.cginc
Pass
{
	Tags{ "LightMode" = "ShadowCaster" }

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

	struct v2f {
V2F_SHADOW_CASTER;
	};

	v2f vert(appdata_base v)
	{
		v2f o;
		TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
			return o;
	}

	float4 frag(v2f i) : SV_Target
	{
		SHADOW_CASTER_FRAGMENT(i)
	}
	ENDCG
}



面部表情

嘴巴部分最简单,用的和身体一样的shader,要变换嘴型的时候,只需要切换材质的贴图。


眼睛部分会比较复杂。眼睛和眉毛的shader是一样的,就是基本的transparent


记住左右眉毛和左右眼睛得用不同的材质,因为不同的表情左右可能是不对称的。

眉毛和眼睛正常了,但是眼珠不正常,这里需要在shader中给眼珠加一个mask,将当前眼白的部分叠加到眼珠上得到最终的结果。

fixed4 frag(v2f i) : SV_Target
{
	// sample texture and return it
	fixed4 col = tex2D(_PupilTex, i.uv);
	fixed4 mask = tex2D(_EyeMaskTex, i.uv);
	col *= mask;
	return col;
}



需要注意的是眼珠的EyeMaskTex 需要和眼白的maintex要同步好,不然就会很囧。


瞳孔的放大缩小

Link眼睛的瞳孔的位置也是可以控制的,用来表现看的方向


同时也是可以放大缩小的,比如说上面的用来表示惊恐的表情

瞳孔的位置处理直接叠加上一个uv偏移就可以了。

_XOffset("X Offset", float) = 0
_YOffset("Y Offset", float) = 0

float2 finalUv = i.uv + float2(_XOffset, _YOffset)
fixed4 col = tex2D(_PupilTex, finalUv);


调一个结果


惊悚的表情需要进行UV缩放,注意不是将uv直接乘以scale值,这样会从左下角为中心缩放,我们的目的是从贴图的中心开始缩放,需要一个简单的换算

float2 scaleCenter = float2(0.5f, 0.5f);
float2 finalUv = (i.uv + float2(_XOffset, _YOffset) - scaleCenter) * _Scale + scaleCenter;
fixed4 col = tex2D(_PupilTex, finalUv);

调个简单的结果

还有一个点要注意,瞳孔的纹理的wrap mode 要设置为


不然会出现…


头发不会挡住眼睛

游戏中是这样的就像下面这样


虽然有时候看起来有点奇怪

但是整体表现还是挺好的。

这里使用的方法是利用Stencil buffer来处理,首先标记出被遮挡的部分,然后清掉那一部分的zbuffer,最后绘制出来,下面是具体的实现。

首先单独给头发一个shader,用来写stencil

Stencil
{
	Ref 1
	Comp always
	Pass replace
}

其余部分和身体的shader一样,头发绘制过的地方Stencil都变成了1。

眼睛分为三个pass绘制,

第一个pass标记出stencil为1,且ztest fail的部位

Stencil
{
	Ref 1
	Comp Equal
	Pass replace
	ZFail IncrSat
}

ZFail IncrSat表示通过stencil test,Ztest失败之后,将stencil的值+1.所以被头发遮挡住的地方stencil变成了2.下面蓝色部分就是


第二个pass,清掉标记区的Zbuffer

ZTest Greater
ZWrite On

Stencil
{
	Ref 2
	Comp Equal
}


第三个pass,就是最简单的transparent

Pass
{
	Tags{ "IgnoreProjector" = "True" "RenderType" = "Transparent" }

	CGPROGRAM
	// use "vert" function as the vertex shader
	#pragma vertex vert
	// use "frag" function as the pixel (fragment) shader
	#pragma fragment frag
	#include "UnityCG.cginc"


	// vertex shader outputs ("vertex to fragment")
	struct v2f
	{
		float2 uv : TEXCOORD0; // texture coordinate
		float4 vertex : SV_POSITION; // clip space position
	};

	// vertex shader
	v2f vert(appdata_base v)
	{
		v2f o;
		o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
		o.uv = v.texcoord;
		return o;
	}

	// texture we will sample
	sampler2D _MainTex;

	fixed4 frag(v2f i) : SV_Target
	{
		fixed4 col = tex2D(_MainTex, i.uv);
		return col;
	}
	ENDCG
}


最终结果


最后来摆几个Pose





小结

相对于UnityChan中完全用骨骼驱动的表情系统(王者荣耀里面角色展示的部分似乎也是这样处理的)



这套表情系统的特点是

非常节省资源,几乎没有什么消耗(也许贴图还可以再pack一下?)。

和美术风格非常的搭。

面部的所有细节都不会糊掉。

无法使用macanim动画系统,需要自己写一套控制角色表情的脚本。

对于在移动平台上想自己实现一个的表情系统的卡通风格项目可以考虑用这种方案。

参考

Twilight Princess Eyes Breakdown

totallysweetredhoodie.blogspot.com

medium.com/@gordonnl/li

图文详解Unity3D中Material的Tiling和Offset是怎么回事 - BIT祝威 - 博客园

Unity Shader-渲染队列,ZTest,ZWrite,Early-Z

Unity3D游戏开发从零单排(十) - 进击的Shader续

编辑于 2017-04-03

文章被以下专栏收录