無意間看到一篇文章棍郎,說是Unity5 demo中為了實現(xiàn)角色的良好陰影钦讳,單獨給角色設計了一個角色陰影系統(tǒng)。而且使用的是比較老的技術掐暮,但效果很好蝎抽。其實在很多時候,我們需要的并不是萬能的陰影光照系統(tǒng),而是局部能做到效果就行樟结。
萬能的好處在于任何情況都能看上去合理养交,但是相對的,性能開銷也大瓢宦,同時為了兼顧各種情況碎连,只能做各種效果的折中,所以我們看到了現(xiàn)在移動平臺上驮履,要么就是沒有實時陰影鱼辙,要么就是充滿鋸齒的實時陰影,要么就是使用2D貼圖來模擬實時陰影玫镐。
用2D貼圖來模擬的效果毫無疑問是最好的倒戏,但問題在于成本太高,很多小團隊資金有限恐似,很難專門為每一個角色都讓美術畫一大堆陰影貼圖杜跷。而這也毫無疑問會增加游戲的大小。
我主要思考的是矫夷,在某種條件下葱椭,是否可以實現(xiàn)局部的良好的陰影。比如角色展臺口四,毫無疑問只會出現(xiàn)一個角色孵运,那么這個情況下,毫無疑問我們需要的是一個完美的陰影蔓彩≈伪浚或者說某一些游戲,視角固定赤嚼,而且能看到的范圍很小旷赖,那么是否只針對這個部分去實現(xiàn)好的陰影系統(tǒng)「洌或者一個很小的室內(nèi)等孵,我們也需要一個好的角色陰影。
ok蹂空,那么開始思考方案俯萌,首先我們應該只需要一個平行光的陰影。一般來說需要獲得這個位置看過去的深度圖上枕。我首先在這個位置上放了一個正交攝像機咐熙,注意如果你想讓角色有陰影,那么必須讓角色處在這個正交攝像機的范圍內(nèi)辨萍,那么現(xiàn)在第一個問題來了棋恼,如何保證角色在正交攝像機的范圍內(nèi)?
方法如下:首先你要獲得你主攝像機內(nèi)的所有的需要陰影的物體,然后將這些物體轉(zhuǎn)化到正交攝像機的坐標中爪飘,計算出這些物體的最大范圍义起,并得出正交矩陣賦值給正交攝像機。(代碼借鑒了http://game.ceeger.com/forum/read.php?tid=22738&fid=2师崎,對這個樓主深表感謝)
這里要注意并扇,Unity計算出來的Z是負值,但OpenGL是正的抡诞,官方說明如下:
Matrix that transforms from world to camera space.
Use this to calculate the camera space position of objects or to provide customcamera's location that is not based on the transform.
Note that camera space matches OpenGL convention: camera's forward is the negativeZ axis. This is different from Unity's convention, where forward is the positive Zaxis.
If you change this matrix, the camera no longer updates its rendering based on its Transform.This lasts until you call ResetWorldToCameraMatrix.
#pragma strict
// Offsets camera's rendering from the transform's position.
public var offset: Vector3 = new Vector3(0, 1, 0);
var camera: Camera;
function Start() {
camera = GetComponent.();
}
function LateUpdate() {
var camoffset: Vector3 = new Vector3(-offset.x, -offset.y, offset.z);
var m: Matrix4x4 = Matrix4x4.TRS(camoffset, Quaternion.identity, new Vector3(1, 1, -1));
camera.worldToCameraMatrix = m * transform.worldToLocalMatrix;
}
不過實際使用過程中穷蛹,我們也許并不需要正確的矩陣賦值,因為你需要的是保證所有的物體在攝像機范圍內(nèi)昼汗,只需要知道AABB盒肴熏,然后把相機設置在AABB盒的中心,同時增加Size即可顷窒。
public ListCharactorList;
void CreateCameraProjecterMatrix()
{
Vector3 v3MaxPosition = -Vector3.one * 500000.0f;
Vector3 v3MinPosition = Vector3.one * 500000.0f;
for (int vertId = 0; vertId < CharactorList.Count; ++vertId)
{
// Light view space
Vector3 v3Position = camera1.worldToCameraMatrix.MultiplyPoint3x4(CharactorList[vertId].position);
if (v3Position.x > v3MaxPosition.x)
{
v3MaxPosition.x = v3Position.x;
}
if (v3Position.y > v3MaxPosition.y)
{
v3MaxPosition.y = v3Position.y;
}
if (v3Position.z > v3MaxPosition.z)
{
v3MaxPosition.z = v3Position.z;
}
if (v3Position.x < v3MinPosition.x)
{
v3MinPosition.x = v3Position.x;
}
if (v3Position.y < v3MinPosition.y)
{
v3MinPosition.y = v3Position.y;
}
if (v3Position.z < v3MinPosition.z)
{
v3MinPosition.z = v3Position.z;
}
}
Vector3 off = v3MaxPosition - v3MinPosition;
Vector3 sizeOff = off;
sizeOff.z = 0;
float dis = sizeOff.magnitude;
//CreateOrthogonalProjectMatrix (ref m_projMatrix, v3MaxPosition, v3MinPosition);
//Debug.Log (v3MaxPosition.ToString() + v3MinPosition.ToString());
//Matrix4x4 m = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1, 1, -1));
//camera1.projectionMatrix = m * m_projMatrix;
camera1.orthographicSize = dis / 1.8f;
camera1.farClipPlane = off.z + 50;
}
void CreateViewMatrix(ref Matrix4x4 viewMatrix,Vector3 look,Vector3 up,Vector3 right,Vector3 pos)
{
look.Normalize ();
up.Normalize ();
right.Normalize ();
float x = -Vector3.Dot (right,pos);
float y = -Vector3.Dot (up,pos);
float z = -Vector3.Dot (look,pos);
viewMatrix.m00 = right.x; viewMatrix.m10 = up.x; viewMatrix.m20 = look.x; viewMatrix.m30 = 0.0f;
viewMatrix.m01 = right.y; viewMatrix.m11 = up.y; viewMatrix.m21 = look.y; viewMatrix.m31 = 0.0f;
viewMatrix.m02 = right.z; viewMatrix.m12 = up.z; viewMatrix.m22 = look.z; viewMatrix.m32 = 0.0f;
viewMatrix.m03 = x; viewMatrix.m13 = y; viewMatrix.m23 = z; viewMatrix.m33 = 1.0f;
}
void CreateOrthogonalProjectMatrix(ref Matrix4x4 projectMatrix,Vector3 v3MaxInViewSpace, Vector3 v3MinInViewSpace)
{
float scaleX, scaleY, scaleZ;
float offsetX, offsetY, offsetZ;
scaleX = 2.0f / (v3MaxInViewSpace.x - v3MinInViewSpace.x);
scaleY = 2.0f / (v3MaxInViewSpace.y - v3MinInViewSpace.y);
offsetX = -0.5f * (v3MaxInViewSpace.x + v3MinInViewSpace.x) * scaleX;
offsetY = -0.5f * (v3MaxInViewSpace.y + v3MinInViewSpace.y) * scaleY;
scaleZ = 1.0f / (v3MaxInViewSpace.z - v3MinInViewSpace.z);
offsetZ = -v3MinInViewSpace.z * scaleZ;
//列矩陣
projectMatrix.m00 = scaleX; projectMatrix.m01 = 0.0f; projectMatrix.m02 = 0.0f; projectMatrix.m03 = offsetX;
projectMatrix.m10 = 0.0f; projectMatrix.m11 = scaleY; projectMatrix.m12 = 0.0f; projectMatrix.m13 = offsetY;
projectMatrix.m20 = 0.0f; projectMatrix.m21 = 0.0f; projectMatrix.m22 = scaleZ; projectMatrix.m23 = offsetZ;
projectMatrix.m30 = 0.0f; projectMatrix.m31 = 0.0f; projectMatrix.m32 = 0.0f; projectMatrix.m33 = 1.0f;
}
你看 所有角色都被包括在內(nèi)了蛙吏。當然具體適合的值你可以自己調(diào)整,這樣我們就解決了第一個問題鞋吉。 如果你的應用場景在室內(nèi)鸦做,你可以無視第一個問題,直接手動設置一個
最合適的值就行了谓着。
第二部泼诱,就是我們需要獲得物體的剪影。就是說將物體的外輪廓給檢錄下來赊锚。當然復雜點就是獲得物體的深度圖治筒。剪影獲得很簡單,我們看下深度圖如何獲得舷蒲。因為在移動平臺上不支持自動生成深度圖耸袜,所以我打算自己使用片段著色器獲得。
Shader "depthShader" {
Properties {
}
SubShader {
//Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass {
CGPROGRAM
// Upgrade NOTE: excluded shader from DX11 and Xbox360; has structs without semantics (struct v2f members pos1)
#pragma exclude_renderers d3d11 xbox360
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D_float _CameraDepthTexture;
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
half4 pos : SV_POSITION;
float2 depth;
};
v2f vert (appdata_base v) {
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.depth = o.pos.zw;
return o;
}
fixed4 frag(v2f i) : COLOR
{
float d = i.depth.x/i.depth.y;
float flag = 0;
if(d < 0)
{
d = abs(d);
flag = 1;
}
float3 kEncodeMul = float3(1.0, 255.0, 65025.0);
float kEncodeBit = 1.0/255.0;
float3 enc = kEncodeMul * d;
enc = frac (enc);
enc -= enc.yzz * kEncodeBit;
return fixed4(flag, enc);
}
ENDCG
}
}
}
本來只要存儲z就好了牲平,不過很遺憾的是堤框,有些平臺的z竟然是負值,負值存儲成像素是無意義的纵柿。所以我用r存儲正負值蜈抓,gba保存數(shù)值,但這樣子性能確實會下降藐窄,畢竟在片段著色器里资昧,考慮到后面需要照顧陰影質(zhì)量,我覺得不使用深度圖荆忍,而使用剪影。
剪影就簡單了,連著色器都不用自己寫刹枉,直接用攝像機渲染的黑圖即可叽唱。這里我將攝像機設置成普通渲染模式,手動調(diào)用render,將貼圖放到rendertexture中微宝。
camera1 = GameObject.Find ("Camera").camera;
//camera1.hideFlags = HideFlags.HideAndDontSave;
camera1.enabled = false;
//camera1.projectionMatrix = camera.projectionMatrix;
int textureSize = 1024;
shadowTexture = new RenderTexture(textureSize , textureSize, 16, RenderTextureFormat.ARGB32);
shadowTexture.name = "shadowTexture" + GetInstanceID();
shadowTexture.isPowerOfTwo = true;
shadowTexture.hideFlags = HideFlags.DontSave;
camera1.targetTexture = shadowTexture;
這樣就可以看到如下貼圖:
1024大小棺亭,內(nèi)存6M 還算可以接受。當然蟋软,因為深度沒有用镶摘,可以取消,這樣會變成4M岳守,其他格式可能會更小凄敢,但手機上不一定支持,所以暫時先這樣吧湿痢。
第三個問題涝缝,就是怎么把這些東西投射到地上變成影子。
首先投射到地上已經(jīng)有現(xiàn)成的Projector組件了譬重,所以問題變成了坐標計算拒逮。
我們先把Projector的位置確定一下,Projector應該和主攝像機放在同一個地方臀规,同時有同樣的參數(shù)設置:
proj = GameObject.Find ("GameObject").GetComponent();
proj.nearClipPlane = camera.nearClipPlane;
proj.farClipPlane = camera.farClipPlane;
proj.fieldOfView = camera.fieldOfView;
這樣就保證了視角內(nèi)的物體都會出現(xiàn)陰影滩援。然后就是坐標計算了,我們想一下塔嬉,假設世界坐標中的點a狠怨,那么我們計算出它在正交攝像機中的坐標,然后根據(jù)坐標取出投影貼圖中的點邑遏,那么假如這個點是全黑的(看你設置的是啥值了)佣赖,那么就是不在陰影區(qū)的,假如不是记盒,那么說明是陰影憎蛤。
有了這個方案,我們就開始計算吧纪吮。首先我們可以輕易獲得物體的坐標俩檬,問題在于怎么獲得它在正交攝像機中的坐標,這個就需要使用正交攝像機的矩陣獲得:
matVP = GL.GetGPUProjectionMatrix (camera1.projectionMatrix, true) * camera1.worldToCameraMatrix;
proj.material.SetMatrix("ShadowMatrix", matVP);
注意碾盟,我將這個計算出來的矩陣賦值給了一個材質(zhì)棚辽,這個材質(zhì)就是Projector使用的,因為是它需要根據(jù)坐標判斷是否有陰影冰肴。
這樣似乎接下來就可以直接寫出著色器了:
v2f vert (appdata_base v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
float4x4 matWVP = mul (ShadowMatrix, _Object2World);
o.uvShadow = mul(matWVP, v.vertex);
return o;
}
注意屈藐,頂點著色器不僅要計算出pos榔组,同時還要獲得正交相機中的坐標,也就是uvShadow.
然后就可以在片段著色器中處理了:
half2 uv = i.uvShadow.xy / i.uvShadow.w * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1 - uv.y;
#endif
fixed4 res = fixed4(0, 0, 0, 0);
fixed4 texS = tex2D(_ShadowTex, uv);
if(texS.a > 0)
{
res.a = 0.5;
}
就這么幾行联逻,陰影就出現(xiàn)了:
但效果似乎不那么盡如人意搓扯,讓我們看下u3d自帶的高品質(zhì)的陰影效果:
其實已經(jīng)很接近了呢,不過鑒于我開頭宣稱要高質(zhì)量陰影包归,所以我打算繼續(xù)優(yōu)化邊緣锨推,因為你可以看到邊緣部分的鋸齒,當然我們可以單純增加貼圖大小公壤,但是假如到2048,那么就要占據(jù)16M的內(nèi)存了换可,所以我暫時打算用另外一種做法,pcf厦幅。就是通過采樣沾鳄,將邊緣像素模糊化。
texS = tex2D(_ShadowTex, uv + half2(-0.94201624/pad, -0.39906216/pad));
if(texS.a > 0)
{
res.a += _Strength;
}
texS = tex2D(_ShadowTex, uv + half2(0.94558609/pad, -0.76890725/pad));
if(texS.a > 0)
{
res.a += _Strength;
}
texS = tex2D(_ShadowTex, uv + half2(-0.094184101/pad, -0.92938870/pad));
if(texS.a > 0)
{
res.a += _Strength;
}
texS = tex2D(_ShadowTex, uv + half2(0.34495938/pad, 0.29387760/pad));
if(texS.a > 0)
{
res.a += _Strength;
}
經(jīng)過采樣后效果如下:
最后 是時候做一次全面比較了慨削,在電腦上我自己寫的完爆自帶的洞渔,因為我的電腦的顯卡很渣,但手機顯卡好一些缚态,所以手機上的結果可能不太一樣磁椒,放到手機上,開啟最強模式玫芦,看看到底性能和效果對比吧浆熔。找了一臺兩年前的1000塊錢的華為:
u3d 高質(zhì)量陰影:
近距離效果:
再看我自己實現(xiàn)的效果:
2048最強模式下:
近距離:
不過幀數(shù)下降了不少,再看看1024的吧桥帆。
效果也是要好上不少吧医增。
后來做了一些優(yōu)化,主要是動態(tài)調(diào)整正交攝像機的位置老虫, 這樣就能夠在遠距離時候使用模糊陰影叶骨,近距離使用高清陰影了, 在我們自己的項目中使用祈匙, 效果十分可以忽刽。
優(yōu)化代碼:
public class VisibleMesh:MonoBehaviour
{
public Transform tr;
void OnWillRenderObject()
{
if(Camera.current == Camera.main)
{
if(SceneShadow.inst.CharactorList.Contains(tr))
{
return;
}
SceneShadow.inst.CharactorList.Add (tr);
}
}
}
Vector3 off = v3MaxPosition - v3MinPosition;
Vector3 pos = (v3MaxPosition + v3MinPosition) / 2;
pos = camera1.cameraToWorldMatrix.MultiplyPoint3x4 (pos);
camera1.transform.position = pos - camera1.transform.forward * (off.z + 10);
Vector3 sizeOff = off;
sizeOff.z = 0;
float dis = sizeOff.x;
if(sizeOff.y > dis)
{
dis = sizeOff.y;
}
camera1.orthographicSize = dis / 2;
camera1.farClipPlane = off.z + 30;