崩三角色浅析

解析卡通渲染

Posted by Bob on January 28, 2019

解读崩三角色卡通渲染制作方式

渲染组成

image

描边

角色描边不是简单的沿着法线扩展绘制,而是通过摄像机fov夹角处理描边粗细变化,顶点z和近裁面距离来处理线的透明度。

image

Shader "Avatar"
{
	Properties
	{

	_MainTex("Texture", 2D) = "white" {}
	_OutlineWidth("Outline Width", Range(0, 100)) = 0.2
	_OutlineColor("Outline Color", Vector) = (0,0,0,1)
	_FadeDistance("Fade Start Distance", Range(0.1, 10)) = 0.5
	_FadeOffset("Fade Start Offset", Range(0, 10)) = 1

	}
		SubShader
	{
		Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" }
		Blend SrcAlpha OneMinusSrcAlpha
		LOD 100
		Cull Front
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

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

			struct v2f
			{
				fixed4 color : COLOR;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			float  _OutlineWidth;
			fixed4 _OutlineColor;
			float _FadeDistance;
			float _FadeOffset;


			v2f vert(appdata v)
			{
				v2f o;

				float4 vert;
				float4 viewPos = mul(mul(UNITY_MATRIX_V, unity_ObjectToWorld),v.vertex);
				vert = viewPos / viewPos.w;

				//unity_CameraProjection:一维的下标来获得该列的向量 二维的小标来获得向量中的元素 第二列向量的y =  cos(fov/2)

				float s = -(viewPos.z / unity_CameraProjection[1].y);
				float power = pow(s, 0.5);
				float4x4 mv = mul(UNITY_MATRIX_V, unity_ObjectToWorld);
				float3 n = mul((float3x3)mv,v.normal);
				n.z = 0.01;
				n = normalize(n);
				float width = power*_OutlineWidth;
				vert.xy += n.xy *width;


				fixed4 color;
				color.xyz = _OutlineColor.xyz;

				//_ProjectionParams:x = 1,如果投影翻转则x = -1 y是camera近裁剪平面 z是camera远裁剪平面 w是1 / 远裁剪平面

				float alpha = clamp((-vert.z - _ProjectionParams.y - _FadeOffset) / _FadeDistance, 0.0, 1.0);
				color.w = alpha;

				vert = mul(UNITY_MATRIX_P, vert);

				o.vertex = vert;
				o.color = color;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				return i.color;
			}
		ENDCG
	}



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

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

				struct v2f
				{
					float2 uv : TEXCOORD0;
					float4 vertex : SV_POSITION;
				};

				sampler2D _MainTex;
				float4 _MainTex_ST;

				v2f vert(appdata v)
				{
					v2f o;
					o.vertex = UnityObjectToClipPos(v.vertex);
					o.uv = TRANSFORM_TEX(v.uv, _MainTex);
					return o;
				}

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

光照和高光

HalfLambert 模型


		o.wNormal = normalize(UnityObjectToWorldNormal(v.normal));

		o.lightColor = dot(o.wNormal, normalize(_WorldSpaceLightPos0.xyz)) * 0.4975 + 0.5;

Blinn-Phong specular 模型

lightMapColor.r通道和lightMapColor.b通道值越大越容易产生高光


		float3 lightDirection = normalize(-_WorldSpaceLightPos0.xyz);
		float3 h = normalize(i.viewDir + lightDirection);
		half spec = pow(max(dot(i.wNormal, h), 0.0), _Shininess);
		float3 specColor;
		if (spec >= (1.0 - tex_Light_Color.z)) {
			specColor = _LightSpecColor * _SpecMulti * tex_Light_Color.x;
		}
		else {
			specColor = float3(0.0, 0.0, 0.0);
		};

具体原理参见之前文章光照模型

阴影

MainTex带局部透明通道 image

LightMapTex分rgb三个通道单独使用 image

如果HalfLambert颜色lightColor + 顶点颜色r通道非常小的话, 在第一层阴影和第二层阴影选一个; 否则, 在基色和第一层阴影中选一个,

第一层阴影

(HalfLambert颜色lightColor + 顶点颜色r通道 * lightMapColor.g通道)*0.5 >= 0.5

_FirstShadowMultColor = (0.9,0.7,0.75,1)

第二层阴影

(HalfLambert颜色lightColor + 顶点颜色r通道 * lightMapColor.g通道)*0.5 < 0.5

_SecondShadowMultColor = (0.75,0.6,0.65,1)

最终卡通渲染效果 image

shader取值 image

完整unity shader如下

Shader "Avatar"
{
	Properties
	{
	_Color("Main Color", Vector) = (1,1,1,1)	
	_MainTex("Texture", 2D) = "white" {}
	_BloomFactor("Bloom Factor", Float) = 1
	_LightMapTex ("Light Map Tex (RGB)", 2D) = "gray" {}
	_LightSpecColor("Light Specular Color", Vector) = (1,1,1,1)
	[HideInInspector] _LightArea("Light Area Threshold", Range(0, 1)) = 0.51
	[HideInInspector] _SecondShadow("Second Shadow Threshold", Range(0, 1)) = 0.51
	_FirstShadowMultColor("First Shadow Multiply Color", Vector) = (0.9,0.7,0.75,1)
	_SecondShadowMultColor("Second Shadow Multiply Color", Vector) = (0.75,0.6,0.65,1)
	_Shininess("Specular Shininess", Range(0.1, 100)) = 10
	_SpecMulti("Specular Multiply Factor", Range(0, 1)) = 0.1


	_OutlineWidth("Outline Width", Range(0, 100)) = 0.01
	_OutlineColor("Outline Color", Vector) = (0,0,0,1)
	_FadeDistance("Fade Start Distance", Range(0.1, 10)) = 0.5
	_FadeOffset("Fade Start Offset", Range(0, 10)) = 1
	}
		SubShader
	{
		
		Pass
		{
			Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" }
			Blend SrcAlpha OneMinusSrcAlpha
			LOD 100
			Cull Front
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

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

			struct v2f
			{
				fixed4 color : COLOR;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			float  _OutlineWidth;
			fixed4 _OutlineColor;
			float _FadeDistance;
			float _FadeOffset;


			v2f vert(appdata v)
			{
				v2f o;

				float4 vert;
				float4 viewPos = mul(mul(UNITY_MATRIX_V, unity_ObjectToWorld),v.vertex);
				vert = viewPos / viewPos.w;

				//unity_CameraProjection:一维的下标来获得该列的向量 二维的小标来获得向量中的元素 第二列向量的y =  cos(fov/2)

				float s = -(viewPos.z / unity_CameraProjection[1].y);
				float power = pow(s, 0.5);
				float4x4 mv = mul(UNITY_MATRIX_V, unity_ObjectToWorld);
				float3 n = mul((float3x3)mv,v.normal);
				n.z = 0.01;
				n = normalize(n);
				float width = power*_OutlineWidth;
				vert.xy += n.xy *width;


				fixed4 color;
				color.xyz = _OutlineColor.xyz;

				//_ProjectionParams:x = 1,如果投影翻转则x = -1 y是camera近裁剪平面 z是camera远裁剪平面 w是1 / 远裁剪平面

				float alpha = clamp((-vert.z - _ProjectionParams.y - _FadeOffset) / _FadeDistance, 0.0, 1.0);
				color.w = alpha;

				vert = mul(UNITY_MATRIX_P, vert);

				o.vertex = vert;
				o.color = color;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				float a = i.color.w - 0.01;
				if (a < 0.0) {
					discard;
				};
				return i.color;
			}
		ENDCG
	}



		Pass
			{
				CGPROGRAM
// Upgrade NOTE: excluded shader from DX11; has structs without semantics (struct v2f members lightColor)
#pragma exclude_renderers d3d11
				
				#pragma vertex vert
				#pragma fragment frag
				#include "UnityCG.cginc"

				struct appdata
				{
                    //模型空间中的顶点位置
                    
					float4 vertex : POSITION;
					float2 uv : TEXCOORD0;
					float3 normal : NORMAL;
					fixed4 color : COLOR;
				};

				struct v2f
				{
					float2 uv : TEXCOORD0;
					float3 wNormal : TEXCOORD1;
					float3 viewDir : TEXCOORD2;
					float4 vertex : SV_POSITION;
					float lightColor;
					fixed4 vertColor : COLOR;
				};

				sampler2D _MainTex;
				float4 _MainTex_ST;
				sampler2D _LightMapTex;
				float4 _LightMapTex_ST;

				float4 _Color;

				float _LightArea;
				float _SecondShadow;
				float4 _FirstShadowMultColor;
				float4 _SecondShadowMultColor;
				float _Shininess;
				float _SpecMulti;
				float3 _LightSpecColor;
				float _BloomFactor;

				v2f vert(appdata v)
				{
					v2f o;
					o.vertex = UnityObjectToClipPos(v.vertex);
					o.viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld,v.vertex).xyz);
					o.wNormal = normalize(mul((float3x3)unity_ObjectToWorld, v.normal));
					o.uv = TRANSFORM_TEX(v.uv, _MainTex);
					o.vertColor = v.color;
					o.lightColor = dot(o.wNormal, normalize(_WorldSpaceLightPos0.xyz)) * 0.4975 + 0.5;
					return o;
				}



				fixed4 frag(v2f i) : SV_Target
				{
					fixed4  baseTexColor = tex2D(_MainTex, i.uv);
				    fixed4  tex_Light_Color = tex2D(_LightMapTex, i.uv);
					fixed4  diffColor = float4(1.0, 1.0, 1.0, _BloomFactor);
					//顶点颜色r通道和lightMapColor.g通道, 两者乘积rg用来做暗面颜色选择
					float  rg = i.vertColor.x * tex_Light_Color.y;
					float  threshold;
					if (rg < 0.09) {
						threshold = (i.lightColor + rg) * 0.5;
						if (threshold < _SecondShadow) {
							diffColor = baseTexColor * _SecondShadowMultColor;
						}
						else {
							diffColor = baseTexColor * _FirstShadowMultColor;
						};
					}
					else {
						float d = rg;
						if (rg >= 0.5) {
							d = (rg * 1.2) - 0.1;
						}
						else {
							d = (rg * 1.25) - 0.125;
						};
						threshold = (i.lightColor + d) * 0.5;
						if (threshold < _LightArea) {
							diffColor = baseTexColor * _FirstShadowMultColor;
						}
						else {
							diffColor = baseTexColor;
						};
					};

					//高光

					float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
					float3 h = normalize(lightDirection + i.viewDir);
					half spec = pow(max(dot(i.wNormal, h), 0.0), _Shininess);
					float3 specColor;
					if (spec >= (1.0 - tex_Light_Color.z)) {
						specColor = _LightSpecColor * _SpecMulti * tex_Light_Color.x;
					}
					else {
						specColor = float3(0.0, 0.0, 0.0);
					};
					
					diffColor.rgb = diffColor.rgb + specColor;
					diffColor.rgb = diffColor.rgb * _Color.rgb;

					//diffColor.rgb = baseTexColor * i.lightColor * spec;
					
					return  diffColor;
				}
					ENDCG
				}
	}
}