《大规模动画优化(一):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)中…...

【前端】几种常见的跨域解决方案
在前端开发中,跨域问题是常见的挑战。以下是几种常见的跨域解决方案: 1. Nginx反向代理 使用 Nginx 进行反向代理是解决跨域问题的一种常见方式。Nginx 会充当一个中间代理服务器,接收来自前端的请求并将其转发到实际的后端 API 服务&#…...
如何在WinForms应用程序中读取和写入App.config文件
如何在WinForms应用程序中读取和写入App.config文件 1. 添加App.config文件2. 配置App.config3. 读取App.config4. 写入App.config 在WinForms应用程序中, App.config文件是用于存储配置数据的标准方式。通过使用.NET框架提供的类库,我们可以方便地对 …...

【分布式理论7】分布式调用之:服务间的(RPC)远程调用
文章目录 一、RPC 调用过程二、RPC 动态代理:屏蔽远程通讯细节1. 动态代理示例2. 如何将动态代理应用于 RPC 三、RPC序列化与协议编码1. RPC 序列化2. RPC 协议编码2.1. 协议编码的作用2.2. RPC 协议消息组成 四、RPC 网络传输1. 网络传输流程2. 关键优化点 一、RPC…...
人工智能应用-智能驾驶精确的目标检测和更高级的路径规划
实现更精确的目标检测和更高级的路径规划策略是自动驾驶领域的核心任务。以下是一个简化的示例,展示如何使用Python和常见的AI库(如TensorFlow、OpenCV和A*算法)来实现这些功能。 1. 环境准备 首先,确保安装了以下库:…...
dynamic_cast和static_cast和const_cast
dynamic_cast 在 C 中的作用 dynamic_cast 是 C 运行时类型转换(RTTI, Run-Time Type Identification)的一部分,主要用于: 安全的多态类型转换检查类型的有效性向下转换(Downcasting)跨类层次的指针或引用…...

DEEPSEEK与GPT等AI技术在机床数据采集与数字化转型中的应用与影响
随着人工智能(AI)技术的迅猛发展,深度学习、自然语言处理等先进技术开始广泛应用于各行各业。在制造业尤其是机床行业,AI技术的融合带来了巨大的变革,尤其在机床数据采集与机床数字化方面的应用。本文将探讨DEEPSEEK、…...
高速存储文章目录
《zynq tcp万兆网和ftp协议分析-CSDN博客》 《国产fpga nvme ip高速存储方案设计_fpga 高速存储-CSDN博客》 《国微pcie switch 8748高速存储方案设计_国产pcie switch-CSDN博客》 《FPGA SATA高速存储设计-CSDN博客》 《FPGA NVME高速存储设计_690t fpga-CSDN博客》 《zy…...

车载测试工具 --- CANoe VH6501 进行Not Acknowledge (NAck) 测试
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...

【清晰教程】通过Docker为本地DeepSeek-r1部署WebUI界面
【清晰教程】本地部署DeepSeek-r1模型-CSDN博客 目录 安装Docker 配置&检查 Open WebUI 部署Open WebUI 安装Docker 完成本地DeepSeek-r1的部署后【清晰教程】本地部署DeepSeek-r1模型-CSDN博客,通过Docker为本地DeepSeek-r1部署WebUI界面。 访问Docker官…...
Linux运维——用户管理
Linux用户管理 一、Linux用户管理要点二、常用命令2.1、groupadd2.2、groupdel2.3、groupmod2.4、groups2.5、useradd2.6、userdel2.7、passwd2.9、su2.10、sudo2.10.1、给普通用户授权 sudo2.10.2、 免密码授权 sudo 一、Linux用户管理要点 创建用户组 - 使用 groupadd删除用…...

mac下dify+deepseek部署,实现私人知识库
目前deepseek 十分火爆,本地部署实现私有知识库,帮助自己日常工作,上一篇使用工具cherry studio可以做到私人知识库。今天学习了一下,使用Dify链接deepseek,实现私人知识库,也非常不错,这里分享…...
Linux中设置开机运行指令
系统:Debian 12 使用systemd来设置开机自启动脚本或命令是一个更加现代且推荐的方法。下面是具体的步骤: 创建守护脚本 首先,你需要创建一个Shell脚本文件,比如mydaemon.sh,并在其中编写你的守护脚本逻辑。确保这个脚…...
IDEA中列举的是否是SpringBoot的依赖项的全部?在哪里能查到所有依赖项,如何开发自己的依赖项让别人使用
在 IntelliJ IDEA 中列举的依赖项并不一定是 Spring Boot 项目的全部依赖项。IDEA 通常只显示你在 pom.xml(Maven)或 build.gradle(Gradle)中显式声明的依赖项,而这些依赖项本身可能还会引入其他传递性依赖。 1. 如何…...
Ollama命令使用指南
Ollama 命令使用指南 Ollama 命令使用指南1. Ollama 命令概览2. Ollama 命令详解2.1 启动 Ollama2.2 创建模型2.3 查看模型信息2.4 运行模型2.5 停止运行的模型2.6 从注册表拉取模型2.7 推送模型到注册表2.8 列出本地模型2.9 查看正在运行的模型2.10 复制模型2.11 删除模型 3. …...

LIMO:上海交大的工作 “少即是多” LLM 推理
25年2月来自上海交大、SII 和 GAIR 的论文“LIMO: Less is More for Reasoning”。 一个挑战是在大语言模型(LLM)中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据(通常超过 100,000 个示例),但本文展…...

Android studio怎么创建assets目录
在Android Studio中创建assets文件夹是一个简单的步骤,通常用于存储不需要编译的资源文件,如文本文件、图片、音频等 main文件夹,邮件new->folder-assets folder...
常见的前端框架和库有哪些
1. React 描述:由 Facebook 开发的一个 JavaScript 库,用于构建用户界面,尤其是单页面应用(SPA)。特点: 基于组件的架构,便于重用 UI 组件。使用虚拟 DOM 提升性能。容易与其他库和框架集成。 …...

【批量获取图片信息】批量获取图片尺寸、海拔、分辨率、GPS经纬度、面积、位深度、等图片属性里的详细信息,提取出来后导出表格,基于WPF的详细解决方案
摄影工作室通常会有大量的图片素材,在进行图片整理和分类时,需要知道每张图片的尺寸、分辨率、GPS 经纬度(如果拍摄时记录了)等信息,以便更好地管理图片资源,比如根据图片尺寸和分辨率决定哪些图片适合用于…...

数据结构与算法(test3)
七、查找 1. 看图填空 查找表是由同一类型的数据元素(或记录)构成的集合。例如上图就是一个查找表。 期中(1)是______________. (2)是______________(3)是_____关键字_______。 2. 查找(Searching) 就是根据给定的某个值, 在查…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...

android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...