diff --git a/DesktopVRIK/DesktopVRIK.cs b/DesktopVRIK/DesktopVRIK.cs index 5e73a26..60e8371 100644 --- a/DesktopVRIK/DesktopVRIK.cs +++ b/DesktopVRIK/DesktopVRIK.cs @@ -1,169 +1,145 @@ -using ABI.CCK.Components; -using ABI_RC.Core.Player; -using ABI_RC.Systems.IK; +using ABI_RC.Core.Player; using ABI_RC.Systems.IK.SubSystems; using ABI_RC.Systems.MovementSystem; using RootMotion.FinalIK; +using System.Reflection; using UnityEngine; -using UnityEngine.Events; namespace NAK.Melons.DesktopVRIK; public class DesktopVRIK : MonoBehaviour { public static DesktopVRIK Instance; + public DesktopVRIKCalibrator Calibrator; - public static bool - Setting_Enabled, - Setting_EnforceViewPosition, - Setting_EmoteVRIK, - Setting_EmoteLookAtIK; + // DesktopVRIK Settings + public bool + Setting_Enabled = true, + Setting_PlantFeet = true; + public float + Setting_BodyLeanWeight, + Setting_BodyHeadingLimit, + Setting_PelvisHeadingWeight, + Setting_ChestHeadingWeight; - public static float - Setting_BodyLeanWeight = 0.5f, - Setting_BodyAngleLimit = 0f; - - public Transform viewpoint; - public Vector3 eyeOffset; + // Internal Stuff + bool ps_emoteIsPlaying; + float ik_SimulatedRootAngle; + Transform desktopCameraTransform; + static readonly FieldInfo ms_isGrounded = typeof(MovementSystem).GetField("_isGrounded", BindingFlags.NonPublic | BindingFlags.Instance); + bool forceSteps; + bool forceStepsNow; void Start() { + desktopCameraTransform = PlayerSetup.Instance.desktopCamera.transform; + Calibrator = new DesktopVRIKCalibrator(); Instance = this; + DesktopVRIKMod.UpdateAllSettings(); } - public void ChangeViewpointHandling(bool enabled) + public void OnSetupAvatarDesktop() { - if (Setting_EnforceViewPosition == enabled) return; - Setting_EnforceViewPosition = enabled; - if (enabled) - { - PlayerSetup.Instance.desktopCamera.transform.localPosition = Vector3.zero; - return; - } - PlayerSetup.Instance.desktopCamera.transform.localPosition = eyeOffset; + if (!Setting_Enabled) return; + Calibrator.CalibrateDesktopVRIK(); + ResetDesktopVRIK(); } - public void AlternativeOnPreSolverUpdate() + public bool OnSetupIKScaling(float scaleDifference) { - //this order matters, rotation offset will be choppy if avatar is not cenetered first + if (Calibrator.vrik == null) return false; - DesktopVRIK_Helper.Instance?.OnUpdateVRIK(); + VRIKUtils.ApplyScaleToVRIK + ( + Calibrator.vrik, + Calibrator.initialFootDistance, + Calibrator.initialStepThreshold, + Calibrator.initialStepHeight, + scaleDifference + ); - //Reset avatar offset (VRIK will literally make you walk away from root otherwise) - IKSystem.vrik.transform.localPosition = Vector3.zero; - IKSystem.vrik.transform.localRotation = Quaternion.identity; - - IKSystem.vrik.solver.plantFeet = true; + ResetDesktopVRIK(); + return true; } - public Animator animator; - - public VRIK AlternativeCalibration(CVRAvatar avatar) + public void OnPlayerSetupUpdate(bool isEmotePlaying) { - animator = avatar.GetComponent(); - Transform avatarHeadBone = animator.GetBoneTransform(HumanBodyBones.Head); + bool changed = isEmotePlaying != ps_emoteIsPlaying; + if (!changed) return; - //Stuff to make bad armatures work (Fuck you Default Robot Kyle) - avatar.transform.localPosition = Vector3.zero; + ps_emoteIsPlaying = isEmotePlaying; - //ikpose layer (specified by avatar author) - int ikposeLayerIndex = animator.GetLayerIndex("IKPose"); - int locoLayerIndex = animator.GetLayerIndex("Locomotion/Emotes"); - if (ikposeLayerIndex != -1) + Calibrator.avatarTransform.localPosition = Vector3.zero; + Calibrator.avatarTransform.localRotation = Quaternion.identity; + + if (Calibrator.lookAtIK != null) + Calibrator.lookAtIK.enabled = !isEmotePlaying; + + BodySystem.TrackingEnabled = !isEmotePlaying; + + Calibrator.vrik.solver?.Reset(); + ResetDesktopVRIK(); + } + + + public void ResetDesktopVRIK() + { + ik_SimulatedRootAngle = transform.eulerAngles.y; + } + + public void OnPreSolverUpdate() + { + if (ps_emoteIsPlaying) return; + + var movementSystem = MovementSystem.Instance; + var vrikSolver = Calibrator.vrik.solver; + var avatarTransform = Calibrator.avatarTransform; + + bool isGrounded = (bool)ms_isGrounded.GetValue(movementSystem); + + // Calculate weight + float weight = vrikSolver.IKPositionWeight; + weight *= 1f - movementSystem.movementVector.magnitude; + weight *= isGrounded ? 1f : 0f; + + // Reset avatar offset + avatarTransform.localPosition = Vector3.zero; + avatarTransform.localRotation = Quaternion.identity; + + // Set plant feet + vrikSolver.plantFeet = Setting_PlantFeet; + + // Emulate old VRChat hip movement + if (Setting_BodyLeanWeight > 0) { - animator.SetLayerWeight(ikposeLayerIndex, 1f); - if (locoLayerIndex != -1) - { - animator.SetLayerWeight(locoLayerIndex, 0f); - } - animator.Update(0f); + float weightedAngle = Setting_BodyLeanWeight * weight; + float angle = desktopCameraTransform.localEulerAngles.x; + angle = angle > 180 ? angle - 360 : angle; + Quaternion rotation = Quaternion.AngleAxis(angle * weightedAngle, avatarTransform.right); + vrikSolver.AddRotationOffset(IKSolverVR.RotationOffset.Head, rotation); } - VRIK vrik = avatar.gameObject.AddComponent(); - vrik.AutoDetectReferences(); - - //fuck toes - vrik.references.leftToes = null; - vrik.references.rightToes = null; - - vrik.fixTransforms = true; - vrik.solver.plantFeet = false; - vrik.solver.locomotion.angleThreshold = 30f; - vrik.solver.locomotion.maxLegStretch = 0.75f; - vrik.solver.spine.minHeadHeight = -100f; - - vrik.solver.spine.bodyRotStiffness = 0.15f; - vrik.solver.spine.headClampWeight = 1f; - vrik.solver.spine.maintainPelvisPosition = 1f; - vrik.solver.spine.neckStiffness = 0f; - - vrik.solver.locomotion.weight = 0f; - vrik.solver.spine.bodyPosStiffness = 0f; - vrik.solver.spine.positionWeight = 0f; - vrik.solver.spine.pelvisPositionWeight = 0f; - vrik.solver.leftArm.positionWeight = 0f; - vrik.solver.leftArm.rotationWeight = 0f; - vrik.solver.rightArm.positionWeight = 0f; - vrik.solver.rightArm.rotationWeight = 0f; - vrik.solver.leftLeg.positionWeight = 0f; - vrik.solver.leftLeg.rotationWeight = 0f; - vrik.solver.rightLeg.positionWeight = 0f; - vrik.solver.rightLeg.rotationWeight = 0f; - vrik.solver.IKPositionWeight = 0f; - - BodySystem.TrackingLeftArmEnabled = false; - BodySystem.TrackingRightArmEnabled = false; - BodySystem.TrackingLeftLegEnabled = false; - BodySystem.TrackingRightLegEnabled = false; - BodySystem.TrackingPositionWeight = 0f; - - //Custom funky AF head ik shit - foreach (Transform transform in DesktopVRIK_Helper.Instance.ik_HeadFollower) + // Make root heading follow within a set limit + if (Setting_BodyHeadingLimit > 0) { - if (transform.name == "Head IK Target") + float weightedAngleLimit = Setting_BodyHeadingLimit * weight; + float currentAngle = Mathf.DeltaAngle(transform.eulerAngles.y, ik_SimulatedRootAngle); + float angleMaxDelta = Mathf.Abs(currentAngle); + if (angleMaxDelta > weightedAngleLimit) { - Destroy(transform.gameObject); + currentAngle = Mathf.Sign(currentAngle) * weightedAngleLimit; + ik_SimulatedRootAngle = Mathf.MoveTowardsAngle(ik_SimulatedRootAngle, transform.eulerAngles.y, angleMaxDelta - weightedAngleLimit); + } + vrikSolver.spine.rootHeadingOffset = currentAngle; + if (Setting_PelvisHeadingWeight > 0) + { + vrikSolver.AddRotationOffset(IKSolverVR.RotationOffset.Pelvis, new Vector3(0f, currentAngle * Setting_PelvisHeadingWeight, 0f)); + vrikSolver.AddRotationOffset(IKSolverVR.RotationOffset.Chest, new Vector3(0f, -currentAngle * Setting_PelvisHeadingWeight, 0f)); + } + if (Setting_ChestHeadingWeight > 0) + { + vrikSolver.AddRotationOffset(IKSolverVR.RotationOffset.Chest, new Vector3(0f, currentAngle * Setting_ChestHeadingWeight, 0f)); } } - - DesktopVRIK_Helper.Instance.avatar_HeadBone = avatarHeadBone; - DesktopVRIK_Helper.Instance.ik_HeadFollower.position = avatarHeadBone.position; - DesktopVRIK_Helper.Instance.ik_HeadFollower.rotation = Quaternion.identity; - VRIKCalibrator.CalibrateHead(vrik, DesktopVRIK_Helper.Instance.ik_HeadFollower.transform, IKSystem.Instance.headAnchorPositionOffset, IKSystem.Instance.headAnchorRotationOffset); - DesktopVRIK_Helper.Instance.ik_HeadFollower.localRotation = Quaternion.identity; - - //force immediate calibration before animator decides to fuck us - vrik.solver.SetToReferences(vrik.references); - vrik.solver.Initiate(vrik.transform); - - if (ikposeLayerIndex != -1) - { - animator.SetLayerWeight(ikposeLayerIndex, 0f); - if (locoLayerIndex != -1) - { - animator.SetLayerWeight(locoLayerIndex, 1f); - } - } - - //Find eyeoffset - eyeOffset = PlayerSetup.Instance.desktopCamera.transform.localPosition; - viewpoint = avatarHeadBone.Find("LocalHeadPoint"); - ChangeViewpointHandling(Setting_EnforceViewPosition); - - //reset ikpose layer - if (ikposeLayerIndex != -1) - { - animator.SetLayerWeight(ikposeLayerIndex, 0f); - if (locoLayerIndex != -1) - { - animator.SetLayerWeight(locoLayerIndex, 1f); - } - } - - vrik?.onPreSolverUpdate.AddListener(new UnityAction(this.AlternativeOnPreSolverUpdate)); - - DesktopVRIK_Helper.Instance?.OnResetIK(); - - return vrik; } } \ No newline at end of file diff --git a/DesktopVRIK/DesktopVRIK.csproj b/DesktopVRIK/DesktopVRIK.csproj index 5a31a95..592135f 100644 --- a/DesktopVRIK/DesktopVRIK.csproj +++ b/DesktopVRIK/DesktopVRIK.csproj @@ -19,7 +19,7 @@ C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp-firstpass.dll - C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll + ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Cohtml.Runtime.dll @@ -44,6 +44,10 @@ + + + + diff --git a/DesktopVRIK/DesktopVRIKCalibrator.cs b/DesktopVRIK/DesktopVRIKCalibrator.cs new file mode 100644 index 0000000..03fe096 --- /dev/null +++ b/DesktopVRIK/DesktopVRIKCalibrator.cs @@ -0,0 +1,367 @@ +using ABI.CCK.Components; +using ABI_RC.Core; +using ABI_RC.Core.Base; +using ABI_RC.Core.Player; +using ABI_RC.Systems.IK; +using ABI_RC.Systems.IK.SubSystems; +using HarmonyLib; +using RootMotion.FinalIK; +using UnityEngine; +using UnityEngine.Events; + +namespace NAK.Melons.DesktopVRIK; + +public class DesktopVRIKCalibrator +{ + // Settings + public bool Setting_UseVRIKToes = true; + public bool Setting_FindUnmappedToes = true; + + // Avatar Component References + public CVRAvatar avatar; + public Animator animator; + public Transform avatarTransform; + public VRIK vrik; + public LookAtIK lookAtIK; + + // Calibrated Values + public float + initialFootDistance, + initialStepThreshold, + initialStepHeight; + + // Calibration Internals + bool fixTransformsRequired; + Vector3 leftKneeNormal, rightKneeNormal; + HumanPose initialHumanPose; + HumanPoseHandler humanPoseHandler; + + // Traverse + IKSystem ikSystem; + PlayerSetup playerSetup; + Traverse + _vrikTraverse, + _lookIKTraverse, + _avatarTraverse, + _animatorManagerTraverse, + _poseHandlerTraverse, + _avatarRootHeightTraverse; + + public DesktopVRIKCalibrator() + { + // Get base game scripts. + ikSystem = IKSystem.Instance; + playerSetup = PlayerSetup.Instance; + + // Get traverse to private shit in iksystem. + _vrikTraverse = Traverse.Create(ikSystem).Field("_vrik"); + _avatarTraverse = Traverse.Create(ikSystem).Field("_avatar"); + _animatorManagerTraverse = Traverse.Create(ikSystem).Field("_animatorManager"); + _poseHandlerTraverse = Traverse.Create(ikSystem).Field("_poseHandler"); + _avatarRootHeightTraverse = Traverse.Create(ikSystem).Field("_avatarRootHeight"); + + // Get traverse to private shit in playersetup. + _lookIKTraverse = Traverse.Create(playerSetup).Field("lookIK"); + } + + public void CalibrateDesktopVRIK() + { + // Scan avatar for issues/references + ScanAvatarForCalibration(); + // Prepare CVR IKSystem for external VRIK + PrepareIKSystem(); + + // Add VRIK and configure + PrepareAvatarVRIK(); + + Initialize(); + + PostInitialize(); + } + + private void Initialize() + { + // Calculate bend normals with motorcycle pose + SetHumanPose(0f); + VRIKUtils.CalculateKneeBendNormals(vrik, out leftKneeNormal, out rightKneeNormal); + + // Calculate initial IK scaling values with IKPose + SetAvatarIKPose(true); + VRIKUtils.CalculateInitialIKScaling(vrik, out initialFootDistance, out initialStepThreshold, out initialStepHeight); + + // Setup HeadIK target & calculate initial footstep values + SetupDesktopHeadIKTarget(); + + // Initiate VRIK manually + VRIKUtils.InitiateVRIKSolver(vrik); + + // Return avatar to original pose + SetAvatarIKPose(false); + } + + private void PostInitialize() + { + VRIKUtils.ApplyScaleToVRIK + ( + vrik, + initialFootDistance, + initialStepThreshold, + initialStepHeight, + 1f + ); + VRIKUtils.ApplyKneeBendNormals(vrik, leftKneeNormal, rightKneeNormal); + vrik.onPreSolverUpdate.AddListener(new UnityAction(DesktopVRIK.Instance.OnPreSolverUpdate)); + } + + private void ScanAvatarForCalibration() + { + // Find required avatar components + avatar = playerSetup._avatar.GetComponent(); + animator = avatar.GetComponent(); + avatarTransform = avatar.transform; + lookAtIK = _lookIKTraverse.GetValue(); + + // Apply some fixes for weird setups + fixTransformsRequired = !animator.enabled; + + // Center avatar local position + avatarTransform.localPosition = Vector3.zero; + + // Create a new human pose handler and dispose the old one + humanPoseHandler?.Dispose(); + humanPoseHandler = new HumanPoseHandler(animator.avatar, avatarTransform); + // Store original human pose + humanPoseHandler.GetHumanPose(ref initialHumanPose); + } + + private void PrepareIKSystem() + { + // Get the animator manager and human pose handler + var animatorManager = _animatorManagerTraverse.GetValue(); + var ikHumanPoseHandler = _poseHandlerTraverse.GetValue(); + + // Store the avatar component + _avatarTraverse.SetValue(avatar); + + // Set the animator for the IK system + ikSystem.animator = animator; + animatorManager.SetAnimator(ikSystem.animator, ikSystem.animator.runtimeAnimatorController); + + // Set the avatar height float + _avatarRootHeightTraverse.SetValue(ikSystem.vrPlaySpace.transform.InverseTransformPoint(avatarTransform.position).y); + + // Create a new human pose handler and dispose the old one + ikHumanPoseHandler?.Dispose(); + ikHumanPoseHandler = new HumanPoseHandler(ikSystem.animator.avatar, avatarTransform); + _poseHandlerTraverse.SetValue(ikHumanPoseHandler); + + // Find valid human bones + IKSystem.BoneExists.Clear(); + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone != HumanBodyBones.LastBone) + { + IKSystem.BoneExists.Add(bone, ikSystem.animator.GetBoneTransform(bone) != null); + } + } + + // Prepare BodySystem for calibration + BodySystem.TrackingLeftArmEnabled = false; + BodySystem.TrackingRightArmEnabled = false; + BodySystem.TrackingLeftLegEnabled = false; + BodySystem.TrackingRightLegEnabled = false; + BodySystem.TrackingPositionWeight = 0f; + } + + private void PrepareAvatarVRIK() + { + // Add and configure VRIK + vrik = avatar.gameObject.AddComponentIfMissing(); + vrik.AutoDetectReferences(); + + VRIKUtils.ConfigureVRIKReferences(vrik, Setting_UseVRIKToes, Setting_FindUnmappedToes, out bool foundUnmappedToes); + + // Fix animator issue or non-human mapped toes + vrik.fixTransforms = fixTransformsRequired || foundUnmappedToes; + + // Default solver settings + vrik.solver.locomotion.weight = 0f; + vrik.solver.locomotion.angleThreshold = 30f; + vrik.solver.locomotion.maxLegStretch = 1f; + vrik.solver.spine.minHeadHeight = 0f; + vrik.solver.IKPositionWeight = 1f; + vrik.solver.spine.chestClampWeight = 0f; + vrik.solver.spine.maintainPelvisPosition = 0f; + + // Body leaning settings + vrik.solver.spine.neckStiffness = 0.0001f; + vrik.solver.spine.bodyPosStiffness = 1f; + vrik.solver.spine.bodyRotStiffness = 0.2f; + + // Disable locomotion + vrik.solver.locomotion.velocityFactor = 0f; + vrik.solver.locomotion.maxVelocity = 0f; + vrik.solver.locomotion.rootSpeed = 1000f; + + // Disable chest rotation by hands + vrik.solver.spine.rotateChestByHands = 0f; + + // Prioritize LookAtIK + vrik.solver.spine.headClampWeight = 0.2f; + + // Disable going on tippytoes + vrik.solver.spine.positionWeight = 0f; + vrik.solver.spine.rotationWeight = 1f; + + // Tell IKSystem about new VRIK + _vrikTraverse.SetValue(vrik); + } + + private void SetupDesktopHeadIKTarget() + { + // 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; + } + + private void SetAvatarIKPose(bool enforceTPose) + { + int ikposeLayerIndex = animator.GetLayerIndex("IKPose"); + int locoLayerIndex = animator.GetLayerIndex("Locomotion/Emotes"); + + // Use custom IKPose if found. + if (ikposeLayerIndex != -1 && locoLayerIndex != -1) + { + animator.SetLayerWeight(ikposeLayerIndex, enforceTPose ? 1f : 0f); + animator.SetLayerWeight(locoLayerIndex, enforceTPose ? 0f : 1f); + animator.Update(0f); + return; + } + + // Otherwise use DesktopVRIK IKPose & revert afterwards. + if (enforceTPose) + { + SetHumanPose(1f); + } + else + { + humanPoseHandler.SetHumanPose(ref initialHumanPose); + } + } + + private void SetHumanPose(float ikPoseWeight = 1f) + { + humanPoseHandler.GetHumanPose(ref ikSystem.humanPose); + for (int i = 0; i < ikSystem.humanPose.muscles.Length; i++) + { + float weight = ikPoseWeight * IKPoseMuscles[i]; + IKSystem.Instance.ApplyMuscleValue((MuscleIndex)i, weight, ref ikSystem.humanPose.muscles); + } + ikSystem.humanPose.bodyRotation = Quaternion.identity; + humanPoseHandler.SetHumanPose(ref ikSystem.humanPose); + } + + private static readonly float[] IKPoseMuscles = new float[] + { + 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/DesktopVRIK/DesktopVRIK_Helper.cs b/DesktopVRIK/DesktopVRIK_Helper.cs deleted file mode 100644 index ae4f433..0000000 --- a/DesktopVRIK/DesktopVRIK_Helper.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UnityEngine; -using ABI_RC.Core.Player; -using ABI_RC.Systems.IK; -using ABI_RC.Systems.IK.SubSystems; -using ABI_RC.Systems.MovementSystem; -using RootMotion.FinalIK; - -namespace NAK.Melons.DesktopVRIK; - -internal class DesktopVRIK_Helper : MonoBehaviour -{ - public static DesktopVRIK_Helper Instance; - - //Avatar - public Transform avatar_HeadBone; - - //DesktopVRIK - public Transform ik_HeadFollower; - public Quaternion ik_HeadRotation; - - public static void CreateInstance() - { - Transform helper = new GameObject("[DesktopVRIK] Virtual Rig").transform; - helper.parent = PlayerSetup.Instance.transform; - helper.localPosition = Vector3.zero; - helper.localRotation = Quaternion.identity; - helper.gameObject.AddComponent(); - } - - void Start() - { - Instance = this; - - Transform headFollower = new GameObject("HeadBone_Follower").transform; - headFollower.parent = transform; - headFollower.localPosition = new Vector3(0f, 1.8f, 0f); - headFollower.localRotation = Quaternion.identity; - ik_HeadFollower = headFollower; - } - - public void OnUpdateVRIK() - { - if (avatar_HeadBone != null) - { - float globalWeight = (1 - MovementSystem.Instance.movementVector.magnitude); - globalWeight *= IKSystem.vrik.solver.locomotion.weight; - - //the most important thing ever - //IKSystem.vrik.solver.spine.rotationWeight = globalWeight; - - HeadIK_FollowPosition(); - - HeadIK_RotateWithWeight(globalWeight); - HeadIK_FollowWithinAngle(globalWeight); - ik_HeadFollower.rotation = ik_HeadRotation; - } - } - - public void OnResetIK() - { - ik_HeadRotation = Quaternion.Euler(transform.eulerAngles.x, transform.eulerAngles.y, transform.eulerAngles.z); - } - - public void HeadIK_FollowPosition() - { - ik_HeadFollower.position = new Vector3(transform.position.x, avatar_HeadBone.position.y, transform.position.z); - } - - public void HeadIK_FollowWithinAngle(float weight) - { - if (DesktopVRIK.Setting_BodyAngleLimit != 0) - { - float weightedAngle = DesktopVRIK.Setting_BodyAngleLimit * weight; - float currentAngle = Mathf.DeltaAngle(transform.eulerAngles.y, ik_HeadRotation.eulerAngles.y); - if (Mathf.Abs(currentAngle) > weightedAngle) - { - float fixedCurrentAngle = currentAngle > 0 ? currentAngle : -currentAngle; - float clampedAngle = Mathf.MoveTowardsAngle(ik_HeadRotation.eulerAngles.y, transform.eulerAngles.y, fixedCurrentAngle - weightedAngle); - ik_HeadRotation = Quaternion.Euler(ik_HeadRotation.eulerAngles.x, clampedAngle, 0); - } - } - else - { - ik_HeadRotation = Quaternion.Euler(ik_HeadRotation.eulerAngles.x, transform.eulerAngles.y, transform.eulerAngles.z); - } - } - - public void HeadIK_RotateWithWeight(float weight) - { - //VRChat hip movement emulation - if (DesktopVRIK.Setting_BodyLeanWeight != 0) - { - float angle = PlayerSetup.Instance.desktopCamera.transform.localEulerAngles.x; - if (angle > 180) angle -= 360; - float leanAmount = angle * weight * DesktopVRIK.Setting_BodyLeanWeight; - ik_HeadRotation = Quaternion.Euler(leanAmount * 0.33f, ik_HeadRotation.eulerAngles.y, 0); - } - else - { - ik_HeadRotation = Quaternion.Euler(transform.eulerAngles.x, ik_HeadRotation.eulerAngles.y, transform.eulerAngles.z); - } - } -} \ No newline at end of file diff --git a/DesktopVRIK/HarmonyPatches.cs b/DesktopVRIK/HarmonyPatches.cs index 2a9a6b1..5eb1c49 100644 --- a/DesktopVRIK/HarmonyPatches.cs +++ b/DesktopVRIK/HarmonyPatches.cs @@ -1,34 +1,21 @@ -using ABI.CCK.Components; -using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; +using ABI_RC.Core.Player; using ABI_RC.Systems.IK; -using ABI_RC.Systems.IK.SubSystems; -using ABI_RC.Systems.MovementSystem; using HarmonyLib; -using RootMotion.FinalIK; using UnityEngine; /** The process of calibrating VRIK is fucking painful. - Immediatly doing GetHumanPose() and then SetHumanPose() fixed heels in ground for all avatars. - - Setting the avatars rotation to identity, head rotation offset to head bone world rotation, and then calibrating head IK target (kinda) fixed only robot kyle. - - Enforcing a TPose only fixed my ferret avatars right shoulder. - - Mix and matching these, and changing order, fucks with random specific avatars with fucky armatures. - MOST AVATARS DONT EVEN CHANGE, ITS JUST THESE FEW SPECIFIC ONES - I NEED to look into an IKPose controller... - Avatars of Note: - TurtleNeck Ferret- broken/inverted right shoulder - Space Robot Kyle- head ik target is rotated -90 90 0, so body/neck is fucked (Fuck you Default Robot Kyle) - Exteratta- the knees bend backwards like a fucking chicken... what the fuck im enforcing a tpose nowww + TurtleNeck Ferret- close feet, far shoulders, nonideal rig. + Space Robot Kyle- the worst bone rolls on the planet, tpose/headikcalibration fixed it mostly... ish. + Exteratta- knees bend backwards without proper tpose. + Chito- left foot is far back without proper tpose & foot ik distance, was uploaded in falling anim state. + Atlas (portal2)- Wide stance, proper feet distance needed to be calculated. + Freddy (gmod)- Doesn't have any fingers, wristToPalmAxis & palmToThumbAxis needed to be set manually. - Most other avatars play just fine. Never changes even when adding Tpose, rotating the avatar, headikrotationoffset, ect... - WHY (Fuck you Default Robot Kyle) + Most other avatars play just fine. **/ @@ -36,210 +23,37 @@ namespace NAK.Melons.DesktopVRIK.HarmonyPatches; class PlayerSetupPatches { - private static bool emotePlayed = false; - [HarmonyPostfix] - [HarmonyPatch(typeof(PlayerSetup), "SetupAvatarGeneral")] - static void SetupDesktopIKSystem(ref CVRAvatar ____avatarDescriptor, ref Animator ____animator) + [HarmonyPatch(typeof(PlayerSetup), "SetupAvatarDesktop")] + static void Postfix_PlayerSetup_SetupAvatarDesktop(ref Animator ____animator) { - if (!MetaPort.Instance.isUsingVr && DesktopVRIK.Setting_Enabled) + if (____animator != null && ____animator.avatar != null && ____animator.avatar.isHuman) { - if (____avatarDescriptor != null && ____animator != null && ____animator.isHuman) - { - //this will stop at the useless isVr return (the function is only ever called by vr anyways...) - IKSystem.Instance.InitializeAvatar(____avatarDescriptor); - } + DesktopVRIK.Instance?.OnSetupAvatarDesktop(); } } [HarmonyPostfix] [HarmonyPatch(typeof(PlayerSetup), "Update")] - private static void CorrectVRIK(ref bool ____emotePlaying, ref LookAtIK ___lookIK) + static void Postfix_PlayerSetup_Update(ref bool ____emotePlaying) { - if (!MetaPort.Instance.isUsingVr && DesktopVRIK.Setting_Enabled) - { - bool changed = ____emotePlaying != emotePlayed; - if (changed) - { - emotePlayed = ____emotePlaying; - IKSystem.vrik.transform.localPosition = Vector3.zero; - IKSystem.vrik.transform.localRotation = Quaternion.identity; - if (DesktopVRIK.Setting_EmoteLookAtIK && ___lookIK != null) - { - ___lookIK.enabled = !____emotePlaying; - } - if (DesktopVRIK.Setting_EmoteVRIK) - { - BodySystem.TrackingEnabled = !____emotePlaying; - IKSystem.vrik.solver?.Reset(); - DesktopVRIK_Helper.Instance?.OnResetIK(); - } - } - } + DesktopVRIK.Instance?.OnPlayerSetupUpdate(____emotePlaying); } - - //should probably patch movement system instead [HarmonyPrefix] - [HarmonyPatch(typeof(PlayerSetup), "HandleDesktopCameraPosition")] - private static void Prefix_PlayerSetup_HandleDesktopCameraPosition - ( - bool ignore, - ref PlayerSetup __instance, - ref MovementSystem - ____movementSystem, - ref int ___headBobbingLevel - ) + [HarmonyPatch(typeof(PlayerSetup), "SetupIKScaling")] + private static bool Prefix_PlayerSetup_SetupIKScaling(float height, ref Vector3 ___scaleDifference) { - if (___headBobbingLevel != 2) - { - return; - } - - if (!DesktopVRIK.Setting_Enabled || !DesktopVRIK.Setting_EnforceViewPosition) - { - return; - } - - if (____movementSystem.disableCameraControl && !ignore) - { - return; - } - - if (DesktopVRIK.Instance.viewpoint == null) - { - return; - } - - __instance.desktopCamera.transform.position = DesktopVRIK.Instance.viewpoint.position; - return; + return !(bool)DesktopVRIK.Instance?.OnSetupIKScaling(1f + ___scaleDifference.y); } } class IKSystemPatches { [HarmonyPostfix] - [HarmonyPatch(typeof(IKSystem), "InitializeAvatar")] - private static void InitializeDesktopAvatarVRIK(CVRAvatar avatar, ref VRIK ____vrik, ref HumanPoseHandler ____poseHandler, ref HumanPose ___humanPose) + [HarmonyPatch(typeof(IKSystem), "Start")] + private static void Postfix_IKSystem_Start(ref IKSystem __instance) { - if (!MetaPort.Instance.isUsingVr && DesktopVRIK.Setting_Enabled) - { - if (IKSystem.Instance.animator != null && IKSystem.Instance.animator.avatar != null && IKSystem.Instance.animator.avatar.isHuman) - { - if (____poseHandler == null) - { - ____poseHandler = new HumanPoseHandler(IKSystem.Instance.animator.avatar, IKSystem.Instance.animator.transform); - } - - ____poseHandler.GetHumanPose(ref ___humanPose); - for (int i = 0; i < IKPoseMuscles.Length; i++) - { - IKSystem.Instance.ApplyMuscleValue((MuscleIndex)i, IKPoseMuscles[i], ref ___humanPose.muscles); - } - ____poseHandler.SetHumanPose(ref ___humanPose); - - ____vrik = DesktopVRIK.Instance.AlternativeCalibration(avatar); - IKSystem.Instance.ApplyAvatarScaleToIk(avatar.viewPosition.y); - } - } + __instance.gameObject.AddComponent(); } - - private static readonly float[] IKPoseMuscles = new float[] - { - 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 - }; } diff --git a/DesktopVRIK/Integrations/BTKUIAddon.cs b/DesktopVRIK/Integrations/BTKUIAddon.cs index e2ab2ab..33a512c 100644 --- a/DesktopVRIK/Integrations/BTKUIAddon.cs +++ b/DesktopVRIK/Integrations/BTKUIAddon.cs @@ -14,35 +14,38 @@ public static class BTKUIAddon Page miscPage = QuickMenuAPI.MiscTabPage; Category miscCategory = miscPage.AddCategory(DesktopVRIKMod.SettingsCategory); - AddMelonToggle(ref miscCategory, DesktopVRIKMod.m_entryEnabled); + AddMelonToggle(ref miscCategory, DesktopVRIKMod.EntryEnabled); //Add my own page to not clog up Misc Menu Page desktopVRIKPage = miscCategory.AddPage("DesktopVRIK Settings", "", "Configure the settings for DesktopVRIK.", "DesktopVRIK"); desktopVRIKPage.MenuTitle = "DesktopVRIK Settings"; - desktopVRIKPage.MenuSubtitle = "Simplified settings for VRIK on Desktop."; - Category desktopVRIKCategory = desktopVRIKPage.AddCategory("DesktopVRIK"); + Category desktopVRIKCategory = desktopVRIKPage.AddCategory(DesktopVRIKMod.SettingsCategory); - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.m_entryEnabled); + // General Settings + AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryEnabled); + AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryPlantFeet); - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.m_entryEnforceViewPosition); + // Calibration Settings + AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryUseVRIKToes); + AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryFindUnmappedToes); - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.m_entryEmoteVRIK); + // Body Leaning Weight + AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryBodyLeanWeight, 0, 1f, 1); - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.m_entryEmoteLookAtIK); - - AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.m_entryBodyLeanWeight, 0, 1f); - - AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.m_entryBodyAngleLimit, 0, 90f); + // Max Root Heading Limit & Weights + AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryBodyHeadingLimit, 0, 90f, 0); + AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryPelvisHeadingWeight, 0, 1f, 1); + AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryChestHeadingWeight, 0, 1f, 1); } private static void AddMelonToggle(ref Category category, MelonLoader.MelonPreferences_Entry entry) { category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b; } - private static void AddMelonSlider(ref Page page, MelonLoader.MelonPreferences_Entry entry, float min, float max) + private static void AddMelonSlider(ref Page page, MelonLoader.MelonPreferences_Entry entry, float min, float max, int decimalPlaces = 2) { - page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max).OnValueUpdated += f => entry.Value = f; + page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max, decimalPlaces).OnValueUpdated += f => entry.Value = f; } } \ No newline at end of file diff --git a/DesktopVRIK/Main.cs b/DesktopVRIK/Main.cs index 524f85b..7adb920 100644 --- a/DesktopVRIK/Main.cs +++ b/DesktopVRIK/Main.cs @@ -1,76 +1,78 @@ -using ABI_RC.Core.Player; -using MelonLoader; +using MelonLoader; using UnityEngine; namespace NAK.Melons.DesktopVRIK; public class DesktopVRIKMod : MelonMod { - internal const string SettingsCategory = "DesktopVRIK"; - internal static MelonPreferences_Category m_categoryDesktopVRIK; - internal static MelonPreferences_Entry m_entryEnabled, - m_entryEnforceViewPosition, - m_entryEmoteVRIK, - m_entryEmoteLookAtIK; - internal static MelonPreferences_Entry - m_entryBodyLeanWeight, - m_entryBodyAngleLimit; + internal static MelonLogger.Instance Logger; + public const string SettingsCategory = "DesktopVRIK"; + public static readonly MelonPreferences_Category CategoryDesktopVRIK = MelonPreferences.CreateCategory(SettingsCategory); + + public static readonly MelonPreferences_Entry EntryEnabled = + CategoryDesktopVRIK.CreateEntry("Enabled", true, description: "Toggle DesktopVRIK entirely. Requires avatar reload."); + + public static readonly MelonPreferences_Entry EntryPlantFeet = + CategoryDesktopVRIK.CreateEntry("Enforce Plant Feet", true, description: "Forces VRIK Plant Feet enabled to prevent hovering when stopping movement."); + + public static readonly MelonPreferences_Entry EntryUseVRIKToes = + CategoryDesktopVRIK.CreateEntry("Use VRIK Toes", false, description: "Determines if VRIK uses humanoid toes for IK solving, which can cause feet to idle behind the avatar."); + + public static readonly MelonPreferences_Entry EntryFindUnmappedToes = + CategoryDesktopVRIK.CreateEntry("Find Unmapped Toes", false, description: "Determines if DesktopVRIK should look for unmapped toe bones if the humanoid rig does not have any."); + + public static readonly MelonPreferences_Entry EntryBodyLeanWeight = + CategoryDesktopVRIK.CreateEntry("Body Lean Weight", 0.5f, description: "Adds rotational influence to the body solver when looking up/down. Set to 0 to disable."); + + public static readonly MelonPreferences_Entry EntryBodyHeadingLimit = + CategoryDesktopVRIK.CreateEntry("Body Heading Limit", 20f, description: "Specifies the maximum angle the lower body can have relative to the head when rotating. Set to 0 to disable."); + + public static readonly MelonPreferences_Entry EntryPelvisHeadingWeight = + CategoryDesktopVRIK.CreateEntry("Pelvis Heading Weight", 0.25f, description: "Determines how much the pelvis will face the Body Heading Limit. Set to 0 to align with head."); + + public static readonly MelonPreferences_Entry EntryChestHeadingWeight = + CategoryDesktopVRIK.CreateEntry("Chest Heading Weight", 0.75f, description: "Determines how much the chest will face the Body Heading Limit. Set to 0 to align with head."); + public override void OnInitializeMelon() { - m_categoryDesktopVRIK = MelonPreferences.CreateCategory(SettingsCategory); - m_entryEnabled = m_categoryDesktopVRIK.CreateEntry("Enabled", true, description: "Toggle DesktopVRIK entirely. Requires avatar reload."); - m_entryEnforceViewPosition = m_categoryDesktopVRIK.CreateEntry("Enforce View Position", false, description: "Corrects view position to use VRIK offsets."); - m_entryEmoteVRIK = m_categoryDesktopVRIK.CreateEntry("Disable Emote VRIK", true, description: "Disable VRIK while emoting. Only disable if you are ok with looking dumb."); - m_entryEmoteLookAtIK = m_categoryDesktopVRIK.CreateEntry("Disable Emote LookAtIK", true, description: "Disable LookAtIK while emoting. This setting doesn't really matter, as LookAtIK isn't networked while doing an emote."); + Logger = LoggerInstance; - m_entryBodyLeanWeight = m_categoryDesktopVRIK.CreateEntry("Body Lean Weight", 0.5f, description: "Emulates old VRChat-like body leaning when looking up/down. Set to 0 to disable."); - m_entryBodyAngleLimit = m_categoryDesktopVRIK.CreateEntry("Body Angle Limit", 0f, description: "Emulates VRChat-like body and head offset when rotating left/right. Set to 0 to disable. (this setting only affects the feet due to chillout not setting up the player controller for VRIK)"); + CategoryDesktopVRIK.Entries.ForEach(e => e.OnEntryValueChangedUntyped.Subscribe(OnUpdateSettings)); - foreach (var setting in m_categoryDesktopVRIK.Entries) - { - setting.OnEntryValueChangedUntyped.Subscribe(OnUpdateSettings); - } - - //BTKUILib Misc Support - if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "BTKUILib")) - { - MelonLogger.Msg("Initializing BTKUILib support."); - BTKUIAddon.Init(); - } - - //Apply patches (i stole) ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); ApplyPatches(typeof(HarmonyPatches.IKSystemPatches)); - MelonLoader.MelonCoroutines.Start(WaitForLocalPlayer()); + InitializeIntegrations(); } - System.Collections.IEnumerator WaitForLocalPlayer() - { - while (PlayerSetup.Instance == null) - yield return null; - - DesktopVRIK_Helper.CreateInstance(); - PlayerSetup.Instance.gameObject.AddComponent(); - - while (DesktopVRIK.Instance == null) - yield return null; - UpdateAllSettings(); - } - - private void UpdateAllSettings() + internal static void UpdateAllSettings() { if (!DesktopVRIK.Instance) return; - DesktopVRIK.Setting_Enabled = m_entryEnabled.Value; - DesktopVRIK.Setting_BodyLeanWeight = Mathf.Clamp01(m_entryBodyLeanWeight.Value); - DesktopVRIK.Setting_BodyAngleLimit = Mathf.Clamp(m_entryBodyAngleLimit.Value, 0f, 90f); - DesktopVRIK.Setting_EmoteVRIK = m_entryEmoteVRIK.Value; - DesktopVRIK.Setting_EmoteLookAtIK = m_entryEmoteLookAtIK.Value; - DesktopVRIK.Instance.ChangeViewpointHandling(m_entryEnforceViewPosition.Value); - } + // DesktopVRIK Settings + DesktopVRIK.Instance.Setting_Enabled = EntryEnabled.Value; + DesktopVRIK.Instance.Setting_PlantFeet = EntryPlantFeet.Value; + DesktopVRIK.Instance.Setting_BodyLeanWeight = Mathf.Clamp01(EntryBodyLeanWeight.Value); + DesktopVRIK.Instance.Setting_BodyHeadingLimit = Mathf.Clamp(EntryBodyHeadingLimit.Value, 0f, 90f); + DesktopVRIK.Instance.Setting_PelvisHeadingWeight = (1f - Mathf.Clamp01(EntryPelvisHeadingWeight.Value)); + DesktopVRIK.Instance.Setting_ChestHeadingWeight = (1f - Mathf.Clamp01(EntryChestHeadingWeight.Value)); + + // Calibration Settings + DesktopVRIK.Instance.Calibrator.Setting_UseVRIKToes = EntryUseVRIKToes.Value; + DesktopVRIK.Instance.Calibrator.Setting_FindUnmappedToes = EntryFindUnmappedToes.Value; + } private void OnUpdateSettings(object arg1, object arg2) => UpdateAllSettings(); + private static void InitializeIntegrations() + { + //BTKUILib Misc Tab + if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "BTKUILib")) + { + Logger.Msg("Initializing BTKUILib support."); + BTKUIAddon.Init(); + } + } + private void ApplyPatches(Type type) { try @@ -79,8 +81,8 @@ public class DesktopVRIKMod : MelonMod } catch (Exception e) { - LoggerInstance.Msg($"Failed while patching {type.Name}!"); - LoggerInstance.Error(e); + Logger.Msg($"Failed while patching {type.Name}!"); + Logger.Error(e); } } } \ No newline at end of file diff --git a/DesktopVRIK/Properties/AssemblyInfo.cs b/DesktopVRIK/Properties/AssemblyInfo.cs index 2494ba3..ace74a2 100644 --- a/DesktopVRIK/Properties/AssemblyInfo.cs +++ b/DesktopVRIK/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ -using DesktopVRIK.Properties; -using MelonLoader; +using MelonLoader; +using NAK.Melons.DesktopVRIK.Properties; using System.Reflection; - [assembly: AssemblyVersion(AssemblyInfoParams.Version)] [assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] [assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] @@ -24,9 +23,9 @@ using System.Reflection; [assembly: MelonOptionalDependencies("BTKUILib")] [assembly: HarmonyDontPatchAll] -namespace DesktopVRIK.Properties; +namespace NAK.Melons.DesktopVRIK.Properties; internal static class AssemblyInfoParams { - public const string Version = "2.0.5"; + public const string Version = "4.0.1"; public const string Author = "NotAKidoS"; } \ No newline at end of file diff --git a/DesktopVRIK/VRIKUtils.cs b/DesktopVRIK/VRIKUtils.cs new file mode 100644 index 0000000..fcdfaa5 --- /dev/null +++ b/DesktopVRIK/VRIKUtils.cs @@ -0,0 +1,183 @@ +using HarmonyLib; +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.Melons.DesktopVRIK; + +public static class VRIKUtils +{ + public static void ConfigureVRIKReferences(VRIK vrik, bool useVRIKToes, bool findUnmappedToes, out bool foundUnmappedToes) + { + foundUnmappedToes = false; + + //might not work over netik + FixChestAndSpineReferences(vrik); + + if (!useVRIKToes) + { + vrik.references.leftToes = null; + vrik.references.rightToes = null; + } + else if (findUnmappedToes) + { + //doesnt work with netik, but its toes... + FindAndSetUnmappedToes(vrik, out foundUnmappedToes); + } + + //bullshit fix to not cause death + FixFingerBonesError(vrik); + } + + private static void FixChestAndSpineReferences(VRIK vrik) + { + Transform leftShoulderBone = vrik.references.leftShoulder; + Transform rightShoulderBone = vrik.references.rightShoulder; + Transform assumedChest = leftShoulderBone?.parent; + + if (assumedChest != null && rightShoulderBone.parent == assumedChest && + vrik.references.chest != assumedChest) + { + vrik.references.chest = assumedChest; + vrik.references.spine = assumedChest.parent; + } + } + + private static void FindAndSetUnmappedToes(VRIK vrik, out bool foundUnmappedToes) + { + foundUnmappedToes = false; + + Transform leftToes = vrik.references.leftToes; + Transform rightToes = vrik.references.rightToes; + + if (leftToes == null && rightToes == null) + { + leftToes = FindUnmappedToe(vrik.references.leftFoot); + rightToes = FindUnmappedToe(vrik.references.rightFoot); + + if (leftToes != null && rightToes != null) + { + vrik.references.leftToes = leftToes; + vrik.references.rightToes = rightToes; + foundUnmappedToes = true; + } + } + } + + private static Transform FindUnmappedToe(Transform foot) + { + foreach (Transform bone in foot) + { + if (bone.name.ToLowerInvariant().Contains("toe") || + bone.name.ToLowerInvariant().EndsWith("_end")) + { + return bone; + } + } + + return null; + } + + private static void FixFingerBonesError(VRIK vrik) + { + FixFingerBones(vrik, vrik.references.leftHand, vrik.solver.leftArm); + FixFingerBones(vrik, vrik.references.rightHand, vrik.solver.rightArm); + } + + private static void FixFingerBones(VRIK vrik, Transform hand, IKSolverVR.Arm armSolver) + { + if (hand.childCount == 0) + { + armSolver.wristToPalmAxis = Vector3.up; + armSolver.palmToThumbAxis = hand == vrik.references.leftHand ? -Vector3.forward : Vector3.forward; + } + } + + public static void CalculateKneeBendNormals(VRIK vrik, out Vector3 leftKneeNormal, out Vector3 rightKneeNormal) + { + // 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) + }; + leftKneeNormal = 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) + }; + rightKneeNormal = Quaternion.Inverse(vrik.references.root.rotation) * GetNormalFromArray(rightVectors); + } + + public static void ApplyKneeBendNormals(VRIK vrik, Vector3 leftKneeNormal, Vector3 rightKneeNormal) + { + // 0 uses bendNormalRelToPelvis, 1 is bendNormalRelToTarget + // modifying pelvis normal weight is better math + vrik.solver.leftLeg.bendToTargetWeight = 0f; + vrik.solver.rightLeg.bendToTargetWeight = 0f; + + var leftLeg_bendNormalRelToPelvisTraverse = Traverse.Create(vrik.solver.leftLeg).Field("bendNormalRelToPelvis"); + var rightLeg_bendNormalRelToPelvisTraverse = Traverse.Create(vrik.solver.rightLeg).Field("bendNormalRelToPelvis"); + + var pelvis_localRotationInverse = Quaternion.Inverse(vrik.references.pelvis.localRotation); + var leftLeg_bendNormalRelToPelvis = pelvis_localRotationInverse * leftKneeNormal; + var rightLeg_bendNormalRelToPelvis = pelvis_localRotationInverse * rightKneeNormal; + + leftLeg_bendNormalRelToPelvisTraverse.SetValue(leftLeg_bendNormalRelToPelvis); + rightLeg_bendNormalRelToPelvisTraverse.SetValue(rightLeg_bendNormalRelToPelvis); + } + + 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, out float initialFootDistance, out float initialStepThreshold, out float initialStepHeight) + { + // 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); + initialFootDistance = footDistance * 0.5f; + initialStepThreshold = footDistance * scaleModifier; + initialStepHeight = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.leftCalf.position) * 0.2f; + } + + public static void ApplyScaleToVRIK(VRIK vrik, float footDistance, float stepThreshold, float stepHeight, float modifier) + { + vrik.solver.locomotion.footDistance = footDistance * modifier; + vrik.solver.locomotion.stepThreshold = stepThreshold * modifier; + ScaleStepHeight(vrik.solver.locomotion.stepHeight, stepHeight * modifier); + } + + private 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/DesktopVRIK/format.json b/DesktopVRIK/format.json index d1d47d6..6337a93 100644 --- a/DesktopVRIK/format.json +++ b/DesktopVRIK/format.json @@ -1,23 +1,25 @@ { "_id": 117, "name": "DesktopVRIK", - "modversion": "2.0.5", + "modversion": "4.0.1", "gameversion": "2022r170", "loaderversion": "0.5.7", "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\nAdditional option to emulate VRChat-like hip movement if you like being fish.", + "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\nIt is highly recommended to use AvatarMotionTweaker alongside this mod.", "searchtags": [ "desktop", "vrik", "ik", - "feet" + "feet", + "fish" ], "requirements": [ - "BTKUILib" + "BTKUILib", + "AvatarMotionTweaker" ], - "downloadlink": "https://github.com/NotAKidOnSteam/DesktopVRIK/releases/download/v2.0.5/DesktopVRIK.dll", + "downloadlink": "https://github.com/NotAKidOnSteam/DesktopVRIK/releases/download/v4.0.1/DesktopVRIK.dll", "sourcelink": "https://github.com/NotAKidOnSteam/DesktopVRIK/", - "changelog": "- Tweaks to VRIK settings so BetterInteractDesktop & PickupArmMovement better behave alongside VRIK.\n- Tweaks to BTKUI integration.", + "changelog": "- Implemented fixes for terrible armatures that had broken knee bending, initial hip rotation, and initial step calculations.\n- Added pelvis & chest weight sliders for heading angle limit option.\n- Implemented bandaid fixes for disabled animator and avatars without fingers.\n- Added options to use or remove toes from VRIK and find unmapped non-humanoid toe bones.\n- Contact me if you find an avatar that is still broken.", "embedcolor": "9b59b6" } \ No newline at end of file diff --git a/README.md b/README.md index 9e5c790..c2d4f73 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,16 @@ Adds VRIK to Desktop ChilloutVR avatars. No longer will you be a liveless slidin (adds the small feet stepping when looking around on Desktop) -Additional option to emulate VRChat-like hip movement if you like becoming a fish. +https://user-images.githubusercontent.com/37721153/221870123-fbe4f5e8-8d6e-4a43-aa5e-f2188e6491a9.mp4 -Control over disabling VRIK & LookAtIK during Emotes. (this can cause funky behavior) +## Configuration: +* Configurable body lean weight. Set to 0 to disable. + +* Configurable max root heading angle with chest/pelvis weight settings. Set to 0 to disable. + +* Options to disable VRIK from using mapped toes &/or find unmapped (non-human) toe bones. + +* Autofixes for avatars without fingers & incorrect chest/spine bone mapping (might not play well with netik). ## Relevant Feedback Posts: https://feedback.abinteractive.net/p/desktop-feet-ik-for-avatars @@ -22,4 +29,3 @@ https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games > 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. -