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. -