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

news2024/11/15 12:07:28

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的所有高级功能;不过对于海量物体来说,通常不需要极致的细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1126048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【深圳1024开发者城市聚会定向征文】

在这个周末&#xff0c;我有幸参加了1024程序员节活动&#xff0c;这是一个专门为程序员们举办的活动&#xff0c;旨在庆祝程序员这个特殊的群体。在这个活动中&#xff0c;我不仅感受到了浓厚的编程氛围&#xff0c;还收获了许多宝贵的经验和知识。 活动在深圳湾科技生态园举…

漏洞复现--金和OASQL注入

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

坚固可靠的多合一轨道交通天线让您的赏秋路途不再枯燥

今年的法定节假日余额已清零&#xff0c;虽然国庆已过&#xff0c;但秋天正是出游的大好时节。在出游计划中&#xff0c;首先面临的就是交通工具的选择这个大难题&#xff0c;到底是选择自由度更高的自驾前往&#xff1f;还是更省心的公共交通&#xff1f;高铁上的信号质量依旧…

ATA-5510前置微小信号放大器在半导体测试中的具体应用

在现代电子技术领域&#xff0c;半导体器件的测试是非常重要的一环。而前置微小信号放大器在半导体测试中扮演着至关重要的角色。本文将介绍前置微小信号放大器在半导体测试中的原理和应用。 在半导体测试中&#xff0c;通常需要测试非常微弱的信号&#xff0c;这些信号可能受到…

大数据分析平台Splunk Enterprise结合cpolar实现公网远程访问

文章目录 前言1. 搭建Splunk Enterprise2. windows 安装 cpolar3. 创建Splunk Enterprise公网访问地址4. 远程访问Splunk Enterprise服务5. 固定远程地址 前言 Splunk Enterprise是一个强大的机器数据管理平台&#xff0c;可帮助客户分析和搜索数据&#xff0c;以及可视化数据…

福建泉州航海快艇蓝光三维扫描全尺寸测量船只外观设计三维建模-CASAIM中科广电

造船行业是一个与全球经济发展密切相关的关键行业。近年来&#xff0c;随着全球经济的快速发展&#xff0c;造船行业也不断进步和发展。快艇制造业作为小型、快速的船只的产业&#xff0c;所生产的船只通常用于娱乐、旅游、商业等方面&#xff0c;因此这种类型的快艇对于外观设…

科普:数控机床主轴的结构选型及维护保养

数控机床主轴是数控机床的重要组成部分&#xff0c;它的性能直接影响到数控机床的加工精度和加工效率。本文将详细介绍数控机床主轴的结构、性能指标、品牌选型以及维护保养等方面的知识&#xff0c;以便更好地了解和掌握数控机床主轴的相关知识。 一、数控机床主轴的结构 数…

react createElement 和 cloneElement 有什么区别?

前言 什么是react React是一个用于构建用户界面的JavaScript库。它旨在帮助开发人员构建可维护、高性能的应用程序界面。React的核心思想是组件化&#xff0c;它允许开发人员将用户界面划分为小块组件&#xff0c;每个组件负责自己的渲染和行为。这种组件化的方法使得代码更容易…

【python入门篇】字符串(4)

这一章节来说下字符串的使用&#xff0c;字符串是 Python 中最常用的数据类型&#xff0c;我们可以使用单引号( &#xff09;或 双引号&#xff08; " )来创建字符串&#xff0c;那么接下来就进入本章节的一个学习。 一、环境配置 我这边python的环境是3.7.8版本的&…

图像压缩(1)RGB888与RGB565图像

图像压缩&#xff08;1&#xff09;RGB888与RGB565图像 前言一. 图像数据格式1.1 不同RGB格式(1)RGB16(2)RGB24(3)RGB32(4)ARGB32 1.2 RGB565与RGB888对比(1)区别(2)各自优缺点 二. 图像格式转换2.1 取位与补位2.2 其他转换方法 三. 图像压缩3.1 G6压缩法&#xff08;16位真彩色…

最新2023版完美可用的聚合支付系统源码,全开源无后门,适合二开

最新2023版完美可用的聚合易支付系统源码&#xff0c;全开源无后门&#xff0c;真正安全可用。 更新日志&#xff1a; 1.新增微信公众号消息提醒功能 2.重构转账付款功能&#xff0c;支持通过插件扩展 3.商户后台新增代付功能 4.后台新增付款记录列表 5.支付宝插件新增预…

城市公安可视化大数据展示平台预测预警,防患未然

在当今数字化、信息化的时代背景下&#xff0c;数字化浪潮席卷着全国各地公安系统&#xff0c;以数字孪生为底座的智慧公安数字孪生可视化管控平台已经成为公共安全领域的一大发展趋势&#xff0c;以其独特的优势为公安机关的日常管理和警务指挥带来了革命性的变革。 智慧公安解…

【FPGA】IIC协议通用主机接口的设计与实现详解

一、认识IIC IIC&#xff08;I2C&#xff09;协议是一种串行通信协议&#xff0c;用于连接微控制器和外围设备。IIC协议只需要两根信号线&#xff08;时钟线SCL和数据线SDA&#xff09;就能完成设备之间的通信&#xff1b;支持多主机和多从机通信&#xff0c;通过设备地址区分不…

cmd命令快速打开MATLAB

文章目录 复制快捷方式添加 -nojvm打开 复制快捷方式 添加 -nojvm 打开 唯一的缺点是无法使用plot&#xff0c;这一点比不上linux系统&#xff0c;不过打开速度还是挺快的。

模式识别——贝叶斯决策理论

模式识别——贝叶斯决策理论BDR 须知基本原则0-1损失下的BDRMAP&#xff08;极大后验&#xff09;log trick 须知 所有内容在分类问题下讨论。 基本原则 定义 X X X为观测 Y Y Y为状态 g ( x ) g(x) g(x)用 x x x对 y y y进行预测预测损失为 L [ g ( x ) , i ] L[g(x),i] L[…

LVS---负载均衡

集群&#xff1a;为解决某个特定问题将多个计算机组合起来形成一个单系统 提高性能的两个方向 垂直扩展&#xff1a;向上扩展&#xff0c;增加单个机器的性能。升级硬件。硬件升级是由瓶颈的水平扩展:向外扩展&#xff0c;增加设备。并行的运行多个服务,通过网络和算法&…

Linux系统编程(4)

分配数组 int *x, *y; x malloc(50*sizeof(int)); if(!x) {perror("malloc");return 1; }y calloc(50, sizeof(int)); if (!y) {perror("calloc");return 1; } calloc会将所申请的内存全部填充0&#xff0c;malloc则不会。 调整内存分配的大小 #incl…

驱动开发4 使用字符设备驱动的分步实现编写LED驱动(LED亮灯)

一、思维导图 二、通过字符设备驱动的分步实现编写LED驱动&#xff0c;另外实现特备文件和设备的绑定 应用程序 test.c #include<stdlib.h> #include<stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include…

嵌入式linux系统中设备树基础知识

笔记整理自百问网正点原子 前言 之前分享的笔记&#xff1a;【Linux笔记】总线设备驱动模型中在platform_device部分有简单说明描述设备有两种方法&#xff1a;一种是使用platform_device结构体来指定&#xff1b;另一种是使用设备树来描述。 本篇笔记我们就来简单地学习一下…

系列三、其他流

一、缓冲流 1.1、概述 缓冲流也叫高效流&#xff0c;是对最基本的FileInputStream、FileOutputStream、FileReader、FileWriter的增强&#xff0c;所以也是4个流&#xff0c;按照数据类型分为如下种类 字节缓冲流&#xff1a;BufferedInputStream、BufferedOutputStream字符…