Unity 使用 ECS With Burst 来再次加速 GPUSkinning

一般来说使用GpuSkinning 已经能得到很不错的性能了,那么能不能再快一点呢?
答案当然是肯定的,这一次我们来使用ECS榨干CPU的部分

先上性能对比图
1万个蒙皮角色,每个角色472面,带有uv0,uv1
测试设备硬件 win10, Intel i7-7700, GPU GTX-1060 6G

可以看到Entity的帧数在 110帧以上, 而传统GPUSkinning 的帧数在 29帧

这个Demo使用的GPU蒙皮方案为 将骨骼矩阵数据以双四元数的方式存储在纹理上,具体实现方法不是这个Demo的重点,大家也可以参考这篇文章

GPU Skinning 加速骨骼动画
https://github.com/chengkehan/GPUSkinning

接下来一步一步开始分解这个Demo
首先实现Shader Include
Skinning.hlsl

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
#ifndef __AOI_GPUSKINNING
#define __AOI_GPUSKINNING

TEXTURE2D(_AnimTex);
SAMPLER(sampler_AnimTex);

inline float2 BoneIndexToTexUV(float index, float4 param) {
    int row = (int)(index / param.y);
    int col = index % param.x;
    return float2(col * param.w, row * param.w);
}

inline float3 QuatMulPos(float4 rotation, float3 rhs)
{
    float3 qVec = half3(rotation.xyz);
    float3 c1 = cross(qVec, rhs);
    float3 c2 = cross(qVec, c1);

    return rhs + 2 * (c1 * rotation.w + c2);
}

inline float3 QuatMulPos(float4 real, float4 dual, float4 rhs) {
    return dual.xyz * rhs.w + QuatMulPos(real, rhs.xyz);
}

inline float4 DQTexSkinning(float4 vertex, float4 texcoord, float4 startData, Texture2D<float4> animTex, SamplerState animTexSample) {

    int index1 = startData.z + texcoord.x;
    float4 boneDataReal1 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index1, startData), 0);
    float4 boneDataDual1 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index1 + 1, startData), 0);
    float4 real1 = boneDataReal1.rgba;
    float4 dual1 = boneDataDual1.rgba;

    int index2 = startData.z + texcoord.z;
    float4 boneDataReal2 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index2, startData), 0);
    float4 boneDataDual2 = SAMPLE_TEXTURE2D_LOD(animTex, animTexSample, BoneIndexToTexUV(index2 + 1, startData), 0);
    float4 real2 = boneDataReal2.rgba;
    float4 dual2 = boneDataDual2.rgba;

    float3 position = (dual1.xyz * vertex.w) + QuatMulPos(real1, vertex.xyz);
    float4 t0 = float4(position, vertex.w);

    position = (dual2.xyz * vertex.w) + QuatMulPos(real2, vertex.xyz);
    float4 t1 = float4(position, vertex.w);

    return t0 * texcoord.y + t1 * texcoord.w;
}

inline void SkinningTex_float(float4 positionOS, float4 texcoord, float4 frameData, Texture2D<float4> animTex, SamplerState animTexSample, out float4 output) {
    output = float4(DQTexSkinning(positionOS, texcoord, frameData, animTex, animTexSample).xyz,1);
}

#endif

这个Shader中,路口函数为SkinningTex_float 需要传入 模型空间原始顶点,uv1(存储的顶点对应的受影响的骨骼ID和对应权重), FrameData当前动画的帧数所在的动作纹理中的像素偏移值信息,animTex蒙皮动作数据纹理,采样器,output蒙皮后输出到模型空间顶点

Include 写完后,可以正式开始编写Shader了,使用的是 HDRP 的ShaderGraph -> LitMaster

左侧 Properties 准备参数
_BaseMap 为角色Diffuse 贴图
_AnimTex 为角色蒙皮动画数据贴图
_FrameData 为当前动画的帧数所在的动作纹理中的像素偏移值信息
_ECS_FrameData 为ECS模式下当前动画的帧数所在的动作纹理中的像素偏移值信息,区别在于勾选了 Hybird Instanced
_ECS_ON 在ECS模式下激活的宏


CustomFunction 展开的结构如图
该节点就是讲之前编写的 Skinning.hlsl 以节点的方式添加到ShaderGraph 中使用
分别是之前介绍的 4个 输入,和一个输出
SkinningTex_float(float4 positionOS, float4 texcoord, float4 frameData, Texture2D animTex, SamplerState animTexSample, out float4 output)


使用 KeyWordNode 来switch ECS 和 非ECS模式下的_FrameData 数据输入来源

ShaderGraph的流程就只有这些节点,较为简单。

ECS实现
正常来说Unity会自动转换 MeshRenderer , SkinnedMeshRenderer 为Entity可用的格式
但是这个Demo中使用了自定义的GPUSkinning 方式,因此需要自己编写一套转换工具
编写 ECS_AnimatorConvertToEntity
Entity的结构如图,本Demo中的角色为拆分为马匹和士兵的2个模型,因此有2个部分,而每个部分各有一个低模,共4个部分,
最后加上一个4个部分共用的父级挂载Animator,MeshLODGroupComponent和动画信息,因此共有5个Entity来表达一个模型逻辑

///

/// 生成结构为
/// Primary Entity (为ECS_SkinnedMatrixAnimator,MeshLODGroupComponent组件所在)
/// |-Attach0
/// | |-LOD0 (RenderMesh, RenderBound, MeshLODComponent)
/// | |-LOD1
/// |
/// |-Attach1
/// | |-LOD0
/// | |-LOD1
///

编写 ECS_AnimatorEntitySpawnerSystem

这部分是将各个部件组合,同时记得添加 ECS_FrameDataMaterialPropertyComponent 组件,这是ECS中使用MaterialPropertyBlock的方式

ECS_FrameDataMaterialPropertyComponent.cs 内容

1
2
3
4
5
6
7
8
9
using Unity.Rendering;
using Unity.Entities;
using Unity.Mathematics;

[MaterialProperty("_ECS_FrameData", MaterialPropertyFormat.Float4)]
public struct ECS_FrameDataMaterialPropertyComponent : IComponentData
{
    public float4 Value;
}

其中 MaterialProperty(“_ECS_FrameData”) 名字要对应上ShaderGraph 中属性定义Reference

最后是ECS GpuSkinning的渲染部分

将计算好的帧数对应的纹理偏移值传给 ECS_FrameDataMaterialPropertyComponent 组件即可

最后运行的FrameDebuger 比较

一万个模型,为5万个Entity


一万个模型下,为6个Srp Batch, 这里要注意 Srp Batch != Instancing Batch Count


一万个模型下, 常规的GPUSkinning 有48个批次

最后是整个项目的源码 github
https://github.com/dreamfairy/Unity_ECS_GPUSkinning

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.