当前位置: 首页 > news >正文

《大规模动画优化(一):GPU 顶点动画的生成》

GPU 顶点动画(Vertex Animation Texture, VAT)

GPU 顶点动画(Vertex Animation Texture, VAT)烘焙的核心思想是: 在 CPU
端预先计算动画顶点数据,并存储到纹理(Texture2D)中,在 GPU 端通过 Shader 读取纹理来播放动画。

📌 1. 烘焙的核心步骤

  1. 采样动画帧(AnimationClip)
  2. 获取 SkinnedMeshRenderer 在每帧的顶点数据
  3. 存储到 Texture2D作为动画纹理
  4. 在 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 顶点动画&#xff08;Vertex Animation Texture, VAT&#xff09; GPU 顶点动画&#xff08;Vertex Animation Texture, VAT&#xff09;烘焙的核心思想是&#xff1a; 在 CPU 端预先计算动画顶点数据&#xff0c;并存储到纹理&#xff08;Texture2D&#xff09;中&#xf…...

【前端】几种常见的跨域解决方案

在前端开发中&#xff0c;跨域问题是常见的挑战。以下是几种常见的跨域解决方案&#xff1a; 1. Nginx反向代理 使用 Nginx 进行反向代理是解决跨域问题的一种常见方式。Nginx 会充当一个中间代理服务器&#xff0c;接收来自前端的请求并将其转发到实际的后端 API 服务&#…...

如何在WinForms应用程序中读取和写入App.config文件

如何在WinForms应用程序中读取和写入App.config文件 1. 添加App.config文件2. 配置App.config3. 读取App.config4. 写入App.config 在WinForms应用程序中&#xff0c; App.config文件是用于存储配置数据的标准方式。通过使用.NET框架提供的类库&#xff0c;我们可以方便地对 …...

【分布式理论7】分布式调用之:服务间的(RPC)远程调用

文章目录 一、RPC 调用过程二、RPC 动态代理&#xff1a;屏蔽远程通讯细节1. 动态代理示例2. 如何将动态代理应用于 RPC 三、RPC序列化与协议编码1. RPC 序列化2. RPC 协议编码2.1. 协议编码的作用2.2. RPC 协议消息组成 四、RPC 网络传输1. 网络传输流程2. 关键优化点 一、RPC…...

人工智能应用-智能驾驶精确的目标检测和更高级的路径规划

实现更精确的目标检测和更高级的路径规划策略是自动驾驶领域的核心任务。以下是一个简化的示例&#xff0c;展示如何使用Python和常见的AI库&#xff08;如TensorFlow、OpenCV和A*算法&#xff09;来实现这些功能。 1. 环境准备 首先&#xff0c;确保安装了以下库&#xff1a;…...

dynamic_cast和static_cast和const_cast

dynamic_cast 在 C 中的作用 dynamic_cast 是 C 运行时类型转换&#xff08;RTTI, Run-Time Type Identification&#xff09;的一部分&#xff0c;主要用于&#xff1a; 安全的多态类型转换检查类型的有效性向下转换&#xff08;Downcasting&#xff09;跨类层次的指针或引用…...

DEEPSEEK与GPT等AI技术在机床数据采集与数字化转型中的应用与影响

随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;深度学习、自然语言处理等先进技术开始广泛应用于各行各业。在制造业尤其是机床行业&#xff0c;AI技术的融合带来了巨大的变革&#xff0c;尤其在机床数据采集与机床数字化方面的应用。本文将探讨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博客&#xff0c;通过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 十分火爆&#xff0c;本地部署实现私有知识库&#xff0c;帮助自己日常工作&#xff0c;上一篇使用工具cherry studio可以做到私人知识库。今天学习了一下&#xff0c;使用Dify链接deepseek&#xff0c;实现私人知识库&#xff0c;也非常不错&#xff0c;这里分享…...

Linux中设置开机运行指令

系统&#xff1a;Debian 12 使用systemd来设置开机自启动脚本或命令是一个更加现代且推荐的方法。下面是具体的步骤&#xff1a; 创建守护脚本 首先&#xff0c;你需要创建一个Shell脚本文件&#xff0c;比如mydaemon.sh&#xff0c;并在其中编写你的守护脚本逻辑。确保这个脚…...

IDEA中列举的是否是SpringBoot的依赖项的全部?在哪里能查到所有依赖项,如何开发自己的依赖项让别人使用

在 IntelliJ IDEA 中列举的依赖项并不一定是 Spring Boot 项目的全部依赖项。IDEA 通常只显示你在 pom.xml&#xff08;Maven&#xff09;或 build.gradle&#xff08;Gradle&#xff09;中显式声明的依赖项&#xff0c;而这些依赖项本身可能还会引入其他传递性依赖。 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”。 一个挑战是在大语言模型&#xff08;LLM&#xff09;中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据&#xff08;通常超过 100,000 个示例&#xff09;&#xff0c;但本文展…...

Android studio怎么创建assets目录

在Android Studio中创建assets文件夹是一个简单的步骤&#xff0c;通常用于存储不需要编译的资源文件&#xff0c;如文本文件、图片、音频等 main文件夹&#xff0c;邮件new->folder-assets folder...

常见的前端框架和库有哪些

1. React 描述&#xff1a;由 Facebook 开发的一个 JavaScript 库&#xff0c;用于构建用户界面&#xff0c;尤其是单页面应用&#xff08;SPA&#xff09;。特点&#xff1a; 基于组件的架构&#xff0c;便于重用 UI 组件。使用虚拟 DOM 提升性能。容易与其他库和框架集成。 …...

【批量获取图片信息】批量获取图片尺寸、海拔、分辨率、GPS经纬度、面积、位深度、等图片属性里的详细信息,提取出来后导出表格,基于WPF的详细解决方案

摄影工作室通常会有大量的图片素材&#xff0c;在进行图片整理和分类时&#xff0c;需要知道每张图片的尺寸、分辨率、GPS 经纬度&#xff08;如果拍摄时记录了&#xff09;等信息&#xff0c;以便更好地管理图片资源&#xff0c;比如根据图片尺寸和分辨率决定哪些图片适合用于…...

数据结构与算法(test3)

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

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…...

高频面试之3Zookeeper

高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个&#xff1f;3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制&#xff08;过半机制&#xff0…...

全球首个30米分辨率湿地数据集(2000—2022)

数据简介 今天我们分享的数据是全球30米分辨率湿地数据集&#xff0c;包含8种湿地亚类&#xff0c;该数据以0.5X0.5的瓦片存储&#xff0c;我们整理了所有属于中国的瓦片名称与其对应省份&#xff0c;方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

Java 加密常用的各种算法及其选择

在数字化时代&#xff0c;数据安全至关重要&#xff0c;Java 作为广泛应用的编程语言&#xff0c;提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景&#xff0c;有助于开发者在不同的业务需求中做出正确的选择。​ 一、对称加密算法…...