参考:
https://connect.unity.com/p/render-crowd-of-animated-characters?signup=true
https://github.com/chenjd/Render-Crowd-Of-Animated-Characters
注意几点:
mesh不太像例子里面一样有scale,这样导致选中物体却找不到物体,因为被缩放为0.01;
动画需要是legacy
gpu instance需要opengl es3.0;
注意在shader里面勾选enable instance选项;
贴图格式为rgbhalf,每个通道16位,增加精度以保存坐标;
shader里面从贴图中采样要用tex2dLod确保采样的是mipmap第0级,因为这里的贴图不是普通意义上的贴图,而主要作用是存放信息。
另外,这个代码有些问题,color里面直接存放坐标,坐标为负数怎么办
所以博主对其进行了修改,先获取所有帧,所有顶点的最小值和最大值,然后每个顶点存放相应的0到1之间的比值。同时,需要把shader里面传入顶点坐标的最大值、最小值。
存放顶点信息的代码如下:
Vector3 minPos = new Vector3(0, 0, 0);
Vector3 maxPos = new Vector3(0, 0, 0);
//所有帧,所有坐标,最大最小值
for(int i = 0; i < curClipFrame; i++)
{
curAnim.time = sampleTime;
this.animData.Value.SampleAnimAndBakeMesh(ref this.bakedMesh);
for (int j = 0; j < this.bakedMesh.vertexCount; j++)
{
Vector3 vertex = this.bakedMesh.vertices[j];
if (vertex.x > maxPos.x)
maxPos.x = vertex.x;
else if (vertex.x < minPos.x)
minPos.x = vertex.x;
if (vertex.y > maxPos.y)
maxPos.y = vertex.y;
else if (vertex.y < minPos.y)
minPos.y = vertex.y;
if (vertex.z > maxPos.z)
maxPos.z = vertex.z;
else if (vertex.z < minPos.z)
minPos.z = vertex.z;
}
sampleTime += perFrameTime;
}
sampleTime = 0;
for (int i = 0; i < curClipFrame; i++)
{
curAnim.time = sampleTime;
this.animData.Value.SampleAnimAndBakeMesh(ref this.bakedMesh);
//x轴,顶点,y轴帧
for (int j = 0; j < this.bakedMesh.vertexCount; j++)
{
Vector3 vertex = this.bakedMesh.vertices[j];
Vector3 diff = new Vector3(0.0f, 0.0f, 0.0f);
diff.x = (vertex.x - minPos.x) / (maxPos.x - minPos.x);
diff.y = (vertex.y - minPos.y) / (maxPos.y - minPos.y);
diff.z = (vertex.z - minPos.z) / (maxPos.z - minPos.z);
animMap.SetPixel(j, i, new Color(diff.x, diff.y, diff.z));
}
sampleTime += perFrameTime;
}
animMap.Apply();
shader里面:
v2f vert (appdata v, uint vid : SV_VertexID)
{
UNITY_SETUP_INSTANCE_ID(v);
float f = _Time.y / _AnimLen;
fmod(f, 1.0);
float animMap_x = (vid + 0.5) * _AnimMap_TexelSize.x;
float animMap_y = f;
//这里贴图里包含的坐标而不是普通的贴图,所以要用lod0
float4 pos = tex2Dlod(_AnimMap, float4(animMap_x, animMap_y, 0, 0));
float3 diff = float3(0, 0, 0);
diff.x = (_MaxPos.x - _MinPos.x) * pos.x;
diff.y = (_MaxPos.y - _MinPos.y) * pos.y;
diff.z = (_MaxPos.z - _MinPos.z) * pos.z;
pos.xyz = _MinPos.xyz + diff;
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(pos);
/*v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);*/
return o;
}
完整代码如下:
AnimMapBakerWindow.cs
/*
* Created by jiadong chen
* http://www.chenjd.me
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
public class AnimMapBakerWindow : EditorWindow {
private enum SaveStrategy
{
AnimMap,//only anim map
Mat,//with shader
Prefab//prefab with mat
}
#region 字段
public static GameObject targetGo;
private static AnimMapBaker baker;
private static string path = "DefaultPath";
private static string subPath = "SubPath";
private static SaveStrategy stratege = SaveStrategy.AnimMap;
private static Shader animMapShader;
#endregion
#region 方法
[MenuItem("Window/AnimMapBaker")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(AnimMapBakerWindow));
baker = new AnimMapBaker();
animMapShader = Shader.Find("chenjd/AnimMapShader");
}
void OnGUI()
{
targetGo = (GameObject)EditorGUILayout.ObjectField(targetGo, typeof(GameObject), true);
subPath = targetGo == null ? subPath : targetGo.name;
EditorGUILayout.LabelField(string.Format("保存路径output path:{0}", Path.Combine(path, subPath)));
path = EditorGUILayout.TextField(path);
subPath = EditorGUILayout.TextField(subPath);
stratege = (SaveStrategy)EditorGUILayout.EnumPopup("保存策略output type:", stratege);
if (GUILayout.Button("Bake"))
{
if(targetGo == null)
{
EditorUtility.DisplayDialog("err", "targetGo is null!", "OK");
return;
}
if(baker == null)
{
baker = new AnimMapBaker();
}
baker.SetAnimData(targetGo);
List<BakedData> list = baker.Bake();
if(list != null)
{
for(int i = 0; i < list.Count; i++)
{
BakedData data = list[i];
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)
{
string folderPath = CreateFolder();
Texture2D 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<SkinnedMeshRenderer>())
{
EditorUtility.DisplayDialog("err", "SkinnedMeshRender is null!!", "OK");
return null;
}
SkinnedMeshRenderer smr = targetGo.GetComponentInChildren<SkinnedMeshRenderer>();
Material mat = new Material(animMapShader);
Texture2D animMap = SaveAsAsset(ref data);
mat.SetTexture("_MainTex", smr.sharedMaterial.mainTexture);
mat.SetTexture("_AnimMap", animMap);
mat.SetFloat("_AnimLen", data.animLen);
Vector4 minPos = new Vector4(data.minPos.x, data.minPos.y, data.minPos.z, 0.0f);
Vector4 maxPos = new Vector4(data.maxPos.x, data.maxPos.y, data.maxPos.z, 0.0f);
mat.SetVector("_MinPos", minPos);
mat.SetVector("_MaxPos", maxPos);
string folderPath = CreateFolder();
AssetDatabase.CreateAsset(mat, Path.Combine(folderPath, data.name + ".mat"));
return mat;
}
private void SaveAsPrefab(ref BakedData data)
{
Material mat = SaveAsMat(ref data);
if(mat == null)
{
EditorUtility.DisplayDialog("err", "mat is null!!", "OK");
return;
}
GameObject go = new GameObject();
go.AddComponent<MeshRenderer>().sharedMaterial = mat;
go.AddComponent<MeshFilter>().sharedMesh = targetGo.GetComponentInChildren<SkinnedMeshRenderer>().sharedMesh;
string folderPath = CreateFolder();
PrefabUtility.CreatePrefab(Path.Combine(folderPath, data.name + ".prefab").Replace("\\", "/"), go);
}
private string CreateFolder()
{
string folderPath = Path.Combine("Assets/" + path, subPath);
if (!AssetDatabase.IsValidFolder(folderPath))
{
AssetDatabase.CreateFolder("Assets/" + path, subPath);
}
return folderPath;
}
#endregion
}
AnimMapBaker
/*
* Created by jiadong chen
* http://www.chenjd.me
*
* 用来烘焙动作贴图。烘焙对象使用animation组件,并且在导入时设置Rig为Legacy
*/
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
/// <summary>
/// 保存需要烘焙的动画的相关数据
/// </summary>
public struct AnimData
{
#region 字段
public int vertexCount;
public int mapWidth;
public List<AnimationState> animClips;
public string name;
private Animation animation;
private SkinnedMeshRenderer skin;
#endregion
public AnimData(Animation anim, SkinnedMeshRenderer smr, string goName)
{
vertexCount = smr.sharedMesh.vertexCount;
mapWidth = Mathf.NextPowerOfTwo(vertexCount);
animClips = new List<AnimationState>(anim.Cast<AnimationState>());
animation = anim;
skin = smr;
name = goName;
}
#region 方法
public void AnimationPlay(string animName)
{
this.animation.Play(animName);
}
public void SampleAnimAndBakeMesh(ref Mesh m)
{
this.SampleAnim();
this.BakeMesh(ref m);
}
private void SampleAnim()
{
if (this.animation == null)
{
Debug.LogError("animation is null!!");
return;
}
this.animation.Sample();
}
private void BakeMesh(ref Mesh m)
{
if (this.skin == null)
{
Debug.LogError("skin is null!!");
return;
}
this.skin.BakeMesh(m);
}
#endregion
}
/// <summary>
/// 烘焙后的数据
/// </summary>
public struct BakedData
{
#region 字段
public string name;
public float animLen;
public byte[] rawAnimMap;
public int animMapWidth;
public int animMapHeight;
public Vector3 minPos;
public Vector3 maxPos;
#endregion
public BakedData(string name, float animLen, Texture2D animMap, Vector3 minPos, Vector3 maxPos)
{
this.name = name;
this.animLen = animLen;
this.animMapHeight = animMap.height;
this.animMapWidth = animMap.width;
this.rawAnimMap = animMap.GetRawTextureData();
this.maxPos = maxPos;
this.minPos = minPos;
}
}
/// <summary>
/// 烘焙器
/// </summary>
public class AnimMapBaker{
#region 字段
private AnimData? animData = null;
private List<Vector3> vertices = new List<Vector3>();
private Mesh bakedMesh;
private List<BakedData> bakedDataList = new List<BakedData>();
#endregion
#region 方法
public void SetAnimData(GameObject go)
{
if(go == null)
{
Debug.LogError("go is null!!");
return;
}
Animation anim = go.GetComponent<Animation>();
SkinnedMeshRenderer smr = go.GetComponentInChildren<SkinnedMeshRenderer>();
if(anim == null || smr == null)
{
Debug.LogError("anim or smr is null!!");
return;
}
this.bakedMesh = new Mesh();
this.animData = new AnimData(anim, smr, go.name);
}
public List<BakedData> Bake()
{
if(this.animData == null)
{
Debug.LogError("bake data is null!!");
return this.bakedDataList;
}
//每一个动作都生成一个动作图
for(int i = 0; i < this.animData.Value.animClips.Count; i++)
{
if(!this.animData.Value.animClips[i].clip.legacy)
{
Debug.LogError(string.Format("{0} is not legacy!!", this.animData.Value.animClips[i].clip.name));
continue;
}
BakePerAnimClip(this.animData.Value.animClips[i]);
}
return this.bakedDataList;
}
private void BakePerAnimClip(AnimationState curAnim)
{
int curClipFrame = 0;
float sampleTime = 0;
float perFrameTime = 0;
//获取总帧数(帧率乘以秒数) 转换成2的幂
curClipFrame = Mathf.ClosestPowerOfTwo((int)(curAnim.clip.frameRate * curAnim.length));
//总秒数/总帧数 获得每帧的时间(s)
perFrameTime = curAnim.length / curClipFrame;
//mapWidth是顶点数 大的最小的2的幂
Texture2D animMap = new Texture2D(this.animData.Value.mapWidth, curClipFrame, TextureFormat.RGBAHalf, false);
animMap.name = string.Format("{0}_{1}.animMap", this.animData.Value.name, curAnim.name);
this.animData.Value.AnimationPlay(curAnim.name);
Vector3 minPos = new Vector3(0, 0, 0);
Vector3 maxPos = new Vector3(0, 0, 0);
//所有帧,所有坐标,最大最小值
for(int i = 0; i < curClipFrame; i++)
{
curAnim.time = sampleTime;
this.animData.Value.SampleAnimAndBakeMesh(ref this.bakedMesh);
for (int j = 0; j < this.bakedMesh.vertexCount; j++)
{
Vector3 vertex = this.bakedMesh.vertices[j];
if (vertex.x > maxPos.x)
maxPos.x = vertex.x;
else if (vertex.x < minPos.x)
minPos.x = vertex.x;
if (vertex.y > maxPos.y)
maxPos.y = vertex.y;
else if (vertex.y < minPos.y)
minPos.y = vertex.y;
if (vertex.z > maxPos.z)
maxPos.z = vertex.z;
else if (vertex.z < minPos.z)
minPos.z = vertex.z;
}
sampleTime += perFrameTime;
}
sampleTime = 0;
for (int i = 0; i < curClipFrame; i++)
{
curAnim.time = sampleTime;
this.animData.Value.SampleAnimAndBakeMesh(ref this.bakedMesh);
//x轴,顶点,y轴帧
for (int j = 0; j < this.bakedMesh.vertexCount; j++)
{
Vector3 vertex = this.bakedMesh.vertices[j];
Vector3 diff = new Vector3(0.0f, 0.0f, 0.0f);
diff.x = (vertex.x - minPos.x) / (maxPos.x - minPos.x);
diff.y = (vertex.y - minPos.y) / (maxPos.y - minPos.y);
diff.z = (vertex.z - minPos.z) / (maxPos.z - minPos.z);
animMap.SetPixel(j, i, new Color(diff.x, diff.y, diff.z));
}
sampleTime += perFrameTime;
}
animMap.Apply();
this.bakedDataList.Add(new BakedData(animMap.name, curAnim.clip.length, animMap, minPos, maxPos));
}
#endregion
}
AnimMapShader
/*
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
_MinPos("Min Pos", Vector) = (0.0, 0, 0, 0)
_MaxPos("Max Pos", Vector) = (0.0, 0, 0, 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"
#pragma target 3.0
struct appdata
{
float2 uv : TEXCOORD0;
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
float4 _MinPos;
float4 _MaxPos;
float _AnimLen;
v2f vert (appdata v, uint vid : SV_VertexID)
{
UNITY_SETUP_INSTANCE_ID(v);
float f = _Time.y / _AnimLen;
fmod(f, 1.0);
float animMap_x = (vid + 0.5) * _AnimMap_TexelSize.x;
float animMap_y = f;
//这里贴图里包含的坐标而不是普通的贴图,所以要用lod0
float4 pos = tex2Dlod(_AnimMap, float4(animMap_x, animMap_y, 0, 0));
float3 diff = float3(0, 0, 0);
diff.x = (_MaxPos.x - _MinPos.x) * pos.x;
diff.y = (_MaxPos.y - _MinPos.y) * pos.y;
diff.z = (_MaxPos.z - _MinPos.z) * pos.z;
pos.xyz = _MinPos.xyz + diff;
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(pos);
/*v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);*/
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}