gpu instancing animation代替骨骼动画的做法

25 篇文章 41 订阅

文章转自:https://blog.csdn.net/yxriyin/article/details/83018985

最早是在Unity推出gpuinstancing后,马上有人做了一个顶点动画代替骨骼动画的方案,当时自己也测试了一下,红米2一千人可以跑60帧,确实非常不错。后来发现UWA群里也有人在讨论这个东西的做法,当时M神说可以用烘焙骨骼的方式代替烘焙顶点,这样子烘焙出来的贴图大小只和骨骼数相关。而小米超神也说是通过烘焙顶点,不过为了减少烘焙文件的大小,使用了类似RGBM的方式存储数据。

    我整合了主流的几种做法,做了一个插件。

    首先展示结果:

    

   场景中可见大概750个角色,batches只有7,去掉地面和天空盒,其实这么多人只有5个Batches.

   贴图大小:

   

  115帧的动画,4秒不到一点,128k,而且看到图中还有剩余,即使动画文件更大一些,依然可以用这张贴图放下。可能现在还看不出来它足够小,等后面和烘焙顶点的做法比较一下,就知道这样做的优势了。

  让我们从头开始。

  一切都必须是opengl 3.0以上。

  unity自带的gpuinstancing可以很好的工作在静态物体上,例如草,树。但遗憾的是暂时还无法对骨骼动画使用这个特性。而我们游戏经常使用上百个小兵单位作战,如果可以让小兵使用这个特性,那么对于性能的提升无疑是很可观的。于是有人提出了将动画信息烘焙到贴图中,在shader里面根据贴图设置顶点位置,也就是我们的顶点动画。这样的话,模型就既可以像骨骼动画那样播放动作,又可以使用gpuinstancing合批了。做法也非常简单,可以参考:https://www.cnblogs.com/murongxiaopifu/p/7250772.html

  本来这样就可以了,但实际使用过程中却发现了几个问题。

  1.烘焙的贴图过大,因为为了存储浮点数,必须使用rgbahalf的格式,这个格式每个像素有64个字节,是真彩色的两倍。假设一个小兵有1000个顶点,那么1s的动作就需要1000*64,也就是64000个字节,而正常情况下,我们小兵在2000个顶点左右,动画在5s以上,那么每个动画贴图大概就在2M以上,甚至有可能是4M。而我们有60多个兵种,这样一算竟然有240M。虽然小米超神使用了RGMB来减少每个像素的大小,但那也高达120M的动画贴图了。而我们知道,原始的骨骼动画数据其实只有几百k左右。

  2.无法计算光照,因为法线始终保持T-pos形态,在shader里面改变顶点位置的时候,无法重新计算法线。为了能够使用正常的光照计算,必须将法线也一起烘焙。幸运的是法线都是单位向量,可以采用rgba存储,但也需要大概1M左右的空间。

  3.没有动画之间的blend,为了实现blend,必须对两个动作的贴图进行采样,然后lerp。这样会导致shader里放两张4M的贴图,对手游来说还是不小的开销。

   综上所述,我最终还是采纳了M神的建议,使用了烘焙骨骼信息的方案。

   来看看原理,烘焙顶点很好理解,就是把位置的值存到贴图中。那么如何烘焙骨骼信息,然后得到顶点位置呢?首先我们要理解骨骼动画的原理,这里引用UWA博客里面的一段话:

   

     当然上面的描述很简单,如果想要了解更加详细的推倒过程,可以看Milo大神的书《游戏引擎架构xxx》里面的蒙皮的数学这一章。

     总之,结论就是从当前骨骼的bindpos一直左乘到根骨骼。

     代码也非常简单:

    

 for (int j = 0; j < bones.Length; j++)
                {
                    GPUSkinningBone currentBone = bones[j];
                    Matrix4x4 lastMat = currentBone.bindpose;
                    while (true)
                    {
                        if (currentBone.parentBoneIndex == -1)
                        {
                            Matrix4x4 mat = Matrix4x4.TRS(currentBone.transform.localPosition, currentBone.transform.localRotation, currentBone.transform.localScale);
                            if(rootBone.transform != go.transform)
                            {
                                mat = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, go.transform.localScale) * mat;
                            }
                            
                            lastMat = mat * lastMat;
                            break;
                        }
                        else
                        {
                            Matrix4x4 mat = Matrix4x4.TRS(currentBone.transform.localPosition, currentBone.transform.localRotation, currentBone.transform.localScale);
                            lastMat = mat * lastMat;
                            currentBone = bones[currentBone.parentBoneIndex];
                        }
                    }
 
                    animMap.SetPixel(j * 3, k + 1, new Color(lastMat.m00, lastMat.m01, lastMat.m02, lastMat.m03));
                    animMap.SetPixel(j * 3 + 1, k + 1, new Color(lastMat.m10, lastMat.m11, lastMat.m12, lastMat.m13));
                    animMap.SetPixel(j * 3 + 2, k + 1, new Color(lastMat.m20, lastMat.m21, lastMat.m22, lastMat.m23));
 
                    if (k == startFrame)
                    {
                        animMap.SetPixel(j * 3, k, new Color(lastMat.m00, lastMat.m01, lastMat.m02, lastMat.m03));
                        animMap.SetPixel(j * 3 + 1, k, new Color(lastMat.m10, lastMat.m11, lastMat.m12, lastMat.m13));
                        animMap.SetPixel(j * 3 + 2, k, new Color(lastMat.m20, lastMat.m21, lastMat.m22, lastMat.m23));
                    }
                    else if(k == curClipFrame1 + startFrame - 3)
                    {
                        animMap.SetPixel(j * 3, k + 2, new Color(lastMat.m00, lastMat.m01, lastMat.m02, lastMat.m03));
                        animMap.SetPixel(j * 3 + 1, k + 2, new Color(lastMat.m10, lastMat.m11, lastMat.m12, lastMat.m13));
                        animMap.SetPixel(j * 3 + 2, k + 2, new Color(lastMat.m20, lastMat.m21, lastMat.m22, lastMat.m23));
                    }
                   
                }
最重要的部分就是生成矩阵的那里。这里有几个注意点,一个是根骨骼可能有多个,那么你只需要将他们共同的父亲放到根节点,把这个其实没有骨骼的节点处理成默认矩阵的情况就可以。第二个是因为贴图采样有可能采样到边缘,为了防止精确度不够引起动画抖动,我前后各多增加了一帧,防止抖动。

然后是shader部分:

v2f vert(appdata v)
            {
                UNITY_SETUP_INSTANCE_ID(v);
                float start = UNITY_ACCESS_INSTANCED_PROP(Props, _AnimStart);
                float end = UNITY_ACCESS_INSTANCED_PROP(Props, _AnimEnd);
                float off = UNITY_ACCESS_INSTANCED_PROP(Props, _AnimOff);
    
                float speed = UNITY_ACCESS_INSTANCED_PROP(Props, _Speed);
                float _AnimLen = (end - start);
                float f = (off + _Time.y * speed) / _AnimLen;
 
                f = fmod(f, 1.0);
 
                float animMap_x1 = (v.uv2.x * 3 + 0.5) * _AnimMap_TexelSize.x;
                float animMap_x2 = (v.uv2.x * 3 + 1.5) * _AnimMap_TexelSize.x;
                float animMap_x3 = (v.uv2.x * 3 + 2.5) * _AnimMap_TexelSize.x;
                float animMap_y = (f * _AnimLen + start) / _AnimAll;
                float4 row0 = tex2Dlod(_AnimMap, float4(animMap_x1, animMap_y, 0, 0));
                float4 row1 = tex2Dlod(_AnimMap, float4(animMap_x2, animMap_y, 0, 0));
                float4 row2 = tex2Dlod(_AnimMap, float4(animMap_x3, animMap_y, 0, 0));
                float4 row3 = float4(0, 0, 0, 1);
                float4x4 mat = float4x4(row0, row1, row2, row3);
                float4 pos = mul(mat, v.vertex);
                float3 normal = mul(mat, float4(v.normal, 0)).xyz;
                v2f o;
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.vertex = UnityObjectToClipPos(pos);
                o.color = float4(0, 0, 0, 0);
                o.worldNormal = UnityObjectToWorldNormal(normal);
 
 
                float3 normalDir = normalize(mul(float4(normal, 0.0), unity_WorldToObject).xyz);
 
                float frezz = UNITY_ACCESS_INSTANCED_PROP(Props, _Frezz);
                float3 normalWorld = o.worldNormal;
                fixed dotProduct = dot(normalWorld, fixed3(0, 1, 0)) / 2;
                dotProduct = max(0, dotProduct);
                o.color = dotProduct.xxxx * frezz;
                return o;
            }
主要就是顶点着色器部分,我们把4x4的骨骼旋转偏移矩阵存在贴图里,因为最后一行是flaot4(0,0,0,1),为了节省空间,我们只存了3x4大小的矩阵,最后一行在shader里补上。然后直接将矩阵和顶点相乘,就可以得到蒙皮后的顶点位置。而且我们看到,法线也可以这么处理,就可以得到蒙皮后正确的法线。这里还有一个我没有做的功能,就是骨骼权重,其实我将骨骼权重存进了顶点的uv2中,uv2.xy是第一根骨骼的索引和权重,uv2.zw是第二根骨骼的索引和权重,理论上需要将两个骨骼结算的结果加权平均一下,但因为我测试发现精度够了,就少采样一次,节省点消耗。如果有需要,可以自己加上这个加权平均。

还有一个未来需要做的,就是动画之间的blend,需要额外增加一个变量控制blend的程度,对两个时刻的动作分别采样计算,然后lerp一下就可以了。

我们看看用贴图存储骨骼需要的大小,假设一个小兵有25个骨骼,那么一个骨骼需要4x3个浮点数,也就是3个像素,那么需要75个像素,一个1s的动画,也只需要75*64,大概4800字节而已。而且重要的是我们不受到顶点数的限制,而一个小兵的骨骼正常情况下就是30以内,我们得到了一个可控的合理的结果。

最后献上商店地址:

https://www.assetstore.unity3d.com/en/?stay#!/content/130516

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值