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);
- }
- }
}