Browse Source

Moved importing and conversion of mesh data into AliasModel class. Also:

- Do not create blend shapes if a model does not have any animations
- Handle cases when mesh does not have any blend shapes
console
Nico de Poel 5 years ago
parent
commit
08c16f71a1
  1. 219
      Assets/Scripts/Modules/AliasModel.cs
  2. 202
      Assets/Scripts/Modules/RenderModule.cs

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

202
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;
}
/// <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);
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);
}
}
}
Loading…
Cancel
Save