一般来说使用GpuSkinning 已经能得到很不错的性能了,那么能不能再快一点呢?
答案当然是肯定的,这一次我们来使用ECS榨干CPU的部分
先上性能对比图
1万个蒙皮角色,每个角色472面,带有uv0,uv1
测试设备硬件 win10, Intel i7-7700, GPU GTX-1060 6G
可以看到Entity的帧数在 110帧以上, 而传统GPUSkinning 的帧数在 29帧
这个Demo使用的GPU蒙皮方案为 将骨骼矩阵数据以双四元数的方式存储在纹理上,具体实现方法不是这个Demo的重点,大家也可以参考这篇文章
接下来一步一步开始分解这个Demo
首先实现Shader Include
Skinning.hlsl
    
    
#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模式下激活的宏 展开的结构如图
该节点就是讲之前编写的 Skinning.hlsl 以节点的方式添加到ShaderGraph 中使用
分别是之前介绍的 4个 输入,和一个输出
SkinningTex_float(float4 positionOS, float4 texcoord, float4 frameData, Texture2D<float4> 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来表达一个模型逻辑
    
    
/// <summary> /// 生成结构为 /// Primary Entity (为ECS_SkinnedMatrixAnimator,MeshLODGroupComponent组件所在) /// |-Attach0 /// | |-LOD0 (RenderMesh, RenderBound, MeshLODComponent) /// | |-LOD1 /// | /// |-Attach1 /// | |-LOD0 /// | |-LOD1 /// </summary>
编写 ECS_AnimatorEntitySpawnerSystem
这部分是将各个部件组合,同时记得添加 ECS_FrameDataMaterialPropertyComponent 组件,这是ECS中使用MaterialPropertyBlock的方式
ECS_FrameDataMaterialPropertyComponent.cs 内容
    
    
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
最后欢迎关注我的博客 http://www.dreamfairy.cn
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐