Unity局部高效实时阴影的思考和实现
发表于2016-08-21
意间看到一篇文章,说是Unity5 demo中为了实现角色的良好阴影,单独给角色设计了一个角色阴影系统。而且使用的是比较老的技术,但效果很好。其实在很多时候,我们需要的并不是万能的阴影光照系统,而是局部能做到效果就行。
万能的好处在于任何情况都能看上去合理,但是相对的,性能开销也大,同时为了兼顾各种情况,只能做各种效果的折中,所以我们看到了现在移动平台上,要么就是没有实时阴影,要么就是充满锯齿的实时阴影,要么就是使用2D贴图来模拟实时阴影。
用2D贴图来模拟的效果毫无疑问是最好的,但问题在于成本太高,很多小团队资金有限,很难专门为每一个角色都让美术画一大堆阴影贴图。而这也毫无疑问会增加游戏的大小。
我主要思考的是,在某种条件下,是否可以实现局部的良好的阴影。比如角色展台,毫无疑问只会出现一个角色,那么这个情况下,毫无疑问我们需要的是一个完美的阴影。或者说某一些游戏,视角固定,而且能看到的范围很小,那么是否只针对这个部分去实现好的阴影系统。或者一个很小的室内,我们也需要一个好的角色阴影。
ok,那么开始思考方案,首先我们应该只需要一个平行光的阴影。一般来说需要获得这个位置看过去的深度图。我首先在这个位置上放了一个正交摄像机,注意如果你想让角色有阴影,那么必须让角色处在这个正交摄像机的范围内,那么现在第一个问题来了,如何保证角色在正交摄像机的范围内?
方法如下:首先你要获得你主摄像机内的所有的需要阴影的物体,然后将这些物体转化到正交摄像机的坐标中,计算出这些物体的最大范围,并得出正交矩阵赋值给正交摄像机。(代码借鉴了http://game.ceeger.com/forum/read.php?tid=22738&fid=2,对这个楼主深表感谢)
这里要注意,Unity计算出来的Z是负值,但OpenGL是正的,官方说明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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; } |
不过实际使用过程中,我们也许并不需要正确的矩阵赋值,因为你需要的是保证所有的物体在摄像机范围内,只需要知道AABB盒,然后把相机设置在AABB盒的中心,同时增加Size即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public List 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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
你看 所有角色都被包括在内了。当然具体适合的值你可以自己调整,这样我们就解决了第一个问题。 如果你的应用场景在室内,你可以无视第一个问题,直接手动设置一个
最合适的值就行了。
第二部,就是我们需要获得物体的剪影。就是说将物体的外轮廓给检录下来。当然复杂点就是获得物体的深度图。剪影获得很简单,我们看下深度图如何获得。因为在移动平台上不支持自动生成深度图,所以我打算自己使用片段着色器获得。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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保存数值,但这样子性能确实会下降,毕竟在片段着色器里,考虑到后面需要照顾阴影质量,我觉得不使用深度图,而使用剪影。
剪影就简单了,连着色器都不用自己写,直接用摄像机渲染的黑图即可。这里我将摄像机设置成普通渲染模式,手动调用render,将贴图放到rendertexture中。
1 2 3 4 5 6 7 8 9 10 | 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大小,内存6M 还算可以接受。当然,因为深度没有用,可以取消,这样会变成4M,其他格式可能会更小,但手机上不一定支持,所以暂时先这样吧。
第三个问题,就是怎么把这些东西投射到地上变成影子。
首先投射到地上已经有现成的Projector组件了,所以问题变成了坐标计算。
我们先把Projector的位置确定一下,Projector应该和主摄像机放在同一个地方,同时有同样的参数设置:
1 2 3 4 | proj = GameObject.Find ( "GameObject" ).GetComponent proj.nearClipPlane = camera.nearClipPlane; proj.farClipPlane = camera.farClipPlane; proj.fieldOfView = camera.fieldOfView; |
这样就保证了视角内的物体都会出现阴影。然后就是坐标计算了,我们想一下,假设世界坐标中的点a,那么我们计算出它在正交摄像机中的坐标,然后根据坐标取出投影贴图中的点,那么假如这个点是全黑的(看你设置的是啥值了),那么就是不在阴影区的,假如不是,那么说明是阴影。
有了这个方案,我们就开始计算吧。首先我们可以轻易获得物体的坐标,问题在于怎么获得它在正交摄像机中的坐标,这个就需要使用正交摄像机的矩阵获得:
1 2 | matVP = GL.GetGPUProjectionMatrix (camera1.projectionMatrix, true ) * camera1.worldToCameraMatrix; proj.material.SetMatrix( "ShadowMatrix" , matVP); |
注意,我将这个计算出来的矩阵赋值给了一个材质,这个材质就是Projector使用的,因为是它需要根据坐标判断是否有阴影。
这样似乎接下来就可以直接写出着色器了:
1 2 3 4 5 6 7 8 | 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.
然后就可以在片段着色器中处理了:
1 2 3 4 5 6 7 8 9 10 | 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; } |
就这么几行,阴影就出现了:
但效果似乎不那么尽如人意,让我们看下u3d自带的高品质的阴影效果:
其实已经很接近了呢,不过鉴于我开头宣称要高质量阴影,所以我打算继续优化边缘,因为你可以看到边缘部分的锯齿,当然我们可以单纯增加贴图大小,但是假如到2048,那么就要占据16M的内存了,所以我暂时打算用另外一种做法,pcf。就是通过采样,将边缘像素模糊化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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; } |
经过采样后效果如下:
最后 是时候做一次全面比较了,在电脑上我自己写的完爆自带的,因为我的电脑的显卡很渣,但手机显卡好一些,所以手机上的结果可能不太一样,放到手机上,开启最强模式,看看到底性能和效果对比吧。找了一台两年前的1000块钱的华为:
u3d 高质量阴影:
近距离效果:
再看我自己实现的效果:
2048最强模式下:
近距离:
不过帧数下降了不少,再看看1024的吧。
效果也是要好上不少吧。
ok,暂时先这样吧
图片似乎变小了 原文图片清楚点:
http://blog.csdn.net/yxriyin/article/details/50160123
最后还是干货:所有代码
本部分内容设定了隐藏,需要回复后才能看到
今天做了一些优化,主要是动态调整正交摄像机的位置, 这样就能够在远距离时候使用模糊阴影,近距离使用高清阴影了, 在我们自己的项目中使用, 效果十分可以。
优化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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; |