【Unity】渲染性能开挂GPU Animation, 动画渲染合批GPU Instance

本文详细介绍了Unity中实现GPU动画的原理和优化,通过烘焙动画到Texture2D,实现GPU驱动的顶点动画,大幅提高渲染性能。支持Animator烘焙和动画切换,适用于大规模动画物体场景。在不同动画系统间进行性能对比,GPU动画在MeshRenderer和BRG合批下分别达到135 fps和202 fps,显著优于传统方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 【万人同屏方案Pro】是以GPU动画为基础实现3D、2D Spine高性能渲染,同时支持GPU Instancing或DOTS Instancing。基于Dots技术栈(Jobs&Burst)实现高性能海量单位锁敌/碰撞检测。同时提供BRG和ECSGraphics两套高性能渲染器, 传统开发方式拥有使用DOTS的渲染性能。无ECS技术门槛,无需写ECS代码,UI程序员也能直接上手写出万人同屏项目。 

 插件获取&Demo下载

Unity万人同屏集成方案ProPC店铺https://shop106471535.taobao.com/

性能/功能/红蓝对抗测试Demohttps://pan.baidu.com/s/1ML0DC8s0RkkTAraN9maltw?pwd=blue

插件视频教程

【Unity万人同屏插件】使用手册 保姆级教程 GPU动画 Jobs多线程渲染_unity 万人同屏-CSDN博客文章浏览阅读7.5k次,点赞20次,收藏52次。《万人同屏插件》是一款基于Dots技术的高性能Unity开发工具,通过GPU动画渲染和BRG/ECSGraphics多线程渲染器,实现3D/2D Spine动画的海量单位渲染(10万单位)。支持武器挂载、动画事件、骨骼信息保留及LOD优化,GPU动画兼容全平台包括WebGL。插件提供一键转换工具将Animator/Spine动画转为GPU动画,大幅降低DrawCall和包体体积。新增ECSGraphics渲染器支持无限实例、8级LOD和动态挂载点切换。配套RVO多线程避障系统可实现大规模单位寻敌与碰撞检_unity 万人同屏 https://blog.csdn.net/final5788/article/details/142005465

【万人同屏插件】实战项目mu'b

gpu动画性能PK和插件使用视频教程:

Unity低成本性能开挂 gpu动画+dots graphics万人同屏渲染 jobs rvo2 弹幕游戏海量单位索敌

GPU骨骼动画的实现:【Unity】GPU骨骼动画 渲染性能开挂 动画合批渲染 支持武器挂载-CSDN博客

GPU Instance和SRP Batcher合批渲染只对静态MeshRenerer有效,对SkinMeshRenderer无效。蒙皮动画性能堪忧,对于海量动画物体怎么解决呢?针对这个问题,GPU Animation就是一个常见又简单的解决方案。

GPU动画实现原理:

实现原理也是简单粗暴,把每一帧动画时刻SkinMeshRenderer所有的顶点坐标写入到Texture2D, 贴图UV中,U按顶点顺序保存顶点坐标,V是第几帧,然后在顶点着色器中读取所有顶点的坐标,根据时间轮流在动画帧数区间从动画Texture2D采样,这样就实现了基于GPU的顶点动画。

优化前后性能对比:

分别使用Animator(新版动画系统)、Animation(旧版动画系统)、GPU动画、BRG + GPU动画,10000个动画单位全部在相机视口内播放相同动画的帧数做比较(测试结果根据顶点个数会有波动):

AnimatorAnimation

GPU动画

(MeshRenderer)

GPU动画

(Batch Renderer Group)

帧数910135202

1. Animator动画系统,9 fps:

2. Animation(旧版动画系统),10 fps:

3. GPU动画,使用MeshRenderer渲染组件 135 fps:

4. GPU动画,使用Batch Renderer Group合批渲染 202 fps:

 GPU动画功能实现:

GPU动画原理已经明确,首先第一步就是把Animation Clip动画每一帧的顶点烘焙到Texture2D中,推荐大家参考github目前star最多的gpu动画开源方案:https://github.com/chenjd/Render-Crowd-Of-Animated-Characters

 不过,此开源方案目前仅支持把旧版Animation Clip烘焙成贴图,不支持Animator动画,并且是每个Animation Clip烘焙成一张Texture文件,对于动画切换不是很友好。

作为一个设计开发工程师这样的工作流是难以忍受的,首先需要解决以下问题:

1. 支持Animator动画烘焙。

2. 更友好的动画切换,通过修改shader参数AnimIndex来切换不同动画,并且支持设置动画速度。

3. 自动把Animator中Animation Clips放入烘焙列表,以及其它用户体验优化功能。

4. 一键生成动画贴图资源、材质球、prefab预制体。

5. 为gpu动画封装一个Amplify Shader Editor函数节点,便于用于使用和扩展自定义shader.

 工具使用工作流:

1. 支持Animator动画烘焙:

Asset Store有现成的插件,可以将各种Animation类型humanoid ⇆ generic ⇆ legacy相互转换:Animation Converter | Animation Tools | Unity Asset Store

只需用插件将Animator的动画转换为Legacy Animation,然后就可以把Legacy Animating烘焙到贴图了;

AnimationConverter.Convert(animationClips, config, out string logMessage);

自动化生成带有Animation组件的prefab,并把转换生成的Legacy Animation Clips赋值到Animation组件:

private void TryAssignAnimationClips(string clipsDir, GameObject animationPrefab, IList<AnimationClip> clips)
    {
        var animation = animationPrefab.GetComponent<Animation>();
        AnimationClip[] newClips = new AnimationClip[clips.Count];
        for (int i = 0; i < clips.Count; i++)
        {
            var clipAsset = Path.Combine(clipsDir, $"{clips[i].name}.anim");
            newClips[i] = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipAsset);
        }
        AnimationUtility.SetAnimationClips(animation, newClips);
        EditorUtility.SetDirty(animationPrefab);
    }

烘焙动画,根据Animation的time,每次采样间隔时间为:animClip.length / (Mathf.CeilToInt(animClip.frameRate * animClip.length)). 即个动画帧采样一次。将当前的SkinMeshRenderer通过BakeMesh得到一个当前动画帧Mesh,然后把Mesh的vertices坐标写入Texture2D。

2. 实现动画切换

 实现用index切换动画,只需创建一个Texture2DArray, 把每个Animation Clip生成的Texture2D写入Texture2DArray,就可以通过index来切换动画贴图了。需要注意的是,Texture2DArray中Texture2D的宽高必须与Texture2DArray宽高一样。也就是说Texture2DArray的宽高需为最大子贴图的宽和高。

对于宽高填充不满的贴图,用程序以Repeat方式填充像素即可,可以有效防止动画贴图采样超过有效区域导致的顶点抖动问题。

动画贴图FilterMode使用Bilinear,线性插值可以让顶点动画更加平滑;需要注意的是,由于Biliner进行了插值过渡,在使用VertexID在贴图U轴采样时需要+0.5偏移,取两个像素的中间值。

另外gpu动画shader中还需要访问每个动画的帧数和时间长度,由于gpu动画只用到了xyz,即像素的rgb通道,可以把动画的帧数和时间长度信息直接存入动画贴图的alpha通道,这样就能在Shader读取使用了。

把多个Animation Clip烘焙到Texture2DArray并保存:

public void BakeAnimation(GameObject animationPrefab, string outputDir)
    {
        Quaternion quaternion = Quaternion.Euler(m_FixRotation);
        var baker = new GPUAnimationBaker();
        baker.SetAnimData(animationPrefab, m_TexPowerOfTwo);
        var bakedDataArr = baker.Bake(m_FixScale, quaternion);
        if (bakedDataArr == null || bakedDataArr.Count < 1)
        {
            return;
        }
        var tex2d = bakedDataArr[0].RawAnimTex;
        Texture2DArray tex2DArray = new Texture2DArray(tex2d.width, tex2d.height, bakedDataArr.Count, tex2d.format, false);
        tex2DArray.filterMode = tex2d.filterMode;
        tex2DArray.wrapMode = tex2d.wrapMode;
        for (int i = 0; i < bakedDataArr.Count; i++)
        {
            var bakeDt = bakedDataArr[i];
            tex2DArray.SetPixelData(bakeDt.RawAnimMap, 0, i);
        }
        tex2DArray.Apply();
        string fileName = Path.Combine(outputDir, $"{animationPrefab.name}_anim_tex.asset");
        AssetDatabase.CreateAsset(tex2DArray, fileName);


        string meshFileName = Path.Combine(outputDir, $"{animationPrefab.name}_mesh.asset");
        var newMesh = Instantiate(baker.AnimBakeData.SkinMesh.sharedMesh);
        var points = newMesh.vertices;
        for (int i = 0; i < points.Length; i++)
        {
            var point = points[i];
            point.Scale(m_FixScale);
            points[i] = quaternion * point;
        }
        newMesh.vertices = points;
        newMesh.RecalculateBounds();
        newMesh.RecalculateNormals();
        newMesh.RecalculateTangents();
        AssetDatabase.CreateAsset(newMesh, meshFileName);

        var newPrefabName = $"{m_Config.Prefabs[0].SourcePrefab.name}_gpu_anim";
        var newPrefab = new GameObject(newPrefabName, typeof(MeshFilter), typeof(MeshRenderer));
        newPrefab.GetComponent<MeshFilter>().mesh = AssetDatabase.LoadAssetAtPath<Mesh>(meshFileName);
        var newMat = new Material(m_Shader);
        newMat.SetTexture("_AnimTexArr", AssetDatabase.LoadAssetAtPath<Texture2DArray>(fileName));
        newMat.SetFloat("_AnimMaxLen", baker.MaxAnimLength);
        var matFileName = Path.Combine(outputDir, $"{newPrefab.name}.mat");
        AssetDatabase.CreateAsset(newMat, matFileName);

        newPrefab.GetComponent<MeshRenderer>().material = AssetDatabase.LoadAssetAtPath<Material>(matFileName);
        PrefabUtility.SaveAsPrefabAsset(newPrefab, Path.Combine(outputDir, $"{newPrefabName}.prefab"));
        DestroyImmediate(newPrefab);
    }

3. GPU动画Shader实现:

本来想用Shader Graph和Amplify Shader Editor各实现一份便于使用。但是Shader Graph Bug太离谱了,遇到几次报错,Shader编辑器无响应,导致所有节点丢失。最离谱的是uint类型不能转换为float,VertexID(uint类型)与float值参与任何运算后,会出现数据类型匹配的情况下,节点却无法连接上。WTF?

果断换用Amplify Shader Editor,就一切正常了。实现如图,待到Shader Graph修复bug可以参考Shader节点移植到Shader Graph:

用法:

使用上图自定义函数GetGPUAnimVertex从动画贴图读取顶点坐标,直接赋值到Vertex Position即可;

总结: 

GPU Animation相比传统动画可以提升10 - 20多倍的性能,它是目前海量物体同屏的最佳方案,但不是完美的,如:不支持动画之间平滑过渡;没有骨骼,也就失去了Animator的所有高级功能;不过对于海量物体来说,通常不需要极致的细节。

### 关于 Unity 中 Unlit 材质的使用教程 #### 创建和配置 Unlit 材质 在 Unity 中创建 Unlit 材质相对简单。Unlit 材料不考虑光源的影响,因此非常适用于不需要光照效果的对象或者背景元素。 1. **新建材质** 在 Project 视图中右键点击,选择 `Create -> Material`。命名新材质并将其 Shader 属性设置为 `Unlit/Texture` 或者其他适的 Unlit 类型[^1]。 2. **调整材质属性** 可以为该材质分配纹理贴图,并通过 Inspector 面板修改颜色和其他参数来定制外观。对于更复杂的视觉效果,可以利用 Shader Graph 工具进一步自定义着色逻辑。 #### 示例代码:简单的 Unlit 着色器实现 下面是一个非常基础的 HLSL 语法编写的 Unlit 着色器例子: ```hlsl Shader "Custom/SimpleUnlit" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag sampler2D _MainTex; struct appdata_t { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert(appdata_t v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag(v2f i) : COLOR { return tex2D(_MainTex, i.uv); } ENDCG } } } ``` 此脚本展示了如何编写一个基本的无光栅化处理(unlit)着色器,它只应用了一张纹理而没有任何光影计算。 #### 性能优化建议 为了提高带有大量相似对象场景下的渲染效率,应该启用 GPU Instancing 功能。这允许硬件一次性绘制多个实例化的几何体而不必重复发送相同的绘图调用给显卡。具体做法是在自定义 shader 的输入输出函数内部加入如下两行 Cg/HLSL 宏指令以支持实例化数据传递[^5]: ```cpp UNITY_SETUP_INSTANCE_ID(v); UNITY_SETUP_INSTANCE_ID(i); ``` 此外,在项目构建前务必确认已勾选 Player Settings 下的 Other Settings 节点里的 “Enable Graphics Jobs” 和 “Use Multi-threading for Animation Skinning and Cloth Simulation”。这些选项有助于充分利用多核 CPU 提升整体性能表现[^3].
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

异趣游戏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值