|
|
|
@ -1,4 +1,5 @@ |
|
|
|
using System.Collections.Generic; |
|
|
|
using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Text.RegularExpressions; |
|
|
|
using UnityEngine; |
|
|
|
|
|
|
|
@ -7,23 +8,21 @@ public class AliasModel |
|
|
|
public static readonly Regex AnimationRegex = new Regex(@"^[a-zA-Z]+"); |
|
|
|
|
|
|
|
private readonly string name; |
|
|
|
private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>(); // TODO: make this a fixed array after initialization
|
|
|
|
private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>(); |
|
|
|
|
|
|
|
public AliasModel(string name) |
|
|
|
{ |
|
|
|
this.name = name; |
|
|
|
} |
|
|
|
|
|
|
|
public void AddAnimation(int startFrame, Mesh mesh) |
|
|
|
{ |
|
|
|
animationMeshes.Add((startFrame, mesh)); |
|
|
|
} |
|
|
|
|
|
|
|
public int GetAnimationFrameCount(float frameNum) |
|
|
|
{ |
|
|
|
if (!FindAnimation((int)frameNum, out Mesh mesh, out int startFrame)) |
|
|
|
return 0; |
|
|
|
|
|
|
|
if (mesh.blendShapeCount == 0) |
|
|
|
return 1; |
|
|
|
|
|
|
|
return mesh.GetBlendShapeFrameCount(0); |
|
|
|
} |
|
|
|
|
|
|
|
@ -35,6 +34,9 @@ public class AliasModel |
|
|
|
if (!FindAnimation(frameIndex, out mesh, out int startFrame)) |
|
|
|
return; |
|
|
|
|
|
|
|
if (mesh.blendShapeCount == 0) |
|
|
|
return; |
|
|
|
|
|
|
|
int numFrames = mesh.GetBlendShapeFrameCount(0); |
|
|
|
blendWeight = ((frameNum - startFrame) / numFrames) % 1; |
|
|
|
} |
|
|
|
@ -56,4 +58,207 @@ public class AliasModel |
|
|
|
|
|
|
|
return false; // Shouldn't happen
|
|
|
|
} |
|
|
|
|
|
|
|
public void ImportMeshData(QAliasHeader header, QTriVertex[][] poseVertices, QTriangle[] triangles, QSTVert[] stVertices) |
|
|
|
{ |
|
|
|
// Massage the input data for easier conversion
|
|
|
|
PreprocessMeshData(header, triangles, ref poseVertices, ref stVertices); |
|
|
|
|
|
|
|
// Triangle indices and UVs are the same for all animation frames
|
|
|
|
ConvertTriangles(triangles, out var indices); |
|
|
|
ConvertUVs(stVertices, header.skinWidth, header.skinHeight, out var uvs); |
|
|
|
|
|
|
|
// Identify animation sequences and turn each one into a separate Mesh with a single blend shape animation
|
|
|
|
Mesh mesh; |
|
|
|
string animName = null; |
|
|
|
int startFrame = 0; |
|
|
|
for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) |
|
|
|
{ |
|
|
|
string frameName = AliasModel.AnimationRegex.Match(header.frames[frameIdx].name).Value; |
|
|
|
if (animName == null) |
|
|
|
{ |
|
|
|
animName = frameName; |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
if (frameName != animName) |
|
|
|
{ |
|
|
|
// New animation sequence; convert the previous sequence into a blend shape animation
|
|
|
|
mesh = CreateAnimatedMesh(header, poseVertices, indices, uvs, $"{name}_{animName}", startFrame, frameIdx); |
|
|
|
animationMeshes.Add((startFrame, mesh)); |
|
|
|
|
|
|
|
animName = frameName; |
|
|
|
startFrame = frameIdx; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Convert the final (or only) sequence into a blend shape animation
|
|
|
|
mesh = CreateAnimatedMesh(header, poseVertices, indices, uvs, $"{name}_{animName}", startFrame, header.numFrames); |
|
|
|
animationMeshes.Add((startFrame, mesh)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Quake has a bit of a weird outdated mesh setup where skin textures are split into a front side and a back side.
|
|
|
|
/// Vertices on the seam between the front and back are used by triangles on both sides, but require a correction
|
|
|
|
/// to their UVs when rendering the backside. To handle this properly, we duplicate these vertices and correct the
|
|
|
|
/// UVs ahead of time. We do this as a separate pre-process step to keep things simple.
|
|
|
|
/// </summary>
|
|
|
|
private static void PreprocessMeshData(QAliasHeader header, QTriangle[] triangles, |
|
|
|
ref QTriVertex[][] poseVertices, ref QSTVert[] stVerts) |
|
|
|
{ |
|
|
|
int newVertCount = 0; |
|
|
|
|
|
|
|
// First count how many new vertices we need to make, so we can preallocate the required arrays
|
|
|
|
for (int triIdx = 0; triIdx < header.numTriangles; ++triIdx) |
|
|
|
{ |
|
|
|
if (triangles[triIdx].facesFront != 0) |
|
|
|
continue; |
|
|
|
|
|
|
|
for (int indIdx = 0; indIdx < 3; ++indIdx) |
|
|
|
{ |
|
|
|
int index = triangles[triIdx].vertIndex[indIdx]; |
|
|
|
if (stVerts[index].onSeam == 0) |
|
|
|
continue; |
|
|
|
|
|
|
|
// Back-side vertex on seam, needs to be duplicated and corrected
|
|
|
|
++newVertCount; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (newVertCount == 0) |
|
|
|
return; |
|
|
|
|
|
|
|
Array.Resize(ref stVerts, header.numVerts + newVertCount); |
|
|
|
for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) |
|
|
|
{ |
|
|
|
Array.Resize(ref poseVertices[frameIdx], header.numVerts + newVertCount); |
|
|
|
} |
|
|
|
|
|
|
|
int newVertIndex = header.numVerts; |
|
|
|
|
|
|
|
// Now we go over all the triangles again and duplicate the vertices that are on a seam
|
|
|
|
for (int triIdx = 0; triIdx < header.numTriangles; ++triIdx) |
|
|
|
{ |
|
|
|
if (triangles[triIdx].facesFront != 0) |
|
|
|
continue; |
|
|
|
|
|
|
|
for (int indIdx = 0; indIdx < 3; ++indIdx) |
|
|
|
{ |
|
|
|
int vertIndex = triangles[triIdx].vertIndex[indIdx]; |
|
|
|
if (stVerts[vertIndex].onSeam == 0) |
|
|
|
continue; |
|
|
|
|
|
|
|
// Clone the ST value and correct it to map onto the backside of the skin
|
|
|
|
QSTVert stVertCopy = stVerts[vertIndex]; |
|
|
|
stVertCopy.s += header.skinWidth / 2; |
|
|
|
stVerts[newVertIndex] = stVertCopy; |
|
|
|
|
|
|
|
// Clone the vertex position data for all frames
|
|
|
|
for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) |
|
|
|
{ |
|
|
|
QTriVertex triVertCopy = poseVertices[frameIdx][vertIndex]; |
|
|
|
poseVertices[frameIdx][newVertIndex] = triVertCopy; |
|
|
|
} |
|
|
|
|
|
|
|
triangles[triIdx].vertIndex[indIdx] = newVertIndex++; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
header.numVerts += newVertCount; |
|
|
|
} |
|
|
|
|
|
|
|
private static void ConvertVertices(QAliasHeader header, QTriVertex[] triVerts, out Vector3[] vertices, out Vector3[] normals) |
|
|
|
{ |
|
|
|
int numVerts = triVerts.Length; |
|
|
|
vertices = new Vector3[numVerts]; |
|
|
|
normals = new Vector3[numVerts]; |
|
|
|
|
|
|
|
Vector3 scale = header.scale.ToVector3(); |
|
|
|
Vector3 origin = header.scaleOrigin.ToVector3(); |
|
|
|
|
|
|
|
for (int i = 0; i < numVerts; ++i) |
|
|
|
{ |
|
|
|
vertices[i] = Vector3.Scale(triVerts[i].ToVector3(), scale); |
|
|
|
normals[i] = QLightNormals.Get(triVerts[i].lightNormalIndex); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void ConvertTriangles(QTriangle[] triangles, out ushort[] indices) |
|
|
|
{ |
|
|
|
int numTris = triangles.Length; |
|
|
|
indices = new ushort[numTris * 3]; |
|
|
|
|
|
|
|
for (int i = 0; i < numTris; ++i) |
|
|
|
{ |
|
|
|
// Quake triangles are wound clockwise, so we reverse them here
|
|
|
|
indices[i * 3 + 0] = (ushort)triangles[i].vertIndex[2]; |
|
|
|
indices[i * 3 + 1] = (ushort)triangles[i].vertIndex[1]; |
|
|
|
indices[i * 3 + 2] = (ushort)triangles[i].vertIndex[0]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void ConvertUVs(QSTVert[] stVerts, int skinWidth, int skinHeight, out Vector2[] uvs) |
|
|
|
{ |
|
|
|
int numVerts = stVerts.Length; |
|
|
|
uvs = new Vector2[numVerts]; |
|
|
|
|
|
|
|
Vector2 scale = new Vector2(1.0f / skinWidth, 1.0f / skinHeight); |
|
|
|
|
|
|
|
for (int i = 0; i < numVerts; ++i) |
|
|
|
{ |
|
|
|
uvs[i] = Vector2.Scale(new Vector2(stVerts[i].s, skinHeight - stVerts[i].t), scale); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static Mesh CreateAnimatedMesh( |
|
|
|
QAliasHeader header, QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs, |
|
|
|
string animationName, int startFrame, int endFrame) |
|
|
|
{ |
|
|
|
ConvertVertices(header, poseVertices[startFrame], out var baseVertices, out var baseNormals); |
|
|
|
|
|
|
|
var mesh = new Mesh { name = animationName }; |
|
|
|
mesh.SetVertices(baseVertices); |
|
|
|
mesh.SetNormals(baseNormals); |
|
|
|
|
|
|
|
if (endFrame - startFrame > 1) |
|
|
|
{ |
|
|
|
CreateBlendShapes(mesh, animationName, baseVertices, baseNormals, header, poseVertices, startFrame, endFrame); |
|
|
|
} |
|
|
|
|
|
|
|
mesh.SetIndices(indices, MeshTopology.Triangles, 0, false); |
|
|
|
mesh.SetUVs(0, uvs); |
|
|
|
mesh.Optimize(); |
|
|
|
mesh.RecalculateBounds(); |
|
|
|
mesh.UploadMeshData(true); |
|
|
|
return mesh; |
|
|
|
} |
|
|
|
|
|
|
|
private static void CreateBlendShapes( |
|
|
|
Mesh mesh, string animName, Vector3[] baseVertices, Vector3[] baseNormals, |
|
|
|
QAliasHeader header, QTriVertex[][] poseVertices, int startFrame, int endFrame) |
|
|
|
{ |
|
|
|
var deltaVertices = new Vector3[header.numVerts]; |
|
|
|
var deltaNormals = new Vector3[header.numVerts]; |
|
|
|
|
|
|
|
Vector3 scale = header.scale.ToVector3(); |
|
|
|
Vector3 origin = header.scaleOrigin.ToVector3(); |
|
|
|
|
|
|
|
int numFrames = endFrame - startFrame; |
|
|
|
|
|
|
|
// Repeat the first frame at the end, so we can smoothly animate the entire cycle and loop the animation without any breaks.
|
|
|
|
for (int index = 1; index <= numFrames; ++index) |
|
|
|
{ |
|
|
|
int frameIdx = startFrame + index % numFrames; |
|
|
|
var poseVerts = poseVertices[frameIdx]; |
|
|
|
|
|
|
|
for (int vertIdx = 0; vertIdx < header.numVerts; ++vertIdx) |
|
|
|
{ |
|
|
|
Vector3 vert = Vector3.Scale(poseVerts[vertIdx].ToVector3(), scale); |
|
|
|
deltaVertices[vertIdx] = vert - baseVertices[vertIdx]; |
|
|
|
deltaNormals[vertIdx] = QLightNormals.Get(poseVerts[vertIdx].lightNormalIndex) - baseNormals[vertIdx]; |
|
|
|
} |
|
|
|
|
|
|
|
mesh.AddBlendShapeFrame(animName, (float)index / numFrames, deltaVertices, deltaNormals, null); |
|
|
|
} |
|
|
|
} |
|
|
|
} |