GpuInstancingAnimation
使用GPU顶点动画替代蒙皮骨骼动画
效果
上万个模型同时播放动画,批次仅22,且稳定在100多帧
原理
使用Animation的Sample与BakeMesh函数将动画的顶点坐标绘制到贴图中,然后在Shader的顶点着色器中使用该贴图反推动画播放过程中顶点的坐标。后续人物动画由Shader完成,且不需要动画组件。
代码
/*
* Created by jiadong chen
* http://www.chenjd.me
*/
using System.IO;
using UnityEditor;
using UnityEngine;
public class AnimMapBakerWindow : EditorWindow
{
private enum SaveStrategy
{
AnimMap,//only anim map
Mat,//with shader
Prefab//prefab with mat
}
#region FIELDS
// 目标对象
private static GameObject _targetGo;
// 烘焙器
private static AnimMapBaker _baker;
// 路径
private static string _path = "DefaultPath";
// 路径补充
private static string _subPath = "SubPath";
// 保存策略
private static SaveStrategy _stratege = SaveStrategy.AnimMap;
// 材质Shader
private static Shader _animMapShader;
#endregion FIELDS
#region METHODS
[MenuItem("Tool/AnimMapBaker")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(AnimMapBakerWindow));
_baker = new AnimMapBaker();
_animMapShader = Shader.Find("chenjd/AnimMapShader");
}
private void OnGUI()
{
_targetGo = (GameObject)EditorGUILayout.ObjectField(_targetGo, typeof(GameObject), true);
_subPath = _targetGo == null ? _subPath : _targetGo.name;
EditorGUILayout.LabelField(string.Format($"output path:{Path.Combine(_path, _subPath)}"));
_path = EditorGUILayout.TextField(_path);
_subPath = EditorGUILayout.TextField(_subPath);
_stratege = (SaveStrategy)EditorGUILayout.EnumPopup("output type:", _stratege);
if (!GUILayout.Button("Bake")) return;
if (_targetGo == null)
{
EditorUtility.DisplayDialog("错误", "目标对象为空", "OK");
return;
}
if (_baker == null)
{
_baker = new AnimMapBaker();
}
// 写入动画对象
_baker.SetAnimData(_targetGo);
// 烘焙
var list = _baker.Bake();
if (list == null) return;
foreach (var t in list)
{
var data = t;
// 保存烘焙信息
Save(ref data);
}
}
private void Save(ref BakedData data)
{
switch (_stratege)
{
case SaveStrategy.AnimMap:
SaveAsAsset(ref data);
break;
case SaveStrategy.Mat:
SaveAsMat(ref data);
break;
case SaveStrategy.Prefab:
SaveAsPrefab(ref data);
break;
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
///
/// 保存为贴图
///
///
///
private Texture2D SaveAsAsset(ref BakedData data)
{
var folderPath = CreateFolder();
// 创建贴图
var animMap = new Texture2D(data.AnimMapWidth, data.AnimMapHeight, TextureFormat.RGBAHalf, false);
// 加载数据到贴图
animMap.LoadRawTextureData(data.RawAnimMap);
// 创建对象
AssetDatabase.CreateAsset(animMap, Path.Combine(folderPath, data.Name + ".asset"));
return animMap;
}
///
/// 保存为材质
///
///
///
private Material SaveAsMat(ref BakedData data)
{
if (_animMapShader == null)
{
EditorUtility.DisplayDialog("err", "shader is null!!", "OK");
return null;
}
if (_targetGo == null || !_targetGo.GetComponentInChildren())
{
EditorUtility.DisplayDialog("err", "SkinnedMeshRender is null!!", "OK");
return null;
}
var smr = _targetGo.GetComponentInChildren();
var mat = new Material(_animMapShader);
var animMap = SaveAsAsset(ref data);
mat.SetTexture("_MainTex", smr.sharedMaterial.mainTexture);
mat.SetTexture("_AnimMap", animMap);
mat.SetFloat("_AnimLen", data.AnimLen);
var folderPath = CreateFolder();
AssetDatabase.CreateAsset(mat, Path.Combine(folderPath, data.Name + ".mat"));
return mat;
}
///
/// 保存为预制体
///
///
private void SaveAsPrefab(ref BakedData data)
{
var mat = SaveAsMat(ref data);
if (mat == null)
{
EditorUtility.DisplayDialog("err", "mat is null!!", "OK");
return;
}
var go = new GameObject();
go.AddComponent().sharedMaterial = mat;
go.AddComponent().sharedMesh = _targetGo.GetComponentInChildren().sharedMesh;
var folderPath = CreateFolder();
PrefabUtility.SaveAsPrefabAsset(go, Path.Combine(folderPath, data.Name + ".prefab")
.Replace("\\", "/"));
}
///
/// 创建文件夹
///
///
private static string CreateFolder()
{
var folderPath = Path.Combine("Assets/" + _path, _subPath);
if (!AssetDatabase.IsValidFolder(folderPath))
{
AssetDatabase.CreateFolder("Assets/" + _path, _subPath);
}
return folderPath;
}
#endregion METHODS
}
/*
* Created by jiadong chen
* http://www.chenjd.me
*
* 用来烘焙动作贴图。烘焙对象使用animation组件,并且在导入时设置Rig为Legacy
*/
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
///
/// 保存需要烘焙的动画的相关数据
///
public struct AnimData
{
#region FIELDS
// 顶点数
private int _vertexCount;
// 贴图宽度
private int _mapWidth;
// 动画Clip
private readonly List _animClips;
private string _name;
private Animation _animation;
private SkinnedMeshRenderer _skin;
public List AnimationClips => _animClips;
public int MapWidth => _mapWidth;
public string Name => _name;
#endregion FIELDS
public AnimData(Animation anim, SkinnedMeshRenderer smr, string goName)
{
_vertexCount = smr.sharedMesh.vertexCount;
_mapWidth = Mathf.NextPowerOfTwo(_vertexCount);
_animClips = new List(anim.Cast());
_animation = anim;
_skin = smr;
_name = goName;
}
#region METHODS
public void AnimationPlay(string animName)
{
_animation.Play(animName);
}
///
/// 采样动画并且烘焙贴图
///
///
public void SampleAnimAndBakeMesh(ref Mesh m)
{
SampleAnim();
BakeMesh(ref m);
}
///
/// 采样动画
///
private void SampleAnim()
{
if (_animation == null)
{
Debug.LogError("animation is null!!");
return;
}
_animation.Sample();
}
///
/// 烘焙材质
///
///
private void BakeMesh(ref Mesh m)
{
if (_skin == null)
{
Debug.LogError("skin is null!!");
return;
}
_skin.BakeMesh(m);
}
#endregion METHODS
}
///
/// 烘焙后的数据
///
public struct BakedData
{
#region FIELDS
private readonly string _name;
private readonly float _animLen;
private readonly byte[] _rawAnimMap;
private readonly int _animMapWidth;
private readonly int _animMapHeight;
#endregion FIELDS
public BakedData(string name, float animLen, Texture2D animMap)
{
_name = name;
_animLen = animLen;
_animMapHeight = animMap.height;
_animMapWidth = animMap.width;
_rawAnimMap = animMap.GetRawTextureData();
}
public int AnimMapWidth => _animMapWidth;
public string Name => _name;
public float AnimLen => _animLen;
public byte[] RawAnimMap => _rawAnimMap;
public int AnimMapHeight => _animMapHeight;
}
///
/// 烘焙器
///
public class AnimMapBaker
{
#region FIELDS
private AnimData? _animData = null;
private Mesh _bakedMesh;
private readonly List _vertices = new List();
private readonly List _bakedDataList = new List();
#endregion FIELDS
#region METHODS
///
/// 设置烘焙对象
///
///
public void SetAnimData(GameObject go)
{
if (go == null)
{
Debug.LogError("go is null!!");
return;
}
// 获取动画组件与蒙皮网格组件
var anim = go.GetComponent();
var smr = go.GetComponentInChildren();
if (anim == null || smr == null)
{
Debug.LogError("anim or smr is null!!");
return;
}
// 创建网格
_bakedMesh = new Mesh();
// 创建动画信息
_animData = new AnimData(anim, smr, go.name);
}
///
/// 开始烘焙
///
///
public List Bake()
{
if (_animData == null)
{
Debug.LogError("bake data is null!!");
return _bakedDataList;
}
//遍历动画动作,每一个动作都生成一个动作图
foreach (var t in _animData.Value.AnimationClips)
{
if (!t.clip.legacy)
{
Debug.LogError(string.Format($"{t.clip.name} is not legacy!!"));
continue;
}
BakePerAnimClip(t);
}
return _bakedDataList;
}
// 烘焙动画剪辑
private void BakePerAnimClip(AnimationState curAnim)
{
var curClipFrame = 0;
float sampleTime = 0;
float perFrameTime = 0;
// 获取离值最近的二次方数(动画帧率*动画长度)
curClipFrame = Mathf.ClosestPowerOfTwo((int)(curAnim.clip.frameRate * curAnim.length));
perFrameTime = curAnim.length / curClipFrame; ;
// 创建贴图
var animMap = new Texture2D(_animData.Value.MapWidth, curClipFrame, TextureFormat.RGBAHalf, true);
// 设置贴图名称
animMap.name = string.Format($"{_animData.Value.Name}_{curAnim.name}.animMap");
// 播放动画
_animData.Value.AnimationPlay(curAnim.name);
for (var i = 0; i < curClipFrame; i++)
{
curAnim.time = sampleTime;
// 采样动画并且烘焙贴图
_animData.Value.SampleAnimAndBakeMesh(ref _bakedMesh);
for (var j = 0; j < _bakedMesh.vertexCount; j++)
{
var vertex = _bakedMesh.vertices[j];
// 将动画顶点坐标写入贴图
animMap.SetPixel(j, i, new Color(vertex.x, vertex.y, vertex.z));
}
sampleTime += perFrameTime;
}
animMap.Apply();
_bakedDataList.Add(new BakedData(animMap.name, curAnim.clip.length, animMap));
}
#endregion METHODS
}
/*
Created by jiadong chen
http://www.chenjd.me
*/
Shader "chenjd/AnimMapShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" { }
// 动画贴图
_AnimMap ("AnimMap", 2D) = "white" { }
// 动画长度
_AnimLen ("Anim Length", Float) = 0
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Cull off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//开启gpu instancing
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float2 uv: TEXCOORD0;
float4 pos: POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv: TEXCOORD0;
float4 vertex: SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
// 动画贴图
sampler2D _AnimMap;
float4 _AnimMap_TexelSize;//x == 1/width
// 动画长度
float _AnimLen;
v2f vert(appdata v, uint vid: SV_VertexID)
{
UNITY_SETUP_INSTANCE_ID(v);
float f = _Time.y / _AnimLen;
// fmod返回余数
fmod(f, 1.0);
float animMap_x = (vid + 0.5) * _AnimMap_TexelSize.x;
float animMap_y = f;
// 返回贴图中坐标位置
float4 pos = tex2Dlod(_AnimMap, float4(animMap_x, animMap_y, 0, 0));
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(pos);
return o;
}
fixed4 frag(v2f i): SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
借鉴于:https://medium.com/chenjd-xyz/how-to-render-10-000-animated-characters-with-20-draw-calls-in-unity-e30a3036349a