【Unity Shader】一个用于手游的次世代角色Shader

时间:2022-03-15 04:32:18

前言

        在当今世代,手游都追求高画质,也有不少手游能逼近主机效果(前提是手机跑得动)。然而要达到这么犀利的效果是要付出代价的,不得不通过各种优化或奇葩的手段(然而最重要是结果)。往往Unity自带的Shader并不能满足美术大大们的需求,所以作为一只逻辑程序猿不得不放下手头工作开始琢磨Shader,以满足需求。如果没有接触过计算机图形学的程序猿要学习Shader是必定痛苦万分的,庆幸的是在大学时期学习过DirectX(然而现在不记得怎么用了)。作为一只从来没接触过Shader的程序猿,看到Unity的ShaderLab,感觉一脸茫然,只好在翻谷歌,爬帖子和看书籍的日子中渡过。坚持了一段时间后,终于有所成就,能独立完成一些美术效果要求,开始不断研究不同的效果和实现方式。最后根据美术大大们的要求,重复实践终于能出来个基本效果,在手机上性能也能过得去(松了一口气)。


需求

        需要的效果:
  • 一个次世代角色材质往往需要不少效果,但往往会有这几种:法线,高光,自发光
  • 还会有一些:半透遮罩颜色,流光,轮廓光
         需要的节点:
  • 颜色贴图:RGB通道用于颜色值,A通道用于透明值来自
  • 混合贴图:R通道用于高光蒙板,G通道用于自发光蒙板,B通道用于颜色遮罩蒙板
  • 法线贴图:自然是切线空间下的法线贴图
  • 流光贴图:以Addtive的方式叠加一层贴图,读取模型的UV2做滚动
  • 透明值:控制角色渐隐的浮点范围值(0-1)
  • 高光颜色:用于控制高光颜色,黑色下表示没有高光
  • 高光强度:用于控制高光强度(倍数)
  • 高光范围:用于控制高光范围(次幂)
  • 自发光强度:用于控制自发光强度
  • 轮廓光颜色:用于控制轮廓光颜色,黑色下表示没有轮廓光
  • 轮廓光强度:用于控制轮廓光强度(次幂)

分析

  • 法线:在Unity中非常多案例,一般是在切线空间下做处理,主要用于跟光向做点积
  • 高光:用法线贴图在切线空间内跟光向和视向的半角向量做点积
  • 自发光:根据蒙板信息,叠加基本颜色
  • 半透:由于是角色,不能关闭深度测试,所以要用AlphaTest来做半透处理,并且渲染队列放到Geometry后面
  • 遮罩颜色:根据蒙板信息,使用遮罩颜色替换基本颜色
  • 流光:在自发光处理中合并处理,根据时间滚UV,叠加流光贴图的颜色
  • 轮廓光:在光照计算里加入一道法线和光向的点积计算

实现

  • 效果
    【Unity Shader】一个用于手游的次世代角色Shader【Unity Shader】一个用于手游的次世代角色Shader

  • Surface Shader
    Shader "Yogi/Character"
    {
    	Properties
    	{
    		_MainTex("Base(RGB) Trans(A)", 2D) = "white" {}
    		_BlendMap("Gloss(R) Illum(G) Mask(B)", 2D) = "black" {}
    		_BumpMap("Normalmap", 2D) = "bump" {}
    		_FlowMap("Flowmap", 2D) = "black" {}
    		_MaskColor("Mask Color", Color) = (1, 1, 1, 1)
    		_Alpha("Alpha", Range(0, 1)) = 1
    		_Specular("Specular", Range(0, 10)) = 1
    		_Shininess("Shininess", Range(0.01, 1)) = 0.5
    		_Emission("Emission", Range(0, 10)) = 1
    		_FlowSpeed("Flow Speed", Range(0, 10)) = 1
    		_RimColor("Rim Color", Color) = (0, 0, 0, 1)
    		_RimPower("Rim Power", Range(0, 10)) = 1
    	}
    
    	SubShader
    	{
    		Tags
    		{
    			"Queue" = "Geometry+1"
    			"RenderType" = "Opaque"
    			"IgnoreProjector" = "True"
    		}
    		Blend SrcAlpha OneMinusSrcAlpha
    		AlphaTest Greater 0.1
    
    		CGPROGRAM
    		#pragma surface surf CustomBlinnPhong nolightmap
    
    		sampler2D _MainTex;
    		sampler2D _BlendMap;
    		sampler2D _FlowMap;
    		sampler2D _BumpMap;
    		fixed3 _MaskColor;
    		fixed3 _RimColor;
    		fixed _Alpha;
    		fixed _Specular;
    		fixed _Shininess;
    		fixed _Emission;
    		fixed _FlowSpeed;
    		fixed _RimPower;
    
    		struct Input
    		{
    			fixed2 uv_MainTex;
    			fixed2 uv2_FlowMap;
    		};
    
    		inline fixed4 LightingCustomBlinnPhong(SurfaceOutput s, fixed3 lightDir, fixed3 viewDir, fixed atten)
    		{
    			fixed3 h = normalize(lightDir + viewDir);
    			fixed diff = saturate(dot(s.Normal, lightDir));
    			fixed nh = saturate(dot(s.Normal, h));
    			fixed spec = pow(nh, s.Specular * 128.0) * s.Gloss;
    			fixed nv = pow(1 - saturate(dot(s.Normal, viewDir)), _RimPower);
    
    			fixed4 c;
    			c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
    			c.rgb += nv * _RimColor;
    			c.a = s.Alpha + _LightColor0.a * _SpecColor.a * spec * atten;
    
    			return c;
    		}
    
    		void surf(Input IN, inout SurfaceOutput o)
    		{
    			fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
    			fixed3 b = tex2D(_BlendMap, IN.uv_MainTex);
    			fixed3 f = tex2D(_FlowMap, IN.uv2_FlowMap + _Time.xx * _FlowSpeed);
    
    			c.rgb = lerp(c.rgb, _MaskColor.rgb, b.b);
    			o.Albedo = c.rgb;
    			o.Alpha = c.a * _Alpha;
    			o.Gloss = b.r * _Specular;
    			o.Specular = _Shininess;
    			o.Emission = c.rgb * b.g * _Emission + f;
    			o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
    		}
    
    		ENDCG
    	}
    
    	FallBack "Mobile/Diffuse"
    }

  • Vert&Frag Shader
    Shader "Yogi/Character"
    {
    	Properties
    	{
    		_MainTex("Base(RGB) Trans(A)", 2D) = "white" {}
    		_BlendMap("Gloss(R) Illum(G) Mask(B)", 2D) = "black" {}
    		_BumpMap("Normalmap", 2D) = "bump" {}
    		_FlowMap("Flowmap", 2D) = "black" {}
    		_MaskColor("Mask Color", Color) = (1, 1, 1, 1)
    		_Alpha("Alpha", Range(0, 1)) = 1
    		_Specular("Specular", Range(0, 10)) = 1
    		_Shininess("Shininess", Range(0.01, 1)) = 0.5
    		_Emission("Emission", Range(0, 10)) = 1
    		_FlowSpeed("Flow Speed", Range(0, 10)) = 1
    		_RimColor("Rim Color", Color) = (0, 0, 0, 1)
    		_RimPower("Rim Power", Range(0, 10)) = 1
    	}
    
    	SubShader
    	{
    		Tags
    		{
    			"Queue" = "Geometry+1"
    			"RenderType" = "Opaque"
    			"IgnoreProjector" = "True"
    		}
    		Blend SrcAlpha OneMinusSrcAlpha
    		AlphaTest Greater 0
    
    		Pass
    		{
    			CGPROGRAM
    			#include "UnityCG.cginc"
    			#include "Lighting.cginc"
    			#include "AutoLight.cginc"
    			#pragma vertex vert
    			#pragma fragment frag
    			#pragma fragmentoption ARB_precision_hint_fastest
    			#pragma multi_compile_fwdbase nolightmap nodirlightmap
    
    			sampler2D _MainTex;
    			sampler2D _BlendMap;
    			sampler2D _FlowMap;
    			sampler2D _BumpMap;
    			fixed4 _MainTex_ST;
    			fixed3 _MaskColor;
    			fixed3 _RimColor;
    			fixed _Alpha;
    			fixed _Specular;
    			fixed _Shininess;
    			fixed _Emission;
    			fixed _FlowSpeed;
    			fixed _RimPower;
    
    			struct a2v
    			{
    				fixed4 vertex : POSITION;
    				fixed3 normal : NORMAL;
    				fixed4 tangent : TANGENT;
    				fixed4 texcoord : TEXCOORD0;
    				fixed4 texcoord1 : TEXCOORD1;
    			};
    
    			struct v2f
    			{
    				fixed4 pos : SV_POSITION;
    				fixed4 uv : TEXCOORD0;
    				fixed3 lightDir : TEXCOORD1;
    				fixed3 vlight : TEXCOORD2;
    				fixed3 viewDir : TEXCOORD3;
    				LIGHTING_COORDS(4,5)
    			};
    
    			v2f vert(a2v v)
    			{
    				v2f o;
    				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    				o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
    				o.uv.zw = v.texcoord1.xy;
    
    				TANGENT_SPACE_ROTATION;
    				o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
    				o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
    
    #ifdef LIGHTMAP_OFF
    				fixed3 worldNormal = mul((fixed3x3)_Object2World, SCALED_NORMAL);
    				o.vlight = ShadeSH9(fixed4(worldNormal, 1.0));
    #ifdef VERTEXLIGHT_ON
    				fixed3 worldPos = mul(_Object2World, v.vertex).xyz;
    				o.vlight += Shade4PointLights(
    					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    					unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
    					unity_4LightAtten0, worldPos, worldNormal);
    #endif
    #endif
    
    				TRANSFER_VERTEX_TO_FRAGMENT(o);
    
    				return o;
    			}
    
    			fixed4 frag(v2f i) : SV_Target
    			{
    				fixed4 c = tex2D(_MainTex, i.uv.xy);
    				fixed3 b = tex2D(_BlendMap, i.uv.xy);
    				fixed3 f = tex2D(_FlowMap, i.uv.zw + _Time.xx * _FlowSpeed);
    				fixed3 n = UnpackNormal(tex2D(_BumpMap, i.uv.xy));
    				fixed atten = LIGHT_ATTENUATION(i);
    				fixed3 h = normalize(i.lightDir + i.viewDir);
    				fixed diff = saturate(dot(n, i.lightDir));
    				fixed nh = saturate(dot(n, h));
    				fixed spec = pow(nh, _Shininess * 128.0) * b.r * _Specular;
    				fixed nv = pow(1 - saturate(dot(n, i.viewDir)), _RimPower);
    				fixed4 o = 0;
    
    				c.rgb = lerp(c.rgb, _MaskColor.rgb, b.b);
    				o.rgb = (c.rgb * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
    				o.rgb += nv * _RimColor;
    				o.rgb += c.rgb * i.vlight;
    				o.rgb += c.rgb * b.g * _Emission + f;
    				o.a = c.a * _Alpha;
    
    				return o;
    			}
    
    			ENDCG
    		}
    	}
    
    	FallBack "Mobile/Diffuse"
    }

最后

        效果最后肯定不能这样直接呈现给玩家看的(乱七八糟的),所以有些效果才做了开关和设置节点,需要利用代码去控制。