手機硬件限制,很多PC上的渲染優化技術是沒辦法直接拿過來用的。目前有些游戲為了實現多部隊戰斗的效果,各種降低骨骼數目,模型面數的方案,但都逃不過骨骼動畫計算這一環節。
在上個公司的時候,自己瞎想了一張方案,沒想到最后還寫出來了, 沒想到最后還用上了。。。
先上張圖,里面有100個士兵和10個萌寶寶的場景,每個角色的動作是分開控制的,在小米3上可以60fps的幀率流暢運行,之前也嘗試過,300多個角色帶動畫也可以55左右的fps運行
先說明一下這套方案的優缺點:
優點:可不用計算骨骼動畫,並能流暢播放骨骼動畫(其實已經不是骨骼動畫了);每個角色動作也可以單獨控制;對cpu很小消耗;
缺點:動作切換沒有過渡(群體作戰的時候,這點基本不影響審美);對內存占用有一點消耗(其實也還好)
再說明方案思路:
為了省掉骨骼動畫計算這一環節,就不能用現有骨骼動畫系統。那么可選的方案也只有頂點變形動畫了,而傳統的頂點動畫不管是內存占用還是不低的,而且unity支持也不算好。
那么既然只能選頂點動畫且為了降低內存消耗,那么可以通過改寫shader,通過gpu來插值實現頂點動畫播放骨骼動畫的效果。
shader插值的方案確實也不少,但結合具體實現上來,我選了兩種實現相對簡單的方案:
1.vertex shader里通過每幀采樣的uv偏移采樣紋理來控制頂點的位置,當我已經在pc端實現的時候才突然發現一個顯而易見的問題:在vertex shader里采樣紋理,pc端部分顯卡是支持的,但手機gpu就不要想了。
2.於是只能第二種方案:還是vextex shader里插值頂點來播放動畫,那么就有一個問題,vertex data從哪里來? unity的mesh結構有很多通道,頂點,顏色,uv,uv2,法線,切線。。這些通道里面除了color(主要是精度問題)和uv(還是必須給紋理坐標一個位置的)通道以外我都可以用來存頂點數據,然后通過控制時間點來組合頂點進行插值,然而還有個問題需要解決,這種方式一個mesh只能插值4個關鍵頂點,那么較長的動畫怎么辦呢,可以通過提前生成多個mesh,來切換。
動畫截取可以通過unity的BakeMesh函數或者美工來幫助實現,下面是組合mesh 的代碼:
byte[] Make(Mesh mesh1, Mesh mesh2, Mesh mesh3, Mesh mesh4, float clipTimeLenghts, float frame2Pos, float frame3Pos) { Mesh[] meshs = new Mesh[] { mesh1, mesh2, mesh3, mesh4}; VertexAnimationResManager.ClipMeshData meshData = new VertexAnimationResManager.ClipMeshData(); meshData.subMeshCount = meshs[0].subMeshCount; int count = meshs[0].vertices.Length; //頂點 if (meshs[0].vertices != null && meshs[0].vertices.Length > 0) { meshData.vertexBuffer = new float[count * 3]; for (int i = 0; i < meshs[0].vertices.Length; i++) { meshData.vertexBuffer[i * 3] = meshs[0].vertices[i].x; meshData.vertexBuffer[i * 3 + 1] = meshs[0].vertices[i].y; meshData.vertexBuffer[i * 3 + 2] = meshs[0].vertices[i].z; } } //uv if (meshs[0].uv != null && meshs[0].uv.Length > 0) { meshData.uvBuffer = new float[count * 2]; for (int i = 0; i < meshs[0].vertices.Length; i++) { meshData.uvBuffer[i * 2] = meshs[0].uv[i].x; meshData.uvBuffer[i * 2 + 1] = meshs[0].uv[i].y; } //GCHandle verSrcHand = GCHandle.Alloc(meshs[0].uv, GCHandleType.Pinned); //Marshal.Copy(verSrcHand.AddrOfPinnedObject(), meshData.uvBuffer, 0, meshData.uvBuffer.Length); //verSrcHand.Free(); } //法線 這里用來存動畫第二幀的頂點信息 if (meshs[1].vertices != null && meshs[1].vertices.Length > 0) { meshData.normalBuffer = new float[count * 3]; for (int i = 0; i < meshs[0].vertices.Length; i++) { meshData.normalBuffer[i * 3] = meshs[1].vertices[i].x; meshData.normalBuffer[i * 3 + 1] = meshs[1].vertices[i].y; meshData.normalBuffer[i * 3 + 2] = meshs[1].vertices[i].z; } } //切線 這里用來存動畫第三幀的頂點信息 if (meshs[2].vertices != null && meshs[2].vertices.Length > 0) { meshData.tangentBuffer = new float[count * 4]; for (int i = 0; i < meshs[0].vertices.Length; i++) { meshData.tangentBuffer[i * 4] = meshs[2].vertices[i].x; meshData.tangentBuffer[i * 4 + 1] = meshs[2].vertices[i].y; meshData.tangentBuffer[i * 4 + 2] = meshs[2].vertices[i].z; meshData.tangentBuffer[i * 4 + 3] = meshs[3].vertices[i].x; } } //UV2 用來存第四個關鍵幀率的 頂點YZ 坐標 X坐標由切線的W通道來存 if (meshs[3].vertices != null && meshs[3].vertices.Length > 0) { meshData.uv2Buffer = new float[count * 2]; for (int i = 0; i < meshs[0].vertices.Length; i++) { meshData.uv2Buffer[i * 2] = meshs[3].vertices[i].y; meshData.uv2Buffer[i * 2 + 1] = meshs[3].vertices[i].z; } } //顏色 用來存第5個頂點信息 //if (meshs[Indexs[4]].vertices != null && meshs[Indexs[4]].vertices.Length > 0) //{ // //顏色通道貌似沒有負數,且范圍為0到1 所有這里需要將模型頂點映射到[0,1]之間,映射范圍為[-1,1]之間 // meshData.colorBuffer = new float[count * 4]; // for (int i = 0; i < meshs[Indexs[4]].vertices.Length; i++) // { // meshData.colorBuffer[i * 4] = (meshs[Indexs[4]].vertices[i].x * 0.5f) + 0.5f; // meshData.colorBuffer[i * 4 + 1] = (meshs[Indexs[4]].vertices[i].y * 0.5f) + 0.5f; // meshData.colorBuffer[i * 4 + 2] = (meshs[Indexs[4]].vertices[i].z * 0.5f) + 0.5f; // } //} count = 0; int len = 0; meshData.subMeshTriangleLens = new int[meshData.subMeshCount]; for (int i = 0; i < meshData.subMeshCount; i++) { len = meshs[0].GetTriangles(i).Length; count += len; meshData.subMeshTriangleLens[i] = len; } meshData.triangleBuffer = new int[count]; len = 0; for (int i = 0; i < meshData.subMeshCount; i++) { meshs[0].GetTriangles(i).CopyTo(meshData.triangleBuffer, len); len += meshData.subMeshTriangleLens[i]; } ByteBuffer bbuffer = new ByteBuffer(); bbuffer.WriteFloat(clipTimeLenghts); bbuffer.WriteFloat(frame2Pos); bbuffer.WriteFloat(frame3Pos); bbuffer.WriteInt(meshs[0].subMeshCount); for(int i=0;i<meshData.subMeshTriangleLens.Length;i++ ) { bbuffer.WriteInt(meshData.subMeshTriangleLens[i]); } bbuffer.WriteInt(meshData.triangleBuffer.Length); for (int i = 0; i < meshData.triangleBuffer.Length; i++) { bbuffer.WriteInt(meshData.triangleBuffer[i]); } bbuffer.WriteInt(meshData.vertexBuffer.Length); for (int i = 0; i < meshData.vertexBuffer.Length; i++) { bbuffer.WriteFloat(meshData.vertexBuffer[i]); } bbuffer.WriteInt(meshData.normalBuffer.Length); for (int i = 0; i < meshData.normalBuffer.Length; i++) { bbuffer.WriteFloat(meshData.normalBuffer[i]); } bbuffer.WriteInt(meshData.tangentBuffer.Length); for (int i = 0; i < meshData.tangentBuffer.Length; i++) { bbuffer.WriteFloat(meshData.tangentBuffer[i]); } bbuffer.WriteInt(meshData.uvBuffer.Length); for (int i = 0; i < meshData.uvBuffer.Length; i++) { bbuffer.WriteFloat(meshData.uvBuffer[i]); } bbuffer.WriteInt(meshData.uv2Buffer.Length); for (int i = 0; i < meshData.uv2Buffer.Length; i++) { bbuffer.WriteFloat(meshData.uv2Buffer[i]); } return bbuffer.ToBytes(); }
截取好后,保存為自己的二進制文件,運行的時候加載並 解析的代碼如下:
public void AddAnimationInfo(string aniName, byte[] clipData) { VertexAnimationClipInfo clipInfo = null; AnimationClipInfos.TryGetValue(aniName, out clipInfo); if(clipInfo!=null) { Debug.LogError("animation clip has exits!"); return; } clipInfo = new VertexAnimationClipInfo(); ByteBuffer bbuffer = new ByteBuffer(clipData); int Count = bbuffer.ReadInt(); for (int i = 0; i < Count; i++) { ClipMeshData meshData = GetMeshData(bbuffer); clipInfo.clipTotalTimeLen += meshData.timeLenth; clipInfo.clipLenghts.Add(meshData.timeLenth); clipInfo.everyClipFrameTimePoints.Add(new Vector3(meshData.Frame2TimePoint, meshData.Frame3TimePoint)); //,meshData.Frame4TimePoint clipInfo.clipMeshs.Add(meshData.GenMesh()); } bbuffer.Close(); AnimationClipInfos.Add(aniName, clipInfo); }
VertexAnimationClipInfo定義如下:
[Serializable] public class VertexAnimationClipInfo { public float clipTotalTimeLen = 0; public List<Mesh> clipMeshs = new List<Mesh>(); public List<Vector2> everyClipFrameTimePoints = new List<Vector2>(); public List<float> clipLenghts = new List<float>(); }
ClipMeshData定義如下:
public class ClipMeshData { public float timeLenth; ///Frame1TimePoint =0 Frame4TimePoint = 1 public float Frame2TimePoint = 0.333f; public float Frame3TimePoint = 0.666f; //public float Frame4TimePoint = 0.75f; public int subMeshCount; public int[] subMeshTriangleLens; public int[] triangleBuffer; public float[] vertexBuffer; public float[] normalBuffer; public float[] tangentBuffer; public float[] uvBuffer; public float[] uv2Buffer; //public float[] colorBuffer; public Mesh GenMesh() { Mesh mesh = new Mesh(); int vertexCount = vertexBuffer.Length / 3; mesh.subMeshCount = subMeshCount; //頂點 Vector3[] vertexs = new Vector3[vertexCount]; for (int i = 0; i < vertexCount; i++) { vertexs[i] = new Vector3(vertexBuffer[i * 3], vertexBuffer[i * 3 + 1], vertexBuffer[i * 3 + 2]); } mesh.vertices = vertexs; //uv Vector2[] uv = new Vector2[vertexCount]; for (int i = 0; i < uv.Length; i++) { uv[i] = new Vector2(uvBuffer[i * 2], uvBuffer[i * 2 + 1]); } mesh.uv = uv; //uv2 Vector2[] uv2 = new Vector2[vertexCount]; for (int i = 0; i < uv.Length; i++) { uv2[i] = new Vector2(uv2Buffer[i * 2], uv2Buffer[i * 2 + 1]); } mesh.uv2 = uv2; //法線 Vector3[] normals = new Vector3[vertexCount]; for (int i = 0; i < normals.Length; i++) { normals[i] = new Vector3(normalBuffer[i * 3], normalBuffer[i * 3 + 1], normalBuffer[i * 3 + 2]); } mesh.normals = normals; //切線 var tangents = new Vector4[vertexCount]; for (int i = 0; i < tangents.Length; i++) { tangents[i] = new Vector4(tangentBuffer[i * 4], tangentBuffer[i * 4 + 1], tangentBuffer[i * 4 + 2], tangentBuffer[i * 4 + 3]); } mesh.tangents = tangents; ////顏色 //Color[] colors = new Color[colorBuffer.Length / 4]; //for (int i = 0; i < colors.Length; i++) //{ // colors[i] = new Vector4(colorBuffer[i * 4], colorBuffer[i * 4 + 1], colorBuffer[i * 4 + 2], 1); //} //mesh.colors = colors; //三角形 int startIndex = 0; int bufferLen = 0; for (int i = 0; i < subMeshCount; i++) { bufferLen = subMeshTriangleLens[i]; if (bufferLen <= 0) continue; var triIndexBuffer = new int[bufferLen]; Array.Copy(triangleBuffer, startIndex, triIndexBuffer, 0, bufferLen); mesh.SetTriangles(triIndexBuffer, i); startIndex += bufferLen; } return mesh; } }
播放動畫切換對應的mesh就可以了,下面是gpu插值所用的shader代碼:
Shader "LXZ_TEST/VertexAnimation-NoColorBuf" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _CurTime("Time", Float) = 0 _Frame2Time("Frame2Time", Float) = 0.333 _Frame3Time("Frame3Time", Float) = 0.666 _Color ("MainColor", color) = (1,1,1,1) } SubShader { // Tags { "QUEUE"="Geometry" "RenderType"="Opaque" } Pass { Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag // #include "UnityCG.cginc" #pragma glsl_no_auto_normalization sampler2D _MainTex; float _CurTime; float _Frame2Time; float _Frame3Time; float4 _Color; struct appdata { float4 vertex : POSITION; float3 vertex1 : NORMAL; float4 vertex2 : TANGENT; float2 texcoord : TEXCOORD0; float2 vertex3: TEXCOORD1; //float3 vertex4: COLOR; }; struct v2f { float4 pos : POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata v) { v2f result; float a = _CurTime - _Frame2Time; float b = _CurTime - _Frame3Time; float3 vec; float3 vertex3 = float3(v.vertex2.w,v.vertex3.xy); if(a<0) vec = v.vertex.xyz + (v.vertex1 - v.vertex.xyz)* _CurTime/_Frame2Time; else if(a>=0 && b<0) { vec = v.vertex1 + (v.vertex2.xyz - v.vertex1)* a/(_Frame3Time-_Frame2Time); } else vec = v.vertex2.xyz + (vertex3 - v.vertex2.xyz)* b/(1-_Frame3Time); result.pos = mul(UNITY_MATRIX_MVP, float4(vec,1)); //result.pos = mul(UNITY_MATRIX_MVP, float4(v.vertex1.xyz,1)); //result.pos = mul(UNITY_MATRIX_MVP, float4(v.vertex2.xyz,1)); // result.pos = mul(UNITY_MATRIX_MVP, float4(vertex3.xyz,1)); // result.pos = mul(UNITY_MATRIX_MVP, float4(vertex4.xyz,1)); result.uv = v.texcoord; return result; } float4 frag(v2f i) : COLOR { float4 color = tex2D(_MainTex, i.uv); return color *_Color; } ENDCG } } FallBack "Diffuse" }
如果各位還有更好的方案,歡迎交流。。不擅長文字的東西,所以直接貼代碼了,有需要可以與我連續交流。。。