【unity】GPU顶点动画

ops/2025/3/17 11:27:17/

unity顶点动画

  • 工具篇
    • 模型合并
    • 生成动画数据
    • 工具调用:
    • Shader处理

工具篇

模型合并

根据原模型创建预制件:
清理旧的合并结果,创建新容器对象
PS:注意 DestroyImmediate仅在 Editor 模式下有效,运行时需用Destroy

var targetObj = GameObject.Find("targetObj");
if (targetObj != null)
{GameObject.DestroyImmediate(targetObj);
}
targetObj = new GameObject("targetObj");
targetObj.transform.localPosition = Vector3.zero;
// ... 其他变换重置 ...

收集网格和材质信息
获取所有需要合并的网格组件,包含静态和蒙皮网格。

MeshRenderer[] allMeshRender = sourceObj.GetComponentsInChildren<MeshRenderer>();
SkinnedMeshRenderer[] SkinnedMeshRenderer = sourceObj.GetComponentsInChildren<SkinnedMeshRenderer>();
MeshFilter[] allfilter = sourceObj.GetComponentsInChildren<MeshFilter>();

填充 MeshInfo 列表
PS: 必须添加:subMeshIndex 因为需要用于处理多材质网格的分段合并。

foreach (var filter in allfilter)
{for (int subMeshIndex = 0; subMeshIndex < filter.sharedMesh.subMeshCount; subMeshIndex++){MeshInfo meshInfo = new MeshInfo();meshInfo.mesh = filter.sharedMesh;meshInfo.subIndex = subMeshIndex;meshInfo.matrix4x4 = filter.transform.localToWorldMatrix;meshInfos.Add(meshInfo);}
}

SkinnedMeshRenderer也需要处理

贴图合并
需要创建一个大的贴图Texture2D,获取所有materials材质球的_MainTex(我只处理了_MainTex,当然也可以处理法线贴图这些其他内容)
PS: COMBINE_TEXTURE_WIDTH是有问题的,后续需要修改,不能这样累加

使用的Texture2D.PackTextures进行打包到图集中的

for (int i = 0; i < materials.Count; i++)
{Texture2D maintexture = materials[i].GetTexture("_MainTex") as Texture2D;COMBINE_TEXTURE_WIDTH += maintexture.width;COMBINE_TEXTURE_HEIGHT += maintexture.height;MainTexs.Add(maintexture);
}
Texture2D newMainTex = new Texture2D(COMBINE_TEXTURE_WIDTH, COMBINE_TEXTURE_HEIGHT, TextureFormat.RGBA32, true);
Rect[] uvs = newMainTex.PackTextures(MainTexs.ToArray(), 0);

UV需要进行偏移

List<Vector2[]> oldUV = new List<Vector2[]>();
Vector2[] uva, uvb;
for (int j = 0; j < combines.Count; j++)
{uva = combines[j].mesh.uv;uvb = new Vector2[uva.Length];for (int k = 0; k < uva.Length; k++){uvb[k] = new Vector2((uva[k].x * uvs[j].width) + uvs[j].x, (uva[k].y * uvs[j].height) + uvs[j].y);}oldUV.Add(uva);combines[j].mesh.uv = uvb;
}

最后保存后记得还原uv:

//重新赋值,以免影响其他对象的Mesh
for (int i = 0; i < combines.Count; i++)
{combines[i].mesh.uv = oldUV[i];
}

最后将贴图赋值到一个新建的材质球上进行保存就好了

Material newMaterial = new Material(mainShader);
newMaterial.SetTexture("_MainTex", newMainTex);

合并网格设置

List<CombineInstance> combines = new List<CombineInstance>();
foreach (var i in meshInfos)
{var ci = new CombineInstance();ci.mesh = i.mesh;ci.subMeshIndex = i.subIndex;ci.transform = i.matrix4x4;combines.Add(ci);
}

需要用到CombineInstance方法进行储存
在这里插入图片描述
然后使用CombineMeshes方法进行合并

Mesh mesh = new Mesh();
mesh.CombineMeshes(combines.ToArray(), true, false);

最后进行保存到对应路径:

AssetDatabase.CreateAsset(newMainTex, $"{MaterialTexturePath}CombineTexture.asset");
AssetDatabase.CreateAsset(newMaterial, $"{MaterialTexturePath}CombineMaterial.mat");
AssetDatabase.CreateAsset(mesh, $"{MaterialTexturePath}CombineMaterial.asset");

完整代码:


using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;public struct MeshInfo
{public Mesh mesh;public Matrix4x4 matrix4x4;public int subIndex;
}public class CombineVertexTool
{const string MaterialTexturePath = "Assets/Resources/Material/";public static void CombineFilter(GameObject sourceObj){var targetObj = GameObject.Find("targetObj");if (targetObj != null){GameObject.DestroyImmediate(targetObj);}targetObj = new GameObject("targetObj");targetObj.transform.localPosition = Vector3.zero;targetObj.transform.localRotation = Quaternion.identity;targetObj.transform.localScale = Vector3.one;MeshRenderer[] allMeshRender = sourceObj.GetComponentsInChildren<MeshRenderer>(); //包含MeshRenderer和SkinnedMeshRendererSkinnedMeshRenderer[] SkinnedMeshRenderer = sourceObj.GetComponentsInChildren<SkinnedMeshRenderer>();MeshFilter[] allfilter = sourceObj.GetComponentsInChildren<MeshFilter>(); //网格List<MeshInfo> meshInfos = new List<MeshInfo>();foreach (var filter in allfilter){for (int subMeshIndex = 0; subMeshIndex < filter.sharedMesh.subMeshCount; subMeshIndex++){MeshInfo meshInfo = new MeshInfo();meshInfo.mesh = filter.sharedMesh;meshInfo.subIndex = subMeshIndex;meshInfo.matrix4x4 = filter.transform.localToWorldMatrix;meshInfos.Add(meshInfo);}}//材质球数组List<Material> materials = new List<Material>();foreach (var i in allMeshRender){foreach (var j in i.sharedMaterials){materials.Add(j);}}foreach (SkinnedMeshRenderer i in SkinnedMeshRenderer){foreach (var j in i.sharedMaterials){materials.Add(j);}for (int subMeshIndex = 0; subMeshIndex < i.sharedMesh.subMeshCount; subMeshIndex++){MeshInfo meshInfo = new MeshInfo();meshInfo.mesh = i.sharedMesh;meshInfo.subIndex = subMeshIndex;meshInfo.matrix4x4 = i.transform.localToWorldMatrix;meshInfos.Add(meshInfo);} }Shader mainShader = materials[0].shader; //要求所有shader都必须使用同一个shader// 合并 Mesh// 后去自身和子物体中所有 MsehFilter 组件List<CombineInstance> combines = new List<CombineInstance>();foreach (var i in meshInfos){var ci = new CombineInstance();ci.mesh = i.mesh;ci.subMeshIndex = i.subIndex;//ci.mesh.triangles = i.mesh.triangles; //添加会改变原模型的网格ci.transform = i.matrix4x4;combines.Add(ci);}//收集所有材质贴图List<Texture2D> MainTexs = new List<Texture2D>();int COMBINE_TEXTURE_WIDTH = 0;int COMBINE_TEXTURE_HEIGHT = 0;for (int i = 0; i < materials.Count; i++){Texture2D maintexture = materials[i].GetTexture("_MainTex") as Texture2D;COMBINE_TEXTURE_WIDTH += maintexture.width;COMBINE_TEXTURE_HEIGHT += maintexture.height;MainTexs.Add(maintexture);}//所有贴图合并到newDiffuseTex这张大贴图上Texture2D newMainTex = new Texture2D(COMBINE_TEXTURE_WIDTH, COMBINE_TEXTURE_HEIGHT, TextureFormat.RGBA32, true);Rect[] uvs = newMainTex.PackTextures(MainTexs.ToArray(), 0);List<Vector2[]> oldUV = new List<Vector2[]>();Vector2[] uva, uvb;// 遍历所有合并实例(CombineInstance)for (int j = 0; j < combines.Count; j++){// 获取当前网格的原始UV数组uva = combines[j].mesh.uv;// 创建新UV数组(长度与原始UV相同)uvb = new Vector2[uva.Length];for (int k = 0; k < uva.Length; k++){uvb[k] = new Vector2((uva[k].x * uvs[j].width) + uvs[j].x, (uva[k].y * uvs[j].height) + uvs[j].y);}oldUV.Add(uva);combines[j].mesh.uv = uvb;}Material newMaterial = new Material(mainShader);newMaterial.SetTexture("_MainTex", newMainTex);Mesh mesh = new Mesh();mesh.CombineMeshes(combines.ToArray(), // 合并实例数组true,              // mergeSubMeshes:是否合并子网格false              // useMatrices:是否应用变换矩阵);AssetDatabase.CreateAsset(newMainTex, $"{MaterialTexturePath}CombineTexture.asset");AssetDatabase.CreateAsset(newMaterial, $"{MaterialTexturePath}CombineMaterial.mat");AssetDatabase.CreateAsset(mesh, $"{MaterialTexturePath}CombineMaterial.asset");MeshFilter meshFilter = targetObj.AddComponent<MeshFilter>();meshFilter.sharedMesh = mesh;MeshRenderer meshRenderer = targetObj.AddComponent<MeshRenderer>();meshRenderer.sharedMaterial = newMaterial;//重新赋值,以免影响其他对象的Meshfor (int i = 0; i < combines.Count; i++){combines[i].mesh.uv = oldUV[i];}AssetDatabase.SaveAssets();AssetDatabase.Refresh();}
}

PS:需要开启纹理贴图,模型可读写:
在这里插入图片描述
在这里插入图片描述

生成动画数据

根据动画时长和帧率计算总帧数,确定纹理尺寸

AnimationClip clip = clips[0];
//foreach (var clip in clips)
//{
//根据动画时长和帧率计算总帧数,确定纹理尺寸
int animLength = Mathf.CeilToInt(clip.frameRate * clip.length);
int texwidth = meshRenderer.sharedMesh.vertexCount;
texwidth = Mathf.NextPowerOfTwo(texwidth);//取顶点数的下一个二次幂(适配 GPU 纹理要求)
int texHeight = animLength;

生成一张纹理:

Texture2D tex = new Texture2D(texwidth, texHeight, TextureFormat.RGBAHalf, false);

逐帧写入纹理

// 步骤3:逐帧处理
for (int i = 0; i < animLength; i++)
{float time = i / clip.frameRate;//Clip.SampleAnimation(GameObject, time)。Clip是我们需要采样的动画剪辑,输入游戏对象后和采样的时间后,就可以在不运行的情况下直接播放动画,加速烘焙过程clip.SampleAnimation(sourceObj, time);// B. 烘焙网格Mesh bakedMesh = new Mesh();//烘焙当前帧顶点数据meshRenderer.BakeMesh(bakedMesh);Vector3[] vertices = bakedMesh.vertices;// 写入纹理行(将没一帧顶点数据转换成color数据保存到图片)for (int frame = 0; frame < meshRenderer.sharedMesh.vertexCount; frame++){// 超出原始顶点数的部分填充黑色Color pixel = Color.black;if (frame <= vertices.Length){Vector3 pos = vertices[frame];pixel = new Color(pos.x, pos.y, pos.z, 1);}tex.SetPixel(frame, i, pixel);//x要设置的像素的 x 坐标。范围为 0 到(纹理宽度 - 1),y要设置的像素的 y 坐标。范围为 0 到(纹理高度 - 1),color要设置的颜色。tex.Apply(false); // 可选:分批提交数据(当设置为 true 时,将重新计算多级渐进纹理级别)}
}

完整代码

/// <summary>
/// 生成动画数据(顶点动画)
/// </summary>
public static void CreateVertexAnimaSettingData(GameObject sourceObj)
{var animator = sourceObj.GetComponent<Animator>();var clips = animator.runtimeAnimatorController.animationClips;//检索控制器使用的所有 AnimationClipanimator.speed = 1;//MeshFilter meshFilter = sourceObj.GetComponentInChildren<MeshFilter>();SkinnedMeshRenderer meshRenderer = sourceObj.GetComponentInChildren<SkinnedMeshRenderer>();AnimationClip clip = clips[0];//foreach (var clip in clips)//{//根据动画时长和帧率计算总帧数,确定纹理尺寸int animLength = Mathf.CeilToInt(clip.frameRate * clip.length);int texwidth = meshRenderer.sharedMesh.vertexCount;texwidth = Mathf.NextPowerOfTwo(texwidth);//取顶点数的下一个二次幂(适配 GPU 纹理要求)int texHeight = animLength;Texture2D tex = new Texture2D(texwidth, texHeight, TextureFormat.RGBAHalf, false);// 步骤3:逐帧处理for (int i = 0; i < animLength; i++){float time = i / clip.frameRate;//Clip.SampleAnimation(GameObject, time)。Clip是我们需要采样的动画剪辑,输入游戏对象后和采样的时间后,就可以在不运行的情况下直接播放动画,加速烘焙过程clip.SampleAnimation(sourceObj, time);// B. 烘焙网格Mesh bakedMesh = new Mesh();//烘焙当前帧顶点数据meshRenderer.BakeMesh(bakedMesh);Vector3[] vertices = bakedMesh.vertices;// 写入纹理行(将没一帧顶点数据转换成color数据保存到图片)for (int frame = 0; frame < meshRenderer.sharedMesh.vertexCount; frame++){// 超出原始顶点数的部分填充黑色Color pixel = Color.black;if (frame <= vertices.Length){Vector3 pos = vertices[frame];pixel = new Color(pos.x, pos.y, pos.z, 1);}tex.SetPixel(frame, i, pixel);//x要设置的像素的 x 坐标。范围为 0 到(纹理宽度 - 1),y要设置的像素的 y 坐标。范围为 0 到(纹理高度 - 1),color要设置的颜色。tex.Apply(false); // 可选:分批提交数据(当设置为 true 时,将重新计算多级渐进纹理级别)}}tex.Apply(true); // 最终提交tex.name = sourceObj.name;//}AssetDatabase.CreateAsset(tex, $"{MaterialTexturePath}{sourceObj.name}_VerTex.asset");AssetDatabase.SaveAssets();AssetDatabase.Refresh();
}

我的动画计算出来是19帧,所以height为19像素
在这里插入图片描述

工具调用:

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using static Unity.VisualScripting.Member;public class BuildGpuAnimaEditorVertex : EditorWindow
{[MenuItem("Tools/GPU动画处理工具(顶点动画)")]static void CreateSecetionData(){EditorWindow.GetWindow<BuildGpuAnimaEditorVertex>(true, "GPU动画处理工具(顶点动画)", true).Show();}private GameObject source;public void OnGUI(){EditorGUILayout.BeginHorizontal();EditorGUILayout.LabelField("源资源:", GUILayout.Width(60));source = EditorGUILayout.ObjectField(source, typeof(GameObject), true) as GameObject;EditorGUILayout.EndHorizontal();EditorGUILayout.Space();EditorGUILayout.LabelField("根据源资源生成对应动画数据贴图资源", GUILayout.Width(240));EditorGUILayout.Space();if (GUILayout.Button("开始", GUILayout.Height(20))){CombineVertexTool.CreateVertexAnimaSettingData(source);}EditorGUILayout.Space();EditorGUILayout.LabelField("根据shader和模型生成材质贴图和合并网格", GUILayout.Width(240));EditorGUILayout.Space();if (GUILayout.Button("开始", GUILayout.Height(20))){CombineVertexTool.CombineFilter(source);}}
}

Shader处理

需要添加 #pragma multi_compile_instancing开启GPU 实例化
可以参考这位大佬讲解(UnityShader支持GPU Instance的方法)

Shader"XXX"{Properties{...}SubShader{...Pass{...CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_instancing //这里,第一步...struct a2v{...UNITY_VERTEX_INPUT_INSTANCE_ID //这里,第二步};struct v2f{...UNITY_VERTEX_INPUT_INSTANCE_ID //这里,第二步};v2f vert(a2v v){v2f o;UNITY_SETUP_INSTANCE_ID(v); //这里第三步UNITY_TRANSFER_INSTANCE_ID(v,o); //第三步...return o;}fixed4 frag(v2f i):SV_Target{UNITY_SETUP_INSTANCE_ID(i); //最后一步...}ENDCG}}FallBack"Diffuse"
}

完整代码:

Shader "Unlit/yu2"
{Properties{_MainTex ("Texture", 2D) = "white" {} // 主纹理,用于基础颜色_AnimTex ("Texture", 2D) = "white" {}// 动画纹理,存储顶点位置_AnimTime ("AnimTime", float) = 1// 动画速度控制参数}SubShader{Tags { "RenderType"="Opaque" }// 不透明渲染类型LOD 100 // 细节层级Pass{CGPROGRAM#pragma target 3.5      //使用Shader Model 3.5#pragma vertex vert    // 顶点着色器#pragma fragment frag  // 像素着色器// make fog work#pragma multi_compile_instancing // 启用GPU实例化#include "UnityCG.cginc" //引用unity自带的UnityCG//顶点输入与输出结构struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;uint vid : SV_VERTEXID;UNITY_VERTEX_INPUT_INSTANCE_ID};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION; //通过vid在动画纹理中定位顶点位置UNITY_VERTEX_INPUT_INSTANCE_ID};//CBUFFER_START 常量缓冲区:使用后才会允许支持我们的Shader被SRP Batcher(可编程渲染管线合批)允许,从而节省渲染上的性能CBUFFER_START(UnityPerMaterial)sampler2D _MainTex;sampler2D _AnimTex;//float4 _MainTex_ST;//必须在特殊命名的常量缓冲区中定义每个实例的属性。使用这对宏来包装对每个实例唯一的属性。UNITY_INSTANCING_BUFFER_START(Props)UNITY_DEFINE_INSTANCED_PROP(float4, _AnimTex_TexelSize)//_TexelSize 是一个内置变量,用于获取纹理的像素尺寸信息。这个变量是一个 Vector4,包含了纹理的宽度、高度以及它们的倒数 _AnimTex_TexelSize.xyzw = (1/width, 1/height, width, height)UNITY_DEFINE_INSTANCED_PROP(float4, _MainTex_ST)//是贴图_MainTex的tiling和offset的四元数,_MainTex_ST.xy 是tiling的值,_MainTex_ST.zw 是offset的值 在Material中可以设置的Tiling就是xy,Offset就是zw。UNITY_DEFINE_INSTANCED_PROP(float, _AnimTime)UNITY_INSTANCING_BUFFER_END(Props)CBUFFER_END//UNITY_ACCESS_INSTANCED_PROP : 从缓冲区中获取实例的属性值,arrayName与UNITY_INSTANCING_BUFFER_START(name)对应v2f vert (appdata v){v2f o;UNITY_SETUP_INSTANCE_ID(v);UNITY_TRANSFER_INSTANCE_ID(v, o);// 仅当您要访问片元着色器中的实例化属性时才需要//为什么要加 0.5//像素中心对齐:避免采样时插值到相邻像素。数学原理:将顶点 ID 映射到纹理像素中心坐标。公式推导:纹理U坐标 = (顶点ID + 0.5) * (1.0 / 纹理宽度)float4 uv_anim = float4((v.vid + 0.5) * UNITY_ACCESS_INSTANCED_PROP(Props, _AnimTex_TexelSize).x, _Time.y / UNITY_ACCESS_INSTANCED_PROP(Props, _AnimTime),0.0,0.0);//tex2Dlod 函数,从动画纹理中读取顶点位置   参数说明 _AnimTex:存储顶点位置数据的纹理//uv_anim.xy:计算得到的 UV 坐标//uv_anim.zw:LOD 级别(此处设为 0)v.vertex = float4(tex2Dlod(_AnimTex, uv_anim).xyz,1.0);//v.vertex.y = v.vertex.y - 0.5;o.vertex = UnityObjectToClipPos(v.vertex);//模型空间转到屏幕裁切空间o.uv = v.uv * UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_ST).xy + UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_ST).zw; //就是将模型顶点的uv和Tiling、Offset两个变量进行运算,计算出实际显示用的定点uv。return o;}fixed4 frag (v2f i) : SV_Target{UNITY_SETUP_INSTANCE_ID(i);// 仅当要在片元着色器中访问任何实例化属性时才需要fixed4 col = tex2D(_MainTex, i.uv);//采样贴图return col;}ENDCG}}
}

大部分参考和借鉴:[Unity] GPU动画实现1-5这位大佬的文章


http://www.ppmy.cn/ops/166479.html

相关文章

Android Room 框架领域层源码深度剖析(二)

一、引言 在 Android 开发的架构设计中&#xff0c;领域层&#xff08;Domain Layer&#xff09;扮演着至关重要的角色。它是应用程序的核心业务逻辑所在之处&#xff0c;负责处理业务规则、协调数据流动以及实现用例。Android Room 框架虽然主要聚焦于数据持久化&#xff0c;…

中考英语之07句子成分

在初中英语中&#xff0c;句子成分主要包括主语、谓语、宾语、表语、定语、状语、补语等。以下是对它们的详细介绍&#xff1a; 主语&#xff1a; 是句子所描述的对象&#xff0c;通常表示动作的执行者或被描述的主体。一般由名词、代词、数词、不定式、动名词或从句等充当。…

深入剖析 MetaSpace OOM 问题:根因分析与高效解决策略

目录 一、MetaSpace 区 OOM&#xff1a;概述 &#xff08;一&#xff09; MetaSpace的变革与挑战 &#xff08;二&#xff09;MetaSpace OOM的影响 &#xff08;三&#xff09; 为什么要关注MetaSpace OOM 二、MetaSpace 区 OOM的根本原因 &#xff08;一&#xff09;Met…

爬虫基础之爬取豆瓣同城信息(保存为csv excel 数据库)

网站:长沙最近一周戏剧活动_豆瓣 温馨提示: 本案例仅供学习交流使用 本案例所使用的模块 requests(发送HTTP请求)pandas(数据保存模块)lxml(用于解析数据模块)csv(用于保存为csv文件)pymysql(用于操作数据库)parsel(解析数据的模块) 确定爬取的信息内容&#xff1a; 戏剧的名称…

Linux中安装MySQL

检查是否有MySQL服务并卸载 检查并卸载 在安装MySQL数据库之前&#xff0c;我们需要先检查一下当前Linux系统中&#xff0c;是否安装的有MySQL的相关服务&#xff08;很多linux安装完毕之后&#xff0c;自带了低版本的mysql的依赖包&#xff09;&#xff0c;如果有&#xff0c…

【JavaEE进阶】@Transactional 详解

目录 &#x1f343;前言 &#x1f332;rollbackFor(异常回滚属性) &#x1f6a9;rollbackFor属性 &#x1f6a9;noRollbackFor属性 &#x1f384;Isolation(事务隔离级别) &#x1f6a9;MySQL事务的隔离级别 &#x1f6a9;Spring事务隔离级别 &#x1f38b;Spring事务传…

【差分约束】 P3275 [SCOI2011] 糖果|省选-

本文涉及知识点 差分约束 P3275 [SCOI2011] 糖果 题目描述 幼儿园里有 N N N 个小朋友&#xff0c; lxhgww \text{lxhgww} lxhgww 老师现在想要给这些小朋友们分配糖果&#xff0c;要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心&#xff0c;总是会提出一些要求&…

嵌入式八股C语言---面向对象篇

面向对象与面向过程 面向过程 就是把整个业务逻辑分成多个步骤,每步或每一个功能都可以使用一个函数来实现面向对象 对象是类的实例化,此时一个类就内部有属性和相应的方法 封装 在C语言里实现封装就是实现一个结构体,里面包括的成员变量和函数指针,然后在构造函数中,为结构体…