Unity3D手遊專案的總結和思考(1)
有朋友私信我問我為啥很久不更新部落格,是不是轉行了...我當然不可能承認自己懶啊,只能回覆說太忙了.不過專案開發中,確實很難有時間和心力去總結和思考一些東西,不過現在忙完一些專案以後,我又回來了.
渲染技術這個東西,在專案前期我並沒有投入太多精力去思考,在當時的環境下我更看中手遊的效能,我做端遊,可以無限地挖掘電腦的效能,比如多執行緒去處理角色的軟體蒙皮之類,但是做手遊,則要保護手機的硬體資源,防止手機耗電快發熱.比如限幀30fps.直到兩年前經歷了一次美術和技術的撕逼以後,我才重新思考了一下這個東西.整個過程也很有趣,老闆覺得遊戲畫面不行,其實是美術做得不好,但是美術領導用了一堆次世代的技術名詞來忽悠老闆說我們技術不行,啥都不支援...一個頁遊公司的美術整天吹噓次世代我也是醉了,真正做過次世代的我反而成了外行...次世代本身就是個大坑,手游上投入的價效比更低,很多大點的公司都深陷次世代的泥潭,我要對遊戲的技術負責和把關,撕逼是難免的.但是撕逼歸撕逼,想要畫面好是大家的共同心願,能不能在保證效率和投入價效比的情況下,提升一些畫面呢?
我的想法是,一些牛逼技術是一定是要有的,如果效能有問題,那麼儘量優化,甚至限制性的使用,有總比沒有好,不管是為了應付美術,還是老闆,當然這不是我的初衷.我作為一個熱愛引擎的程式設計師,畫面好本身就是我們追求的東西.做技術的一定要有追求.
然後對整個渲染技術架構思考了幾天,做出了一個大膽的想法,就是升級Unity4到Unity5,然後放棄Unity自帶的surface shader和實時光照,利用Unity5新增的shader_feature功能自己寫一套比較萬能通用的standard shader,而避免整個專案一堆亂七八糟混亂的shader.至於為什麼要這麼幹,主要原因有幾個.
1.角色的動態光照,用全域性shader引數來模擬燈光,比在Unity場景裡面擺幾個光源的效率更高一些.
2.surface shader的一些預設的定製會佔據一些texture interpolator,對於我實現擴充套件一些效果有限制.而且不夠清爽.
3.一些特殊效果需要場景所有物件shader的支援,比如天氣系統的漸變過渡.如果所有shader都自己寫的,那會特別方便.
4.Unity4的時候,光照圖和Unity的動態光源是不能疊加的,如果自己實現的動態光源,就可以疊加的,當然Unity5後來是可以疊加的.
我這套shader基本涵蓋了場景中的所有物件:
1.物件用SceneStandard.shader(有PBR版本)
2.角色用CharacterStandard.shader(有PBR版本)
3.粒子用ParticleStandard.shader
4.天空用SkyBox.shader
5.地形用TerrainT4M.shader
6.水面用Water.shader
7.小草用Grass.shader
整個場景的光照方案,場景用光照圖 + 全域性shader光源,角色用全域性shader光源 + projector shadowmap.對於角色和場景,都有兩套shader,一套傳統光照模型,一套PBR光照模型.
以一個傳統光照模型的SceneStandard.shader來說.
Shader "Luoyinan/Scene/SceneStandard"
{
Properties
{
_Color ("Main Color", Color) = (1, 1, 1, 1)
_MainTex("Main Texture RGB(Albedo) A(Gloss & Alpha)", 2D) = "white" {}
_NormalTex("Normal Texture", 2D) = "bump" {}
_GlossTex ("Gloss Texture", 2D) = "white" {}
_HalfLambert("Half Lambert", Range (0.5, 1)) = 0.75
_SpecularIntensity("Specular Intensity", Range (0, 2)) = 0
_SpecularSharp("Specular Sharp",Float) = 32
_SpecularLuminanceMask("Specular Luminance Mask", Range (0, 2)) = 0
}
SubShader
{
Tags
{
"Queue" = "Background"
"RenderType" = "Opaque" // 支援渲染到_CameraDepthNormalsTexture
}
Pass
{
Lighting Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
#pragma shader_feature _NORMAL_MAP
#pragma shader_feature _LUMINANCE_MASK_ON
#pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON
#pragma multi_compile __ _FADING_ON
#pragma multi_compile __ _POINT_LIGHT
#pragma multi_compile __ _FANCY_STUFF
struct appdata_lightmap
{
float4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
half2 texcoord1 : TEXCOORD1;
#if _FANCY_STUFF
half3 normal : NORMAL;
#if _NORMAL_MAP
half4 tangent : TANGENT;
#endif
#endif
};
// SM2.0的texture interpolator只有8個,要合理規劃.
struct v2f
{
float4 pos : SV_POSITION;
half2 uv0 : TEXCOORD0;
#ifndef LIGHTMAP_OFF
half2 uv1 : TEXCOORD1;
#endif
UNITY_FOG_COORDS(2)
float3 posWorld : TEXCOORD3;
#if _FANCY_STUFF
half3 normalWorld : TEXCOORD4;
#if _NORMAL_MAP
half3 tangentWorld : TEXCOORD5;
half3 binormalWorld : TEXCOORD6;
#endif
#endif
};
fixed4 _Color;
sampler2D _MainTex;
half4 _MainTex_ST;
#if _POINT_LIGHT
float4 _GlobalPointLightPos;
fixed4 _GlobalPointLightColor;
fixed _GlobalPointLightRange;
#endif
#ifndef LIGHTMAP_OFF
#if _FADING_ON
sampler2D _GlobalLightMap;
fixed _GlobalFadingFactor;
#endif
#endif
#if _FANCY_STUFF
sampler2D _GlossTex;
fixed _HalfLambert;
fixed _SpecularIntensity;
fixed _SpecularSharp;
half4 _GlobalMainLightDir;
fixed4 _GlobalMainLightColor;
half4 _GlobalBackLightDir;
fixed4 _GlobalBackLightColor;
#if _LUMINANCE_MASK_ON
fixed _SpecularLuminanceMask;
#endif
#if _NORMAL_MAP
uniform sampler2D _NormalTex;
half4 _NormalTex_ST;
#endif
#endif
v2f vert(appdata_lightmap i)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.uv0 = TRANSFORM_TEX(i.texcoord, _MainTex);
#ifndef LIGHTMAP_OFF
o.uv1 = i.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
o.posWorld = mul(unity_ObjectToWorld, i.vertex).xyz;
#if _FANCY_STUFF
o.normalWorld = UnityObjectToWorldNormal(i.normal);
#if _NORMAL_MAP
o.tangentWorld = UnityObjectToWorldDir(i.tangent);
o.binormalWorld = cross(o.normalWorld, o.tangentWorld) * i.tangent.w;
#endif
#endif
UNITY_TRANSFER_FOG(o, o.pos);
return o;
}
fixed4 frag(v2f i) : COLOR
{
fixed4 mainColor = tex2D(_MainTex, i.uv0);
fixed alpha = mainColor.a;
fixed4 finalColor = mainColor * _Color;
// lightmap
#ifndef LIGHTMAP_OFF
fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.uv1));
#if _FADING_ON
fixed3 lm_fading = DecodeLightmap(UNITY_SAMPLE_TEX2D(_GlobalLightMap, i.uv1));
lm = lerp(lm, lm_fading, _GlobalFadingFactor);
#endif
#if _FANCY_STUFF && _LUMINANCE_MASK_ON
half lumin = saturate(Luminance(lm) * _SpecularLuminanceMask);
#endif
finalColor.rgb *= lm;
#endif
#if _FANCY_STUFF
// gloss
alpha *= tex2D(_GlossTex, i.uv0).r;
// normalmap
#if _NORMAL_MAP
fixed3x3 tangentToWorld = fixed3x3(i.tangentWorld, i.binormalWorld, i.normalWorld);
half3 normalMap = UnpackNormal(tex2D(_NormalTex, i.uv0));
half3 fixedNormal = normalize(mul(normalMap, tangentToWorld));
#else
half3 fixedNormal = normalize(i.normalWorld);
#endif
// main light diffuse
#if _NORMAL_MAP || LIGHTMAP_OFF
half nl = dot(fixedNormal, normalize(_GlobalMainLightDir.xyz));
half diff = saturate(nl) * (1 - _HalfLambert) + _HalfLambert;
finalColor *= diff;
#endif
#if _NORMAL_MAP
// main light specular
half3 viewDir = normalize(_WorldSpaceCameraPos - i.posWorld);
half3 h = normalize(normalize(_GlobalMainLightDir.xyz) + viewDir);
half nh = saturate(dot(fixedNormal, h));
nh = pow(nh, _SpecularSharp) * _SpecularIntensity;
#if _LUMINANCE_MASK_ON && LIGHTMAP_ON
finalColor.rgb += _GlobalMainLightColor.rgb * nh * alpha * _GlobalMainLightColor.a * lumin;
#else
finalColor.rgb += _GlobalMainLightColor.rgb * nh * alpha * _GlobalMainLightColor.a;
#endif
// back light specular
h = normalize(normalize(_GlobalBackLightDir.xyz) + viewDir);
nh = saturate(dot(fixedNormal, h));
nh = pow(nh, _SpecularSharp) * _SpecularIntensity;
#if _LUMINANCE_MASK_ON && LIGHTMAP_ON
finalColor.rgb += _GlobalBackLightColor.rgb * nh * alpha * _GlobalBackLightColor.a * lumin;
#else
finalColor.rgb += _GlobalBackLightColor.rgb * nh * alpha * _GlobalBackLightColor.a;
#endif
#endif
#if _POINT_LIGHT
half3 toLight = _GlobalPointLightPos.xyz - i.posWorld ;
half ratio = saturate(length(toLight) / _GlobalPointLightRange);
//half attenuation = 1 - ratio; // linear attenuation
ratio *= ratio;
half attenuation = 1.0 / (1.0 + 0.01 * ratio) * (1 - ratio); // quadratic attenuation
if (attenuation > 0) // performance
{
// point light diffuse
toLight = normalize(toLight);
half intensity = 8;
half nl2 = max(0, dot(fixedNormal, toLight));
finalColor.rgb += mainColor.rgb * _GlobalPointLightColor.rgb * nl2 * attenuation * intensity;
// point light specular
#if _NORMAL_MAP
h = normalize(toLight + viewDir);
nh = saturate(dot(fixedNormal, h));
nh = pow(nh, _SpecularSharp) * _SpecularIntensity;
intensity *= _GlobalPointLightColor.a;
finalColor.rgb += _GlobalPointLightColor.rgb * nh * alpha * attenuation * intensity;
#endif
}
#endif
#endif
UNITY_APPLY_FOG(i.fogCoord, finalColor);
// 沒有高光貼圖,alpha預設為0,便於處理Bloom的Alpha Gloss
#if _NORMAL_MAP
finalColor.a = alpha;
#else
finalColor.a = 0;
#endif
return finalColor;
}
ENDCG
}
// 沒用Unity自帶的陰影,只是用來來渲染_CameraDepthsTexture.
Pass
{
Tags { "LightMode" = "ShadowCaster" }
Fog { Mode Off }
ZWrite On
Offset 1, 1
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER(o)
return o;
}
fixed4 frag(v2f i) : COLOR
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
Fallback off
CustomEditor "SceneStandard_ShaderGUI"
}
這shader比較簡單,主要用於場景中的物件,實現了幾個功能
1.光照圖和光照圖的融合過渡.
光照圖過渡用於天氣系統的漸變效果處理,比如白天的光照圖和夜晚的光照圖的漸變處理.
2.法線貼圖和高光貼圖
法線貼圖可用高模去生成,投入有限的公司建議直接工具生成,效果差點但是價效比高,高光貼圖的解析度可減半,效果差不多.高光貼圖的通道渲染進螢幕的alpha通道,對於後期生成基於gloss的bloom效果很有用.
3.光照圖陰影區域的高光處理
理論上來說,靜態陰影區域不應該有高光效果的,但是Unity的光照圖,沒用alpha通道來儲存陰影區域的掩碼,導致我們無法知道陰影區域,這也是我一直吐槽Unity的地方,較新版本的Unity已經支援光照圖陰影掩碼...但是我們不可能升級到最新的Unity版本...所以只好選用了一個有些開銷的方案來處理,就是用亮度來判斷這是否是陰影區域.然後淡化高光.
4.點光源
我原來只打算用全域性shader引數模擬兩盞方向光,對於有些地宮的暗黑場景,確實需要點光源的支援,這樣角色走到哪裡,就能照亮到哪裡,最早考慮過一個面片疊加的假的光源繫結在角色身上,但是效果一般,最後還是模擬了一個實時點光源.逐畫素的.對於點光源的衰減處理,Unity是用一張圖來處理,我試了幾種曲線函式,最終選用
half attenuation = 1.0 / (1.0 + 0.01 * ratio) * (1 - ratio); // quadratic attenuation
這個shader的效果圖如下:
漫反射貼圖:
漫反射貼圖 + 光照圖:
漫反射貼圖 + 光照圖 + 高光:
漫反射貼圖 + 光照圖 + 高光 + 法線貼圖 + 高光貼圖:
對於SceneStardard來說,有個功能,就是植被的隨風擺動問題,原來整合在一起的,後來獨立開來了一個shader.
Shader "Luoyinan/Scene/SceneStandard_Cutout"
{
Properties
{
_Color ("Main Color", Color) = (1, 1, 1, 1)
_MainTex("Main Texture RGB(Albedo) A(Gloss & Alpha)", 2D) = "white" {}
_NormalTex("Normal Texture", 2D) = "bump" {}
_GlossTex ("Gloss Texture", 2D) = "white" {}
_HalfLambert("Half Lambert", Range (0.5, 1)) = 0.75
_SpecularIntensity("Specular Intensity", Range (0, 2)) = 0
_SpecularSharp("Specular Sharp",Float) = 32
_MainBendingFactor ("Wind Main Bending Factor (Blue)", float) = 0.25
_MainBendingFactor2 ("Wind Main Bending Factor 2 (Blue)", float) = 1.0
_BranchBendingFactor ("Wind Branch Bending Factor (Red)", float) = 2.5
_EdgeBendingFactor ("Wind Edge Bending Factor (Green)", float) = 1.0
_EdgeFrequencyFactor ("Wind Edge Frequency Factor", float) = 1.0
[HideInInspector] _DoubleSided("", Float) = 2.0
[HideInInspector] _WindOn("", Float) = 1.0
[HideInInspector] _Cutoff("", Float) = 0.2
}
自定義了shader以後,那麼shader編輯器,也需要自定義.
// 2016.5.14 luoyinan 自定義shader的編輯器
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
public class SceneStandard_Cutout_ShaderGUI : ShaderGUI
{
MaterialProperty cutOff = null;
MaterialProperty wind = null;
public void FindProperties(MaterialProperty[] props)
{
wind = FindProperty("_WindOn", props);
cutOff = FindProperty("_Cutoff", props);
}
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
FindProperties(props);
// Check change
EditorGUI.BeginChangeCheck();
{
float co = cutOff.floatValue;
cutOff.floatValue = EditorGUILayout.Slider("Cut Off", co, 0f, 1f);
bool w = wind.floatValue == 0;
wind.floatValue = EditorGUILayout.Toggle("Wind", w) ? 0 : 1;
// Render the default gui
base.OnGUI(materialEditor, props);
}
if (EditorGUI.EndChangeCheck())
{
foreach (var obj in materialEditor.targets)
{
MaterialChanged((Material)obj);
}
}
}
public static void MaterialChanged(Material material)
{
float value = material.GetFloat("_Cutoff");
material.SetFloat("_Cutoff", value);
if (value > 0)
{
material.EnableKeyword("_ALPHA_TEST_ON");
material.SetFloat("_DoubleSided", (float)CullMode.Off);
}
else
{
material.DisableKeyword("_ALPHA_TEST_ON");
material.SetFloat("_DoubleSided", (float)CullMode.Back);
}
value = material.GetFloat("_WindOn");
material.SetFloat("_WindOn", value);
SetKeyword(material, "_WIND_ON", value == 0);
// 有紋理就自動開啟
SetKeyword(material, "_NORMAL_MAP", material.GetTexture("_NormalTex"));
}
private static void SetKeyword(Material mat, string keyword, bool enable)
{
if (enable)
mat.EnableKeyword(keyword);
else
mat.DisableKeyword(keyword);
}
}
傳統光照模型的效果,除了反射,基本都實現了,那麼反射呢,我是去掉了,移到了PBR裡面,因為傳統光照模型的環境反射效果其實並不好,要用反射,可用PBR的shader代替.
Shader "Luoyinan/Scene/SceneStandard_PBR"
{
Properties
{
_Color ("Main Color", Color) = (1, 1, 1, 1)
_MainTex("Main Texture RGB(Albedo) A(Gloss & Alpha)", 2D) = "white" {}
_NormalTex("Normal Texture", 2D) = "bump" {}
_GlossTex ("Gloss Texture", 2D) = "white" {}
_SpecularColor ("Specular Color", Color) = (1, 1, 1, 0.5)
_Roughness ("Roughness", Range (0, 1)) = 0
_RefectionTex("Refection Texture (Cubemap)", Cube) = "" {}
_RefectionColor ("Refection Color", Color) = (1, 1, 1, 1)
[HideInInspector] _UseRoughness("", Float) = 0
}
如果要整合很多效果在一個shader裡面,就需要利用multi_compile和shader_feature設定好各種效果的功能開關,高中低的畫質的切換也特別方便.
以上主要是對場景shader的介紹,由於內容太多了,今天我就先寫到這裡,下一篇我會對其他渲染技術做一些分享.