using System; using UnityEngine; using UnityEngine.Rendering; public partial class RenderModule { private readonly UniQuake uq; public RenderModule(UniQuake uniQuake) { uq = uniQuake; BuildCallbacks(); } private float xPos = -8f; private int UploadAliasModel(string name, QAliasHeader header, QTriVertex[][] poseVertices, QTriangle[] triangles, QSTVert[] stVertices) { 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); var go = new GameObject(modelName); go.transform.SetPositionAndRotation(new Vector3(xPos, 0, 0), Quaternion.Euler(-90, 90, 0)); if (header.numFrames > 1) { var mr = go.AddComponent(); mr.material = new Material(Shader.Find("Universal Render Pipeline/Simple Lit")); mr.sharedMesh = mesh; mr.shadowCastingMode = ShadowCastingMode.Off; mr.receiveShadows = false; mr.lightProbeUsage = LightProbeUsage.Off; mr.reflectionProbeUsage = ReflectionProbeUsage.Off; var animator = go.AddComponent(); animator.aliasModel = aliasModel; } else { var mf = go.AddComponent(); mf.sharedMesh = mesh; var mr = go.AddComponent(); mr.material = new Material(Shader.Find("Universal Render Pipeline/Simple Lit")); mr.shadowCastingMode = ShadowCastingMode.Off; mr.receiveShadows = false; mr.lightProbeUsage = LightProbeUsage.Off; mr.reflectionProbeUsage = ReflectionProbeUsage.Off; } 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); } } }