《大规模动画优化(一):GPU 顶点动画的生成》
GPU 顶点动画(Vertex Animation Texture, VAT)
GPU 顶点动画(Vertex Animation Texture, VAT)烘焙的核心思想是: 在 CPU
端预先计算动画顶点数据,并存储到纹理(Texture2D)中,在 GPU 端通过 Shader 读取纹理来播放动画。
📌 1. 烘焙的核心步骤
- 采样动画帧(AnimationClip)
- 获取 SkinnedMeshRenderer 在每帧的顶点数据
- 存储到 Texture2D作为动画纹理
- 在 GPU 端使用 Shader 读取并应用动画
🎯 Step 1: 获取角色的 SkinnedMeshRenderer
角色网格 = (SkinnedMeshRenderer)EditorGUILayout.ObjectField("角色网格", 角色网格, typeof(SkinnedMeshRenderer), true);
这里 角色网格 是 SkinnedMeshRenderer,它是 Unity 蒙皮网格(Skinned Mesh)动画系统的核心组件。
为什么要用 SkinnedMeshRenderer?
Unity 角色动画一般都是骨骼动画,蒙皮网格的顶点由骨骼驱动,因此我们要从SkinnedMeshRenderer 获取最终变形后的顶点数据,而不能直接用 MeshFilter.mesh。
🎯 Step 2: 采样动画帧
在 角色动画烘焙类.cs 里,我们通过 AnimationMode.SampleAnimationClip 让角色处于特定动画帧。
AnimationMode.StartAnimationMode();
AnimationMode.BeginSampling();
AnimationMode.StartAnimationMode()
让 Unity 进入动画采样模式,这样可以在不播放动画的情况下手动设定帧数并获取角色姿态。
AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);
让角色静态停留在某个时间点的动画帧。
🎯 Step 3: 获取烘焙网格数据
角色网格.BakeMesh(烘焙网格);
Vector3[] 顶点数据 = 烘焙网格.vertices;
BakeMesh():
让 SkinnedMeshRenderer 计算当前帧的最终顶点位置,存入 Mesh 对象中。
这个方法会把蒙皮骨骼变换后的顶点复制出来,避免骨骼动画影响原始 MeshFilter.mesh。
烘焙网格.vertices:
获取角色在当前动画帧的所有变形后的顶点位置。
🎯 Step 4: 存储动画数据到 Texture2D
如果需要存储发现数据和切线数据,则可以另外存储
动画纹理.SetPixel(i, j, new Color(顶点.x, 顶点.y, 顶点.z, 1));
动画纹理.SetPixel(i, animLength + j + previewAnimationLength, normalsData); // 存储法线数据 (偏移 animLength 行)
// 处理切线数据 (直接存储)
Color tangentsData = bakeMeshTangents[j];
动画纹理.SetPixel(i, animLength * 2 + j + previewAnimationLength, tangentsData); // 存储切线数据 (偏移 2 * animLength 行)
🎯 Step 5: 处理所有帧
for (int i = 0; i < 总帧数; i++)
{float 归一化时间 = (float)i / (float)(总帧数 - 1) * 动画片段.length;AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);角色网格.BakeMesh(烘焙网格);Vector3[] 顶点数据 = 烘焙网格.vertices;for (int j = 0; j < 顶点数据.Length; j++){Vector3 顶点 = 顶点数据[j];动画纹理.SetPixel(i, j, new Color(顶点.x, 顶点.y, 顶点.z, 1));}
}
动画纹理.Apply();
遍历每一帧,采样动画数据
总帧数 由 动画片段长度 × 采样帧率 确定
归一化时间 归一化时间 = i / (总帧数 - 1) * 动画片段.length
每帧都 BakeMesh(),然后 SetPixel() 存入纹理
🎯 Step 6: 导出并保存 .png
byte[] 纹理数据 = 动画纹理.EncodeToPNG();
File.WriteAllBytes(完整路径, 纹理数据);
AssetDatabase.Refresh();
EncodeToPNG():把 Texture2D 转换为 PNG 格式
File.WriteAllBytes():将数据保存到硬盘
AssetDatabase.Refresh():通知 Unity 刷新资源库,🌟 最终结果
✅ 我们获得了一张 动画数据纹理,类似下面这样:
动画帧(宽度) → 顶点 1 顶点 2 顶点 3 …
帧 1 (x,y,z) (x,y,z) (x,y,z) …
帧 2 (x,y,z) (x,y,z) (x,y,z) …
… … … … …
🔹 横向 代表动画帧,🔹 纵向 代表模型的每个顶点。
在 GPU 端,我们可以 直接用 Shader 采样这个纹理,快速应用动画,而无需再让 CPU 计算骨骼动画!让 .png 文件显示在 Project 视图中
🎯 总结
| 步骤 | 代码操作 | 作用 |
|---|---|---|
| 获取角色网格 | SkinnedMeshRenderer | 选择要烘焙的角色 |
| 启动动画模式 | AnimationMode.StartAnimationMode() | 让 Unity 进入动画编辑模式 |
| 逐帧采样动画 | AnimationMode.SampleAnimationClip() | 让角色停留在特定动画帧 |
| 获取顶点数据 | BakeMesh().vertices | 采集当前帧的蒙皮网格数据 |
| 存储到纹理 | SetPixel() | 将动画帧存入 Texture2D |
| 保存文件 | EncodeToPNG() | 导出动画数据纹理 |
✅ 烘焙后的动画纹理可以直接用于 GPU 渲染,实现海量动画角色的高效渲染!
附上完整代码
using UnityEngine;
using UnityEditor;
using System.IO;/// <summary>
/// GPU 顶点动画烘焙窗口
/// </summary>
public class 角色动画烘焙窗口类 : EditorWindow
{private SkinnedMeshRenderer 角色网格;private AnimationClip 动画片段;private int 采样帧数 = 30;private string 存储路径 = "Assets/GPUAnimations/";[MenuItem("工具/GPU 顶点动画烘焙")]public static void 打开窗口方法(){角色动画烘焙窗口类 窗口 = (角色动画烘焙窗口类)GetWindow(typeof(角色动画烘焙窗口类));窗口.titleContent = new GUIContent("GPU 顶点动画烘焙");窗口.Show();}private void OnGUI(){GUILayout.Label("GPU 顶点动画烘焙", EditorStyles.boldLabel);角色网格 = (SkinnedMeshRenderer)EditorGUILayout.ObjectField("角色网格", 角色网格, typeof(SkinnedMeshRenderer), true);动画片段 = (AnimationClip)EditorGUILayout.ObjectField("动画片段", 动画片段, typeof(AnimationClip), false);采样帧数 = EditorGUILayout.IntField("采样帧数", 采样帧数);存储路径 = EditorGUILayout.TextField("存储路径", 存储路径);if (GUILayout.Button("开始烘焙")){if (角色网格 == null || 动画片段 == null){EditorUtility.DisplayDialog("错误", "请指定角色网格和动画片段!", "确定");return;}开始烘焙方法();}}private void 开始烘焙方法(){角色动画烘焙类.烘焙动画方法(角色网格, 动画片段, 采样帧数, 存储路径);}
}
/// <summary>
/// 角色 GPU 顶点动画烘焙
/// </summary>
public class 角色动画烘焙类
{public static void 烘焙动画方法(SkinnedMeshRenderer 角色网格, AnimationClip 动画片段, int 采样帧数, string 存储路径){Mesh 烘焙网格 = new Mesh();int 总帧数 = 采样帧数;int 顶点数 = 角色网格.sharedMesh.vertexCount;string 文件名 = 角色网格.gameObject.name + "_" + 动画片段.name + ".png";string 完整路径 = Path.Combine(存储路径, 文件名);// 创建纹理Texture2D 动画纹理 = new Texture2D(总帧数, 顶点数, TextureFormat.RGBAFloat, false);Animator 角色动画器 = 角色网格.GetComponentInParent<Animator>();if (角色动画器 == null){Debug.LogError("角色缺少 Animator 组件!");return;}// 预览播放动画//让 Unity 进入动画采样模式,这样可以在不播放动画的情况下手动设定帧数并获取角色姿态。AnimationMode.StartAnimationMode();AnimationMode.BeginSampling();//让角色静态停留在某个时间点的动画帧。AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 0);for (int i = 0; i < 总帧数; i++){float 归一化时间 = (float)i / (float)(总帧数 - 1) * 动画片段.length;AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);角色网格.BakeMesh(烘焙网格);Vector3[] 顶点数据 = 烘焙网格.vertices;Vector3[] 法线数据 = 烘焙网格.normals; // 获取法线Vector4[] 切线数据 = 烘焙网格.tangents; // 获取切线// 存储顶点位置、法线和切线for (int j = 0; j < 顶点数据.Length; j++){Vector3 顶点 = 顶点数据[j];Vector3 法线 = 法线数据[j];Vector4 切线 = 切线数据[j];// 使用 RGBA 通道存储数据// R: 顶点位置 X// G: 顶点位置 Y// B: 顶点位置 Z// A: 法线 X// 将法线的 Y、Z、W 存储到 RGB 通道中Color pixelData = new Color(顶点.x, 顶点.y, 顶点.z, 法线.x); // 顶点位置和法线X// 存储到纹理中动画纹理.SetPixel(i, j, pixelData);// 将法线Y、Z以及切线的X、Y、Z存储到同一纹理的其他通道pixelData = new Color(法线.y, 法线.z, 切线.x, 切线.y); // 法线 Y, Z 和 切线 X, Y动画纹理.SetPixel(i, j + 顶点数, pixelData);// 存储切线的 W 分量pixelData = new Color(切线.z, 切线.w, 0, 0);动画纹理.SetPixel(i, j + 2 * 顶点数, pixelData);}}动画纹理.Apply();// 保存到本地byte[] 纹理数据 = 动画纹理.EncodeToPNG();File.WriteAllBytes(完整路径, 纹理数据);AssetDatabase.Refresh();AnimationMode.EndSampling();AnimationMode.StopAnimationMode();Debug.Log($"GPU 顶点动画烘焙完成!文件保存至 {完整路径}");}
}
🚀 下一步:使用 Shader 读取纹理,在 GPU 端播放动画!
相关文章:
《大规模动画优化(一):GPU 顶点动画的生成》
GPU 顶点动画(Vertex Animation Texture, VAT) GPU 顶点动画(Vertex Animation Texture, VAT)烘焙的核心思想是: 在 CPU 端预先计算动画顶点数据,并存储到纹理(Texture2D)中…...
webpack【初体验】使用 webpack 打包一个程序
打包前 共 3 个文件 dist\index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Webpack 示例&…...
VMware安装CentOS 7(全网超详细图文保姆版教程)
文章目录 一、下载及安装 VMware1.1 VMware下载1.2 CentOS下载 二、搭建虚拟机环境2.1 创建新虚拟机2.2 选择自定义2.3 选择虚拟机硬件兼容性2.4 选择稍后安装操作系统2.5 选择Linux系统 版本选择 centos 7 64位2.6 设备你虚拟机的名字和保存位置(保存位置建议在编辑…...
mysql BUG 导致 show processlist 有大量的show slave stauts 处于init状态
一、详细报错信息: 1、执行show slave status\G 卡住 && stop slave也卡住 2、show processlist 发现 Waiting for commit lock NULL 锁 3、错误日志报错主备同步用户认证失败 二、报错原因(分析过程): 1、排查备库日志…...
机器学习在癌症分子亚型分类中的应用
学习笔记:机器学习在癌症分子亚型分类中的应用——Cancer Cell 研究解析 1. 文章基本信息 标题:Classification of non-TCGA cancer samples to TCGA molecular subtypes using machine learning发表期刊:Cancer Cell发表时间:20…...
从MySQL优化到脑力健康:技术人与效率的双重提升
文章目录 零:前言一:MySQL性能优化的核心知识点1. 索引优化的最佳实践实战案例: 2. 高并发事务的处理机制实战案例: 3. 查询性能调优实战案例: 4. 缓存与连接池的优化实战案例: 二:技术工作者的…...
Qt:项目文件解析
目录 QWidget基础项目文件解析 .pro文件解析 widget.h文件解析 widget.cpp文件解析 widget.ui文件解析 main.cpp文件解析 认识对象模型 窗口坐标系 QWidget基础项目文件解析 .pro文件解析 工程新建好之后,在工程目录列表中有⼀个后缀为 ".pro" …...
react使用if判断
1、第一种 function Dade(req:any){console.log(req)if(req.data.id 1){return <span>66666</span>}return <span style{{color:"red"}}>8888</span>}2、使用 {win.map((req,index) > ( <> <Dade data{req}/>{req.id 1 ?…...
conda 修复 libstdc++.so.6: version `GLIBCXX_3.4.30‘ not found 简便方法
ImportError: /data/home/hum/anaconda3/envs/ipc/bin/../lib/libstdc.so.6: version GLIBCXX_3.4.30 not found (required by /home/hum/anaconda3/envs/ipc/lib/python3.11/site-packages/paddle/base/libpaddle.so) 1. 检查版本 strings /data/home/hum/anaconda3/envs/ipc/…...
在服务器部署JVM后,如何评估JVM的工作能力,比如吞吐量
在服务器部署JVM后,评估其工作能力(如吞吐量)可以通过以下步骤进行: 1. 选择合适的基准测试工具 JMH (Java Microbenchmark Harness):适合微基准测试,测量特定代码片段的性能。Apache JMeter:…...
python学opencv|读取图像(六十)先后使用cv2.erode()函数和cv2.dilate()函数实现图像处理
【1】引言 前序学习进程中,先后了解了使用cv2.erode()函数和cv2.dilate()函数实现图像腐蚀和膨胀处理的效果,相关文章链接为: python学opencv|读取图像(五十八)使用cv2.erode()函数实现图像腐蚀处理-CSDN博客 pytho…...
Itext源代码阅读(2) -- PdfReader
本文基于Itext 5,Itext7相较itext5虽然有较大变化,但是原理是一样的。 参考资料: 使用iText处理pdf文件的入门级教程_itextpdf 教程-CSDN博客 比较详实的介绍了长用的itext 的pdf处理。 深入iText7:第5章源代码实践指南-CSDN博…...
JavaScript-Object 对象的相关方法
1. Object.getPrototypeOf() Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。 var F function () {}; var f new F(); Object.getPrototypeOf(f) F.prototype // true 上面代码中,实例对象 f的原型是 F.prototype。 下面是几种特殊对…...
Flink 内存模型各部分大小计算公式
Flink 的运行平台 如果 Flink 是运行在 yarn 或者 standalone 模式的话,其实都是运行在 JVM 的基础上的,所以首先 Flink 组件运行所需要给 JVM 本身要耗费的内存大小。无论是 JobManager 或者 TaskManager ,他们 JVM 内存的大小都是一样的&a…...
每日一题——缺失的第一个正整数
缺失的第一个正整数 题目描述进阶:数据范围: 示例示例 1示例 2示例 3 题解思路代码实现代码解释复杂度分析总结 题目描述 给定一个无重复元素的整数数组 nums,请你找出其中没有出现的最小的正整数。 进阶: 时间复杂度ÿ…...
Qt修仙之路2-1 仿QQ登入 法宝初成
widget.cpp #include "widget.h" #include<QDebug> //实现槽函数 void Widget::login1() {QString userusername_input->text();QString passpassword_input->text();//如果不勾选无法登入if(!check->isChecked()){qDebug()<<"xxx"&…...
从家庭IP到全球网络资源的无缝连接:Cliproxy的专业解决方案
数字化时代,家庭IP作为个人或家庭接入互联网的门户,其重要性日益凸显。然而,要实现从家庭IP到全球网络资源的无缝连接,并享受高效、安全、稳定的网络访问体验,往往需要借助专业的代理服务。Cliproxy,作为业…...
Python 脚本实现数据可视化
使用 Python 脚本实现数据可视化可以通过以下步骤: 一、准备工作 安装必要的库: matplotlib:这是一个广泛使用的 Python 2D 绘图库,可以生成各种静态、动态和交互式的图表。seaborn:建立在 matplotlib 之上ÿ…...
【Java】多线程和高并发编程(四):阻塞队列(上)基础概念、ArrayBlockingQueue
文章目录 四、阻塞队列1、基础概念1.1 生产者消费者概念1.2 JUC阻塞队列的存取方法 2、ArrayBlockingQueue2.1 ArrayBlockingQueue的基本使用2.2 生产者方法实现原理2.2.1 ArrayBlockingQueue的常见属性2.2.2 add方法实现2.2.3 offer方法实现2.2.4 offer(time,unit)方法2.2.5 p…...
TCP/IP 协议图解 | TCP 协议详解 | IP 协议详解
注:本文为 “TCP/IP 协议” 相关文章合辑。 未整理去重。 TCP/IP 协议图解 退休的汤姆 于 2021-07-01 16:14:25 发布 TCP/IP 协议简介 TCP/IP 协议包含了一系列的协议,也叫 TCP/IP 协议族(TCP/IP Protocol Suite,或 TCP/IP Pr…...
点大商城V2-2.6.6源码全开源uniapp +搭建教程
一.介绍 点大商城V2独立开源版本,版本更新至2.6.6,系统支持多端,前端为UNiapp,多端编译。 二.搭建环境: 系统环境:CentOS、 运行环境:宝塔 Linux 网站环境:Nginx 1.21 MySQL 5.…...
【GitHub】相关工具下载及使用
目录 背景GitHub的使用Git工具下载及安装 背景 需要在GitHub查阅相关资料,以下是对使用GitHub做相关记录。 GitHub的使用 参考链接: GitHub入门指南:一步一步教你使用GitHub Git工具下载及安装 参考链接: windows安装git(全网最详细&…...
阿里云百炼初探DeepSeek模型调用
阿里云百炼初探DeepSeek模型调用 阿里云百炼为什么选择百炼开始使用百炼方式一:文本对话方式二:文本调试方式三:API调用 DeepSeek调用1、搜索模型2、查看API调用3、开始调用安装依赖查看API Key运行以下代码 4、流式输出 总结 阿里云百炼 阿…...
蓝桥杯备赛——“双指针”“三指针”解决vector相关问题
一、寄包柜 相关代码: #include <iostream> #include <vector> using namespace std; const int N 1e5 10; int n, q; vector<int> a[N]; // 创建 N 个柜⼦ int main() {cin >> n >> q;while(q--){int op, i, j, k;cin >> …...
【Java 面试 八股文】Redis篇
Redis 1. 什么是缓存穿透?怎么解决?2. 你能介绍一下布隆过滤器吗?3. 什么是缓存击穿?怎么解决?4. 什么是缓存雪崩?怎么解决?5. redis做为缓存,mysql的数据如何与redis进行同步呢&…...
SIPp的参数及命令示例
以下是SIPp参数的分类表格整理,方便快速查阅和使用: SIPp 参数分类表格 分类参数描述默认值示例基本参数-sc指定XML场景文件(客户端模式)无-sc uac.xml-sd指定XML场景文件(服务器端模式)无-sd uas.xml-i本…...
全面理解-友元(friend关键字)
在 C 中,friend 关键字用于授予其他类或函数访问当前类的 私有(private)和保护(protected)成员 的权限。这种机制打破了严格的封装性,但可以在特定场景下提高代码的灵活性和效率。以下是 friend 的详细说明…...
【Java】多线程和高并发编程(三):锁(下)深入ReentrantReadWriteLock
文章目录 4、深入ReentrantReadWriteLock4.1 为什么要出现读写锁4.2 读写锁的实现原理4.3 写锁分析4.3.1 写锁加锁流程概述4.3.2 写锁加锁源码分析4.3.3 写锁释放锁流程概述&释放锁源码 4.4 读锁分析4.4.1 读锁加锁流程概述4.4.1.1 基础读锁流程4.4.1.2 读锁重入流程4.4.1.…...
macbook2015升级最新MacOS 白苹果变黑苹果
原帖:https://www.bilibili.com/video/BV13V411c7xz/MAC OS系统发布了最新的Sonoma,超酷的动效锁屏壁纸,多样性的桌面小组件,但是也阉割了很多老款机型的升级权利,所以我们可以逆向操作,依旧把老款MAC设备强…...
如何使用C++将处理后的信号保存为PNG和TIFF格式
在信号处理领域,我们常常需要将处理结果以图像的形式保存下来,方便后续分析和展示。C提供了多种库来处理图像数据,本文将介绍如何使用stb_image_write库保存为PNG格式图像以及使用OpenCV库保存为TIFF格式图像。 1. PNG格式保存 使用stb_ima…...
