From f668c97b5208eede1385807723901e0043d2d472 Mon Sep 17 00:00:00 2001 From: Nico de Poel Date: Mon, 26 Jul 2021 21:05:06 +0200 Subject: [PATCH] Reworked the entity animation system, so that blending between poses is pulled fully to the Unity side. With this we can detect the situation where we are trying to blend from one animation sequence to another. Since this is not possible in UniQuake due to each animation sequence being converted to a separate mesh, we instead hold the poses for a frame as if we're not using animation lerping at all. This fixes the occasional animation glitch where the previous sequence would be animated a bit too far, which can be very noticeable on non-looping animation sequences (e.g. the Chthon's rise animation). --- Assets/Scripts/Game/Entity.cs | 6 +- Assets/Scripts/Modules/GameModule.Interop.cs | 6 +- Assets/Scripts/Modules/GameModule.cs | 4 +- Assets/Scripts/Support/AliasModel.cs | 62 ++++++++++++++++---- engine/Quake/client.h | 2 +- engine/Quake/r_alias.c | 5 +- engine/UniQuake/game_uniquake.c | 6 +- 7 files changed, 65 insertions(+), 26 deletions(-) diff --git a/Assets/Scripts/Game/Entity.cs b/Assets/Scripts/Game/Entity.cs index 71599b6..3b3d06e 100644 --- a/Assets/Scripts/Game/Entity.cs +++ b/Assets/Scripts/Game/Entity.cs @@ -79,7 +79,7 @@ public class Entity AssignMeshRenderer(); // Set a default pose based on the first animation frame - UpdateAnimation(0); + UpdateAnimation(0, 0, 0f); if (model.AutoAnimate) { @@ -103,11 +103,11 @@ public class Entity worldModel.transform.SetParent(gameObject.transform); } - public void UpdateAnimation(float frameNum) + public void UpdateAnimation(int pose1, int pose2, float blend) { if (aliasModel != null) { - aliasModel.Animate(frameNum, out Mesh mesh, out float blendWeight); + aliasModel.Animate(pose1, pose2, blend, out Mesh mesh, out float blendWeight); if (skinnedMeshRenderer.enabled) { diff --git a/Assets/Scripts/Modules/GameModule.Interop.cs b/Assets/Scripts/Modules/GameModule.Interop.cs index 3e72b1b..022aafc 100644 --- a/Assets/Scripts/Modules/GameModule.Interop.cs +++ b/Assets/Scripts/Modules/GameModule.Interop.cs @@ -66,12 +66,12 @@ public partial class GameModule : CallbackHandler } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void GameUpdateEntityAnimationCallback(IntPtr target, int entityNum, float frameNum); + private delegate void GameUpdateEntityAnimationCallback(IntPtr target, int entityNum, int pose1, int pose2, float blend); [MonoPInvokeCallback(typeof(GameUpdateEntityAnimationCallback))] - private static void Callback_GameUpdateEntityAnimation(IntPtr target, int entityNum, float frameNum) + private static void Callback_GameUpdateEntityAnimation(IntPtr target, int entityNum, int pose1, int pose2, float blend) { - GetSelf(target).UpdateEntityAnimation(entityNum, frameNum); + GetSelf(target).UpdateEntityAnimation(entityNum, pose1, pose2, blend); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] diff --git a/Assets/Scripts/Modules/GameModule.cs b/Assets/Scripts/Modules/GameModule.cs index e4b5e7a..5f7e3a8 100644 --- a/Assets/Scripts/Modules/GameModule.cs +++ b/Assets/Scripts/Modules/GameModule.cs @@ -58,9 +58,9 @@ public partial class GameModule uq.GameState.RemoveEntity(entityNum); } - private void UpdateEntityAnimation(int entityNum, float frameNum) + private void UpdateEntityAnimation(int entityNum, int pose1, int pose2, float blend) { - uq.GameState.GetEntity(entityNum)?.UpdateAnimation(frameNum); + uq.GameState.GetEntity(entityNum)?.UpdateAnimation(pose1, pose2, blend); } private void SetEntitySkin(int entityNum, int skinNum) diff --git a/Assets/Scripts/Support/AliasModel.cs b/Assets/Scripts/Support/AliasModel.cs index 781ae9c..c35edac 100644 --- a/Assets/Scripts/Support/AliasModel.cs +++ b/Assets/Scripts/Support/AliasModel.cs @@ -26,7 +26,9 @@ public class AliasModel public int GetAnimationFrameCount(float frameNum) { - if (!FindAnimation((int)frameNum, out Mesh mesh, out int startFrame)) + GetAnimationMesh((int)frameNum, out Mesh mesh, out int startFrame); + + if (mesh == null) return 0; if (mesh.blendShapeCount == 0) @@ -37,34 +39,74 @@ public class AliasModel public void Animate(float frameNum, out Mesh mesh, out float blendWeight) { + GetAnimationMesh((int)frameNum, out mesh, out int startFrame); + + if (mesh == null || mesh.blendShapeCount == 0) + { + blendWeight = 0; + return; + } + + int numFrames = mesh.GetBlendShapeFrameCount(0); + blendWeight = ((frameNum - startFrame) / numFrames) % 1; + } + + public void Animate(int pose1, int pose2, float blend, out Mesh mesh, out float blendWeight) + { + int startFrame, numFrames; blendWeight = 0; - int frameIndex = (int)frameNum; - if (!FindAnimation(frameIndex, out mesh, out int startFrame)) + // Don't interpolate if we're on a fixed pose + if (pose1 == pose2) + { + GetAnimationMesh(pose1, out mesh, out startFrame); + if (mesh == null || mesh.blendShapeCount == 0) + return; + + numFrames = mesh.GetBlendShapeFrameCount(0); + blendWeight = ((float)pose1 - startFrame) / numFrames % 1; return; + } + + GetAnimationMesh(pose1, out var mesh1, out int startFrame1); + GetAnimationMesh(pose2, out var mesh2, out int startFrame2); + float pose; + if (mesh1 != mesh2) + { + // We cannot blend from one animation sequence to another, since each sequence is a separate Unity mesh. + // Therefore when transitioning between sequences, we hold the previous and next pose for half a frame each. + mesh = blend > 0.5f ? mesh2 : mesh1; + startFrame = blend > 0.5f ? startFrame2 : startFrame1; + pose = blend > 0.5f ? pose2 : pose1; + } + else + { + mesh = mesh1; + startFrame = startFrame1; + pose = pose1 + blend; + } + if (mesh == null || mesh.blendShapeCount == 0) return; - int numFrames = mesh.GetBlendShapeFrameCount(0); - blendWeight = ((frameNum - startFrame) / numFrames) % 1; + numFrames = mesh.GetBlendShapeFrameCount(0); + blendWeight = (pose - startFrame) / numFrames % 1; } - private bool FindAnimation(int frameIndex, out Mesh mesh, out int startFrame) + private void GetAnimationMesh(int pose, 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; + if (animationMeshes[i].Item1 > pose) + return; startFrame = animationMeshes[i].Item1; mesh = animationMeshes[i].Item2; } - - return true; } public void Dispose() diff --git a/engine/Quake/client.h b/engine/Quake/client.h index 843ee6e..529d1aa 100644 --- a/engine/Quake/client.h +++ b/engine/Quake/client.h @@ -375,7 +375,7 @@ void Chase_UpdateForDrawing (void); //johnfitz void UQ_Game_SetEntityModel(int entityNum, const char *modelName); void UQ_Game_SetEntityTransform(int entityNum, vec3_t origin, vec3_t angles); void UQ_Game_RemoveEntity(int entityNum); -void UQ_Game_UpdateEntityAnimation(int entityNum, float frameNum); +void UQ_Game_UpdateEntityAnimation(int entityNum, int pose1, int pose2, float blend); void UQ_Game_SetEntitySkin(int entityNum, int skinNum); #endif /* _CLIENT_H_ */ diff --git a/engine/Quake/r_alias.c b/engine/Quake/r_alias.c index f787047..e4bfe6e 100644 --- a/engine/Quake/r_alias.c +++ b/engine/Quake/r_alias.c @@ -642,10 +642,7 @@ void R_DrawAliasModel (entity_t *e) R_SetupEntityTransform (e, &lerpdata); UQ_Game_SetEntityTransform(e->num, lerpdata.origin, lerpdata.angles); - if (lerpdata.pose1 == lerpdata.pose2) - UQ_Game_UpdateEntityAnimation(e->num, (float)lerpdata.pose1); // Fixed pose, don't lerp - else - UQ_Game_UpdateEntityAnimation(e->num, (float)lerpdata.pose1 + lerpdata.blend); + UQ_Game_UpdateEntityAnimation(e->num, lerpdata.pose1, lerpdata.pose2, lerpdata.blend); // // cull it diff --git a/engine/UniQuake/game_uniquake.c b/engine/UniQuake/game_uniquake.c index 2bbdf86..0f057f9 100644 --- a/engine/UniQuake/game_uniquake.c +++ b/engine/UniQuake/game_uniquake.c @@ -9,7 +9,7 @@ typedef struct unity_gamecalls_s void(*SetEntityModel)(void *target, int entityNum, const char *modelName); void(*SetEntityTransform)(void *target, int entityNum, vec3_t origin, vec3_t angles); void(*RemoveEntity)(void *target, int entityNum); - void(*UpdateEntityAnimation)(void *target, int entityNum, float frameNum); + void(*UpdateEntityAnimation)(void *target, int entityNum, int pose1, int pose2, float blend); void(*SetEntitySkin)(void *target, int entityNum, int skinNum); } unity_gamecalls_t; @@ -30,9 +30,9 @@ void UQ_Game_RemoveEntity(int entityNum) unity_gamecalls->RemoveEntity(unity_gamecalls->target, entityNum); } -void UQ_Game_UpdateEntityAnimation(int entityNum, float frameNum) +void UQ_Game_UpdateEntityAnimation(int entityNum, int pose1, int pose2, float blend) { - unity_gamecalls->UpdateEntityAnimation(unity_gamecalls->target, entityNum, frameNum); + unity_gamecalls->UpdateEntityAnimation(unity_gamecalls->target, entityNum, pose1, pose2, blend); } void UQ_Game_SetEntitySkin(int entityNum, int skinNum)