gpu動畫實現方式

一般我們要做動畫有好幾種實現方式

第一種是骨骼動畫:

直接用animator或animation控制帶蒙皮的角色來控制骨骼。

骨骼動畫的原理:

首先你需要有一個模型,2D或者3D的,這些模型是由頂點組成的,2d模型的頂點就是一個個四邊形的四個頂點,3D模型的頂點就是每個Mesh網格的三角面頂點。 
然後,你需要搭建一套骨骼,這些骨骼是樹形結構的,也就是有父子連接關係的,父級骨骼在做運動的時候,子級骨骼是跟隨父級骨骼運動,在這個基礎上然後子級也可以自己運動而不影響父級骨骼。 
接下來,你需要把模型的頂點和骨骼做一個對應關係,這就是所謂的蒙皮權重。蒙皮要做的事情,是指定某個頂點受到多少根骨骼的影響,然後在骨骼運動的時候,頂點根據權重的百分比來跟隨骨骼運動。比如一個頂點是受到了2跟骨骼的影響,第一根骨骼的權重是30%,第二根骨骼的權重是70%。在兩根骨骼同時移動的時候,第一根骨骼向左移動了10米,第二根骨骼向右移動了10米,假若向右是正方向,那麼這個頂點實際移動的位置應該就是-10*0.3+10*0.7 = 4,也就是向右移動了4米。在實際的計算中,我們不會這麼簡單的乘以百分比,是會用矩陣來運算,分別算出正常受到每一根骨骼矩陣影響之後該頂點的最終座標,然後再乘以百分比相加。 
最後再來說說骨骼父子關係。每一個子級的骨骼,需要先獲取到它的父級,通過矩陣來轉換局部座標系,算出子級相對父級的局部位移旋轉縮放,再將座標系轉換到世界座標系,得到子級相對於父級的位移旋轉縮放在世界座標的實際位置,得到最終在動畫中這根子骨骼的實際座標。如果一個角色的骨骼數量越多,嵌套的父子關係越複雜,那麼這個轉換座標系計算的過程就越複雜,消耗的cpu運算就越多。

第二種是序列幀的方法:

這種方法非常簡單,只需要美術導出動畫中的每一幀的圖片,然後客戶端每幀切換圖片就好了。

這種方式基本沒有計算量,但問題是他不能接受真實光照等真實信息,只能是當前顯示效果。

第三種是cpu頂點動畫:

也就是cpu每幀改變頂點的信息來讓他顯示效果。但這樣也會產生cpu的效果,只是減少了一些空間轉換等計算。

第四種是gpu動畫:

也就是我這章主要說的內容:

首先看看效果:

不帶陰影的骨骼動畫,有60個(fps在25左右):

帶陰影的60個骨骼動畫(fps也基本在25左右)

然後我們看看gpu動畫:

不帶陰影的60個gpu動畫(fps在46左右)

帶陰影的60個gpu動畫(fps在39左右)

整體來看gpu動畫還是比骨骼動畫會快不少的。

 

那麼我們來了解下gpu動畫的實現原理:

1。首先需要把可以切換的動畫確定好,或者動態確定。

2.對角色身上的skinnedmesh創建新的mesh,並確定頂點信息,uv2和uv3信息傳入(這裏需要注意的是要把接收的對象放到TEXCOORD1和TEXCOORD2中,對應的就是uv2和uv3.不然相關的骨骼索引和骨骼權重信息對應不上)

newMesh.vertices = vertices;
newMesh.uv2 = boneIds;
newMesh.uv3 = boneInfluences;

頂點信息就還是原來的頂點,

uv2是用當前骨骼所對應的頂點的索引0和索引1的一半跟總骨骼數量相除得到他的區間

uv3是用當前骨骼的權重0和權重1,跟權重0和權重1的總和相除得到的區間

相當於說是存儲蒙皮信息的頂點索引的值和蒙皮骨骼的權重均值

boneIds[i] = new Vector2((boneIndex0 + 0.5f) / bones.Length, (boneIndex1 + 0.5f) / bones.Length);

boneInfluences[i] = new Vector2(boneWeights[i].weight0 / mostInfluentialBonesWeight, boneWeights[i].weight1 / mostInfluentialBonesWeight);

然後再採樣出每個動作每幀的動作信息,然後轉換到世界座標中,得到這個骨骼動畫當前的世界座標的矩陣。把這些矩陣信息存放在三張貼圖裏,三張貼圖對應矩陣的row0,row1和row2信息,這裏的row0,row1,row2是4x3的矩陣,已經包含了縮放旋轉位移信息了(因爲GetRow(0),GetRow(2),GetRow(2)剛好對應的是矩陣的0到1的vector4,所以和顏色空間剛好對應)

int runningTotalNumberOfKeyframes = 0;
            for (int i = 0; i < sampledBoneMatrices.Count; i++)
            {
                for (int boneIndex = 0; boneIndex < sampledBoneMatrices[i].GetLength(1); boneIndex++)
                {
                    for (int keyframeIndex = 0; keyframeIndex < sampledBoneMatrices[i].GetLength(0); keyframeIndex++
                        int index = Get1DCoord(runningTotalNumberOfKeyframes + keyframeIndex, boneIndex, tex0.width);

                        texture0Color[index] = sampledBoneMatrices[i][keyframeIndex, boneIndex].GetRow(0);
                        texture1Color[index] = sampledBoneMatrices[i][keyframeIndex, boneIndex].GetRow(1);
                        texture2Color[index] = sampledBoneMatrices[i][keyframeIndex, boneIndex].GetRow(2);
                    }
                }

在shader中採樣相應的row0,row1,row2信息來獲得當前動作幀的骨骼矩陣信息,這裏的boneIds和boneInfluences就是再cpu中傳過來的uv2和uv3的信息,對應shader就是

#define UNITY_VERTEX_INPUT_GPUANIMATION float2 boneIds  : TEXCOORD1; float2 boneInfluences : TEXCOORD2; 

然後傳進去

VertexPositionInputs2 vertexInput = GetVertexPositionInputs_GPUAnimation(v.vertex.xyz, v.boneIds, v.boneInfluences);

inline float4x4 CreateMatrix(float texturePosition, float boneId)
{
	float4 row0 = tex2Dlod(_AnimationTexture0, float4(texturePosition, boneId, 0, 0));
	float4 row1 = tex2Dlod(_AnimationTexture1, float4(texturePosition, boneId, 0, 0));
	float4 row2 = tex2Dlod(_AnimationTexture2, float4(texturePosition, boneId, 0, 0));

	float4x4 reconstructedMatrix = float4x4(row0, row1, row2, float4(0, 0, 0, 1));

	return reconstructedMatrix;
}

inline float4x4 CalculateSkinMatrix(float4 animationTextureCoords, float2 boneIds, float2 boneInfluences)
{
	// We interpolate between two matrices
	float4x4 frame0_BoneMatrix0 = CreateMatrix(animationTextureCoords.x, boneIds.x);
	float4x4 frame0_BoneMatrix1 = CreateMatrix(animationTextureCoords.y, boneIds.x);
	float4x4 frame0_BoneMatrix = frame0_BoneMatrix0 * (1 - animationTextureCoords.z) + frame0_BoneMatrix1 * animationTextureCoords.z;

	float4x4 frame1_BoneMatrix0 = CreateMatrix(animationTextureCoords.x, boneIds.y);
	float4x4 frame1_BoneMatrix1= CreateMatrix(animationTextureCoords.y, boneIds.y);
	float4x4 frame1_BoneMatrix = frame1_BoneMatrix0 * (1 - animationTextureCoords.z) + frame1_BoneMatrix1 * animationTextureCoords.z;

	return frame0_BoneMatrix * boneInfluences.x + frame1_BoneMatrix * boneInfluences.y;
}

上面的animationTextureCoords是cpu中傳過來的之前記錄下來的每幀每個蒙皮的綁定信息,這個是當前幀傳過來的具體的骨骼蒙皮的綁定信息。主要算法是通過cpu中使用bindpose下的頂點來計算當前頂點。最終頂點 = 骨骼矩陣*權重偏移量

其中tex2Dlod是採樣指定lod下的信息,如果我們本身帶lod信息的話就可以根據不同的lod採樣了。

for (int j = 0; j < bones.Length; j++)
                    boneMatrices[i, j] = bones[j].localToWorldMatrix * bindPoses[j];

採樣記錄下來的矩陣然後做插值得到骨骼矩陣的均值。最後用我們傳進來的boneweight(在這裏是boneInfluences來確定最後的矩陣)

注意:一個動作會根據有多少幀而開啓多少個像素。如果有n個動作,則會有n*每個動作的長度,組成圖片。注意看下面的numberOfKeyFrames

            int numberOfKeyFrames = 0;

            for (int i = 0; i < animationClips.Length; i++)
            {
                var sampledMatrix = SampleAnimationClip(instance, animationClips[i], skinRenderer, bakedData.Framerate);
                sampledBoneMatrices.Add(sampledMatrix);

                numberOfKeyFrames += sampledMatrix.GetLength(0);
            }

            int numberOfBones = sampledBoneMatrices[0].GetLength(1);

            var tex0 = bakedData.AnimationTextures.Animation0 = new Texture2D(numberOfKeyFrames, numberOfBones, TextureFormat.RGBAFloat, false);
            tex0.wrapMode = TextureWrapMode.Clamp;
            tex0.filterMode = FilterMode.Point;
            tex0.anisoLevel = 0;

如果有多個動作就需要確定動作的起始像素和終止像素了,注意下面的PixelStart,PixelEnd

                AnimationClipData clipData = new AnimationClipData
                {
                    Clip = animationClips[i],
                    PixelStart = runningTotalNumberOfKeyframes + 1,
                    PixelEnd = runningTotalNumberOfKeyframes + sampledBoneMatrices[i].GetLength(0) - 1
                };

 

 

3.每幀需要做動作改變,但是因爲我們是gpu動畫,所以是需要把相關的轉換後的世界座標矩陣和動作播放的時間的歸一化數據傳給每個不同的材質去做。

首先世界座標就是角色的世界座標了。

歸一化數據就是根據動作播放的時間轉換爲:轉換爲歸一化的時間後跟圖片的位置信息運算得到偏移,主要想得到的是這個角色要採樣圖像的uv位置。

4.最後最關鍵的就是要把圖給畫出來,一般可以用

Graphics.DrawMeshInstancedIndirect,Graphics.DrawMesh或Graphics.DrawMeshInstanced。但如果是一個mesh多個材質的話只能用Graphics.DrawMeshInstancedIndirect,Graphics.DrawMesh。

5.shader中就是根據傳進來的數據用於轉到屏幕座標顯示,這裏需要注意如果需要陰影,shadow一樣也要處理轉換到屏幕座標的流程。

轉換到屏幕座標的流程是:

(1)根據圖像信息和uv2,uv3(也就是對應的TEXCOORD1; TEXCOORD2)來得到模型當前動作當前幀的座標的矩陣,再跟角色自身的世界座標矩陣相乘得到世界座標。得到的這個世界座標再跟當前的真實模型座標轉換,就得到了當前動作再世界座標中的頂點信息了。最後再轉正常乘屏幕矩陣得到屏幕座標。

float4x4 skinMatrix = CalculateSkinMatrix(textureCoordinatesBuffer, boneIds, boneInfluences);
    input.positionWS = TransformObjectToWorld_CustomMatrix(mul(objectToWorldBuffer, skinMatrix), positionOS);

input.positionVS = TransformWorldToView2(input.positionWS);
input.positionCS = TransformWorldToHClip2(input.positionWS);

之後的運算就是正常的光照運算了。就不多說了。但是要注意的時法線也需要計算

VertexNormalInputs2 normalInput = GetVertexNormalInputs_GPUAnimation(v.normal, v.tangent, v.boneIds, v.boneInfluences);

還有渲染的layer設置爲相關要顯示的layer

Graphics.DrawMesh(mesh, pos, mDefaultQuaternion, material, mRTModelLayer);

但要注意的是如果是多材質的渲染,需要添加兩個參數,一個攝像機的參數,一個是子材質的索引。並且要保證我們的mesh是包括多材質的mesh

Graphics.DrawMesh(mesh, pos, mDefaultQuaternion, material, mRTModelLayer, Camera.main, subMeshIndex);

另外還有一點就是這裏給的pos和mDefaultQuaternion也就是位置和旋轉要注意,如果設置和當前顯示的位置和旋轉不一致,可能會顯示不出陰影來(因爲你所處的位置被改變了)

 

 

當然這個方法主要用到了gpuinstance的特性,所以會合並部分dc,但是因爲我們每個材質都是重新new出來的,信息不一樣。所以沒辦法靜態合併之類的。

 

附上流程圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章