You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
12 KiB
322 lines
12 KiB
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 QAliasHeader header;
|
|
private readonly QAliasFrameType frameType;
|
|
private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>();
|
|
|
|
public string Name => name;
|
|
public bool IsAnimated => header.numPoses > 1;
|
|
public bool AutoAnimate => frameType == QAliasFrameType.Group;
|
|
public TextureSet Textures { get; } = new TextureSet();
|
|
|
|
public AliasModel(string name, QAliasHeader header, QAliasFrameType frameType)
|
|
{
|
|
this.name = name;
|
|
this.header = header;
|
|
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 == null || 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)
|
|
{
|
|
if (animationMeshes[i].Item1 > frameIndex)
|
|
return true;
|
|
|
|
startFrame = animationMeshes[i].Item1;
|
|
mesh = animationMeshes[i].Item2;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var mesh in animationMeshes)
|
|
{
|
|
UnityEngine.Object.Destroy(mesh.Item2);
|
|
}
|
|
|
|
animationMeshes.Clear();
|
|
}
|
|
|
|
public void ImportMeshData(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(poseVertices, indices, uvs);
|
|
else
|
|
ImportGroupFrameAnimations(poseVertices, indices, uvs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single frame animations are used for dynamically animated entities. The game logic determines which frame is displayed at which time.
|
|
/// In Unity we still need to identify groups of frames that form a single animation sequence, so that we can smoothly loop animations when interpolation is enabled.
|
|
/// </summary>
|
|
private void ImportSingleFrameAnimations(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;
|
|
}
|
|
|
|
// TODO do we maybe need to distinguish between frames and poses here after all?
|
|
// In most cases frames and poses are correlated 1-to-1, but perhaps not always. This could cause subtle animation glitches.
|
|
|
|
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>
|
|
/// Grouped frame animations are used for entities that animate automatically in an endless cycle, e.g. flames.
|
|
/// They are set up once and then the renderer ensures they are updated at a steady rate.
|
|
/// </summary>
|
|
private void ImportGroupFrameAnimations(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));
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
HashSet<int> seamVertices = new HashSet<int>();
|
|
|
|
// 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<int, int> vertexCopies = new Dictionary<int, int>();
|
|
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(false); // Meshes have to stay in RAM, otherwise they won't show up in standalone builds
|
|
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);
|
|
}
|
|
}
|
|
}
|