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