diff --git a/DesktopVRIK/DesktopVRIK.csproj b/DesktopVRIK/DesktopVRIK.csproj
new file mode 100644
index 0000000..60fc7d3
--- /dev/null
+++ b/DesktopVRIK/DesktopVRIK.csproj
@@ -0,0 +1,57 @@
+
+
+
+
+ net472
+ enable
+ latest
+ false
+ True
+
+
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\0Harmony.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\Assembly-CSharp.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\Assembly-CSharp-firstpass.dll
+
+
+ ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\Cohtml.Runtime.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\MelonLoader.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\UnityEngine.AnimationModule.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\UnityEngine.AssetBundleModule.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\UnityEngine.CoreModule.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\UnityEngine.InputLegacyModule.dll
+
+
+ C:\Users\krist\Documents\GitHub\_ChilloutVR Modding\_ManagedLibs\UnityEngine.PhysicsModule.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DesktopVRIK/DesktopVRIK.sln b/DesktopVRIK/DesktopVRIK.sln
new file mode 100644
index 0000000..6aba351
--- /dev/null
+++ b/DesktopVRIK/DesktopVRIK.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32630.192
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopVRIK", "DesktopVRIK.csproj", "{07F06485-C387-470A-A43D-F3779A059F30}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {07F06485-C387-470A-A43D-F3779A059F30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07F06485-C387-470A-A43D-F3779A059F30}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07F06485-C387-470A-A43D-F3779A059F30}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07F06485-C387-470A-A43D-F3779A059F30}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {05F83429-FD88-4C7A-B7D5-40516382B6FD}
+ EndGlobalSection
+EndGlobal
diff --git a/DesktopVRIK/DesktopVRIKSystem.cs b/DesktopVRIK/DesktopVRIKSystem.cs
new file mode 100644
index 0000000..7c25cd4
--- /dev/null
+++ b/DesktopVRIK/DesktopVRIKSystem.cs
@@ -0,0 +1,677 @@
+using ABI.CCK.Components;
+using ABI_RC.Core.Base;
+using ABI_RC.Core.Player;
+using ABI_RC.Systems.IK;
+using ABI_RC.Systems.IK.SubSystems;
+using ABI_RC.Systems.MovementSystem;
+using RootMotion.FinalIK;
+using UnityEngine;
+using UnityEngine.Events;
+
+namespace NAK.Melons.DesktopVRIK;
+
+internal class DesktopVRIKSystem : MonoBehaviour
+{
+ public static DesktopVRIKSystem Instance;
+ public static Dictionary BoneExists;
+ public 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
+ };
+
+ enum AvatarPose
+ {
+ Default = 0,
+ Initial = 1,
+ IKPose = 2,
+ TPose = 3
+ }
+
+ // DesktopVRIK Settings
+ public bool Setting_Enabled = true;
+ public bool Setting_PlantFeet;
+ public bool Setting_ResetFootsteps;
+ public bool Setting_ProneThrusting;
+ public float Setting_BodyLeanWeight;
+ public float Setting_BodyHeadingLimit;
+ public float Setting_PelvisHeadingWeight;
+ public float Setting_ChestHeadingWeight;
+ public float Setting_IKLerpSpeed;
+
+ // Calibration Settings
+ public bool Setting_UseVRIKToes;
+ public bool Setting_FindUnmappedToes;
+
+ // Integration Settings
+ public bool Setting_IntegrationAMT;
+
+ // Avatar Components
+ public CVRAvatar avatarDescriptor = null;
+ public Animator avatarAnimator = null;
+ public Transform avatarTransform = null;
+ public LookAtIK avatarLookAtIK = null;
+ public VRIK avatarVRIK = null;
+ public IKSolverVR avatarIKSolver = null;
+
+ // ChilloutVR Player Components
+ PlayerSetup playerSetup;
+ MovementSystem movementSystem;
+
+ // Calibration Objects
+ HumanPose _humanPose;
+ HumanPose _humanPoseInitial;
+ HumanPoseHandler _humanPoseHandler;
+
+ // Animator Info
+ int _animLocomotionLayer = -1;
+ int _animIKPoseLayer = -1;
+
+ // VRIK Calibration Info
+ Vector3 _vrikKneeNormalLeft;
+ Vector3 _vrikKneeNormalRight;
+ Vector3 _vrikInitialFootPosLeft;
+ Vector3 _vrikInitialFootPosRight;
+ Quaternion _vrikInitialFootRotLeft;
+ Quaternion _vrikInitialFootRotRight;
+ float _vrikInitialFootDistance;
+ float _vrikInitialStepThreshold;
+ float _vrikInitialStepHeight;
+ bool _vrikFixTransformsRequired;
+
+ // Player Info
+ Transform _cameraTransform;
+ bool _ikEmotePlaying;
+ float _ikWeightLerp = 1f;
+ float _ikSimulatedRootAngle = 0f;
+ float _locomotionWeight = 1f;
+ float _scaleDifference = 1f;
+
+ // Last Movement Parent Info
+ Vector3 _movementPosition;
+ Quaternion _movementRotation;
+
+ DesktopVRIKSystem()
+ {
+ BoneExists = new Dictionary();
+ }
+
+ void Start()
+ {
+ Instance = this;
+
+ playerSetup = GetComponent();
+ movementSystem = GetComponent();
+
+ _cameraTransform = playerSetup.desktopCamera.transform;
+
+ DesktopVRIKMod.UpdateAllSettings();
+ }
+
+ void Update()
+ {
+ if (avatarVRIK == null) return;
+
+ HandleLocomotionTracking();
+ UpdateLocomotionWeight();
+ ApplyBodySystemWeights();
+ }
+
+ void HandleLocomotionTracking()
+ {
+ bool shouldTrackLocomotion = ShouldTrackLocomotion();
+
+ if (shouldTrackLocomotion != BodySystem.TrackingLocomotionEnabled)
+ {
+ BodySystem.TrackingLocomotionEnabled = shouldTrackLocomotion;
+ avatarIKSolver.Reset();
+ ResetDesktopVRIK();
+ if (shouldTrackLocomotion) IKResetFootsteps();
+ }
+ }
+
+ bool ShouldTrackLocomotion()
+ {
+ bool isMoving = movementSystem.movementVector.magnitude > 0f;
+ bool isGrounded = movementSystem._isGrounded;
+ bool isCrouching = movementSystem.crouching;
+ bool isProne = movementSystem.prone;
+ bool isFlying = movementSystem.flying;
+ bool isStanding = IsStanding();
+
+ return !(isMoving || isCrouching || isProne || isFlying || !isGrounded || !isStanding);
+ }
+
+ bool IsStanding()
+ {
+ // Let AMT handle it if available
+ if (Setting_IntegrationAMT) return true;
+
+ // Get Upright value
+ Vector3 delta = avatarIKSolver.spine.headPosition - avatarTransform.position;
+ Vector3 deltaRotated = Quaternion.Euler(0, avatarTransform.rotation.eulerAngles.y, 0) * delta;
+ float upright = Mathf.InverseLerp(0f, avatarIKSolver.spine.headHeight * _scaleDifference, deltaRotated.y);
+ return upright > 0.85f;
+ }
+
+
+ void UpdateLocomotionWeight()
+ {
+ float targetWeight = BodySystem.TrackingEnabled && BodySystem.TrackingLocomotionEnabled ? 1.0f : 0.0f;
+ if (Setting_IKLerpSpeed > 0)
+ {
+ _ikWeightLerp = Mathf.Lerp(_ikWeightLerp, targetWeight, Time.deltaTime * Setting_IKLerpSpeed);
+ _locomotionWeight = Mathf.Lerp(_locomotionWeight, targetWeight, Time.deltaTime * Setting_IKLerpSpeed * 2f);
+ }
+ else
+ {
+ _ikWeightLerp = targetWeight;
+ _locomotionWeight = targetWeight;
+ }
+ }
+
+ void ApplyBodySystemWeights()
+ {
+ void SetArmWeight(IKSolverVR.Arm arm, bool isTracked)
+ {
+ arm.positionWeight = isTracked ? 1f : 0f;
+ arm.rotationWeight = isTracked ? 1f : 0f;
+ arm.shoulderRotationWeight = isTracked ? 1f : 0f;
+ arm.shoulderTwistWeight = isTracked ? 1f : 0f;
+ }
+ void SetLegWeight(IKSolverVR.Leg leg, bool isTracked)
+ {
+ leg.positionWeight = isTracked ? 1f : 0f;
+ leg.rotationWeight = isTracked ? 1f : 0f;
+ }
+ if (BodySystem.TrackingEnabled)
+ {
+ avatarVRIK.enabled = true;
+ avatarIKSolver.IKPositionWeight = BodySystem.TrackingPositionWeight;
+ avatarIKSolver.locomotion.weight = _locomotionWeight;
+
+ SetArmWeight(avatarIKSolver.leftArm, BodySystem.TrackingLeftArmEnabled && avatarIKSolver.leftArm.target != null);
+ SetArmWeight(avatarIKSolver.rightArm, BodySystem.TrackingRightArmEnabled && avatarIKSolver.rightArm.target != null);
+ SetLegWeight(avatarIKSolver.leftLeg, BodySystem.TrackingLeftLegEnabled && avatarIKSolver.leftLeg.target != null);
+ SetLegWeight(avatarIKSolver.rightLeg, BodySystem.TrackingRightLegEnabled && avatarIKSolver.rightLeg.target != null);
+ }
+ else
+ {
+ avatarVRIK.enabled = false;
+ avatarIKSolver.IKPositionWeight = 0f;
+ avatarIKSolver.locomotion.weight = 0f;
+
+ SetArmWeight(avatarIKSolver.leftArm, false);
+ SetArmWeight(avatarIKSolver.rightArm, false);
+ SetLegWeight(avatarIKSolver.leftLeg, false);
+ SetLegWeight(avatarIKSolver.rightLeg, false);
+ }
+ }
+
+ public void OnSetupAvatarDesktop()
+ {
+ if (!Setting_Enabled) return;
+
+ CalibrateDesktopVRIK();
+ ResetDesktopVRIK();
+ }
+
+ public bool OnSetupIKScaling(float scaleDifference)
+ {
+ if (avatarVRIK == null) return false;
+
+ VRIKUtils.ApplyScaleToVRIK
+ (
+ avatarVRIK,
+ _vrikInitialFootDistance,
+ _vrikInitialStepThreshold,
+ _vrikInitialStepHeight,
+ scaleDifference
+ );
+
+ _scaleDifference = scaleDifference;
+
+ avatarIKSolver.Reset();
+ ResetDesktopVRIK();
+ return true;
+ }
+
+ public void OnPlayerSetupUpdate(bool isEmotePlaying)
+ {
+ if (avatarVRIK == null) return;
+
+ if (isEmotePlaying == _ikEmotePlaying) return;
+ _ikEmotePlaying = isEmotePlaying;
+
+ if (avatarLookAtIK != null)
+ avatarLookAtIK.enabled = !isEmotePlaying;
+
+ // Disable tracking completely while emoting
+ BodySystem.TrackingEnabled = !isEmotePlaying;
+ avatarIKSolver.Reset();
+ ResetDesktopVRIK();
+ }
+
+ public bool OnPlayerSetupResetIk()
+ {
+ if (avatarVRIK == null) return false;
+
+ // Check if PlayerSetup.ResetIk() was called for movement parent
+ CVRMovementParent currentParent = movementSystem._currentParent;
+ if (currentParent == null || currentParent._referencePoint == null) return false;
+
+ // 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 = transform.position;
+
+ // Add platform motion to IK solver
+ avatarIKSolver.AddPlatformMotion(deltaPosition, deltaRotation, platformPivot);
+
+ // Store for next frame
+ _movementPosition = currentPosition;
+ _movementRotation = currentRotation;
+
+ ResetDesktopVRIK();
+ return true;
+ }
+
+ public void OnPreSolverUpdate()
+ {
+ // Reset avatar offset
+ avatarTransform.localPosition = Vector3.zero;
+ avatarTransform.localRotation = Quaternion.identity;
+
+ // Don't run during emotes
+ if (_ikEmotePlaying) return;
+
+ // Set plant feet
+ avatarIKSolver.plantFeet = Setting_PlantFeet;
+
+ // Apply custom VRIK solving effects
+ IKBodyLeaningOffset(_ikWeightLerp);
+ IKBodyHeadingOffset(_ikWeightLerp);
+ }
+
+ void IKBodyLeaningOffset(float weight)
+ {
+ // Emulate old VRChat hip movement
+ if (Setting_BodyLeanWeight <= 0) return;
+
+ if (Setting_ProneThrusting) weight = 1f;
+ float weightedAngle = Setting_BodyLeanWeight * weight;
+ float angle = _cameraTransform.localEulerAngles.x;
+ angle = angle > 180 ? angle - 360 : angle;
+ Quaternion rotation = Quaternion.AngleAxis(angle * weightedAngle, avatarTransform.right);
+ avatarIKSolver.spine.headRotationOffset *= rotation;
+ }
+
+ void IKBodyHeadingOffset(float weight)
+ {
+ // Make root heading follow within a set limit
+ if (Setting_BodyHeadingLimit <= 0) return;
+
+ float weightedAngleLimit = Setting_BodyHeadingLimit * weight;
+ float deltaAngleRoot = Mathf.DeltaAngle(transform.eulerAngles.y, _ikSimulatedRootAngle);
+ float absDeltaAngleRoot = Mathf.Abs(deltaAngleRoot);
+
+ if (absDeltaAngleRoot > weightedAngleLimit)
+ {
+ deltaAngleRoot = Mathf.Sign(deltaAngleRoot) * weightedAngleLimit;
+ _ikSimulatedRootAngle = Mathf.MoveTowardsAngle(_ikSimulatedRootAngle, transform.eulerAngles.y, absDeltaAngleRoot - weightedAngleLimit);
+ }
+
+ avatarIKSolver.spine.rootHeadingOffset = deltaAngleRoot;
+
+ if (Setting_PelvisHeadingWeight > 0)
+ {
+ avatarIKSolver.spine.pelvisRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * Setting_PelvisHeadingWeight, 0f);
+ avatarIKSolver.spine.chestRotationOffset *= Quaternion.Euler(0f, -deltaAngleRoot * Setting_PelvisHeadingWeight, 0f);
+ }
+
+ if (Setting_ChestHeadingWeight > 0)
+ {
+ avatarIKSolver.spine.chestRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * Setting_ChestHeadingWeight, 0f);
+ }
+ }
+
+ void IKResetFootsteps()
+ {
+ // Reset footsteps immediatly to initial
+ if (!Setting_ResetFootsteps) return;
+
+ VRIKUtils.SetFootsteps
+ (
+ avatarVRIK,
+ _vrikInitialFootPosLeft,
+ _vrikInitialFootPosRight,
+ _vrikInitialFootRotLeft,
+ _vrikInitialFootRotRight
+ );
+ }
+
+ void ResetDesktopVRIK()
+ {
+ _ikSimulatedRootAngle = transform.eulerAngles.y;
+ }
+
+ void CalibrateDesktopVRIK()
+ {
+ ScanAvatar();
+ SetupVRIK();
+ CalibrateVRIK();
+ ConfigureVRIK();
+ }
+
+ void ScanAvatar()
+ {
+ // Find required avatar components
+ avatarDescriptor = playerSetup._avatarDescriptor;
+ avatarAnimator = playerSetup._animator;
+ avatarTransform = playerSetup._avatar.transform;
+ avatarLookAtIK = playerSetup.lookIK;
+
+ // Get animator layer inticies
+ _animIKPoseLayer = avatarAnimator.GetLayerIndex("IKPose");
+ _animLocomotionLayer = avatarAnimator.GetLayerIndex("Locomotion/Emotes");
+
+ // Dispose and create new _humanPoseHandler
+ _humanPoseHandler?.Dispose();
+ _humanPoseHandler = new HumanPoseHandler(avatarAnimator.avatar, avatarTransform);
+
+ // Get initial human poses
+ _humanPoseHandler.GetHumanPose(ref _humanPose);
+ _humanPoseHandler.GetHumanPose(ref _humanPoseInitial);
+
+ // Dumb fix for rare upload issue
+ _vrikFixTransformsRequired = !avatarAnimator.enabled;
+
+ // Find available HumanoidBodyBones
+ BoneExists.Clear();
+ foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones)))
+ {
+ if (bone != HumanBodyBones.LastBone)
+ {
+ BoneExists.Add(bone, avatarAnimator.GetBoneTransform(bone) != null);
+ }
+ }
+ }
+
+ void SetupVRIK()
+ {
+ // Add and configure VRIK
+ avatarVRIK = avatarTransform.AddComponentIfMissing();
+ avatarVRIK.AutoDetectReferences();
+ avatarIKSolver = avatarVRIK.solver;
+
+ VRIKUtils.ConfigureVRIKReferences(avatarVRIK, Setting_UseVRIKToes, Setting_FindUnmappedToes, out bool foundUnmappedToes);
+
+ // Fix animator issue or non-human mapped toes
+ avatarVRIK.fixTransforms = _vrikFixTransformsRequired || foundUnmappedToes;
+
+ // Default solver settings
+ avatarIKSolver.locomotion.weight = 0f;
+ avatarIKSolver.locomotion.angleThreshold = 30f;
+ avatarIKSolver.locomotion.maxLegStretch = 1f;
+ avatarIKSolver.spine.minHeadHeight = 0f;
+ avatarIKSolver.IKPositionWeight = 1f;
+ avatarIKSolver.spine.chestClampWeight = 0f;
+ avatarIKSolver.spine.maintainPelvisPosition = 0f;
+
+ // Body leaning settings
+ avatarIKSolver.spine.neckStiffness = 0.0001f;
+ avatarIKSolver.spine.bodyPosStiffness = 1f;
+ avatarIKSolver.spine.bodyRotStiffness = 0.2f;
+
+ // Disable locomotion
+ avatarIKSolver.locomotion.velocityFactor = 0f;
+ avatarIKSolver.locomotion.maxVelocity = 0f;
+ avatarIKSolver.locomotion.rootSpeed = 1000f;
+
+ // Disable chest rotation by hands
+ avatarIKSolver.spine.rotateChestByHands = 0f;
+
+ // Prioritize LookAtIK
+ avatarIKSolver.spine.headClampWeight = 0.2f;
+
+ // Disable going on tippytoes
+ avatarIKSolver.spine.positionWeight = 0f;
+ avatarIKSolver.spine.rotationWeight = 1f;
+
+ // Set so emotes play properly
+ avatarIKSolver.spine.maxRootAngle = 180f;
+
+ // We disable these ourselves now, as we no longer use BodySystem
+ avatarIKSolver.spine.maintainPelvisPosition = 1f;
+ avatarIKSolver.spine.positionWeight = 0f;
+ avatarIKSolver.spine.pelvisPositionWeight = 0f;
+ avatarIKSolver.leftArm.positionWeight = 0f;
+ avatarIKSolver.leftArm.rotationWeight = 0f;
+ avatarIKSolver.rightArm.positionWeight = 0f;
+ avatarIKSolver.rightArm.rotationWeight = 0f;
+ avatarIKSolver.leftLeg.positionWeight = 0f;
+ avatarIKSolver.leftLeg.rotationWeight = 0f;
+ avatarIKSolver.rightLeg.positionWeight = 0f;
+ avatarIKSolver.rightLeg.rotationWeight = 0f;
+
+ // This is now our master Locomotion weight
+ avatarIKSolver.locomotion.weight = 1f;
+ avatarIKSolver.IKPositionWeight = 1f;
+ }
+
+ void CalibrateVRIK()
+ {
+ SetAvatarPose(AvatarPose.Default);
+
+ // Calculate bend normals with motorcycle pose
+ VRIKUtils.CalculateKneeBendNormals(avatarVRIK, out _vrikKneeNormalLeft, out _vrikKneeNormalRight);
+
+ SetAvatarPose(AvatarPose.IKPose);
+
+ // Calculate initial IK scaling values with IKPose
+ VRIKUtils.CalculateInitialIKScaling(avatarVRIK, out _vrikInitialFootDistance, out _vrikInitialStepThreshold, out _vrikInitialStepHeight);
+
+ // Calculate initial Footstep positions
+ VRIKUtils.CalculateInitialFootsteps(avatarVRIK, out _vrikInitialFootPosLeft, out _vrikInitialFootPosRight, out _vrikInitialFootRotLeft, out _vrikInitialFootRotRight);
+
+ // Setup HeadIKTarget
+ VRIKUtils.SetupHeadIKTarget(avatarVRIK);
+
+ // Initiate VRIK manually
+ VRIKUtils.InitiateVRIKSolver(avatarVRIK);
+
+ SetAvatarPose(AvatarPose.Initial);
+ }
+
+ void ConfigureVRIK()
+ {
+ // Reset scale diffrence
+ _scaleDifference = 1f;
+ VRIKUtils.ApplyScaleToVRIK
+ (
+ avatarVRIK,
+ _vrikInitialFootDistance,
+ _vrikInitialStepThreshold,
+ _vrikInitialStepHeight,
+ 1f
+ );
+ VRIKUtils.ApplyKneeBendNormals(avatarVRIK, _vrikKneeNormalLeft, _vrikKneeNormalRight);
+ avatarVRIK.onPreSolverUpdate.AddListener(new UnityAction(DesktopVRIKSystem.Instance.OnPreSolverUpdate));
+ }
+
+ void SetAvatarPose(AvatarPose pose)
+ {
+ switch (pose)
+ {
+ case AvatarPose.Default:
+ SetMusclesToValue(0f);
+ break;
+ case AvatarPose.Initial:
+ if (HasCustomIKPose())
+ {
+ SetCustomLayersWeights(0f, 1f);
+ return;
+ }
+ _humanPoseHandler.SetHumanPose(ref _humanPoseInitial);
+ break;
+ case AvatarPose.IKPose:
+ if (HasCustomIKPose())
+ {
+ SetCustomLayersWeights(1f, 0f);
+ return;
+ }
+ SetMusclesToPose(IKPoseMuscles);
+ break;
+ case AvatarPose.TPose:
+ SetMusclesToPose(BodySystem.TPoseMuscles);
+ break;
+ default:
+ break;
+ }
+ }
+
+ bool HasCustomIKPose()
+ {
+ return _animLocomotionLayer != -1 && _animIKPoseLayer != -1;
+ }
+
+ void SetCustomLayersWeights(float customIKPoseLayerWeight, float locomotionLayerWeight)
+ {
+ avatarAnimator.SetLayerWeight(_animIKPoseLayer, customIKPoseLayerWeight);
+ avatarAnimator.SetLayerWeight(_animLocomotionLayer, locomotionLayerWeight);
+ avatarAnimator.Update(0f);
+ }
+
+ void SetMusclesToValue(float value)
+ {
+ _humanPoseHandler.GetHumanPose(ref _humanPose);
+
+ for (int i = 0; i < _humanPose.muscles.Length; i++)
+ {
+ ApplyMuscleValue((MuscleIndex)i, value, ref _humanPose.muscles);
+ }
+
+ _humanPose.bodyRotation = Quaternion.identity;
+ _humanPoseHandler.SetHumanPose(ref _humanPose);
+ }
+
+ void SetMusclesToPose(float[] muscles)
+ {
+ _humanPoseHandler.GetHumanPose(ref _humanPose);
+
+ for (int i = 0; i < _humanPose.muscles.Length; i++)
+ {
+ ApplyMuscleValue((MuscleIndex)i, muscles[i], ref _humanPose.muscles);
+ }
+
+ _humanPose.bodyRotation = Quaternion.identity;
+ _humanPoseHandler.SetHumanPose(ref _humanPose);
+ }
+
+ void ApplyMuscleValue(MuscleIndex index, float value, ref float[] muscles)
+ {
+ if (BoneExists.ContainsKey(IKSystem.MusclesToHumanBodyBones[(int)index]) && BoneExists[IKSystem.MusclesToHumanBodyBones[(int)index]])
+ {
+ muscles[(int)index] = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DesktopVRIK/HarmonyPatches.cs b/DesktopVRIK/HarmonyPatches.cs
new file mode 100644
index 0000000..a72b331
--- /dev/null
+++ b/DesktopVRIK/HarmonyPatches.cs
@@ -0,0 +1,63 @@
+using ABI_RC.Core.Player;
+using HarmonyLib;
+using UnityEngine;
+
+/**
+
+ The process of calibrating VRIK is fucking painful.
+
+ Avatars of Note:
+ 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.
+ Small Cheese- Emotes are angled wrong due to maxRootAngle..???
+
+ Most other avatars play just fine.
+
+**/
+
+namespace NAK.Melons.DesktopVRIK.HarmonyPatches;
+
+class PlayerSetupPatches
+{
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PlayerSetup), "Start")]
+ static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance)
+ {
+ __instance.gameObject.AddComponent();
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PlayerSetup), "SetupAvatarDesktop")]
+ static void Postfix_PlayerSetup_SetupAvatarDesktop(ref Animator ____animator)
+ {
+ if (____animator != null && ____animator.avatar != null && ____animator.avatar.isHuman)
+ {
+ DesktopVRIKSystem.Instance?.OnSetupAvatarDesktop();
+ }
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PlayerSetup), "Update")]
+ static void Postfix_PlayerSetup_Update(ref bool ____emotePlaying)
+ {
+ DesktopVRIKSystem.Instance?.OnPlayerSetupUpdate(____emotePlaying);
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PlayerSetup), "SetupIKScaling")]
+ private static bool Prefix_PlayerSetup_SetupIKScaling(float height, ref Vector3 ___scaleDifference)
+ {
+ return !(bool)DesktopVRIKSystem.Instance?.OnSetupIKScaling(1f + ___scaleDifference.y);
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PlayerSetup), "ResetIk")]
+ static bool Prefix_PlayerSetup_ResetIk()
+ {
+ return !(bool)DesktopVRIKSystem.Instance?.OnPlayerSetupResetIk();
+ }
+}
diff --git a/DesktopVRIK/Integrations/BTKUIAddon.cs b/DesktopVRIK/Integrations/BTKUIAddon.cs
new file mode 100644
index 0000000..5c25427
--- /dev/null
+++ b/DesktopVRIK/Integrations/BTKUIAddon.cs
@@ -0,0 +1,55 @@
+using BTKUILib;
+using BTKUILib.UIObjects;
+using System.Runtime.CompilerServices;
+
+namespace NAK.Melons.DesktopVRIK;
+
+public static class BTKUIAddon
+{
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void Init()
+ {
+ //Add myself to the Misc Menu
+
+ Page miscPage = QuickMenuAPI.MiscTabPage;
+ Category miscCategory = miscPage.AddCategory(DesktopVRIKMod.SettingsCategory);
+
+ 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";
+ Category desktopVRIKCategory = desktopVRIKPage.AddCategory(DesktopVRIKMod.SettingsCategory);
+
+ // General Settings
+ AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryPlantFeet);
+
+ // Calibration Settings
+ AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryUseVRIKToes);
+ AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryFindUnmappedToes);
+
+ // Fine-tuning Settings
+ AddMelonToggle(ref desktopVRIKCategory, DesktopVRIKMod.EntryResetFootstepsOnIdle);
+
+ // Body Leaning Weight
+ AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryBodyLeanWeight, 0, 1f, 1);
+
+ // 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);
+
+ // Lerp Speed
+ AddMelonSlider(ref desktopVRIKPage, DesktopVRIKMod.EntryIKLerpSpeed, 0, 20f, 0);
+ }
+
+ 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, int decimalPlaces = 2)
+ {
+ 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
new file mode 100644
index 0000000..c7739b4
--- /dev/null
+++ b/DesktopVRIK/Main.cs
@@ -0,0 +1,118 @@
+using MelonLoader;
+using UnityEngine;
+
+namespace NAK.Melons.DesktopVRIK;
+
+public class DesktopVRIKMod : MelonMod
+{
+ 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 EntryResetFootstepsOnIdle =
+ CategoryDesktopVRIK.CreateEntry("Reset Footsteps on Idle", true, description: "Determins if the Locomotion Footsteps will be reset to their calibration position when entering idle.");
+
+ 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 static readonly MelonPreferences_Entry EntryIKLerpSpeed =
+ CategoryDesktopVRIK.CreateEntry("IK Lerp Speed", 10f, description: "Determines fast the IK & Locomotion weights blend after entering idle. Set to 0 to disable.");
+
+ public static readonly MelonPreferences_Entry EntryProneThrusting =
+ CategoryDesktopVRIK.CreateEntry("Prone Thrusting", false, description: "Allows Body Lean Weight to take affect while crouched or prone.");
+
+ public static readonly MelonPreferences_Entry EntryIntegrationAMT =
+ CategoryDesktopVRIK.CreateEntry("AMT Integration", true, description: "Relies on AvatarMotionTweaker to handle VRIK Locomotion weights if available.");
+
+ public static bool integration_AMT = false;
+
+ public override void OnInitializeMelon()
+ {
+ Logger = LoggerInstance;
+
+ CategoryDesktopVRIK.Entries.ForEach(e => e.OnEntryValueChangedUntyped.Subscribe(OnUpdateSettings));
+
+ ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
+
+ //BTKUILib Misc Tab
+ if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "BTKUILib"))
+ {
+ Logger.Msg("Initializing BTKUILib support.");
+ BTKUIAddon.Init();
+ }
+ //AvatarMotionTweaker Handling
+ if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "AvatarMotionTweaker"))
+ {
+ Logger.Msg("AvatarMotionTweaker was found. Relying on it to handle VRIK locomotion.");
+ integration_AMT = true;
+ }
+ else
+ {
+ Logger.Msg("AvatarMotionTweaker was not found. Using built-in VRIK locomotion handling.");
+ }
+ }
+
+ internal static void UpdateAllSettings()
+ {
+ if (!DesktopVRIKSystem.Instance) return;
+ // DesktopVRIK Settings
+ DesktopVRIKSystem.Instance.Setting_Enabled = EntryEnabled.Value;
+ DesktopVRIKSystem.Instance.Setting_PlantFeet = EntryPlantFeet.Value;
+ DesktopVRIKSystem.Instance.Setting_ResetFootsteps = EntryResetFootstepsOnIdle.Value;
+
+ DesktopVRIKSystem.Instance.Setting_BodyLeanWeight = Mathf.Clamp01(EntryBodyLeanWeight.Value);
+ DesktopVRIKSystem.Instance.Setting_BodyHeadingLimit = Mathf.Clamp(EntryBodyHeadingLimit.Value, 0f, 90f);
+ DesktopVRIKSystem.Instance.Setting_PelvisHeadingWeight = (1f - Mathf.Clamp01(EntryPelvisHeadingWeight.Value));
+ DesktopVRIKSystem.Instance.Setting_ChestHeadingWeight = (1f - Mathf.Clamp01(EntryChestHeadingWeight.Value));
+ DesktopVRIKSystem.Instance.Setting_ChestHeadingWeight = (1f - Mathf.Clamp01(EntryChestHeadingWeight.Value));
+ DesktopVRIKSystem.Instance.Setting_IKLerpSpeed = Mathf.Clamp(EntryIKLerpSpeed.Value, 0f, 20f);
+
+ // Calibration Settings
+ DesktopVRIKSystem.Instance.Setting_UseVRIKToes = EntryUseVRIKToes.Value;
+ DesktopVRIKSystem.Instance.Setting_FindUnmappedToes = EntryFindUnmappedToes.Value;
+
+ // Fine-tuning Settings
+ DesktopVRIKSystem.Instance.Setting_ResetFootsteps = EntryResetFootstepsOnIdle.Value;
+
+ // Integration Settings
+ DesktopVRIKSystem.Instance.Setting_IntegrationAMT = EntryIntegrationAMT.Value && integration_AMT;
+
+ // Funny Settings
+ DesktopVRIKSystem.Instance.Setting_ProneThrusting = EntryProneThrusting.Value;
+ }
+ void OnUpdateSettings(object arg1, object arg2) => UpdateAllSettings();
+
+ 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/DesktopVRIK/Properties/AssemblyInfo.cs b/DesktopVRIK/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..25bdf29
--- /dev/null
+++ b/DesktopVRIK/Properties/AssemblyInfo.cs
@@ -0,0 +1,31 @@
+using MelonLoader;
+using NAK.Melons.DesktopVRIK.Properties;
+using System.Reflection;
+
+[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyTitle(nameof(NAK.Melons.DesktopVRIK))]
+[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
+[assembly: AssemblyProduct(nameof(NAK.Melons.DesktopVRIK))]
+
+[assembly: MelonInfo(
+ typeof(NAK.Melons.DesktopVRIK.DesktopVRIKMod),
+ nameof(NAK.Melons.DesktopVRIK),
+ AssemblyInfoParams.Version,
+ AssemblyInfoParams.Author,
+ downloadLink: "https://github.com/NotAKidOnSteam/DesktopVRIK"
+)]
+
+[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
+[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
+[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
+[assembly: MelonOptionalDependencies("BTKUILib")]
+[assembly: HarmonyDontPatchAll]
+
+namespace NAK.Melons.DesktopVRIK.Properties;
+internal static class AssemblyInfoParams
+{
+ public const string Version = "4.1.5";
+ 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..21c36c3
--- /dev/null
+++ b/DesktopVRIK/VRIKUtils.cs
@@ -0,0 +1,217 @@
+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 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 * leftKneeNormal;
+ vrik.solver.rightLeg.bendNormalRelToPelvis = pelvis_localRotationInverse * rightKneeNormal;
+ }
+
+ 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 CalculateInitialFootsteps(VRIK vrik, out Vector3 initialFootPosLeft, out Vector3 initialFootPosRight, out Quaternion initialFootRotLeft, out Quaternion initialFootRotRight)
+ {
+ 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
+ initialFootPosLeft = root.InverseTransformPoint(leftFoot.position);
+ initialFootPosRight = root.InverseTransformPoint(rightFoot.position);
+ initialFootRotLeft = Quaternion.Inverse(rootWorldRot) * leftFoot.rotation;
+ initialFootRotRight = Quaternion.Inverse(rootWorldRot) * rightFoot.rotation;
+ }
+
+ public static void SetFootsteps(VRIK vrik, Vector3 footPosLeft, Vector3 footPosRight, Quaternion footRotLeft, Quaternion footRotRight)
+ {
+ var locomotionSolver = vrik.solver.locomotion;
+
+ var footsteps = locomotionSolver.footsteps;
+ var footstepLeft = footsteps[0];
+ var footstepRight = footsteps[1];
+
+ var rootWorldRot = vrik.references.root.rotation;
+ footstepLeft.Reset(rootWorldRot, vrik.transform.TransformPoint(footPosLeft), rootWorldRot * footRotLeft);
+ footstepRight.Reset(rootWorldRot, vrik.transform.TransformPoint(footPosRight), rootWorldRot * footRotRight);
+ }
+
+ public static void SetupHeadIKTarget(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;
+ }
+
+ 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
new file mode 100644
index 0000000..f1305c7
--- /dev/null
+++ b/DesktopVRIK/format.json
@@ -0,0 +1,24 @@
+{
+ "_id": 117,
+ "name": "DesktopVRIK",
+ "modversion": "4.1.5",
+ "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\nOptional BTKUILib integration.",
+ "searchtags": [
+ "desktop",
+ "vrik",
+ "ik",
+ "feet",
+ "fish"
+ ],
+ "requirements": [
+ "BTKUILib"
+ ],
+ "downloadlink": "https://github.com/NotAKidOnSteam/DesktopVRIK/releases/download/v4.1.5/DesktopVRIK.dll",
+ "sourcelink": "https://github.com/NotAKidOnSteam/DesktopVRIK/",
+ "changelog": "- No longer requires AvatarMotionTweaker.\n- No longer piggybacks on IKSystem/PlayerSetup.\n- Tweaks to Locomotion & IKSolver weight blending.\n\n DesktopVRIK will now handle VRIK Locomotion weight instead of relying on CVR & AMT to handle it. This means LeapMotionExtension, PickupArmMovement, and CVRLimbGrabber will now work while in crouch/prone.",
+ "embedcolor": "9b59b6"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3c2268e..618b6f6 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,27 @@ There is no native method to clear notifications, so I force an immediate notifi
* Enter Drowsy Time - How many minutes without movement until enter drowsy mode.
* Enter Sleep Time - How many seconds without movement until enter sleep mode.
+# DesktopVRIK
+Adds VRIK to Desktop ChilloutVR avatars. No longer will you be a liveless sliding statue~!
+
+(adds the small feet stepping when looking around on Desktop)
+
+https://user-images.githubusercontent.com/37721153/221870123-fbe4f5e8-8d6e-4a43-aa5e-f2188e6491a9.mp4
+
+## 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
+
+https://feedback.abinteractive.net/p/pivot-desktop-camera-with-head
+
---
Here is the block of text where I tell you this mod is not affiliated or endorsed by ABI.
@@ -75,4 +96,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.
-