Unity引擎制作万人同屏效果
大家好,我是阿赵。
之前介绍了怎样去用Shader播放顶点动画VAT。但这里有一个前提,就是这张顶点动画的贴图,是怎样生成的呢?
由于是先说明了Shader的写法,所以可能会有一种先入为主的想法,是不是VAT贴图一定就要按照这种方式去播放呢?其实并不是。应该是反过来,由生成的格式决定播放的方法。不过由于要先说明VAT的原理,才好说明怎样去生成这类型的资源,所以我的说明是反过来了。
现在再回头来看看这张VAT的贴图。之前漏了说一个比较重要的地方。由于我们需要获取的是绝对像素颜色来转换成顶点的坐标,所以这个颜色值的含义其实只是一个Vector3,也就是Shader里面的Float3,所以,sRGB不要勾选,这样不会受色彩空间的影响。然后FilterMode要选择成Point,也就是说没有任何插值。
接下来开始分析我们需要的数据了。
从比较简单的例子开始,假如现在有一个模型,它只包含一个网格,然后有6个动作,分别是idle、attack、damage、dead、run、walk。
1、生成UV2的网格
首先,我们要通过UV2来读取VAT,那么我们需要有一个地方去存储UV2。而且由于是需要能在Shader直接读取的,所以存储在Mesh的UV2信息就很合适。但我们不一定能修改原始模型的Mesh,所以我的做法是复制一份原始模型的Mesh出来,把UV2设置进去。
设置UV2的方式也很简单,直接按照顶点的顺序设置就行。
private void CreateTempMesh()
{
Mesh shareMesh = render.sharedMesh;
tempMesh = new Mesh();
Vector3[] tempVerts = new Vector3[shareMesh.vertices.Length];
for(int i = 0;i<tempVerts.Length;i++)
{
tempVerts[i] = render.gameObject.transform.TransformPoint(shareMesh.vertices[i]);
}
tempMesh.vertices = tempVerts;
tempMesh.uv = shareMesh.uv;
tempMesh.triangles = shareMesh.triangles;
tempMesh.normals = shareMesh.normals;
tempMesh.tangents = shareMesh.tangents;
Vector2[] uv2 = new Vector2[vertexCount];
float maxSize = VertexAnimCommonTool.GetNearestTexSize(vertexCount);
for(int i = 0;i<vertexCount;i++)
{
float u = (float)i / maxSize;
uv2[i] = new Vector2(u, 0);
}
tempMesh.uv2 = uv2;
//这里可以把网格保存下来,具体怎样保存就看个人需要
}
2、 逐个动作保存信息
有了UV2之后,根据上一篇的Shader里面需要的数据,我们还需要保存的信息有:
- 顶点坐标的范围最大最小值
- 动作每一帧的顶点坐标相对于最大最小值的比例
- 动作的最大帧率
- 动作的播放速度
- 贴图的最大高度
由于这些信息,在不同动作时是不一样的,所以到这里是需要分开逐个动作去生成。
主要的思路很简单,就是通过固定的帧率时间间隔,让带有Animator的动画跳转到固定某一帧,然后bake一个mesh出来,把上面的顶点信息都记录下来而已。旧版的Animation也一样。
跳转动画主要是靠animator.playbackTime来控制的,不过我发现一个问题,这个api在Unity的非播放阶段控制起来比较困难。所以我的选择是,在导出工具运行的时候,先自动进入Unity的运行状态,再去控制动画播放。
这是切换动作时的方法:
private void ChangeAnim(string str)
{
float animLength = GetAnimLength(str);
int frameCount = (int)(animLength * frameRate);
animator.Rebind();
animator.StopPlayback();
animator.recorderStartTime = 0;
animator.Play(str);
// 开始记录指定的帧数
animator.StartRecording(frameCount);
for (var i = 0; i < frameCount - 1; i++)
{
// 记录每一帧
animator.Update(1.0f / frameRate);
}
// 完成记录
animator.StopRecording();
animator.StartPlayback();
animator.playbackTime = 0;
animator.Update(0);
}
然后通过获得动作的长度,然后根据帧率来遍历每一帧,记录每一帧的动作:
for (int i = 0;i<frameCount;i++)
{
float curTime = i * frameTime;
animator.playbackTime = curTime;
animator.Update(0);
foreach(KeyValuePair<string,SkinnedMeshRenderer>item in skinMeshDict)
{
ParsePartFrameAnim(item.Key,animName);
}
}
bake每一帧的mesh待用
public void ParsePartFrameAnim(string animName)
{
if(render == null)
{
return;
}
if(anims == null||anims.ContainsKey(animName)==false)
{
return;
}
Mesh mesh = new Mesh();
render.BakeMesh(mesh);
anims[animName].AddToFrameMeshList(mesh);
}
创建空贴图:
private void CreateTempTexture()
{
texWidth = VertexAnimCommonTool.GetNearestTexSize(shareMesh.vertexCount);
texHeight = VertexAnimCommonTool.GetNearestTexSize(frameCount);
speed = 1 / time;
tempTex = new Texture2D(texWidth, texHeight);
Color blackColor = new Color(0, 0, 0, 1);
for (int i = 0;i< texWidth; i++)
{
for(int j = 0;j< texHeight; j++)
{
tempTex.SetPixel(i, j, blackColor);
}
}
tempTex.Apply();
}
最后记录信息:
private void ParseFrameTex()
{
float minX = 10000;
float minY = 10000;
float minZ = 10000;
float maxX = -10000;
float maxY = -10000;
float maxZ = -10000;
int vertCount = shareMesh.vertexCount;
if(frameMeshList == null)
{
return;
}
int frameCount = frameMeshList.Count;
for (int i = 0; i < frameMeshList.Count; i++)
{
Vector3[] verts = frameMeshList[i].vertices;
for (int j = 0; j < verts.Length; j++)
{
Vector3 vert = verts[j];
if (vert.x < minX)
{
minX = vert.x;
}
if (vert.y < minY)
{
minY = vert.y;
}
if (vert.z < minZ)
{
minZ = vert.z;
}
if (vert.x > maxX)
{
maxX = vert.x;
}
if (vert.y > maxY)
{
maxY = vert.y;
}
if (vert.z > maxZ)
{
maxZ = vert.z;
}
}
}
float offsetX = maxX - minX;
float offsetY = maxY - minY;
float offsetZ = maxZ - minZ;
for (int i = 0; i < frameMeshList.Count; i++)
{
Vector3[] verts = frameMeshList[i].vertices;
for (int j = 0; j < vertCount; j++)
{
Vector3 vert = verts[j];
float r = (vert.x - minX) / offsetX;
float g = (vert.y - minY) / offsetY;
float b = (vert.z - minZ) / offsetZ;
Color col = new Color(r, g, b);
tempTex.SetPixel(j, i, col);
}
}
tempTex.Apply();
boundMin = new Vector3(minX, minY, minZ);
boundMax = new Vector3(maxX, maxY, maxZ);
}
3、 保存除了贴图以外的信息。
每一帧的顶点数据,可以保存在贴图里面,但由于最大帧数、贴图高度、bound的最大最小值这些,是不适合保存在贴图里面的,所以,可以单独保存在一个txt文本里面,然后运行的时候再读取出来,赋予给对应的材质球。
还有,如果原来的材质比较规范,我们可以通过复制原来的模型的材质球上面的_MainTex,这样就可以方便很多。
最后,把导出的资源都放在一起,一个角色最终导出的资源会是这样: