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左右):

gpu动画实现方式_第1张图片

带阴影的60个骨骼动画(fps也基本在25左右)

gpu动画实现方式_第2张图片

然后我们看看gpu动画:

不带阴影的60个gpu动画(fps在46左右)

gpu动画实现方式_第3张图片

带阴影的60个gpu动画(fps在39左右)

gpu动画实现方式_第4张图片

整体来看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出来的,信息不一样。所以没办法静态合并之类的。

 

附上流程图

gpu动画实现方式_第5张图片

你可能感兴趣的:(gpuinstance,gpu动画,顶点偏移)