diff --git a/Assets/Scripts/Modules/AliasModel.cs b/Assets/Scripts/Modules/AliasModel.cs index eac59e6..00a80c4 100644 --- a/Assets/Scripts/Modules/AliasModel.cs +++ b/Assets/Scripts/Modules/AliasModel.cs @@ -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)); + } + + /// + /// 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) + { + 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); + } + } } diff --git a/Assets/Scripts/Modules/RenderModule.cs b/Assets/Scripts/Modules/RenderModule.cs index 55a883a..3d758d5 100644 --- a/Assets/Scripts/Modules/RenderModule.cs +++ b/Assets/Scripts/Modules/RenderModule.cs @@ -1,5 +1,4 @@ -using System; -using UnityEngine; +using UnityEngine; using UnityEngine.Rendering; public partial class RenderModule @@ -19,45 +18,14 @@ public partial class RenderModule { Debug.Log($"Alias model '{name}' with {header.numVerts} vertices, {header.numTriangles} triangles, {header.numFrames} frame(s)"); - // 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); - string modelName = System.IO.Path.GetFileNameWithoutExtension(name); AliasModel aliasModel = new AliasModel(modelName); - - // 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, $"{modelName}_{animName}", startFrame, frameIdx); - aliasModel.AddAnimation(startFrame, mesh); - - animName = frameName; - startFrame = frameIdx; - } - } - - mesh = CreateAnimatedMesh(header, poseVertices, indices, uvs, $"{modelName}_{animName}", startFrame, header.numFrames); - aliasModel.AddAnimation(startFrame, mesh); + aliasModel.ImportMeshData(header, poseVertices, triangles, stVertices); var go = new GameObject(modelName); go.transform.SetPositionAndRotation(new Vector3(xPos, 0, 0), Quaternion.Euler(-90, 90, 0)); + + aliasModel.Animate(0, out Mesh mesh, out float blendWeight); if (header.numFrames > 1) { @@ -87,166 +55,4 @@ public partial class RenderModule xPos += 0.5f; return 1; } - - /// - /// 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) - { - 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); - - 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); - } - } }