using System; using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEngine; public class AliasModel { private static readonly Regex AnimationRegex = new Regex(@"^[a-zA-Z]+"); private readonly string name; private readonly QAliasFrameType frameType; private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>(); public string Name => name; public AliasModel(string name, QAliasFrameType frameType) { this.name = name; this.frameType = frameType; } 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); } public void Animate(float frameNum, out Mesh mesh, out float blendWeight) { blendWeight = 0; int frameIndex = (int)frameNum; if (!FindAnimation(frameIndex, out mesh, out int startFrame)) return; if (mesh.blendShapeCount == 0) return; int numFrames = mesh.GetBlendShapeFrameCount(0); blendWeight = ((frameNum - startFrame) / numFrames) % 1; } private bool FindAnimation(int frameIndex, out Mesh mesh, out int startFrame) { mesh = null; startFrame = 0; for (int i = 0; i < animationMeshes.Count; ++i) { startFrame = animationMeshes[i].Item1; if (frameIndex >= startFrame) { mesh = animationMeshes[i].Item2; return true; } } return false; // Shouldn't happen } public void Dispose() { foreach (var mesh in animationMeshes) { UnityEngine.Object.Destroy(mesh.Item2); } animationMeshes.Clear(); } 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 if (frameType == QAliasFrameType.Single) ImportSingleFrameAnimations(header, poseVertices, indices, uvs); else ImportGroupFrameAnimations(header, poseVertices, indices, uvs); } private void ImportSingleFrameAnimations(QAliasHeader header, QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs) { Mesh mesh; string animName = null; int startFrame = 0; for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) { // Individual sequences are identified by their prefix string frameName = 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)); } private void ImportGroupFrameAnimations(QAliasHeader header, QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs) { for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) { var frame = header.frames[frameIdx]; Mesh mesh = CreateAnimatedMesh(header, poseVertices, indices, uvs, $"{name}_{frameIdx}", frame.firstPose, frame.firstPose + frame.numPoses); animationMeshes.Add((frame.firstPose, mesh)); } } /// /// 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. /// private static void PreprocessMeshData(QAliasHeader header, QTriangle[] triangles, ref QTriVertex[][] poseVertices, ref QSTVert[] stVerts) { HashSet seamVertices = new HashSet(); // 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 vertIndex = triangles[triIdx].vertIndex[indIdx]; if (stVerts[vertIndex].onSeam == 0) continue; // Back-side vertex on seam, needs to be duplicated and corrected seamVertices.Add(vertIndex); } } if (seamVertices.Count == 0) return; Array.Resize(ref stVerts, header.numVerts + seamVertices.Count); for (int frameIdx = 0; frameIdx < header.numPoses; ++frameIdx) { Array.Resize(ref poseVertices[frameIdx], header.numVerts + seamVertices.Count); } Dictionary vertexCopies = new Dictionary(); 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; if (vertexCopies.ContainsKey(vertIndex)) { // We've already duplicated this vertex before triangles[triIdx].vertIndex[indIdx] = vertexCopies[vertIndex]; 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.numPoses; ++frameIdx) { QTriVertex triVertCopy = poseVertices[frameIdx][vertIndex]; poseVertices[frameIdx][newVertIndex] = triVertCopy; } triangles[triIdx].vertIndex[indIdx] = newVertIndex; vertexCopies.Add(vertIndex, newVertIndex++); } } header.numVerts = newVertIndex; } 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) + origin).ToUnity(); normals[i] = QLightNormals.Get(triVerts[i].lightNormalIndex).ToUnity(); } } 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) { indices[i * 3 + 0] = (ushort)triangles[i].vertIndex[0]; indices[i * 3 + 1] = (ushort)triangles[i].vertIndex[1]; indices[i * 3 + 2] = (ushort)triangles[i].vertIndex[2]; } } 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, stVerts[i].t), scale); } } private static Mesh CreateAnimatedMesh( QAliasHeader header, QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs, string animationName, int startPose, int endPose) { ConvertVertices(header, poseVertices[startPose], out var baseVertices, out var baseNormals); var mesh = new Mesh { name = animationName }; mesh.SetVertices(baseVertices); mesh.SetNormals(baseNormals); if (endPose - startPose > 1) { CreateBlendShapes(mesh, animationName, baseVertices, baseNormals, header, poseVertices, startPose, endPose); } 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 startPose, int endPose) { var deltaVertices = new Vector3[header.numVerts]; var deltaNormals = new Vector3[header.numVerts]; Vector3 scale = header.scale.ToVector3(); Vector3 origin = header.scaleOrigin.ToVector3(); int numPoses = endPose - startPose; // Repeat the first pose at the end, so we can smoothly animate the entire cycle and loop the animation without any breaks. for (int index = 1; index <= numPoses; ++index) { int poseIdx = startPose + index % numPoses; var poseVerts = poseVertices[poseIdx]; for (int vertIdx = 0; vertIdx < header.numVerts; ++vertIdx) { Vector3 vert = (Vector3.Scale(poseVerts[vertIdx].ToVector3(), scale) + origin).ToUnity(); deltaVertices[vertIdx] = vert - baseVertices[vertIdx]; deltaNormals[vertIdx] = QLightNormals.Get(poseVerts[vertIdx].lightNormalIndex).ToUnity() - baseNormals[vertIdx]; } mesh.AddBlendShapeFrame(animName, (float)index / numPoses, deltaVertices, deltaNormals, null); } } }