From 41382907234d7ac613e36c4f4a70be3c0e21cd41 Mon Sep 17 00:00:00 2001 From: Nico de Poel Date: Sat, 17 Apr 2021 12:33:29 +0200 Subject: [PATCH] Further mesh research: create an individual mesh for each animation sequence, with a single blend shape animation per mesh. Added an AliasModel class to manage meshes and find the correct animations. With this we can smoothly animate looped animation cycles. --- Assets/Scripts/Modules/AliasModel.cs | 53 ++++++++++++++ Assets/Scripts/Modules/AliasModel.cs.meta | 3 + Assets/Scripts/Modules/AliasModelAnimator.cs | 10 +-- Assets/Scripts/Modules/RenderModule.cs | 76 +++++++++++++++----- 4 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 Assets/Scripts/Modules/AliasModel.cs create mode 100644 Assets/Scripts/Modules/AliasModel.cs.meta diff --git a/Assets/Scripts/Modules/AliasModel.cs b/Assets/Scripts/Modules/AliasModel.cs new file mode 100644 index 0000000..3d83899 --- /dev/null +++ b/Assets/Scripts/Modules/AliasModel.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UnityEngine; + +public class AliasModel +{ + public static readonly Regex AnimationRegex = new Regex(@"^[a-zA-Z]+"); + + private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>(); // TODO: make this a fixed array after initialization + + 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; + + 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; + + 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) + { + startFrame = animationMeshes[i].Item1; + if (frameIndex >= startFrame) + { + mesh = animationMeshes[i].Item2; + return true; + } + } + + return false; // Shouldn't happen + } +} diff --git a/Assets/Scripts/Modules/AliasModel.cs.meta b/Assets/Scripts/Modules/AliasModel.cs.meta new file mode 100644 index 0000000..2fd3168 --- /dev/null +++ b/Assets/Scripts/Modules/AliasModel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74fb95086b6c4400bc3696481e4679e6 +timeCreated: 1618650253 \ No newline at end of file diff --git a/Assets/Scripts/Modules/AliasModelAnimator.cs b/Assets/Scripts/Modules/AliasModelAnimator.cs index e5c8cd9..aad728b 100644 --- a/Assets/Scripts/Modules/AliasModelAnimator.cs +++ b/Assets/Scripts/Modules/AliasModelAnimator.cs @@ -3,6 +3,8 @@ using UnityEngine; public class AliasModelAnimator : MonoBehaviour { + public AliasModel aliasModel; + //private int frameNumber = 0; private float frameNumber = 0; @@ -15,19 +17,19 @@ public class AliasModelAnimator : MonoBehaviour yield break; } - var mesh = meshRenderer.sharedMesh; - int numFrames = mesh.GetBlendShapeFrameCount(0); + int numFrames = aliasModel.GetAnimationFrameCount(0); while (true) { - float blendWeight = (float)frameNumber / numFrames; + aliasModel.Animate(frameNumber, out Mesh mesh, out float blendWeight); + meshRenderer.sharedMesh = mesh; meshRenderer.SetBlendShapeWeight(0, blendWeight); //yield return new WaitForSeconds(0.1f); // Animate at 10 fps yield return null; //frameNumber = (frameNumber + 1) % numFrames; - frameNumber = (frameNumber + Time.deltaTime * 10) % numFrames; + frameNumber = (frameNumber + Time.deltaTime * 5) % numFrames; } } } diff --git a/Assets/Scripts/Modules/RenderModule.cs b/Assets/Scripts/Modules/RenderModule.cs index 3e8ca52..f318353 100644 --- a/Assets/Scripts/Modules/RenderModule.cs +++ b/Assets/Scripts/Modules/RenderModule.cs @@ -18,25 +18,40 @@ public partial class RenderModule { Debug.Log($"Alias model '{name}' with {header.numVerts} vertices, {header.numTriangles} triangles, {header.numFrames} frame(s)"); - ConvertVertices(header, poseVertices[0], out var vertices, out var normals); ConvertTriangles(triangles, out var indices); ConvertUVs(stVertices, header.skinWidth, header.skinHeight, out var uvs); + + AliasModel aliasModel = new AliasModel(); - var mesh = new Mesh { name = name }; - mesh.SetVertices(vertices); - mesh.SetNormals(normals); - mesh.SetIndices(indices, MeshTopology.Triangles, 0, false); - mesh.SetUVs(0, uvs); - - CreateBlendShapes(mesh, vertices, normals, header, poseVertices); + 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, animName, startFrame, frameIdx); + aliasModel.AddAnimation(startFrame, mesh); + + animName = frameName; + startFrame = frameIdx; + } + } - mesh.Optimize(); // This ensures that triangles will be properly fused and organized in the best possible way - mesh.RecalculateBounds(); - mesh.UploadMeshData(true); + mesh = CreateAnimatedMesh(header, poseVertices, indices, uvs, animName, startFrame, header.numFrames); + aliasModel.AddAnimation(startFrame, mesh); var go = new GameObject(System.IO.Path.GetFileNameWithoutExtension(name)); go.transform.SetPositionAndRotation(new Vector3(xPos, 0, 0), Quaternion.Euler(-90, 90, 0)); - + if (header.numFrames > 1) { var mr = go.AddComponent(); @@ -46,7 +61,9 @@ public partial class RenderModule mr.receiveShadows = false; mr.lightProbeUsage = LightProbeUsage.Off; mr.reflectionProbeUsage = ReflectionProbeUsage.Off; - go.AddComponent(); + + var animator = go.AddComponent(); + animator.aliasModel = aliasModel; } else { @@ -109,9 +126,29 @@ public partial class RenderModule } } + 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, Vector3[] baseVertices, Vector3[] baseNormals, - QAliasHeader header, QTriVertex[][] poseVertices) + 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]; @@ -119,9 +156,12 @@ public partial class RenderModule Vector3 scale = header.scale.ToVector3(); Vector3 origin = header.scaleOrigin.ToVector3(); - // Frame 0 is the base pose, so we start with frame 1 - for (int frameIdx = 1; frameIdx < header.numFrames; ++frameIdx) + 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) @@ -131,7 +171,7 @@ public partial class RenderModule deltaNormals[vertIdx] = QLightNormals.Get(poseVerts[vertIdx].lightNormalIndex) - baseNormals[vertIdx]; } - mesh.AddBlendShapeFrame(mesh.name, (float)frameIdx / (header.numFrames-1), deltaVertices, deltaNormals, null); + mesh.AddBlendShapeFrame(animName, (float)index / numFrames, deltaVertices, deltaNormals, null); } } }