Unity最近推出了ECS。 在学习过程中,我对如何使动画和ECS成为朋友很感兴趣。 在搜索过程中,我遇到了一种有趣的技术,来自NORDVEUS的家伙在他们的Unite Austin 2017报告演示中使用了该技术。
团结奥斯汀2017年-法术世界的大规模战斗。
该报告包含许多有趣的解决方案,但是今天我们将讨论骨骼动画在纹理中的保存,以期进一步应用。
你问为什么会有这样的困难?
来自NORDVEUS的家伙同时在屏幕上绘制了大量相同类型的动画对象:骨骼,剑客。 在使用传统方法的情况下: SkinnedMeshRenderers和Animation \ Animator将增加绘制调用,并增加CPU渲染动画的负担。 为了解决这些问题,将动画移到了GPU的一侧,或者移到了顶点着色器。
我对这种方法非常感兴趣,因此决定更详细地解决它,并且由于我没有找到有关此主题的文章,因此进入了代码。 在研究这个问题的过程中,这篇文章诞生了,而我解决这个问题的愿景也由此诞生。
因此,让我们将大象切成碎片:
- 从剪辑中获取动画关键点
- 将数据保存到纹理
- 网格(网格)准备
- 着色器
- 全部放在一起
从动画剪辑中获取动画关键点
从SkinnedMeshRenderers的组件中, 我们获得了一组骨骼和一个网格。 动画组件提供了可用动画的列表。 因此,对于每个剪辑,我们必须为网格的所有骨骼逐帧保存变换矩阵。 换句话说,我们保持角色单位时间的姿势。
我们选择一个二维数组,其中将存储数据。 其一维具有帧数乘以剪辑长度(以秒为单位)。 另一个是网格中的骨骼总数:
var boneMatrices = new Matrix4x4[Mathf.CeilToInt(frameRate * clip.length), renderer.bones.Length];
在下面的示例中,我们一一更改剪辑的帧并保存矩阵:
矩阵是4×4,但最后一行始终看起来像(0,0,0,1)。 因此,出于略微优化的目的,可以跳过它。 反过来,这将减少处理器和视频卡之间的数据传输成本。
a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 0 0 0 1
将数据保存到纹理
要计算纹理的大小,我们将所有动画剪辑中的帧总数乘以骨骼数和矩阵中的行数(我们同意保存前3行)。
var dataSize = numberOfBones * numberOfKeyFrames * MATRIX_ROWS_COUNT);
我们将数据写入纹理。 对于每个剪辑,我们逐帧保存转换矩阵。 数据格式如下。 剪辑被一个接一个地记录,并由一组帧组成。 依次由一组骨骼组成。 每个骨骼包含3行矩阵。
Clip0[Frame0[Bone0[row0,row1,row2]...BoneN[row0,row1,row2].]...FramM[bone0[row0,row1,row2]...ClipK[...]
以下是保存代码:
var textureColor = new Color[texture.width * texture.height]; var clipOffset = 0; for (var clipIndex = 0; clipIndex < sampledBoneMatrices.Count; clipIndex++) { var framesCount = sampledBoneMatrices[clipIndex].GetLength(0); for (var keyframeIndex = 0; keyframeIndex < framesCount; keyframeIndex++) { var frameOffset = keyframeIndex * numberOfBones * 3; for (var boneIndex = 0; boneIndex < numberOfBones; boneIndex++) { var index = clipOffset + frameOffset + boneIndex * 3; var matrix = sampledBoneMatrices[clipIndex][keyframeIndex, boneIndex]; textureColor[index + 0] = matrix.GetRow(0); textureColor[index + 1] = matrix.GetRow(1); textureColor[index + 2] = matrix.GetRow(2); } } } texture.SetPixels(textureColor); texture.Apply(false, false);
网格(网格)准备
添加一组额外的纹理坐标,对于每个顶点,我们将保存相关的骨骼索引以及骨骼对该顶点的影响权重。
Unity提供了一种数据结构,其中一个顶点最多可以包含4个骨骼。 下面是将数据写入uv的代码。 我们将骨骼索引保存在UV1中,将权重保存在UV2中。
var boneWeights = mesh.boneWeights; var boneIds = new List<Vector4>(mesh.vertexCount); var boneInfluences = new List<Vector4>(mesh.vertexCount); for (var i = 0; i < mesh.vertexCount; i++) { boneIds.Add(new Vector4(bw.boneIndex0, bw.boneIndex1, bw.boneIndex2, bw.boneIndex3); boneInfluences.Add(new Vector4(bw.weight0, bw.weight1, bw.weight2, bw.weight3)); } mesh.SetUVs(1, boneIds); mesh.SetUVs(2, boneInfluences);
着色器
着色器的主要任务是找到与顶点关联的骨骼的转换矩阵,并将顶点的坐标乘以该矩阵。 为此,我们需要一组带有索引和骨骼权重的坐标。 我们还需要当前帧的索引,它会随着时间变化并从CPU传输出去。
因此,我们获得了矩阵第一行的索引,第二行和第三行的索引分别为+ 1,+ 2。 仍然需要将一维索引转换为纹理的规范化坐标,为此,我们需要纹理的大小。
inline float4 IndexToUV(int index, float2 size) { return float4(((float)((int)(index % size.x)) + 0.5) / size.x, ((float)((int)(index / size.x)) + 0.5) / size.y, 0, 0); }
减去各行后,我们收集矩阵而不会忘记最后一行,该行始终等于(0,0,0,1)。
float4 row0 = tex2Dlod(frameOffset, IndexToUV(index + 0, animationTextureSize)); float4 row1 = tex2Dlod(frameOffset, IndexToUV(index + 1, animationTextureSize)); float4 row2 = tex2Dlod(frameOffset, IndexToUV(index + 2, animationTextureSize)); float4 row3 = float4(0, 0, 0, 1); return float4x4(row0, row1, row2, row3);
同时,多个骨骼可以一次影响一个顶点。 所得矩阵将是影响顶点的所有矩阵的总和乘以其影响权重。
float4x4 m0 = CreateMatrix(frameOffset, bones.x) * boneInfluences.x; float4x4 m1 = CreateMatrix(frameOffset, bones.y) * boneInfluences.y; float4x4 m2 = CreateMatrix(frameOffset, bones.z) * boneInfluences.z; float4x4 m3 = CreateMatrix(frameOffset, bones.w) * boneInfluences.w; return m0 + m1 + m2 + m3;
收到矩阵后,我们将其乘以顶点的坐标。 因此,所有顶点都将移动到角色的姿势,该姿势与当前帧相对应。 更改框架,我们将对角色进行动画处理。
全部放在一起
为了显示对象,我们将使用Graphics.DrawMeshInstancedIndirect,将准备好的网格和材质转移到其中。 同样,在材质中,对于当前每个对象,我们必须将带有动画的纹理传递给纹理大小,并传递一个带有指向框架的指针的数组。 作为附加信息,我们传递每个对象的位置和旋转。 在[article]中可以找到如何更改着色器侧的位置和旋转。
在Update方法中,增加从Time.deltaTime上的动画开始经过的时间。
为了计算帧索引,我们必须通过将时间除以剪辑的长度来归一化时间。 因此,剪辑中的帧索引将是归一化时间乘以帧数的乘积。 纹理中的帧索引将是当前剪辑的开始位置偏移量与当前帧乘以该帧中存储的数据量的乘积之和。
var offset = clipStart + frameIndex * bonesCount * 3.0f
可能所有这些数据都已传递给着色器,我们使用准备好的网格物体和材质将其称为Graphics.DrawMeshInstancedIndirect。
结论
在带有1050图形卡的计算机上测试该技术后,性能提高了大约2倍。

在CPU上对4000个相同类型的对象进行动画处理

GPU上8000个相同类型的对象的动画
同时,在具有集成显卡的macbook pro 15上测试此场景时,结果相反。 GPU无耻地丢失(大约2-3次),这并不奇怪。
视频卡上的动画是可以在您的应用程序中使用的另一种工具。 但是,像所有工具一样,应该明智地使用它,并且不恰当。
参考文献
[GitHub项目代码]谢谢您的关注。
PS:我是Unity的新手,不知道所有的细节,本文可能有误。 我希望在您的帮助下修复它们,并更好地理解该主题。