前言
在当今世代,手游都追求高画质,也有不少手游能逼近主机效果(前提是手机跑得动)。然而要达到这么犀利的效果是要付出代价的,不得不通过各种优化或奇葩的手段(然而最重要是结果)。往往Unity自带的Shader并不能满足美术大大们的需求,所以作为一只逻辑程序猿不得不放下手头工作开始琢磨Shader,以满足需求。如果没有接触过计算机图形学的程序猿要学习Shader是必定痛苦万分的,庆幸的是在大学时期学习过DirectX(然而现在不记得怎么用了)。作为一只从来没接触过Shader的程序猿,看到Unity的ShaderLab,感觉一脸茫然,只好在翻谷歌,爬帖子和看书籍的日子中渡过。坚持了一段时间后,终于有所成就,能独立完成一些美术效果要求,开始不断研究不同的效果和实现方式。最后根据美术大大们的要求,重复实践,终于能出来个基本效果,在手机上性能也能过得去(松了一口气)。
需求
需要的效果:
- 一个次世代角色材质往往需要不少效果,但往往会有这几种:法线,高光,自发光
-
还会有一些:半透,遮罩颜色,流光,轮廓光
- 颜色贴图:RGB通道用于颜色值,A通道用于透明值来自
- 混合贴图:R通道用于高光蒙板,G通道用于自发光蒙板,B通道用于颜色遮罩蒙板
- 法线贴图:自然是切线空间下的法线贴图
- 流光贴图:以Addtive的方式叠加一层贴图,读取模型的UV2做滚动
- 透明值:控制角色渐隐的浮点范围值(0-1)
- 高光颜色:用于控制高光颜色,黑色下表示没有高光
- 高光强度:用于控制高光强度(倍数)
- 高光范围:用于控制高光范围(次幂)
- 自发光强度:用于控制自发光强度
- 轮廓光颜色:用于控制轮廓光颜色,黑色下表示没有轮廓光
- 轮廓光强度:用于控制轮廓光强度(次幂)
分析
- 法线:在Unity中非常多案例,一般是在切线空间下做处理,主要用于跟光向做点积
- 高光:用法线贴图在切线空间内跟光向和视向的半角向量做点积
- 自发光:根据蒙板信息,叠加基本颜色
- 半透:由于是角色,不能关闭深度测试,所以要用AlphaTest来做半透处理,并且渲染队列放到Geometry后面
- 遮罩颜色:根据蒙板信息,使用遮罩颜色替换基本颜色
- 流光:在自发光处理中合并处理,根据时间滚UV,叠加流光贴图的颜色
- 轮廓光:在光照计算里加入一道法线和光向的点积计算
实现
-
效果
-
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" }
最后
效果最后肯定不能这样直接呈现给玩家看的(乱七八糟的),所以有些效果才做了开关和设置节点,需要利用代码去控制。