From e925b0cfcceb569d578442cfde7d6e4de2bbe673 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidOnSteam@users.noreply.github.com> Date: Tue, 11 Jul 2023 19:37:36 -0500 Subject: [PATCH] [AlternateIKSystem] Initial commit - Testing shit this out in a day, going to see how much i can do before exploding --- AlternateIKSystem/AlternateIKSystem.csproj | 2 + AlternateIKSystem/HarmonyPatches.cs | 113 +++++ AlternateIKSystem/IK/BodyControl.cs | 95 ++++ AlternateIKSystem/IK/IKCalibrator.cs | 183 ++++++++ AlternateIKSystem/IK/IKHandlers/IKHandler.cs | 37 ++ .../IK/IKHandlers/IKHandlerDesktop.cs | 188 ++++++++ AlternateIKSystem/IK/IKManager.cs | 416 ++++++++++++++++++ AlternateIKSystem/IK/MusclePoses.cs | 46 ++ .../IK/VRIKHelpers/VRIKCalibrationData.cs | 31 ++ AlternateIKSystem/IK/VRIKHelpers/VRIKUtils.cs | 125 ++++++ AlternateIKSystem/Main.cs | 37 ++ AlternateIKSystem/ModSettings.cs | 17 + AlternateIKSystem/Properties/AssemblyInfo.cs | 31 ++ AlternateIKSystem/README.md | 16 + AlternateIKSystem/format.json | 24 + NAK_CVR_Mods.sln | 6 + 16 files changed, 1367 insertions(+) create mode 100644 AlternateIKSystem/AlternateIKSystem.csproj create mode 100644 AlternateIKSystem/HarmonyPatches.cs create mode 100644 AlternateIKSystem/IK/BodyControl.cs create mode 100644 AlternateIKSystem/IK/IKCalibrator.cs create mode 100644 AlternateIKSystem/IK/IKHandlers/IKHandler.cs create mode 100644 AlternateIKSystem/IK/IKHandlers/IKHandlerDesktop.cs create mode 100644 AlternateIKSystem/IK/IKManager.cs create mode 100644 AlternateIKSystem/IK/MusclePoses.cs create mode 100644 AlternateIKSystem/IK/VRIKHelpers/VRIKCalibrationData.cs create mode 100644 AlternateIKSystem/IK/VRIKHelpers/VRIKUtils.cs create mode 100644 AlternateIKSystem/Main.cs create mode 100644 AlternateIKSystem/ModSettings.cs create mode 100644 AlternateIKSystem/Properties/AssemblyInfo.cs create mode 100644 AlternateIKSystem/README.md create mode 100644 AlternateIKSystem/format.json diff --git a/AlternateIKSystem/AlternateIKSystem.csproj b/AlternateIKSystem/AlternateIKSystem.csproj new file mode 100644 index 0000000..e94f9dc --- /dev/null +++ b/AlternateIKSystem/AlternateIKSystem.csproj @@ -0,0 +1,2 @@ + + diff --git a/AlternateIKSystem/HarmonyPatches.cs b/AlternateIKSystem/HarmonyPatches.cs new file mode 100644 index 0000000..11f4073 --- /dev/null +++ b/AlternateIKSystem/HarmonyPatches.cs @@ -0,0 +1,113 @@ +using ABI.CCK.Components; +using ABI_RC.Core.Player; +using ABI_RC.Systems.IK; +using HarmonyLib; +using NAK.AlternateIKSystem.IK; +using UnityEngine; + +namespace NAK.AlternateIKSystem.HarmonyPatches; + +internal class PlayerSetupPatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] + private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance) + { + __instance.gameObject.AddComponent(); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))] + private static void Postfix_PlayerSetup_SetupAvatar(GameObject inAvatar) + { + if (!ModSettings.EntryEnabled.Value) + return; + + try + { + IKManager.Instance?.OnAvatarInitialized(inAvatar); + } + catch (Exception e) + { + AlternateIKSystem.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetupAvatar)}"); + AlternateIKSystem.Logger.Error(e); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ClearAvatar))] + private static void Postfix_PlayerSetup_ClearAvatar() + { + try + { + IKManager.Instance?.OnAvatarDestroyed(); + } + catch (Exception e) + { + AlternateIKSystem.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_ClearAvatar)}"); + AlternateIKSystem.Logger.Error(e); + } + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupIKScaling))] + private static void Prefix_PlayerSetup_SetupIKScaling(ref Vector3 ___scaleDifference, ref bool __runOriginal) + { + try + { + if (IKManager.Instance != null) + __runOriginal = !IKManager.Instance.OnPlayerScaled(1f + ___scaleDifference.y); + } + catch (Exception e) + { + AlternateIKSystem.Logger.Error($"Error during the patched method {nameof(Prefix_PlayerSetup_SetupIKScaling)}"); + AlternateIKSystem.Logger.Error(e); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetSitting))] + private static void Postfix_PlayerSetup_SetSitting(ref bool ___isCurrentlyInSeat) + { + try + { + IKManager.Instance?.OnPlayerSeatedStateChanged(___isCurrentlyInSeat); + } + catch (Exception e) + { + AlternateIKSystem.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetSitting)}"); + AlternateIKSystem.Logger.Error(e); + } + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ResetIk))] + private static void Prefix_PlayerSetup_ResetIk(ref PlayerSetup __instance, ref bool __runOriginal) + { + try + { + CVRMovementParent currentParent = __instance._movementSystem._currentParent; + if (currentParent?._referencePoint == null) + return; + + if (IKManager.Instance != null) + __runOriginal = !IKManager.Instance.OnPlayerHandleMovementParent(currentParent); + } + catch (Exception e) + { + AlternateIKSystem.Logger.Error($"Error during the patched method {nameof(Prefix_PlayerSetup_ResetIk)}"); + AlternateIKSystem.Logger.Error(e); + } + } +} + +internal class IKSystemPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(IKSystem), nameof(IKSystem.InitializeAvatar))] + private static void Prefix_IKSystem_InitializeAvatar(ref bool __runOriginal) + { + // Don't setup with native IKSystem + __runOriginal = !ModSettings.EntryEnabled.Value; + } +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/BodyControl.cs b/AlternateIKSystem/IK/BodyControl.cs new file mode 100644 index 0000000..7b4c9f9 --- /dev/null +++ b/AlternateIKSystem/IK/BodyControl.cs @@ -0,0 +1,95 @@ +using RootMotion.FinalIK; + +namespace NAK.AlternateIKSystem.IK; + +public static class BodyControl +{ + #region Tracking Controls + + public static bool TrackingAll = true; + public static bool TrackingHead = true; + public static bool TrackingPelvis = true; + public static bool TrackingLeftArm = true; + public static bool TrackingRightArm = true; + public static bool TrackingLeftLeg = true; + public static bool TrackingRightLeg = true; + + //TODO: dont do this, it is effective but lazy + public static bool TrackingLocomotion + { + get => _trackingLocomotion; + set + { + if (_trackingLocomotion == value) + return; + + _trackingLocomotion = value; + IKManager.solver?.Reset(); + } + } + private static bool _trackingLocomotion = true; + + public static float TrackingPositionWeight = 1f; + + // TODO: decide if these are considered "Tracking Controls" + public static float TrackingUpright = 1f; + public static float TrackingMaxRootAngle = 0f; + + #endregion + + #region Player Settings + + public static bool useHipTracking = true; + public static bool useChestTracking = true; + public static bool useLeftFootTracking = true; + public static bool useRightFootTracking = true; + + public static bool useLeftElbowTracking = false; + public static bool useRightElbowTracking = false; + public static bool useLeftKneeTracking = false; + public static bool useRightKneeTracking = false; + + public static bool useLocomotionAnimations = true; + + #endregion + + #region BodyControl Configuration + + public static float InvalidTrackerDistance = 1f; + + #endregion + + #region Solver Weight Helpers + + public static void SetHeadWeight(IKSolverVR.Spine spine, LookAtIK lookAtIk, float weight) + { + spine.positionWeight = weight; + spine.rotationWeight = weight; + if (lookAtIk != null) + lookAtIk.solver.IKPositionWeight = weight; + } + + public static void SetArmWeight(IKSolverVR.Arm arm, float weight) + { + arm.positionWeight = weight; + arm.rotationWeight = weight; + arm.shoulderRotationWeight = weight; + arm.shoulderTwistWeight = weight; + arm.bendGoalWeight = arm.bendGoal != null ? weight : 0f; + } + + public static void SetLegWeight(IKSolverVR.Leg leg, float weight) + { + leg.positionWeight = weight; + leg.rotationWeight = weight; + leg.bendGoalWeight = leg.usingKneeTracker ? weight : 0f; + } + + public static void SetPelvisWeight(IKSolverVR.Spine spine, float weight) + { + spine.pelvisPositionWeight = weight; + spine.pelvisRotationWeight = weight; + } + + #endregion +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/IKCalibrator.cs b/AlternateIKSystem/IK/IKCalibrator.cs new file mode 100644 index 0000000..17e92c8 --- /dev/null +++ b/AlternateIKSystem/IK/IKCalibrator.cs @@ -0,0 +1,183 @@ +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.AlternateIKSystem.IK; + +internal static class IKCalibrator +{ + public static VRIK SetupVrIk(Animator animator) + { + if (animator.gameObject.TryGetComponent(out VRIK vrik)) + UnityEngine.Object.DestroyImmediate(vrik); + + vrik = animator.gameObject.AddComponent(); + vrik.AutoDetectReferences(); + + if (!ModSettings.EntryUseVRIKToes.Value) + { + vrik.references.leftToes = null; + vrik.references.rightToes = null; + } + + vrik.solver.SetToReferences(vrik.references); + + GuessWristPalmAxis(vrik.references.leftHand, vrik.references.leftForearm, vrik.solver.leftArm); + GuessWristPalmAxis(vrik.references.rightHand, vrik.references.rightForearm, vrik.solver.rightArm); + + SafePalmToThumbAxis(vrik.references.leftHand, vrik.references.leftForearm, vrik.solver.leftArm, + animator.GetBoneTransform(HumanBodyBones.LeftThumbProximal)); + SafePalmToThumbAxis(vrik.references.rightHand, vrik.references.rightForearm, vrik.solver.rightArm, + animator.GetBoneTransform(HumanBodyBones.RightThumbProximal)); + + AddTwistRelaxer(vrik.references.leftForearm, vrik, vrik.references.leftHand); + AddTwistRelaxer(vrik.references.rightForearm, vrik, vrik.references.rightHand); + + //vrik.solver.leftArm.shoulderRotationMode = (IKSolverVR.Arm.ShoulderRotationMode)IkTweaksSettings.ShoulderMode; + //vrik.solver.rightArm.shoulderRotationMode = (IKSolverVR.Arm.ShoulderRotationMode)IkTweaksSettings.ShoulderMode; + + // zero all weights controlled by BodyControl + vrik.solver.locomotion.weight = 0f; + vrik.solver.IKPositionWeight = 0f; + + vrik.solver.spine.pelvisPositionWeight = 0f; + vrik.solver.spine.pelvisRotationWeight = 0f; + vrik.solver.spine.positionWeight = 0f; + vrik.solver.spine.rotationWeight = 0f; + + vrik.solver.leftLeg.positionWeight = 0f; + vrik.solver.leftLeg.rotationWeight = 0f; + vrik.solver.rightLeg.positionWeight = 0f; + vrik.solver.rightLeg.rotationWeight = 0f; + vrik.solver.leftArm.positionWeight = 0f; + vrik.solver.leftArm.rotationWeight = 0f; + vrik.solver.rightArm.positionWeight = 0f; + vrik.solver.rightArm.rotationWeight = 0f; + + vrik.solver.leftLeg.bendGoalWeight = 0f; + vrik.solver.rightLeg.bendGoalWeight = 0f; + + // these weights are fine + vrik.solver.leftArm.shoulderRotationWeight = 0.8f; + vrik.solver.rightArm.shoulderRotationWeight = 0.8f; + + vrik.solver.leftLeg.bendToTargetWeight = 0.75f; + vrik.solver.rightLeg.bendToTargetWeight = 0.75f; + + // hack to prevent death + vrik.fixTransforms = !animator.enabled; + + // purposefully initiating early + vrik.solver.Initiate(vrik.transform); + vrik.solver.Reset(); + + return vrik; + } + + private static void GuessWristPalmAxis(Transform hand, Transform forearm, IKSolverVR.Arm arm) + { + arm.wristToPalmAxis = VRIKCalibrator.GuessWristToPalmAxis( + hand, + forearm + ); + } + + private static void SafePalmToThumbAxis(Transform hand, Transform forearm, IKSolverVR.Arm arm, Transform thumbBone = null) + { + if (hand.childCount == 0) + { + arm.palmToThumbAxis = Vector3.one; + return; + } + + arm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis( + hand, + forearm, + thumbBone + ); + } + + private static void AddTwistRelaxer(Transform forearm, VRIK ik, Transform hand) + { + if (forearm == null) return; + TwistRelaxer twistRelaxer = forearm.gameObject.AddComponent(); + twistRelaxer.ik = ik; + twistRelaxer.weight = 0.5f; + twistRelaxer.child = hand; + twistRelaxer.parentChildCrossfade = 0.8f; + } + + public static void ConfigureDesktopVrIk(VRIK vrik) + { + // From DesktopVRIK + // https://github.com/NotAKidOnSteam/NAK_CVR_Mods/blob/fca0a32257311f044d1a9d6e68269baa4a65a45c/DesktopVRIK/DesktopVRIKCalibrator.cs#L219C2-L247C103 + + vrik.solver.spine.bodyPosStiffness = 1f; + vrik.solver.spine.bodyRotStiffness = 0.2f; + vrik.solver.spine.neckStiffness = 0.0001f; + vrik.solver.spine.rotateChestByHands = 0f; + + vrik.solver.spine.minHeadHeight = 0f; + vrik.solver.locomotion.angleThreshold = 30f; + vrik.solver.locomotion.maxLegStretch = 1f; + + vrik.solver.spine.chestClampWeight = 0f; + vrik.solver.spine.headClampWeight = 0.2f; + + vrik.solver.spine.maintainPelvisPosition = 0f; + vrik.solver.spine.moveBodyBackWhenCrouching = 0f; + + vrik.solver.locomotion.velocityFactor = 0f; + vrik.solver.locomotion.maxVelocity = 0f; + vrik.solver.locomotion.rootSpeed = 1000f; + + vrik.solver.spine.positionWeight = 0f; + vrik.solver.spine.rotationWeight = 1f; + + vrik.solver.spine.maxRootAngle = 180f; + + vrik.solver.plantFeet = true; + } + + public static void ConfigureHalfBodyVrIk(VRIK vrik) + { + // From IKTweaks + // https://github.com/knah/VRCMods/blob/a22bb73a5e40c75152c6e5db2a7a9afb13e42ba5/IKTweaks/FullBodyHandling.cs#L384C1-L394C71 + + vrik.solver.spine.bodyPosStiffness = 1f; + vrik.solver.spine.bodyRotStiffness = 0f; + vrik.solver.spine.neckStiffness = 0.5f; + vrik.solver.spine.rotateChestByHands = .25f; + + vrik.solver.spine.minHeadHeight = -100f; + vrik.solver.locomotion.angleThreshold = 60f; + vrik.solver.locomotion.maxLegStretch = 1f; + + vrik.solver.spine.chestClampWeight = 0f; + vrik.solver.spine.headClampWeight = 0f; + + vrik.solver.spine.maintainPelvisPosition = 0f; + vrik.solver.spine.moveBodyBackWhenCrouching = 0f; + + vrik.solver.locomotion.velocityFactor = 0.4f; + vrik.solver.locomotion.maxVelocity = 0.4f; + vrik.solver.locomotion.rootSpeed = 20f; + + vrik.solver.spine.positionWeight = 1f; + vrik.solver.spine.rotationWeight = 1f; + + vrik.solver.spine.maxRootAngle = 0f; + + vrik.solver.plantFeet = false; + } + + // TODO: figure out proper Desktop & VR organization + public static void SetupHeadIKTargetDesktop(VRIK vrik) + { + // Lazy HeadIKTarget calibration + if (vrik.solver.spine.headTarget == null) + vrik.solver.spine.headTarget = new GameObject("Head IK Target").transform; + vrik.solver.spine.headTarget.parent = vrik.references.head; + vrik.solver.spine.headTarget.localPosition = Vector3.zero; + vrik.solver.spine.headTarget.localRotation = Quaternion.identity; + } +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/IKHandlers/IKHandler.cs b/AlternateIKSystem/IK/IKHandlers/IKHandler.cs new file mode 100644 index 0000000..92bf802 --- /dev/null +++ b/AlternateIKSystem/IK/IKHandlers/IKHandler.cs @@ -0,0 +1,37 @@ +using ABI.CCK.Components; +using NAK.AlternateIKSystem.VRIKHelpers; +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.AlternateIKSystem.IK.IKHandlers; + +internal class IKHandler +{ + internal VRIK _vrik; + internal IKSolverVR _solver; + + // Last Movement Parent Info + internal Vector3 _movementPosition; + internal Quaternion _movementRotation; + internal CVRMovementParent _movementParent; + + #region Virtual Methods + + public virtual void OnInitializeIk() + { + } + + public virtual void OnUpdate() + { + } + + public virtual void OnPlayerScaled(float scaleDifference, VRIKCalibrationData calibrationData) + { + } + + public virtual void OnPlayerHandleMovementParent(CVRMovementParent currentParent) + { + } + + #endregion +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/IKHandlers/IKHandlerDesktop.cs b/AlternateIKSystem/IK/IKHandlers/IKHandlerDesktop.cs new file mode 100644 index 0000000..c7bb41f --- /dev/null +++ b/AlternateIKSystem/IK/IKHandlers/IKHandlerDesktop.cs @@ -0,0 +1,188 @@ +using ABI.CCK.Components; +using NAK.AlternateIKSystem.VRIKHelpers; +using RootMotion.FinalIK; +using UnityEngine; +using UnityEngine.Events; + +namespace NAK.AlternateIKSystem.IK.IKHandlers; + +internal class IKHandlerDesktop : IKHandler +{ + public IKHandlerDesktop(VRIK vrik) + { + _vrik = vrik; + _solver = vrik.solver; + } + + #region Overrides + + public override void OnInitializeIk() + { + _vrik.onPreSolverUpdate.AddListener(new UnityAction(OnPreSolverUpdate)); + } + + public override void OnUpdate() + { + // Reset avatar local position + _vrik.transform.localPosition = Vector3.zero; + _vrik.transform.localRotation = Quaternion.identity; + + UpdateWeights(); + } + + public override void OnPlayerScaled(float scaleDifference, VRIKCalibrationData calibrationData) + { + VRIKUtils.ApplyScaleToVRIK + ( + _vrik, + calibrationData, + scaleDifference + ); + } + + public override void OnPlayerHandleMovementParent(CVRMovementParent currentParent) + { + // Get current position + var currentPosition = currentParent._referencePoint.position; + var currentRotation = Quaternion.Euler(0f, currentParent.transform.rotation.eulerAngles.y, 0f); + + // Convert to delta position (how much changed since last frame) + var deltaPosition = currentPosition - _movementPosition; + var deltaRotation = Quaternion.Inverse(_movementRotation) * currentRotation; + + // Desktop pivots from playerlocal transform + var platformPivot = IKManager.Instance.transform.position; + + // Prevent targeting other parent position + if (_movementParent == currentParent) + { + _solver.AddPlatformMotion(deltaPosition, deltaRotation, platformPivot); + _ikSimulatedRootAngle = Mathf.Repeat(_ikSimulatedRootAngle + deltaRotation.eulerAngles.y, 360f); + } + + // Store for next frame + _movementParent = currentParent; + _movementPosition = currentPosition; + _movementRotation = currentRotation; + } + + #endregion + + #region VRIK Solver Events + + //TODO: properly expose these settings + + private bool EntryPlantFeet = true; + + private float EntryBodyLeanWeight = 1f; + private bool EntryProneThrusting = true; + + private float EntryBodyHeadingLimit = 30f; + private float EntryPelvisHeadingWeight = 0.25f; + private float EntryChestHeadingWeight = 0.75f; + + private float _ikSimulatedRootAngle = 0f; + + private void OnPreSolverUpdate() + { + _solver.plantFeet = EntryPlantFeet; + + // Emulate old VRChat hip movement + if (EntryBodyLeanWeight > 0) + { + float weightedAngle = EntryProneThrusting ? 1f : EntryBodyLeanWeight * _solver.locomotion.weight; + float angle = IKManager.Instance._desktopCamera.localEulerAngles.x; + angle = angle > 180 ? angle - 360 : angle; + Quaternion rotation = Quaternion.AngleAxis(angle * weightedAngle, _vrik.transform.right); + _solver.spine.headRotationOffset *= rotation; + } + + // Make root heading follow within a set limit + if (EntryBodyHeadingLimit > 0) + { + float weightedAngleLimit = EntryBodyHeadingLimit * _solver.locomotion.weight; + float deltaAngleRoot = Mathf.DeltaAngle(IKManager.Instance.transform.eulerAngles.y, _ikSimulatedRootAngle); + float absDeltaAngleRoot = Mathf.Abs(deltaAngleRoot); + + if (absDeltaAngleRoot > weightedAngleLimit) + { + deltaAngleRoot = Mathf.Sign(deltaAngleRoot) * weightedAngleLimit; + _ikSimulatedRootAngle = Mathf.MoveTowardsAngle(_ikSimulatedRootAngle, IKManager.Instance.transform.eulerAngles.y, absDeltaAngleRoot - weightedAngleLimit); + } + + _solver.spine.rootHeadingOffset = deltaAngleRoot; + + if (EntryPelvisHeadingWeight > 0) + { + _solver.spine.pelvisRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * EntryPelvisHeadingWeight, 0f); + _solver.spine.chestRotationOffset *= Quaternion.Euler(0f, -deltaAngleRoot * EntryPelvisHeadingWeight, 0f); + } + + if (EntryChestHeadingWeight > 0) + { + _solver.spine.chestRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * EntryChestHeadingWeight, 0f); + } + } + } + + #endregion + + #region Private Methods + + private float _locomotionWeight = 1f; + + private void UpdateWeights() + { + // Lerp locomotion weight, lerp to BodyControl.TrackingUpright??? + float targetWeight = + (BodyControl.TrackingAll && BodyControl.TrackingLocomotion && BodyControl.TrackingUpright > 0.8f) + ? 1f + : 0.0f; + _locomotionWeight = Mathf.Lerp(_locomotionWeight, targetWeight, Time.deltaTime * 20f); + + if (BodyControl.TrackingAll) + { + _vrik.enabled = true; + _solver.IKPositionWeight = BodyControl.TrackingPositionWeight; + _solver.locomotion.weight = _locomotionWeight; + _solver.spine.maxRootAngle = BodyControl.TrackingMaxRootAngle; + + // Hack to make knees bend properly when in custom pose animations + bool useAnimatedBendNormal = _locomotionWeight <= 0.5f; + _solver.leftLeg.useAnimatedBendNormal = useAnimatedBendNormal; + _solver.rightLeg.useAnimatedBendNormal = useAnimatedBendNormal; + + BodyControl.SetHeadWeight(_solver.spine, IKManager.lookAtIk, BodyControl.TrackingHead ? 1f : 0f); + + BodyControl.SetArmWeight(_solver.leftArm, BodyControl.TrackingLeftArm && _solver.leftArm.target != null ? 1f : 0f); + BodyControl.SetArmWeight(_solver.rightArm, BodyControl.TrackingRightArm && _solver.rightArm.target != null ? 1f : 0f); + + BodyControl.SetLegWeight(_solver.leftLeg, BodyControl.TrackingLeftLeg && _solver.leftLeg.target != null ? 1f : 0f); + BodyControl.SetLegWeight(_solver.rightLeg, BodyControl.TrackingRightLeg && _solver.rightLeg.target != null ? 1f : 0f); + + BodyControl.SetPelvisWeight(_solver.spine, BodyControl.TrackingPelvis && _solver.spine.pelvisTarget != null ? 1f : 0f); + } + else + { + _vrik.enabled = false; + _solver.IKPositionWeight = 0f; + _solver.locomotion.weight = 0f; + _solver.spine.maxRootAngle = 0f; + + _solver.leftLeg.useAnimatedBendNormal = true; + _solver.rightLeg.useAnimatedBendNormal = true; + + BodyControl.SetHeadWeight(_solver.spine, IKManager.lookAtIk, 0f); + BodyControl.SetArmWeight(_solver.leftArm, 0f); + BodyControl.SetArmWeight(_solver.rightArm, 0f); + BodyControl.SetLegWeight(_solver.leftLeg, 0f); + BodyControl.SetLegWeight(_solver.rightLeg, 0f); + BodyControl.SetPelvisWeight(_solver.spine, 0f); + } + + // Desktop should never have head position weight + _solver.spine.positionWeight = 0f; + } + + #endregion +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/IKManager.cs b/AlternateIKSystem/IK/IKManager.cs new file mode 100644 index 0000000..cfe2229 --- /dev/null +++ b/AlternateIKSystem/IK/IKManager.cs @@ -0,0 +1,416 @@ +using ABI.CCK.Components; +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using ABI_RC.Systems.MovementSystem; +using NAK.AlternateIKSystem.IK.IKHandlers; +using NAK.AlternateIKSystem.VRIKHelpers; +using RootMotion.FinalIK; +using UnityEngine; +using UnityEngine.Events; + +namespace NAK.AlternateIKSystem.IK; + +public class IKManager : MonoBehaviour +{ + public static IKManager Instance; + + public static VRIK vrik => _vrik; + private static VRIK _vrik; + public static IKSolverVR solver => _vrik?.solver; + private static LookAtIK _lookAtIk; + public static LookAtIK lookAtIk => _lookAtIk; + + private bool _isAvatarInitialized = false; + + // IK Handling + private IKHandler _ikHandler; + + // Player Info + internal Transform _desktopCamera; + internal Transform _vrCamera; + + // Avatar Info + private Animator _animator; + private Transform _hipTransform; + + // Animator Info + private int _animLocomotionLayer = -1; + private int _animIKPoseLayer = -1; + + // Pose Info + private HumanPoseHandler _humanPoseHandler; + private HumanPose _humanPose; + private HumanPose _humanPoseInitial; + + // VRIK Calibration Info + private VRIKCalibrationData _calibrationData; + + private void Start() + { + if (Instance != null) + { + Destroy(this); + return; + } + Instance = this; + + _desktopCamera = PlayerSetup.Instance.desktopCamera.transform; + _vrCamera = PlayerSetup.Instance.vrCamera.transform; + } + + private void Update() + { + BodyControl.TrackingAll = ShouldTrackAll(); + BodyControl.TrackingUpright = GetPlayerUpright(); + BodyControl.TrackingLocomotion = ShouldTrackLocomotion(); + + if (!_isAvatarInitialized) + return; + + _ikHandler?.OnUpdate(); + } + + #region Avatar Events + + public void OnAvatarInitialized(GameObject inAvatar) + { + if (_isAvatarInitialized) + return; + + if (!inAvatar.TryGetComponent(out _animator)) + return; + + if (_animator.avatar == null || !_animator.avatar.isHuman) + return; + + _lookAtIk = inAvatar.GetComponent(); + + _animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; + + _animIKPoseLayer = _animator.GetLayerIndex("IKPose"); + _animLocomotionLayer = _animator.GetLayerIndex("Locomotion/Emotes"); + + _hipTransform = _animator.GetBoneTransform(HumanBodyBones.Hips); + + _humanPoseHandler?.Dispose(); + _humanPoseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); + + _humanPoseHandler.GetHumanPose(ref _humanPose); + _humanPoseHandler.GetHumanPose(ref _humanPoseInitial); + + if (MetaPort.Instance.isUsingVr) + { + InitializeHalfBodyIk(); + } + else + { + InitializeDesktopIk(); + } + + _isAvatarInitialized = true; + } + + public void OnAvatarDestroyed() + { + if (!_isAvatarInitialized) + return; + + _vrik = null; + _lookAtIk = null; + _animator = null; + _animIKPoseLayer = -1; + _animLocomotionLayer = -1; + _hipTransform = null; + _humanPoseHandler?.Dispose(); + _humanPoseHandler = null; + _ikHandler = null; + + _isAvatarInitialized = false; + } + + #endregion + + #region Game Events + + public bool OnPlayerScaled(float scaleDifference) + { + if (!_isAvatarInitialized) + return false; + + _ikHandler?.OnPlayerScaled(scaleDifference, _calibrationData); + return true; + } + + public void OnPlayerSeatedStateChanged(bool isSitting) + { + if (!_isAvatarInitialized) + return; + + // update immediately, ShouldTrackLocomotion() will catch up next frame + if (isSitting) + BodyControl.TrackingLocomotion = false; + } + + public bool OnPlayerHandleMovementParent(CVRMovementParent movementParent) + { + if (!_isAvatarInitialized) + return false; + + _ikHandler?.OnPlayerHandleMovementParent(movementParent); + return true; + } + + #endregion + + #region Private Methods + + private bool ShouldTrackAll() + { + return !PlayerSetup.Instance._emotePlaying; + } + + private bool ShouldTrackLocomotion() + { + return !(MovementSystem.Instance.movementVector.magnitude > 0f + || MovementSystem.Instance.crouching + || MovementSystem.Instance.prone + || MovementSystem.Instance.flying + || MovementSystem.Instance.sitting + || !MovementSystem.Instance._isGrounded); + } + + private float GetPlayerUpright() + { + float avatarHeight = PlayerSetup.Instance._avatarHeight; + float currentHeight = PlayerSetup.Instance.GetViewRelativePosition().y; + return Mathf.Clamp01((avatarHeight > 0f) ? (currentHeight / avatarHeight) : 0f); + } + + #endregion + + #region IK Initialization + + private void InitializeDesktopIk() + { + PreSetupIkGeneral(); + IKCalibrator.ConfigureDesktopVrIk(_vrik); + IKCalibrator.SetupHeadIKTargetDesktop(_vrik); + InitializeIkGeneral(); + + _ikHandler = new IKHandlerDesktop(_vrik); + _ikHandler.OnInitializeIk(); + } + + private void InitializeHalfBodyIk() + { + PreSetupIkGeneral(); + IKCalibrator.ConfigureHalfBodyVrIk(_vrik); + InitializeIkGeneral(); + } + + private void PreSetupIkGeneral() + { + SetAvatarPose(AvatarPose.Default); + _vrik = IKCalibrator.SetupVrIk(_animator); + } + + private void InitializeIkGeneral() + { + SetAvatarPose(AvatarPose.IKPose); + + VRIKUtils.CalculateInitialIKScaling(_vrik, ref _calibrationData); + VRIKUtils.CalculateInitialFootsteps(_vrik, ref _calibrationData); + + VRIKUtils.ApplyScaleToVRIK(_vrik, _calibrationData, 1f); + + VRIKUtils.InitiateVRIKSolver(_vrik); // initiate again to store ikpose + + _vrik.onPreSolverUpdate.AddListener(new UnityAction(OnPreSolverUpdateGeneral)); + _vrik.onPostSolverUpdate.AddListener(new UnityAction(OnPostSolverUpdateGeneral)); + } + + #endregion + + #region VRIK Solver Events General + + private void OnPreSolverUpdateGeneral() + { + if (solver.IKPositionWeight < 0.9f) + return; + + Vector3 hipPos = _hipTransform.position; + Quaternion hipRot = _hipTransform.rotation; + + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < _humanPose.muscles.Length; i++) + { + //if (IkTweaksSettings.IgnoreAnimationsModeParsed == IgnoreAnimationsMode.All && IKTweaksMod.ourRandomPuck.activeInHierarchy) + //{ + // muscles[i] *= ourBoneResetMasks[i] == BoneResetMask.Never ? 1 : 0; + // continue; + //} + switch (ourBoneResetMasks[i]) + { + case BoneResetMask.Never: + break; + case BoneResetMask.Spine: + _humanPose.muscles[i] *= 1 - solver.spine.pelvisPositionWeight; + break; + case BoneResetMask.LeftArm: + _humanPose.muscles[i] *= 1 - solver.leftArm.positionWeight; + break; + case BoneResetMask.RightArm: + _humanPose.muscles[i] *= 1 - solver.rightArm.positionWeight; + break; + case BoneResetMask.LeftLeg: + _humanPose.muscles[i] *= 1 - solver.leftLeg.positionWeight; + break; + case BoneResetMask.RightLeg: + _humanPose.muscles[i] *= 1 - solver.rightLeg.positionWeight; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + _humanPoseHandler.SetHumanPose(ref _humanPose); + + _hipTransform.position = hipPos; + _hipTransform.rotation = hipRot; + } + + private void OnPostSolverUpdateGeneral() + { + Vector3 hipPos = _hipTransform.position; + Quaternion hipRot = _hipTransform.rotation; + + _humanPoseHandler.GetHumanPose(ref _humanPose); + _humanPoseHandler.SetHumanPose(ref _humanPose); + + _hipTransform.position = hipPos; + _hipTransform.rotation = hipRot; + } + + #endregion + + #region Avatar Pose Utilities + + private enum AvatarPose + { + Default = 0, + Initial = 1, + IKPose = 2, + TPose = 3, + APose = 4 + } + + private void SetAvatarPose(AvatarPose pose) + { + switch (pose) + { + case AvatarPose.Default: + SetMusclesToValue(0f); + break; + case AvatarPose.Initial: + if (HasCustomIKPose()) + SetCustomLayersWeights(0f, 1f); + _humanPoseHandler.SetHumanPose(ref _humanPoseInitial); + break; + case AvatarPose.IKPose: + if (HasCustomIKPose()) + { + SetCustomLayersWeights(1f, 0f); + return; + } + SetMusclesToPose(MusclePoses.IKPoseMuscles); + break; + case AvatarPose.TPose: + SetMusclesToPose(MusclePoses.TPoseMuscles); + break; + case AvatarPose.APose: + SetMusclesToPose(MusclePoses.APoseMuscles); + break; + } + } + + private bool HasCustomIKPose() + { + return _animLocomotionLayer != -1 && _animIKPoseLayer != -1; + } + + private void SetCustomLayersWeights(float customIKPoseLayerWeight, float locomotionLayerWeight) + { + _animator.SetLayerWeight(_animIKPoseLayer, customIKPoseLayerWeight); + _animator.SetLayerWeight(_animLocomotionLayer, locomotionLayerWeight); + _animator.Update(0f); + } + + private void SetMusclesToValue(float value) + { + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < ourBoneResetMasks.Length; i++) + { + if (ourBoneResetMasks[i] != BoneResetMask.Never) + _humanPose.muscles[i] = value; + } + + _humanPose.bodyPosition = Vector3.up; + _humanPose.bodyRotation = Quaternion.identity; + _humanPoseHandler.SetHumanPose(ref _humanPose); + } + + private void SetMusclesToPose(float[] muscles) + { + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < ourBoneResetMasks.Length; i++) + { + if (ourBoneResetMasks[i] != BoneResetMask.Never) + _humanPose.muscles[i] = muscles[i]; + } + + _humanPose.bodyPosition = Vector3.up; + _humanPose.bodyRotation = Quaternion.identity; + _humanPoseHandler.SetHumanPose(ref _humanPose); + } + + #endregion + + #region BodyHandling + + public enum BoneResetMask + { + Never, + Spine, + LeftArm, + RightArm, + LeftLeg, + RightLeg, + } + + private static readonly string[] ourNeverBones = { "Index", "Thumb", "Middle", "Ring", "Little", "Jaw", "Eye" }; + private static readonly string[] ourArmBones = { "Arm", "Forearm", "Hand", "Shoulder" }; + private static readonly string[] ourLegBones = { "Leg", "Foot", "Toes" }; + + private static BoneResetMask JudgeBone(string name) + { + if (ourNeverBones.Any(name.Contains)) + return BoneResetMask.Never; + + if (ourArmBones.Any(name.Contains)) + { + return name.Contains("Left") ? BoneResetMask.LeftArm : BoneResetMask.RightArm; + } + + if (ourLegBones.Any(name.Contains)) + return name.Contains("Left") ? BoneResetMask.LeftLeg : BoneResetMask.RightLeg; + + return BoneResetMask.Spine; + } + + internal static readonly BoneResetMask[] ourBoneResetMasks = HumanTrait.MuscleName.Select(JudgeBone).ToArray(); + + #endregion +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/MusclePoses.cs b/AlternateIKSystem/IK/MusclePoses.cs new file mode 100644 index 0000000..e53856e --- /dev/null +++ b/AlternateIKSystem/IK/MusclePoses.cs @@ -0,0 +1,46 @@ +namespace NAK.AlternateIKSystem.IK; + +public static class MusclePoses +{ + public static readonly float[] TPoseMuscles = + { + 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0.6001086f, 8.6213E-05f, + -0.0003308152f, 0.9999163f, -9.559652E-06f, 3.41413E-08f, -3.415095E-06f, -1.024528E-07f, 0.6001086f, + 8.602679E-05f, -0.0003311098f, 0.9999163f, -9.510122E-06f, 1.707468E-07f, -2.732077E-06f, 2.035554E-15f, + -2.748694E-07f, 2.619475E-07f, 0.401967f, 0.3005583f, 0.04102772f, 0.9998822f, -0.04634236f, 0.002522987f, + 0.0003842837f, -2.369134E-07f, -2.232262E-07f, 0.4019674f, 0.3005582f, 0.04103433f, 0.9998825f, + -0.04634996f, 0.00252335f, 0.000383302f, -1.52127f, 0.2634507f, 0.4322457f, 0.6443988f, 0.6669409f, + -0.4663372f, 0.8116828f, 0.8116829f, 0.6678119f, -0.6186608f, 0.8116842f, 0.8116842f, 0.6677991f, + -0.619225f, 0.8116842f, 0.811684f, 0.6670032f, -0.465875f, 0.811684f, 0.8116836f, -1.520098f, 0.2613016f, + 0.432256f, 0.6444503f, 0.6668426f, -0.4670413f, 0.8116828f, 0.8116828f, 0.6677986f, -0.6192409f, 0.8116841f, + 0.811684f, 0.6677839f, -0.6198869f, 0.8116839f, 0.8116838f, 0.6668782f, -0.4667901f, 0.8116842f, 0.811684f + }; + + public static readonly float[] APoseMuscles = + { + 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0.6001087f, 0f, + -0.0003306383f, 0.9999163f, 0f, 0f, 0f, 0f, 0.6001087f, 0f, -0.0003306384f, 0.9999163f, 0f, 0f, 0f, 0f, 0f, + 0f, -0.1071228f, 0.258636f, 0.1567371f, 0.9998825f, -0.0463457f, 0.002523606f, 0.0003833446f, 0f, 0f, + -0.1036742f, 0.2589961f, 0.1562322f, 0.9998825f, -0.04634446f, 0.002522176f, 0.0003835156f, -1.52127f, + 0.2634749f, 0.4322476f, 0.6443989f, 0.6669405f, -0.4663376f, 0.8116828f, 0.8116829f, 0.6678116f, + -0.6186616f, 0.8116839f, 0.8116837f, 0.6677991f, -0.6192248f, 0.8116839f, 0.8116842f, 0.6670038f, + -0.4658763f, 0.8116841f, 0.811684f, -1.520108f, 0.2612858f, 0.4322585f, 0.6444519f, 0.6668428f, -0.4670413f, + 0.8116831f, 0.8116828f, 0.6677985f, -0.6192364f, 0.8116842f, 0.8116842f, 0.667784f, -0.6198866f, 0.8116841f, + 0.8116835f, 0.6668782f, -0.4667891f, 0.8116841f, 0.811684f + }; + + public static readonly float[] IKPoseMuscles = + { + 0.00133321f, 8.195831E-06f, 8.537738E-07f, -0.002669832f, -7.651234E-06f, -0.001659694f, 0f, 0f, 0f, + 0.04213953f, 0.0003007996f, -0.008032114f, -0.03059979f, -0.0003182998f, 0.009640567f, 0f, 0f, 0f, 0f, 0f, + 0f, 0.5768794f, 0.01061097f, -0.1127839f, 0.9705755f, 0.07972051f, -0.0268422f, 0.007237188f, 0f, + 0.5768792f, 0.01056608f, -0.1127519f, 0.9705756f, 0.07971933f, -0.02682396f, 0.007229362f, 0f, + -5.651802E-06f, -3.034899E-07f, 0.4100508f, 0.3610304f, -0.0838329f, 0.9262537f, 0.1353517f, -0.03578902f, + 0.06005657f, -4.95989E-06f, -1.43007E-06f, 0.4096187f, 0.363263f, -0.08205152f, 0.9250782f, 0.1345718f, + -0.03572125f, 0.06055461f, -1.079177f, 0.2095419f, 0.6140652f, 0.6365265f, 0.6683931f, -0.4764312f, + 0.8099416f, 0.8099371f, 0.6658203f, -0.7327053f, 0.8113618f, 0.8114051f, 0.6643661f, -0.40341f, 0.8111364f, + 0.8111367f, 0.6170399f, -0.2524227f, 0.8138723f, 0.8110135f, -1.079171f, 0.2095456f, 0.6140658f, 0.6365255f, + 0.6683878f, -0.4764301f, 0.8099402f, 0.8099376f, 0.6658241f, -0.7327023f, 0.8113653f, 0.8113793f, 0.664364f, + -0.4034042f, 0.811136f, 0.8111364f, 0.6170469f, -0.2524345f, 0.8138595f, 0.8110138f + }; +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/VRIKHelpers/VRIKCalibrationData.cs b/AlternateIKSystem/IK/VRIKHelpers/VRIKCalibrationData.cs new file mode 100644 index 0000000..c49e4b3 --- /dev/null +++ b/AlternateIKSystem/IK/VRIKHelpers/VRIKCalibrationData.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +namespace NAK.AlternateIKSystem.VRIKHelpers; + +public struct VRIKCalibrationData +{ + public Vector3 KneeNormalLeft; + public Vector3 KneeNormalRight; + public Vector3 InitialFootPosLeft; + public Vector3 InitialFootPosRight; + public Quaternion InitialFootRotLeft; + public Quaternion InitialFootRotRight; + public float InitialHeadHeight; + public float InitialFootDistance; + public float InitialStepThreshold; + public float InitialStepHeight; + + public void Clear() + { + KneeNormalLeft = Vector3.zero; + KneeNormalRight = Vector3.zero; + InitialFootPosLeft = Vector3.zero; + InitialFootPosRight = Vector3.zero; + InitialFootRotLeft = Quaternion.identity; + InitialFootRotRight = Quaternion.identity; + InitialHeadHeight = 0f; + InitialFootDistance = 0f; + InitialStepThreshold = 0f; + InitialStepHeight = 0f; + } +} \ No newline at end of file diff --git a/AlternateIKSystem/IK/VRIKHelpers/VRIKUtils.cs b/AlternateIKSystem/IK/VRIKHelpers/VRIKUtils.cs new file mode 100644 index 0000000..2f06a32 --- /dev/null +++ b/AlternateIKSystem/IK/VRIKHelpers/VRIKUtils.cs @@ -0,0 +1,125 @@ +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.AlternateIKSystem.VRIKHelpers; + +public static class VRIKUtils +{ + public static void CalculateKneeBendNormals(VRIK vrik, ref VRIKCalibrationData calibrationData) + { + // Helper function to get position or default to Vector3.zero + Vector3 GetPositionOrDefault(Transform transform) => transform?.position ?? Vector3.zero; + + // Get assumed left knee normal + Vector3[] leftVectors = { + GetPositionOrDefault(vrik.references.leftThigh), + GetPositionOrDefault(vrik.references.leftCalf), + GetPositionOrDefault(vrik.references.leftFoot) + }; + calibrationData.KneeNormalLeft = Quaternion.Inverse(vrik.references.root.rotation) * GetNormalFromArray(leftVectors); + + // Get assumed right knee normal + Vector3[] rightVectors = { + GetPositionOrDefault(vrik.references.rightThigh), + GetPositionOrDefault(vrik.references.rightCalf), + GetPositionOrDefault(vrik.references.rightFoot) + }; + calibrationData.KneeNormalRight = Quaternion.Inverse(vrik.references.root.rotation) * GetNormalFromArray(rightVectors); + } + + public static void ApplyKneeBendNormals(VRIK vrik, VRIKCalibrationData calibrationData) + { + // 0 uses bendNormalRelToPelvis, 1 is bendNormalRelToTarget + // modifying pelvis normal weight is easier math + vrik.solver.leftLeg.bendToTargetWeight = 0f; + vrik.solver.rightLeg.bendToTargetWeight = 0f; + + var pelvis_localRotationInverse = Quaternion.Inverse(vrik.references.pelvis.localRotation); + vrik.solver.leftLeg.bendNormalRelToPelvis = pelvis_localRotationInverse * calibrationData.KneeNormalLeft; + vrik.solver.rightLeg.bendNormalRelToPelvis = pelvis_localRotationInverse * calibrationData.KneeNormalRight; + } + + private static Vector3 GetNormalFromArray(Vector3[] positions) + { + Vector3 centroid = Vector3.zero; + for (int i = 0; i < positions.Length; i++) + { + centroid += positions[i]; + } + centroid /= positions.Length; + + Vector3 normal = Vector3.zero; + for (int i = 0; i < positions.Length - 2; i++) + { + Vector3 side1 = positions[i] - centroid; + Vector3 side2 = positions[i + 1] - centroid; + normal += Vector3.Cross(side1, side2); + } + return normal.normalized; + } + + public static void CalculateInitialIKScaling(VRIK vrik, ref VRIKCalibrationData calibrationData) + { + // Get distance between feet and thighs + float scaleModifier = Mathf.Max(1f, vrik.references.pelvis.lossyScale.x); + float footDistance = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.rightFoot.position); + + calibrationData.InitialFootDistance = footDistance * 0.5f; + calibrationData.InitialStepThreshold = footDistance * scaleModifier; + calibrationData.InitialStepHeight = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.leftCalf.position) * 0.2f; + calibrationData.InitialHeadHeight = Mathf.Abs(vrik.references.head.position.y - vrik.references.rightFoot.position.y); + } + + public static void CalculateInitialFootsteps(VRIK vrik, ref VRIKCalibrationData calibrationData) + { + Transform root = vrik.references.root; + Transform leftFoot = vrik.references.leftFoot; + Transform rightFoot = vrik.references.rightFoot; + + // Calculate the world rotation of the root bone at the current frame + Quaternion rootWorldRot = root.rotation; + + // Calculate the world rotation of the left and right feet relative to the root bone + calibrationData.InitialFootPosLeft = root.parent.InverseTransformPoint(leftFoot.position); + calibrationData.InitialFootPosRight = root.parent.InverseTransformPoint(rightFoot.position); + calibrationData.InitialFootRotLeft = Quaternion.Inverse(rootWorldRot) * leftFoot.rotation; + calibrationData.InitialFootRotRight = Quaternion.Inverse(rootWorldRot) * rightFoot.rotation; + } + + public static void ResetToInitialFootsteps(VRIK vrik, VRIKCalibrationData calibrationData, float scaleModifier) + { + var locomotionSolver = vrik.solver.locomotion; + + var footsteps = locomotionSolver.footsteps; + var footstepLeft = footsteps[0]; + var footstepRight = footsteps[1]; + + var root = vrik.references.root; + var rootWorldRot = vrik.references.root.rotation; + + // hack, use parent transform instead as setting feet position moves root + footstepLeft.Reset(rootWorldRot, root.parent.TransformPoint(calibrationData.InitialFootPosLeft * scaleModifier), rootWorldRot * calibrationData.InitialFootRotLeft); + footstepRight.Reset(rootWorldRot, root.parent.TransformPoint(calibrationData.InitialFootPosRight * scaleModifier), rootWorldRot * calibrationData.InitialFootRotRight); + } + + public static void ApplyScaleToVRIK(VRIK vrik, VRIKCalibrationData calibrationData, float scaleModifier) + { + var locomotionSolver = vrik.solver.locomotion; + locomotionSolver.footDistance = calibrationData.InitialFootDistance * scaleModifier; + locomotionSolver.stepThreshold = calibrationData.InitialStepThreshold * scaleModifier; + ScaleStepHeight(locomotionSolver.stepHeight, calibrationData.InitialStepHeight * scaleModifier); + } + + static void ScaleStepHeight(AnimationCurve stepHeightCurve, float mag) + { + Keyframe[] keyframes = stepHeightCurve.keys; + keyframes[1].value = mag; + stepHeightCurve.keys = keyframes; + } + + public static void InitiateVRIKSolver(VRIK vrik) + { + vrik.solver.SetToReferences(vrik.references); + vrik.solver.Initiate(vrik.transform); + } +} \ No newline at end of file diff --git a/AlternateIKSystem/Main.cs b/AlternateIKSystem/Main.cs new file mode 100644 index 0000000..033c084 --- /dev/null +++ b/AlternateIKSystem/Main.cs @@ -0,0 +1,37 @@ +using MelonLoader; + +namespace NAK.AlternateIKSystem; + +// IKManager is what the game talks to +// BodyControl is the master vrik tracking weights + +// IKHandler is created by IKManager, specific to Desktop/VR +// It will look at BodyControl to manage its own vrik solver + +// IKCalibrator will setup vrik & its settings + +public class AlternateIKSystem : MelonMod +{ + internal static MelonLogger.Instance Logger; + + public override void OnInitializeMelon() + { + Logger = LoggerInstance; + + ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); + ApplyPatches(typeof(HarmonyPatches.IKSystemPatches)); + } + + private void ApplyPatches(Type type) + { + try + { + HarmonyInstance.PatchAll(type); + } + catch (Exception e) + { + Logger.Msg($"Failed while patching {type.Name}!"); + Logger.Error(e); + } + } +} \ No newline at end of file diff --git a/AlternateIKSystem/ModSettings.cs b/AlternateIKSystem/ModSettings.cs new file mode 100644 index 0000000..ae014ba --- /dev/null +++ b/AlternateIKSystem/ModSettings.cs @@ -0,0 +1,17 @@ +using MelonLoader; + +namespace NAK.AlternateIKSystem; + +public static class ModSettings +{ + internal const string SettingsCategory = nameof(AlternateIKSystem); + + public static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(SettingsCategory); + + public static readonly MelonPreferences_Entry EntryEnabled = + Category.CreateEntry("Enabled", true, description: "Toggle AlternateIKSystem entirely. Requires avatar reload."); + + public static readonly MelonPreferences_Entry EntryUseVRIKToes = + Category.CreateEntry("Use VRIK Toes", false, description: "Determines if VRIK uses humanoid toes for IK solving, which can cause feet to idle behind the avatar."); +} \ No newline at end of file diff --git a/AlternateIKSystem/Properties/AssemblyInfo.cs b/AlternateIKSystem/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..45baab8 --- /dev/null +++ b/AlternateIKSystem/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using MelonLoader; +using NAK.AlternateIKSystem.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.AlternateIKSystem))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.AlternateIKSystem))] + +[assembly: MelonInfo( + typeof(NAK.AlternateIKSystem.AlternateIKSystem), + nameof(NAK.AlternateIKSystem), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/AlternateIKSystem" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonOptionalDependencies("BTKUILib")] +[assembly: HarmonyDontPatchAll] + +namespace NAK.AlternateIKSystem.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/AlternateIKSystem/README.md b/AlternateIKSystem/README.md new file mode 100644 index 0000000..891746a --- /dev/null +++ b/AlternateIKSystem/README.md @@ -0,0 +1,16 @@ +## AlternateIKSystem + +This is a super fucked mix of DesktopVRIK, StateBehaviours, IKFixes, & IKTweaks with general structure inspired by ChilloutVRs existing IKSystem. + +It is a personal project that I am unsure how far I will go with. Currently only Desktop VRIK works as that was easiest to implement. + +--- + +Here is the block of text where I tell you this mod is not affiliated or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation and is not affiliated with, supported by or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/AlternateIKSystem/format.json b/AlternateIKSystem/format.json new file mode 100644 index 0000000..92d9abc --- /dev/null +++ b/AlternateIKSystem/format.json @@ -0,0 +1,24 @@ +{ + "_id": 117, + "name": "AlternateIKSystem", + "modversion": "4.2.0", + "gameversion": "2022r170p1", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Adds VRIK to Desktop avatars. No longer will you be a liveless sliding statue~!\nAdds the small feet stepping when looking around on Desktop.\n\nOptional BTKUILib integration.", + "searchtags": [ + "desktop", + "vrik", + "ik", + "feet", + "fish" + ], + "requirements": [ + "BTKUILib" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r5/AlternateIKSystem.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/AlternateIKSystem/", + "changelog": "- Fixed IK not being reset when teleporting/respawning/sitting.\n- Fixed IK Locomotion not being disabled while sitting.\n- Cleanup of VRIK Calibration & Configuration to make updating & debugging easier.\n- Added a temporary fix for DesktopVRSwitch BodySystem handling to ensure tracking is correctly enabled after switch.\n- Added \"NetIKPass\" after VRIK solving, which fixes many rotational errors that were only visible on the local client.\n- Removed Chest VRIK reference fix & find unmapped toe options as they didn't stream over NetIK.", + "embedcolor": "9b59b6" +} \ No newline at end of file diff --git a/NAK_CVR_Mods.sln b/NAK_CVR_Mods.sln index 002bf24..a26599d 100644 --- a/NAK_CVR_Mods.sln +++ b/NAK_CVR_Mods.sln @@ -75,6 +75,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MuteSFX", "MuteSFX\MuteSFX. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EzCurls", "EzCurls\EzCurls.csproj", "{7EF74A8D-0421-4B6D-809E-40F9C2DFC7F8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlternateIKSystem", "AlternateIKSystem\AlternateIKSystem.csproj", "{CAB05E13-B529-4CDA-A2B5-B62E122408F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -225,6 +227,10 @@ Global {7EF74A8D-0421-4B6D-809E-40F9C2DFC7F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EF74A8D-0421-4B6D-809E-40F9C2DFC7F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EF74A8D-0421-4B6D-809E-40F9C2DFC7F8}.Release|Any CPU.Build.0 = Release|Any CPU + {CAB05E13-B529-4CDA-A2B5-B62E122408F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAB05E13-B529-4CDA-A2B5-B62E122408F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAB05E13-B529-4CDA-A2B5-B62E122408F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAB05E13-B529-4CDA-A2B5-B62E122408F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE