diff --git a/DesktopVRIK/DesktopVRIK - Backup.csproj b/DesktopVRIK/DesktopVRIK - Backup.csproj new file mode 100644 index 0000000..9daf839 --- /dev/null +++ b/DesktopVRIK/DesktopVRIK - Backup.csproj @@ -0,0 +1,15 @@ + + + + + netstandard1.0 + + + + + $(MsBuildThisFileDirectory)\..\.ManagedLibs\BTKUILib.dll + False + + + + \ No newline at end of file diff --git a/DesktopVRIK/DesktopVRIK.csproj b/DesktopVRIK/DesktopVRIK.csproj index e94f9dc..3d7c8cf 100644 --- a/DesktopVRIK/DesktopVRIK.csproj +++ b/DesktopVRIK/DesktopVRIK.csproj @@ -1,2 +1,23 @@ - + + + + netstandard2.1 + + + + + + + + + + + + + $(MsBuildThisFileDirectory)\..\.ManagedLibs\BTKUILib.dll + False + + + + \ No newline at end of file diff --git a/DesktopVRIK/DesktopVRIKCalibrator.cs b/DesktopVRIK/DesktopVRIKCalibrator.cs deleted file mode 100644 index 525e609..0000000 --- a/DesktopVRIK/DesktopVRIKCalibrator.cs +++ /dev/null @@ -1,379 +0,0 @@ -using ABI_RC.Core.Base; -using ABI_RC.Systems.IK; -using ABI_RC.Systems.IK.SubSystems; -using NAK.DesktopVRIK.VRIKHelper; -using RootMotion.FinalIK; -using UnityEngine; -using UnityEngine.Events; - -namespace NAK.DesktopVRIK; - -internal class DesktopVRIKCalibrator -{ - 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 - } - - readonly DesktopVRIKSystem ikSystem; - - // Avatar Components - Animator _animator; - - // Calibration Objects - HumanPoseHandler _humanPoseHandler; - HumanPose _humanPose; - HumanPose _humanPoseInitial; - - // Animator Info - int _animLocomotionLayer = -1; - int _animIKPoseLayer = -1; - - internal DesktopVRIKCalibrator() - { - ikSystem = DesktopVRIKSystem.Instance; - BoneExists = new Dictionary(); - } - - public void ApplyNetIKPass() - { - Transform hipTransform = _animator.GetBoneTransform(HumanBodyBones.Hips); - Vector3 hipPosition = hipTransform.position; - Quaternion hipRotation = hipTransform.rotation; - - _humanPoseHandler.GetHumanPose(ref _humanPose); - _humanPoseHandler.SetHumanPose(ref _humanPose); - - hipTransform.position = hipPosition; - hipTransform.rotation = hipRotation; - } - - public void CalibrateDesktopVRIK(Animator animator) - { - ScanAvatar(animator); - SetupVRIK(); - CalibrateVRIK(); - ConfigureVRIK(); - } - - void ScanAvatar(Animator animator) - { - // Find required avatar components - _animator = animator; - ikSystem.avatarTransform = animator.gameObject.transform; - ikSystem.avatarLookAtIK = animator.gameObject.GetComponent(); - - // Get animator layer inticies - _animIKPoseLayer = _animator.GetLayerIndex("IKPose"); - _animLocomotionLayer = _animator.GetLayerIndex("Locomotion/Emotes"); - - // Dispose and create new _humanPoseHandler - _humanPoseHandler?.Dispose(); - _humanPoseHandler = new HumanPoseHandler(_animator.avatar, ikSystem.avatarTransform); - - // Get initial human poses - _humanPoseHandler.GetHumanPose(ref _humanPose); - _humanPoseHandler.GetHumanPose(ref _humanPoseInitial); - - ikSystem.calibrationData.Clear(); - - // Dumb fix for rare upload issue - ikSystem.calibrationData.FixTransformsRequired = !_animator.enabled; - - // Find available HumanoidBodyBones - BoneExists.Clear(); - foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) - { - if (bone != HumanBodyBones.LastBone) - { - BoneExists.Add(bone, _animator.GetBoneTransform(bone) != null); - } - } - } - - void SetupVRIK() - { - // Add and configure VRIK - ikSystem.avatarVRIK = ikSystem.avatarTransform.AddComponentIfMissing(); - ikSystem.avatarVRIK.AutoDetectReferences(); - - // Why do I love to overcomplicate things? - VRIKUtils.ConfigureVRIKReferences(ikSystem.avatarVRIK, DesktopVRIK.EntryUseVRIKToes.Value); - - // Fix animator issue - ikSystem.avatarVRIK.fixTransforms = ikSystem.calibrationData.FixTransformsRequired; - - CachedSolver solver = new CachedSolver(ikSystem.avatarVRIK.solver); - - // Default solver settings - solver.Locomotion.weight = 0f; - solver.Locomotion.angleThreshold = 30f; - solver.Locomotion.maxLegStretch = 1f; - solver.Spine.minHeadHeight = 0f; - solver.Spine.chestClampWeight = 0f; - solver.Spine.maintainPelvisPosition = 0f; - solver.Solver.IKPositionWeight = 1f; - - // Body leaning settings - solver.Spine.bodyPosStiffness = 1f; - solver.Spine.bodyRotStiffness = 0.2f; - // this is a hack, allows chest to rotate slightly - // independent from hip rotation. Funny Spine.Solve()->Bend() - solver.Spine.neckStiffness = 0.0001f; - - // Disable locomotion - // Setting velocity to 0 aleviated nameplate jitter issue on remote - solver.Locomotion.velocityFactor = 0f; - solver.Locomotion.maxVelocity = 0f; - solver.Locomotion.rootSpeed = 1000f; - - // Disable chest rotation by hands - // this fixed Effector, Player Arm Movement, BetterInteractDesktop, ect - // from making entire body shake, as well as while running - solver.Spine.rotateChestByHands = 0f; - - // Prioritize LookAtIK - solver.Spine.headClampWeight = 0.2f; - - // Disable going on tippytoes - solver.Spine.positionWeight = 0f; - solver.Spine.rotationWeight = 1f; - - // Set so emotes play properly - solver.Spine.maxRootAngle = 180f; - // this is different in VR, as CVR player controller is not set up optimally for VRIK. - // Desktop avatar rotates 1:1 with _PlayerLocal. VR has a disconnect because you can turn IRL. - - // We disable these ourselves now, as we no longer use BodySystem - solver.Spine.maintainPelvisPosition = 1f; - solver.Spine.positionWeight = 0f; - solver.Spine.pelvisPositionWeight = 0f; - solver.LeftArm.positionWeight = 0f; - solver.LeftArm.rotationWeight = 0f; - solver.RightArm.positionWeight = 0f; - solver.RightArm.rotationWeight = 0f; - solver.LeftLeg.positionWeight = 0f; - solver.LeftLeg.rotationWeight = 0f; - solver.RightLeg.positionWeight = 0f; - solver.RightLeg.rotationWeight = 0f; - - // This is now our master Locomotion weight - solver.Locomotion.weight = 1f; - solver.Solver.IKPositionWeight = 1f; - - ikSystem.cachedSolver = solver; - } - - void CalibrateVRIK() - { - SetAvatarPose(AvatarPose.Default); - - // Calculate bend normals with motorcycle pose - VRIKUtils.CalculateKneeBendNormals(ikSystem.avatarVRIK, ref ikSystem.calibrationData); - - SetAvatarPose(AvatarPose.IKPose); - - // Calculate initial IK scaling values with IKPose - VRIKUtils.CalculateInitialIKScaling(ikSystem.avatarVRIK, ref ikSystem.calibrationData); - - // Calculate initial Footstep positions - VRIKUtils.CalculateInitialFootsteps(ikSystem.avatarVRIK, ref ikSystem.calibrationData); - - // Setup HeadIKTarget - VRIKUtils.SetupHeadIKTarget(ikSystem.avatarVRIK); - - // Initiate VRIK manually - VRIKUtils.InitiateVRIKSolver(ikSystem.avatarVRIK); - - SetAvatarPose(AvatarPose.Initial); - } - - void ConfigureVRIK() - { - ikSystem.OnSetupIKScaling(1f); - - VRIKUtils.ApplyKneeBendNormals(ikSystem.avatarVRIK, ikSystem.calibrationData); - - ikSystem.avatarVRIK.onPreSolverUpdate.AddListener(new UnityAction(ikSystem.OnPreSolverUpdate)); - ikSystem.avatarVRIK.onPostSolverUpdate.AddListener(new UnityAction(ikSystem.OnPostSolverUpdate)); - } - - 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) - { - _animator.SetLayerWeight(_animIKPoseLayer, customIKPoseLayerWeight); - _animator.SetLayerWeight(_animLocomotionLayer, locomotionLayerWeight); - _animator.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; - } - } -} diff --git a/DesktopVRIK/DesktopVRIKSystem.cs b/DesktopVRIK/DesktopVRIKSystem.cs deleted file mode 100644 index 68be22d..0000000 --- a/DesktopVRIK/DesktopVRIKSystem.cs +++ /dev/null @@ -1,338 +0,0 @@ -using ABI.CCK.Components; -using ABI_RC.Core.Player; -using ABI_RC.Systems.IK.SubSystems; -using ABI_RC.Systems.MovementSystem; -using NAK.DesktopVRIK.VRIKHelper; -using RootMotion.FinalIK; -using UnityEngine; - -namespace NAK.DesktopVRIK; - -internal class DesktopVRIKSystem : MonoBehaviour -{ - public static DesktopVRIKSystem Instance; - public static DesktopVRIKCalibrator Calibrator; - - // VRIK Calibration Info - public VRIKCalibrationData calibrationData; - - // Avatar Components - public VRIK avatarVRIK = null; - public LookAtIK avatarLookAtIK = null; - public Transform avatarTransform = null; - public CachedSolver cachedSolver; - - // ChilloutVR Player Components - PlayerSetup playerSetup; - MovementSystem movementSystem; - - // 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; - CVRMovementParent _movementParent; - - void Start() - { - Instance = this; - Calibrator = new DesktopVRIKCalibrator(); - - playerSetup = GetComponent(); - movementSystem = GetComponent(); - - _cameraTransform = playerSetup.desktopCamera.transform; - } - - void Update() - { - if (avatarVRIK == null) return; - - HandleLocomotionTracking(); - UpdateLocomotionWeight(); - ApplyBodySystemWeights(); - ResetAvatarLocalPosition(); - } - - void HandleLocomotionTracking() - { - bool shouldTrackLocomotion = ShouldTrackLocomotion(); - - if (shouldTrackLocomotion != BodySystem.TrackingLocomotionEnabled) - { - BodySystem.TrackingLocomotionEnabled = shouldTrackLocomotion; - IKResetSolver(); - 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 isSitting = movementSystem.sitting; - bool isStanding = IsStanding(); - - return !(isMoving || isCrouching || isProne || isFlying || isSitting || !isGrounded || !isStanding); - } - - bool IsStanding() - { - // Let AMT handle it if available - if (DesktopVRIK.EntryIntegrationAMT.Value) return true; - - // Get Upright value - Vector3 delta = cachedSolver.Spine.headPosition - avatarTransform.position; - Vector3 deltaRotated = Quaternion.Euler(0, avatarTransform.rotation.eulerAngles.y, 0) * delta; - float upright = Mathf.InverseLerp(0f, calibrationData.InitialHeadHeight * _scaleDifference, deltaRotated.y); - return upright > 0.85f; - } - - void UpdateLocomotionWeight() - { - float targetWeight = BodySystem.TrackingEnabled && BodySystem.TrackingLocomotionEnabled ? 1.0f : 0.0f; - if (DesktopVRIK.EntryIKLerpSpeed.Value > 0) - { - _ikWeightLerp = Mathf.Lerp(_ikWeightLerp, targetWeight, Time.deltaTime * DesktopVRIK.EntryIKLerpSpeed.Value); - _locomotionWeight = Mathf.Lerp(_locomotionWeight, targetWeight, Time.deltaTime * DesktopVRIK.EntryIKLerpSpeed.Value * 2f); - return; - } - _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; - cachedSolver.Solver.IKPositionWeight = BodySystem.TrackingPositionWeight; - cachedSolver.Locomotion.weight = _locomotionWeight; - - bool useAnimatedBendNormal = _locomotionWeight <= 0.5f; - cachedSolver.LeftLeg.useAnimatedBendNormal = useAnimatedBendNormal; - cachedSolver.RightLeg.useAnimatedBendNormal = useAnimatedBendNormal; - SetArmWeight(cachedSolver.LeftArm, BodySystem.TrackingLeftArmEnabled && cachedSolver.LeftArm.target != null); - SetArmWeight(cachedSolver.RightArm, BodySystem.TrackingRightArmEnabled && cachedSolver.RightArm.target != null); - SetLegWeight(cachedSolver.LeftLeg, BodySystem.TrackingLeftLegEnabled && cachedSolver.LeftLeg.target != null); - SetLegWeight(cachedSolver.RightLeg, BodySystem.TrackingRightLegEnabled && cachedSolver.RightLeg.target != null); - } - else - { - avatarVRIK.enabled = false; - cachedSolver.Solver.IKPositionWeight = 0f; - cachedSolver.Locomotion.weight = 0f; - - cachedSolver.LeftLeg.useAnimatedBendNormal = false; - cachedSolver.RightLeg.useAnimatedBendNormal = false; - SetArmWeight(cachedSolver.LeftArm, false); - SetArmWeight(cachedSolver.RightArm, false); - SetLegWeight(cachedSolver.LeftLeg, false); - SetLegWeight(cachedSolver.RightLeg, false); - } - } - - void ResetBodySystem() - { - // DesktopVRSwitch should handle this, but I am not pushing an update yet. - BodySystem.TrackingEnabled = true; - BodySystem.TrackingPositionWeight = 1f; - BodySystem.isCalibratedAsFullBody = false; - BodySystem.isCalibrating = false; - BodySystem.isRecalibration = false; - } - - void ResetAvatarLocalPosition() - { - // Reset avatar offset - avatarTransform.localPosition = Vector3.zero; - avatarTransform.localRotation = Quaternion.identity; - } - - public void OnSetupAvatarDesktop(Animator animator) - { - if (!DesktopVRIK.EntryEnabled.Value) return; - - // only run for humanoid avatars - if (animator != null && animator.avatar != null && animator.avatar.isHuman) - { - Calibrator.CalibrateDesktopVRIK(animator); - ResetBodySystem(); - ResetDesktopVRIK(); - } - } - - public void OnSetupIKScaling(float scaleDifference) - { - _scaleDifference = scaleDifference; - - VRIKUtils.ApplyScaleToVRIK - ( - avatarVRIK, - calibrationData, - _scaleDifference - ); - } - - public void OnPlayerSetupUpdate(bool isEmotePlaying) - { - if (isEmotePlaying == _ikEmotePlaying) return; - _ikEmotePlaying = isEmotePlaying; - - if (avatarLookAtIK != null) - avatarLookAtIK.enabled = !isEmotePlaying; - - // Disable tracking completely while emoting - BodySystem.TrackingEnabled = !isEmotePlaying; - IKResetSolver(); - ResetDesktopVRIK(); - } - - public void OnPlayerSetupSetSitting() - { - IKResetSolver(); - ResetDesktopVRIK(); - } - - public void OnPlayerSetupResetIk() - { - // Check if PlayerSetup.ResetIk() was called for movement parent - CVRMovementParent currentParent = movementSystem._currentParent; - if (currentParent != null && currentParent._referencePoint != null) - { - // 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; - - // Prevent targeting other parent position - if (_movementParent == currentParent) - { - // Add platform motion to IK solver - cachedSolver.Solver.AddPlatformMotion(deltaPosition, deltaRotation, platformPivot); - ResetDesktopVRIK(); - } - - // Store for next frame - _movementParent = currentParent; - _movementPosition = currentPosition; - _movementRotation = currentRotation; - return; - } - - // if not for movementparent, reset ik - IKResetSolver(); - IKResetFootsteps(); - ResetDesktopVRIK(); - } - - public void OnPreSolverUpdate() - { - // Set plant feet - cachedSolver.Solver.plantFeet = DesktopVRIK.EntryPlantFeet.Value; - - // Apply custom VRIK solving effects - IKBodyLeaningOffset(_ikWeightLerp); - IKBodyHeadingOffset(_ikWeightLerp); - - void IKBodyLeaningOffset(float weight) - { - // Emulate old VRChat hip movement - if (DesktopVRIK.EntryBodyLeanWeight.Value <= 0) return; - - if (DesktopVRIK.EntryProneThrusting.Value) weight = 1f; - float weightedAngle = DesktopVRIK.EntryBodyLeanWeight.Value * weight; - float angle = _cameraTransform.localEulerAngles.x; - angle = angle > 180 ? angle - 360 : angle; - Quaternion rotation = Quaternion.AngleAxis(angle * weightedAngle, avatarTransform.right); - cachedSolver.Spine.headRotationOffset *= rotation; - } - - void IKBodyHeadingOffset(float weight) - { - // Make root heading follow within a set limit - if (DesktopVRIK.EntryBodyHeadingLimit.Value <= 0) return; - - float weightedAngleLimit = DesktopVRIK.EntryBodyHeadingLimit.Value * 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); - } - - cachedSolver.Spine.rootHeadingOffset = deltaAngleRoot; - - if (DesktopVRIK.EntryPelvisHeadingWeight.Value > 0) - { - cachedSolver.Spine.pelvisRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * DesktopVRIK.EntryPelvisHeadingWeight.Value, 0f); - cachedSolver.Spine.chestRotationOffset *= Quaternion.Euler(0f, -deltaAngleRoot * DesktopVRIK.EntryPelvisHeadingWeight.Value, 0f); - } - - if (DesktopVRIK.EntryChestHeadingWeight.Value > 0) - { - cachedSolver.Spine.chestRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * DesktopVRIK.EntryChestHeadingWeight.Value, 0f); - } - } - } - - public void OnPostSolverUpdate() - { - if (!DesktopVRIK.EntryNetIKPass.Value) return; - Calibrator.ApplyNetIKPass(); - } - - void IKResetSolver() - { - cachedSolver.Solver.Reset(); - } - - void IKResetFootsteps() - { - // Reset footsteps immediatly to initial - if (!DesktopVRIK.EntryResetFootstepsOnIdle.Value) return; - - VRIKUtils.ResetToInitialFootsteps - ( - avatarVRIK, - calibrationData, - _scaleDifference - ); - } - - void ResetDesktopVRIK() - { - _ikSimulatedRootAngle = transform.eulerAngles.y; - } -} \ No newline at end of file diff --git a/DesktopVRIK/HarmonyPatches.cs b/DesktopVRIK/HarmonyPatches.cs index c4417e4..fbfaa5f 100644 --- a/DesktopVRIK/HarmonyPatches.cs +++ b/DesktopVRIK/HarmonyPatches.cs @@ -1,5 +1,7 @@ -using ABI_RC.Core.Player; +using ABI.CCK.Components; +using ABI_RC.Core.Player; using HarmonyLib; +using NAK.DesktopVRIK.IK; using UnityEngine; /** @@ -25,73 +27,97 @@ using UnityEngine; namespace NAK.DesktopVRIK.HarmonyPatches; -class PlayerSetupPatches +internal class PlayerSetupPatches { [HarmonyPostfix] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] - static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance) + private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance) { - __instance.gameObject.AddComponent(); + __instance.gameObject.AddComponent(); } [HarmonyPostfix] - [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatarDesktop))] - static void Postfix_PlayerSetup_SetupAvatarDesktop(ref Animator ____animator) + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))] + private static void Postfix_PlayerSetup_SetupAvatar(GameObject inAvatar) { - // only intercept if DesktopVRIK is being used - if (DesktopVRIKSystem.Instance != null) + if (!ModSettings.EntryEnabled.Value) + return; + + try { - DesktopVRIKSystem.Instance.OnSetupAvatarDesktop(____animator); + IKManager.Instance?.OnAvatarInitialized(inAvatar); + } + catch (Exception e) + { + DesktopVRIK.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetupAvatar)}"); + DesktopVRIK.Logger.Error(e); } } [HarmonyPostfix] - [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Update))] - static void Postfix_PlayerSetup_Update(ref bool ____emotePlaying) + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ClearAvatar))] + private static void Postfix_PlayerSetup_ClearAvatar() { - // only intercept if DesktopVRIK is being used - if (DesktopVRIKSystem.Instance?.avatarVRIK != null) + try { - DesktopVRIKSystem.Instance.OnPlayerSetupUpdate(____emotePlaying); + IKManager.Instance?.OnAvatarDestroyed(); + } + catch (Exception e) + { + DesktopVRIK.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_ClearAvatar)}"); + DesktopVRIK.Logger.Error(e); } } [HarmonyPrefix] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupIKScaling))] - private static bool Prefix_PlayerSetup_SetupIKScaling(float height, ref Vector3 ___scaleDifference) + private static void Prefix_PlayerSetup_SetupIKScaling(ref Vector3 ___scaleDifference, ref bool __runOriginal) { - // only intercept if DesktopVRIK is being used - if (DesktopVRIKSystem.Instance?.avatarVRIK != null) + try { - DesktopVRIKSystem.Instance.OnSetupIKScaling(1f + ___scaleDifference.y); - return false; + if (IKManager.Instance != null) + __runOriginal = !IKManager.Instance.OnPlayerScaled(1f + ___scaleDifference.y); + } + catch (Exception e) + { + DesktopVRIK.Logger.Error($"Error during the patched method {nameof(Prefix_PlayerSetup_SetupIKScaling)}"); + DesktopVRIK.Logger.Error(e); } - - return true; } [HarmonyPostfix] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetSitting))] - static void Postfix_PlayerSetup_SetSitting() + private static void Postfix_PlayerSetup_SetSitting(ref bool ___isCurrentlyInSeat) { - // only intercept if DesktopVRIK is being used - if (DesktopVRIKSystem.Instance?.avatarVRIK != null) + try { - DesktopVRIKSystem.Instance.OnPlayerSetupSetSitting(); + IKManager.Instance?.OnPlayerSeatedStateChanged(___isCurrentlyInSeat); + } + catch (Exception e) + { + DesktopVRIK.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetSitting)}"); + DesktopVRIK.Logger.Error(e); } } [HarmonyPrefix] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ResetIk))] - static bool Prefix_PlayerSetup_ResetIk() + private static void Prefix_PlayerSetup_ResetIk(ref PlayerSetup __instance, ref bool __runOriginal) { - // only intercept if DesktopVRIK is being used - if (DesktopVRIKSystem.Instance?.avatarVRIK != null) + try { - DesktopVRIKSystem.Instance.OnPlayerSetupResetIk(); - return false; - } + if (IKManager.Instance == null) + return; - return true; + CVRMovementParent currentParent = __instance._movementSystem._currentParent; + __runOriginal = currentParent?._referencePoint != null + ? IKManager.Instance.OnPlayerHandleMovementParent(currentParent) + : IKManager.Instance.OnPlayerTeleported(); + } + catch (Exception e) + { + DesktopVRIK.Logger.Error($"Error during the patched method {nameof(Prefix_PlayerSetup_ResetIk)}"); + DesktopVRIK.Logger.Error(e); + } } } \ No newline at end of file diff --git a/DesktopVRIK/IK/IKCalibrator.cs b/DesktopVRIK/IK/IKCalibrator.cs new file mode 100644 index 0000000..7e7c469 --- /dev/null +++ b/DesktopVRIK/IK/IKCalibrator.cs @@ -0,0 +1,229 @@ +using RootMotion.FinalIK; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace NAK.DesktopVRIK.IK; + +internal static class IKCalibrator +{ + #region VRIK Solver Setup + + public static VRIK SetupVrIk(Animator animator) + { + if (animator.gameObject.TryGetComponent(out VRIK vrik)) + Object.DestroyImmediate(vrik); + + vrik = animator.gameObject.AddComponent(); + vrik.AutoDetectReferences(); + + if (!ModSettings.EntryUseToesForVRIK.Value) + { + vrik.references.leftToes = null; + vrik.references.rightToes = null; + } + + vrik.solver.SetToReferences(vrik.references); + + GuessWristPalmAxis(vrik.references.leftHand, vrik.references.leftForearm, vrik.solver.leftArm); + GuessWristPalmAxis(vrik.references.rightHand, vrik.references.rightForearm, vrik.solver.rightArm); + + SafePalmToThumbAxis(vrik.references.leftHand, vrik.references.leftForearm, vrik.solver.leftArm, + animator.GetBoneTransform(HumanBodyBones.LeftThumbProximal)); + SafePalmToThumbAxis(vrik.references.rightHand, vrik.references.rightForearm, vrik.solver.rightArm, + animator.GetBoneTransform(HumanBodyBones.RightThumbProximal)); + + AddTwistRelaxer(vrik.references.leftForearm, vrik, vrik.references.leftHand); + AddTwistRelaxer(vrik.references.rightForearm, vrik, vrik.references.rightHand); + + //vrik.solver.leftArm.shoulderRotationMode = (IKSolverVR.Arm.ShoulderRotationMode)IkTweaksSettings.ShoulderMode; + //vrik.solver.rightArm.shoulderRotationMode = (IKSolverVR.Arm.ShoulderRotationMode)IkTweaksSettings.ShoulderMode; + + // zero all weights controlled by BodyControl + vrik.solver.locomotion.weight = 0f; + vrik.solver.IKPositionWeight = 0f; + + vrik.solver.spine.pelvisPositionWeight = 0f; + vrik.solver.spine.pelvisRotationWeight = 0f; + vrik.solver.spine.positionWeight = 0f; + vrik.solver.spine.rotationWeight = 0f; + + vrik.solver.leftLeg.positionWeight = 0f; + vrik.solver.leftLeg.rotationWeight = 0f; + vrik.solver.rightLeg.positionWeight = 0f; + vrik.solver.rightLeg.rotationWeight = 0f; + vrik.solver.leftArm.positionWeight = 0f; + vrik.solver.leftArm.rotationWeight = 0f; + vrik.solver.rightArm.positionWeight = 0f; + vrik.solver.rightArm.rotationWeight = 0f; + + vrik.solver.leftLeg.bendGoalWeight = 0f; + vrik.solver.rightLeg.bendGoalWeight = 0f; + + // these weights are fine + vrik.solver.leftArm.shoulderRotationWeight = 0.8f; + vrik.solver.rightArm.shoulderRotationWeight = 0.8f; + + vrik.solver.leftLeg.bendToTargetWeight = 0.75f; + vrik.solver.rightLeg.bendToTargetWeight = 0.75f; + + // hack to prevent death + vrik.fixTransforms = !animator.enabled; + + // Avatar Motion Tweaker uses this hack! + vrik.solver.leftLeg.useAnimatedBendNormal = false; + vrik.solver.rightLeg.useAnimatedBendNormal = false; + + // purposefully initiating early + vrik.solver.Initiate(vrik.transform); + vrik.solver.Reset(); + + return vrik; + } + + private static void GuessWristPalmAxis(Transform hand, Transform forearm, IKSolverVR.Arm arm) + { + arm.wristToPalmAxis = VRIKCalibrator.GuessWristToPalmAxis( + hand, + forearm + ); + } + + private static void SafePalmToThumbAxis(Transform hand, Transform forearm, IKSolverVR.Arm arm, Transform thumbBone = null) + { + if (hand.childCount == 0) + { + arm.palmToThumbAxis = Vector3.one; + return; + } + + arm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis( + hand, + forearm, + thumbBone + ); + } + + private static void AddTwistRelaxer(Transform forearm, VRIK ik, Transform hand) + { + if (forearm == null) return; + TwistRelaxer twistRelaxer = forearm.gameObject.AddComponent(); + twistRelaxer.ik = ik; + twistRelaxer.weight = 0.5f; + twistRelaxer.child = hand; + twistRelaxer.parentChildCrossfade = 0.8f; + } + + #endregion + + #region VRIK Configuration + + public static void ConfigureDesktopVrIk(VRIK vrik) + { + // From DesktopVRIK + // https://github.com/NotAKidOnSteam/NAK_CVR_Mods/blob/fca0a32257311f044d1a9d6e68269baa4a65a45c/DesktopVRIK/DesktopVRIKCalibrator.cs#L219C2-L247C103 + + vrik.solver.spine.bodyPosStiffness = 1f; + vrik.solver.spine.bodyRotStiffness = 0.2f; + vrik.solver.spine.neckStiffness = 0.0001f; + vrik.solver.spine.rotateChestByHands = 0f; + + vrik.solver.spine.minHeadHeight = 0f; + vrik.solver.locomotion.angleThreshold = 30f; + vrik.solver.locomotion.maxLegStretch = 1f; + + vrik.solver.spine.chestClampWeight = 0f; + vrik.solver.spine.headClampWeight = 0.2f; + + vrik.solver.spine.maintainPelvisPosition = 0f; + vrik.solver.spine.moveBodyBackWhenCrouching = 0f; + + vrik.solver.locomotion.velocityFactor = 0f; + vrik.solver.locomotion.maxVelocity = 0f; + vrik.solver.locomotion.rootSpeed = 1000f; + + vrik.solver.spine.positionWeight = 0f; + vrik.solver.spine.rotationWeight = 1f; + + vrik.solver.spine.maxRootAngle = 180f; + + vrik.solver.plantFeet = true; + } + + public static void ConfigureHalfBodyVrIk(VRIK vrik) + { + // From IKTweaks + // https://github.com/knah/VRCMods/blob/a22bb73a5e40c75152c6e5db2a7a9afb13e42ba5/IKTweaks/FullBodyHandling.cs#L384C1-L394C71 + + vrik.solver.spine.bodyPosStiffness = 1f; + vrik.solver.spine.bodyRotStiffness = 0f; + vrik.solver.spine.neckStiffness = 0.5f; + vrik.solver.spine.rotateChestByHands = .25f; + + vrik.solver.spine.minHeadHeight = -100f; + vrik.solver.locomotion.angleThreshold = 60f; + vrik.solver.locomotion.maxLegStretch = 1f; + + vrik.solver.spine.chestClampWeight = 0f; + vrik.solver.spine.headClampWeight = 0f; + + vrik.solver.spine.maintainPelvisPosition = 0f; + vrik.solver.spine.moveBodyBackWhenCrouching = 0f; + + vrik.solver.locomotion.velocityFactor = 0.4f; + vrik.solver.locomotion.maxVelocity = 0.4f; + vrik.solver.locomotion.rootSpeed = 20f; + + vrik.solver.spine.positionWeight = 1f; + vrik.solver.spine.rotationWeight = 1f; + + vrik.solver.spine.maxRootAngle = 25f; + + vrik.solver.plantFeet = false; + } + + #endregion + + #region VRIK Calibration + + public static void SetupHeadIKTarget(VRIK vrik, Transform parent = null) + { + Transform existingTarget = parent?.Find("Head IK Target"); + if (existingTarget != null) + Object.DestroyImmediate(existingTarget.gameObject); + + parent ??= vrik.references.head; + + vrik.solver.spine.headTarget = new GameObject("Head IK Target").transform; + vrik.solver.spine.headTarget.SetParent(parent); + vrik.solver.spine.headTarget.localPosition = Vector3.zero; + vrik.solver.spine.headTarget.localRotation = CalculateLocalRotation(vrik.references.root, vrik.references.head); + } + + public static void SetupHandIKTarget(VRIK vrik, Transform handAnchor, bool isLeft) + { + Transform parent = handAnchor.parent; + Transform handRef = isLeft ? vrik.references.leftHand : vrik.references.rightHand; + + handAnchor.SetParent(parent); + handAnchor.localPosition = Vector3.zero; + handAnchor.localRotation = CalculateLocalRotation(vrik.references.root, handRef); + + if (isLeft) + vrik.solver.leftArm.target = handAnchor; + else + vrik.solver.rightArm.target = handAnchor; + } + + #endregion + + #region Private Methods + + private static Quaternion CalculateLocalRotation(Transform root, Transform reference) + { + Vector3 forward = Quaternion.Inverse(reference.rotation) * root.forward; + Vector3 upwards = Quaternion.Inverse(reference.rotation) * root.up; + return Quaternion.Inverse(reference.rotation * Quaternion.LookRotation(forward, upwards)) * reference.rotation; + } + + #endregion +} \ No newline at end of file diff --git a/DesktopVRIK/IK/IKHandlers/IKHandler.cs b/DesktopVRIK/IK/IKHandlers/IKHandler.cs new file mode 100644 index 0000000..8435a43 --- /dev/null +++ b/DesktopVRIK/IK/IKHandlers/IKHandler.cs @@ -0,0 +1,169 @@ +using ABI.CCK.Components; +using ABI_RC.Systems.IK.SubSystems; +using NAK.DesktopVRIK.IK.VRIKHelpers; +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.DesktopVRIK.IK.IKHandlers; + +internal abstract class IKHandler +{ + #region Variables + + internal VRIK _vrik; + internal IKSolverVR _solver; + + // VRIK Calibration Info + internal VRIKLocomotionData _locomotionData; + + // Last Movement Parent Info + internal Vector3 _movementPosition; + internal Quaternion _movementRotation; + internal CVRMovementParent _movementParent; + + // Solver Info + internal float _scaleDifference = 1f; + internal float _ikWeight = 1f; + internal float _locomotionWeight = 1f; + internal float _ikSimulatedRootAngle; + internal bool _wasTrackingLocomotion; + + #endregion + + #region Virtual Game Methods + + public virtual void OnInitializeIk() { } + + public virtual void OnPlayerScaled(float scaleDifference) + { + VRIKUtils.ApplyScaleToVRIK + ( + _vrik, + _locomotionData, + _scaleDifference = scaleDifference + ); + } + + public virtual void OnPlayerHandleMovementParent(CVRMovementParent currentParent, Vector3 platformPivot) + { + Vector3 currentPosition = currentParent._referencePoint.position; + Quaternion currentRotation = Quaternion.Euler(0f, currentParent.transform.rotation.eulerAngles.y, 0f); + + Vector3 deltaPosition = currentPosition - _movementPosition; + Quaternion deltaRotation = Quaternion.Inverse(_movementRotation) * currentRotation; + + if (_movementParent == currentParent) + { + _solver.AddPlatformMotion(deltaPosition, deltaRotation, platformPivot); + _ikSimulatedRootAngle = Mathf.Repeat(_ikSimulatedRootAngle + deltaRotation.eulerAngles.y, 360f); + } + + _movementParent = currentParent; + _movementPosition = currentPosition; + _movementRotation = currentRotation; + } + + #endregion + + #region Virtual IK Weights + + public virtual void UpdateWeights() + { + Update_HeadWeight(); + + Update_LeftArmWeight(); + Update_RightArmWeight(); + + Update_LeftLegWeight(); + Update_RightLegWeight(); + + Update_PelvisWeight(); + + Update_LocomotionWeight(); + ResetSolverIfNeeded(); + + Update_IKPositionWeight(); + } + + protected virtual void Update_HeadWeight() + { + // There is no Head tracking setting + _solver.spine.rotationWeight = _solver.leftArm.positionWeight = + GetTargetWeight(BodySystem.TrackingEnabled, _solver.spine.headTarget != null); + } + + protected virtual void Update_LeftArmWeight() + { + _solver.leftArm.rotationWeight = _solver.leftArm.positionWeight = + GetTargetWeight(BodySystem.TrackingLeftArmEnabled, _solver.leftArm.target != null); + } + + protected virtual void Update_RightArmWeight() + { + _solver.rightArm.rotationWeight = _solver.rightArm.positionWeight = + GetTargetWeight(BodySystem.TrackingRightArmEnabled, _solver.rightArm.target != null); + } + + protected virtual void Update_LeftLegWeight() + { + _solver.leftLeg.rotationWeight = _solver.leftLeg.positionWeight = + GetTargetWeight(BodySystem.TrackingLeftLegEnabled, _solver.leftLeg.target != null); + } + + protected virtual void Update_RightLegWeight() + { + _solver.rightLeg.rotationWeight = _solver.rightLeg.positionWeight = + GetTargetWeight(BodySystem.TrackingRightLegEnabled, _solver.rightLeg.target != null); + } + + protected virtual void Update_PelvisWeight() + { + // There is no Pelvis tracking setting + _solver.spine.pelvisRotationWeight = _solver.spine.pelvisPositionWeight = + GetTargetWeight(BodySystem.TrackingEnabled, _solver.spine.pelvisTarget != null); + } + + protected virtual void Update_LocomotionWeight() + { + _solver.locomotion.weight = _locomotionWeight = Mathf.Lerp(_locomotionWeight, BodySystem.TrackingLocomotionEnabled ? 1f : 0f, + Time.deltaTime * ModSettings.EntryIKLerpSpeed.Value * 2f); + } + + protected virtual void Update_IKPositionWeight() + { + _solver.IKPositionWeight = _ikWeight = Mathf.Lerp(_ikWeight, BodySystem.TrackingEnabled ? BodySystem.TrackingPositionWeight : 0f, + Time.deltaTime * ModSettings.EntryIKLerpSpeed.Value); + } + + public virtual void Reset() + { + _ikSimulatedRootAngle = _vrik.transform.eulerAngles.y; + if(ModSettings.EntryResetFootstepsOnIdle.Value) + VRIKUtils.ResetToInitialFootsteps(_vrik, _locomotionData, _scaleDifference); + + _solver.Reset(); + } + + #endregion + + #region Private Methods + + private float GetTargetWeight(bool isTracking, bool hasTarget) + { + return isTracking && hasTarget ? 1f : 0f; + } + + private void ResetSolverIfNeeded() + { + if (_wasTrackingLocomotion == BodySystem.TrackingLocomotionEnabled) + return; + + _wasTrackingLocomotion = BodySystem.TrackingLocomotionEnabled; + if (ModSettings.EntryResetFootstepsOnIdle.Value) + VRIKUtils.ResetToInitialFootsteps(_vrik, _locomotionData, _scaleDifference); + + _solver.Reset(); + } + + #endregion +} \ No newline at end of file diff --git a/DesktopVRIK/IK/IKHandlers/IKHandlerDesktop.cs b/DesktopVRIK/IK/IKHandlers/IKHandlerDesktop.cs new file mode 100644 index 0000000..09ee809 --- /dev/null +++ b/DesktopVRIK/IK/IKHandlers/IKHandlerDesktop.cs @@ -0,0 +1,115 @@ +using ABI_RC.Core.Player; +using ABI_RC.Systems.IK.SubSystems; +using ABI_RC.Systems.MovementSystem; +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.DesktopVRIK.IK.IKHandlers; + +internal class IKHandlerDesktop : IKHandler +{ + public IKHandlerDesktop(VRIK vrik) + { + _vrik = vrik; + _solver = vrik.solver; + } + + #region Game Overrides + + public override void OnInitializeIk() + { + _vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateDesktop); + } + + #endregion + + #region Weight Overrides + + public override void UpdateWeights() + { + // Reset avatar local position + _vrik.transform.localPosition = Vector3.zero; + _vrik.transform.localRotation = Quaternion.identity; + + UpdateBodySystemTracking(); + + base.UpdateWeights(); + } + + #endregion + + #region VRIK Solver Events + + private void OnPreSolverUpdateDesktop() + { + _solver.plantFeet = ModSettings.EntryPlantFeet.Value; + + // Emulate old VRChat hip movement + if (ModSettings.EntryBodyLeanWeight.Value > 0) + { + float weightedAngle = ModSettings.EntryProneThrusting.Value ? 1f : ModSettings.EntryBodyLeanWeight.Value * _solver.locomotion.weight; + float angle = IKManager.Instance._desktopCamera.localEulerAngles.x; + angle = angle > 180 ? angle - 360 : angle; + Quaternion rotation = Quaternion.AngleAxis(angle * weightedAngle, _vrik.transform.right); + _solver.spine.headRotationOffset *= rotation; + } + + // Make root heading follow within a set limit + if (ModSettings.EntryBodyHeadingLimit.Value > 0) + { + float weightedAngleLimit = ModSettings.EntryBodyHeadingLimit.Value * _solver.locomotion.weight; + float deltaAngleRoot = Mathf.DeltaAngle(IKManager.Instance.transform.eulerAngles.y, _ikSimulatedRootAngle); + float absDeltaAngleRoot = Mathf.Abs(deltaAngleRoot); + + if (absDeltaAngleRoot > weightedAngleLimit) + { + deltaAngleRoot = Mathf.Sign(deltaAngleRoot) * weightedAngleLimit; + _ikSimulatedRootAngle = Mathf.MoveTowardsAngle(_ikSimulatedRootAngle, IKManager.Instance.transform.eulerAngles.y, absDeltaAngleRoot - weightedAngleLimit); + } + + _solver.spine.rootHeadingOffset = deltaAngleRoot; + + if (ModSettings.EntryPelvisHeadingWeight.Value > 0) + { + _solver.spine.pelvisRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * ModSettings.EntryPelvisHeadingWeight.Value, 0f); + _solver.spine.chestRotationOffset *= Quaternion.Euler(0f, -deltaAngleRoot * ModSettings.EntryPelvisHeadingWeight.Value, 0f); + } + + if (ModSettings.EntryChestHeadingWeight.Value > 0) + { + _solver.spine.chestRotationOffset *= Quaternion.Euler(0f, deltaAngleRoot * ModSettings.EntryChestHeadingWeight.Value, 0f); + } + } + } + + #endregion + + #region Private Methods + + private void UpdateBodySystemTracking() + { + BodySystem.TrackingEnabled = ShouldTrackAll(); + BodySystem.TrackingLocomotionEnabled = ShouldTrackLocomotion(); + } + + private bool ShouldTrackAll() + { + return !PlayerSetup.Instance._emotePlaying; + } + + private bool ShouldTrackLocomotion() + { + bool isMoving = MovementSystem.Instance.movementVector.magnitude > 0f; + bool isGrounded = MovementSystem.Instance._isGrounded; + bool isCrouching = MovementSystem.Instance.crouching; + bool isProne = MovementSystem.Instance.prone; + bool isFlying = MovementSystem.Instance.flying; + bool isSitting = MovementSystem.Instance.sitting; + bool isStanding = PlayerSetup.Instance.avatarUpright >= + Mathf.Max(PlayerSetup.Instance.avatarProneLimit, PlayerSetup.Instance.avatarCrouchLimit); + + return !(isMoving || isCrouching || isProne || isFlying || isSitting || !isGrounded || !isStanding); + } + + #endregion +} \ No newline at end of file diff --git a/DesktopVRIK/IK/IKManager.cs b/DesktopVRIK/IK/IKManager.cs new file mode 100644 index 0000000..f37e87d --- /dev/null +++ b/DesktopVRIK/IK/IKManager.cs @@ -0,0 +1,413 @@ +using ABI.CCK.Components; +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using NAK.DesktopVRIK.IK.IKHandlers; +using NAK.DesktopVRIK.IK.VRIKHelpers; +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.DesktopVRIK.IK; + +public class IKManager : MonoBehaviour +{ + public static IKManager Instance; + + #region Variables + + private static VRIK _vrik; + private static IKSolverVR _solver; + + private bool _isAvatarInitialized; + + // IK Handling + private IKHandler _ikHandler; + + // Player Info + internal Transform _desktopCamera; + internal Transform _vrCamera; + + // Avatar Info + private Animator _animator; + private Transform _hipTransform; + + // Animator Info + private int _animLocomotionLayer = -1; + private int _animIKPoseLayer = -1; + private const string _locomotionLayerName = "Locomotion/Emotes"; + private const string _ikposeLayerName = "IKPose"; + + // Pose Info + private HumanPoseHandler _humanPoseHandler; + private HumanPose _humanPose; + private HumanPose _humanPoseInitial; + + #endregion + + #region Unity Methods + + private void Start() + { + if (Instance != null) + { + Destroy(this); + return; + } + Instance = this; + + _desktopCamera = PlayerSetup.Instance.desktopCamera.transform; + _vrCamera = PlayerSetup.Instance.vrCamera.transform; + } + + private void Update() + { + if (!_isAvatarInitialized) + return; + + _ikHandler?.UpdateWeights(); + } + + #endregion + + #region Avatar Events + + public void OnAvatarInitialized(GameObject inAvatar) + { + if (MetaPort.Instance.isUsingVr) + return; + + if (_isAvatarInitialized) + return; + + if (!inAvatar.TryGetComponent(out _animator)) + return; + + if (_animator.avatar == null || !_animator.avatar.isHuman) + return; + + _animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; + + _animIKPoseLayer = _animator.GetLayerIndex(_ikposeLayerName); + _animLocomotionLayer = _animator.GetLayerIndex(_locomotionLayerName); + + _hipTransform = _animator.GetBoneTransform(HumanBodyBones.Hips); + + _humanPoseHandler?.Dispose(); + _humanPoseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); + + _humanPoseHandler.GetHumanPose(ref _humanPose); + _humanPoseHandler.GetHumanPose(ref _humanPoseInitial); + + InitializeDesktopIk(); + + _isAvatarInitialized = true; + } + + public void OnAvatarDestroyed() + { + if (!_isAvatarInitialized) + return; + + _vrik = null; + _solver = null; + _animator = null; + _animIKPoseLayer = -1; + _animLocomotionLayer = -1; + _hipTransform = null; + _humanPoseHandler?.Dispose(); + _humanPoseHandler = null; + _ikHandler = null; + + _isAvatarInitialized = false; + } + + #endregion + + #region Game Events + + public bool OnPlayerScaled(float scaleDifference) + { + if (!_isAvatarInitialized) + return false; + + _ikHandler?.OnPlayerScaled(scaleDifference); + return true; + } + + public void OnPlayerSeatedStateChanged(bool isSitting) + { + if (!_isAvatarInitialized) + return; + + _ikHandler?.Reset(); + } + + public bool OnPlayerHandleMovementParent(CVRMovementParent movementParent) + { + if (!_isAvatarInitialized) + return false; + + _ikHandler?.OnPlayerHandleMovementParent(movementParent, GetPlayerPosition()); + return true; + } + + public bool OnPlayerTeleported() + { + if (!_isAvatarInitialized) + return false; + + _ikHandler?.Reset(); + return true; + } + + #endregion + + #region IK Initialization + + private void InitializeDesktopIk() + { + SetupIkGeneral(); + + IKCalibrator.ConfigureDesktopVrIk(_vrik); + _ikHandler = new IKHandlerDesktop(_vrik); + + IKCalibrator.SetupHeadIKTarget(_vrik); + + InitializeIkGeneral(); + + _ikHandler.OnInitializeIk(); + } + + private void SetupIkGeneral() + { + _animator.transform.position = GetPlayerPosition(); + _animator.transform.rotation = GetPlayerRotation(); + SetAvatarPose(AvatarPose.Default); + _vrik = IKCalibrator.SetupVrIk(_animator); + _solver = _vrik.solver; + } + + private void InitializeIkGeneral() + { + SetAvatarPose(AvatarPose.IKPose); + + VRIKUtils.CalculateInitialIKScaling(_vrik, ref _ikHandler._locomotionData); + VRIKUtils.CalculateInitialFootsteps(_vrik, ref _ikHandler._locomotionData); + _solver.Initiate(_vrik.transform); // initiate a second time + + SetAvatarPose(AvatarPose.Initial); + + VRIKUtils.ApplyScaleToVRIK(_vrik, _ikHandler._locomotionData, 1f); + _vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateGeneral); + _vrik.onPostSolverUpdate.AddListener(OnPostSolverUpdateGeneral); + } + + #endregion + + #region Public Methods + + public Vector3 GetPlayerPosition() + { + if (!MetaPort.Instance.isUsingVr) + return transform.position; + + Vector3 vrPosition = _vrCamera.transform.position; + vrPosition.y = transform.position.y; + return vrPosition; + } + + public Quaternion GetPlayerRotation() + { + if (!MetaPort.Instance.isUsingVr) + return transform.rotation; + + Vector3 vrForward = _vrCamera.transform.forward; + vrForward.y = 0f; + return Quaternion.LookRotation(vrForward, Vector3.up); + } + + #endregion + + #region VRIK Solver Events General + + private void OnPreSolverUpdateGeneral() + { + if (_solver.IKPositionWeight < 0.9f) + return; + + Vector3 hipPos = _hipTransform.position; + Quaternion hipRot = _hipTransform.rotation; + + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < _humanPose.muscles.Length; i++) + { + //if (IkTweaksSettings.IgnoreAnimationsModeParsed == IgnoreAnimationsMode.All && IKTweaksMod.ourRandomPuck.activeInHierarchy) + //{ + // muscles[i] *= ourBoneResetMasks[i] == BoneResetMask.Never ? 1 : 0; + // continue; + //} + switch (ourBoneResetMasks[i]) + { + case BoneResetMask.Never: + break; + case BoneResetMask.Spine: + _humanPose.muscles[i] *= 1 - _solver.spine.pelvisPositionWeight; + break; + case BoneResetMask.LeftArm: + _humanPose.muscles[i] *= 1 - _solver.leftArm.positionWeight; + break; + case BoneResetMask.RightArm: + _humanPose.muscles[i] *= 1 - _solver.rightArm.positionWeight; + break; + case BoneResetMask.LeftLeg: + _humanPose.muscles[i] *= 1 - _solver.leftLeg.positionWeight; + break; + case BoneResetMask.RightLeg: + _humanPose.muscles[i] *= 1 - _solver.rightLeg.positionWeight; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + _humanPoseHandler.SetHumanPose(ref _humanPose); + + _hipTransform.position = hipPos; + _hipTransform.rotation = hipRot; + } + + // "NetIk Pass", or "Additional Humanoid Pass" hack + private void OnPostSolverUpdateGeneral() + { + Vector3 hipPos = _hipTransform.position; + Quaternion hipRot = _hipTransform.rotation; + + _humanPoseHandler.GetHumanPose(ref _humanPose); + _humanPoseHandler.SetHumanPose(ref _humanPose); + + _hipTransform.position = hipPos; + _hipTransform.rotation = hipRot; + } + + #endregion + + #region Avatar Pose Utilities + + private enum AvatarPose + { + Default = 0, + Initial = 1, + IKPose = 2, + TPose = 3, + APose = 4 + } + + private void SetAvatarPose(AvatarPose pose) + { + switch (pose) + { + case AvatarPose.Default: + SetMusclesToValue(0f); + break; + case AvatarPose.Initial: + if (HasCustomIKPose()) + SetCustomLayersWeights(0f, 1f); + _humanPoseHandler.SetHumanPose(ref _humanPoseInitial); + break; + case AvatarPose.IKPose: + if (HasCustomIKPose()) + { + SetCustomLayersWeights(1f, 0f); + return; + } + SetMusclesToPose(MusclePoses.IKPoseMuscles); + break; + case AvatarPose.TPose: + SetMusclesToPose(MusclePoses.TPoseMuscles); + break; + case AvatarPose.APose: + SetMusclesToPose(MusclePoses.APoseMuscles); + break; + } + } + + private bool HasCustomIKPose() + { + return _animLocomotionLayer != -1 && _animIKPoseLayer != -1; + } + + private void SetCustomLayersWeights(float customIKPoseLayerWeight, float locomotionLayerWeight) + { + _animator.SetLayerWeight(_animIKPoseLayer, customIKPoseLayerWeight); + _animator.SetLayerWeight(_animLocomotionLayer, locomotionLayerWeight); + _animator.Update(0f); + } + + private void SetMusclesToValue(float value) + { + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < ourBoneResetMasks.Length; i++) + { + if (ourBoneResetMasks[i] != BoneResetMask.Never) + _humanPose.muscles[i] = value; + } + + _humanPose.bodyPosition = Vector3.up; + _humanPose.bodyRotation = Quaternion.identity; + _humanPoseHandler.SetHumanPose(ref _humanPose); + } + + private void SetMusclesToPose(float[] muscles) + { + _humanPoseHandler.GetHumanPose(ref _humanPose); + + for (var i = 0; i < ourBoneResetMasks.Length; i++) + { + if (ourBoneResetMasks[i] != BoneResetMask.Never) + _humanPose.muscles[i] = muscles[i]; + } + + _humanPose.bodyPosition = Vector3.up; + _humanPose.bodyRotation = Quaternion.identity; + _humanPoseHandler.SetHumanPose(ref _humanPose); + } + + #endregion + + #region BodyHandling + + public enum BoneResetMask + { + Never, + Spine, + LeftArm, + RightArm, + LeftLeg, + RightLeg, + } + + private static readonly string[] ourNeverBones = { "Index", "Thumb", "Middle", "Ring", "Little", "Jaw", "Eye" }; + private static readonly string[] ourArmBones = { "Arm", "Forearm", "Hand", "Shoulder" }; + private static readonly string[] ourLegBones = { "Leg", "Foot", "Toes" }; + + private static BoneResetMask JudgeBone(string name) + { + if (ourNeverBones.Any(name.Contains)) + return BoneResetMask.Never; + + if (ourArmBones.Any(name.Contains)) + { + return name.Contains("Left") ? BoneResetMask.LeftArm : BoneResetMask.RightArm; + } + + if (ourLegBones.Any(name.Contains)) + return name.Contains("Left") ? BoneResetMask.LeftLeg : BoneResetMask.RightLeg; + + return BoneResetMask.Spine; + } + + internal static readonly BoneResetMask[] ourBoneResetMasks = HumanTrait.MuscleName.Select(JudgeBone).ToArray(); + + #endregion +} \ No newline at end of file diff --git a/DesktopVRIK/IK/MusclePoses.cs b/DesktopVRIK/IK/MusclePoses.cs new file mode 100644 index 0000000..50ae8fe --- /dev/null +++ b/DesktopVRIK/IK/MusclePoses.cs @@ -0,0 +1,46 @@ +namespace NAK.DesktopVRIK.IK; + +public static class MusclePoses +{ + public static readonly float[] TPoseMuscles = + { + 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0.6001086f, 8.6213E-05f, + -0.0003308152f, 0.9999163f, -9.559652E-06f, 3.41413E-08f, -3.415095E-06f, -1.024528E-07f, 0.6001086f, + 8.602679E-05f, -0.0003311098f, 0.9999163f, -9.510122E-06f, 1.707468E-07f, -2.732077E-06f, 2.035554E-15f, + -2.748694E-07f, 2.619475E-07f, 0.401967f, 0.3005583f, 0.04102772f, 0.9998822f, -0.04634236f, 0.002522987f, + 0.0003842837f, -2.369134E-07f, -2.232262E-07f, 0.4019674f, 0.3005582f, 0.04103433f, 0.9998825f, + -0.04634996f, 0.00252335f, 0.000383302f, -1.52127f, 0.2634507f, 0.4322457f, 0.6443988f, 0.6669409f, + -0.4663372f, 0.8116828f, 0.8116829f, 0.6678119f, -0.6186608f, 0.8116842f, 0.8116842f, 0.6677991f, + -0.619225f, 0.8116842f, 0.811684f, 0.6670032f, -0.465875f, 0.811684f, 0.8116836f, -1.520098f, 0.2613016f, + 0.432256f, 0.6444503f, 0.6668426f, -0.4670413f, 0.8116828f, 0.8116828f, 0.6677986f, -0.6192409f, 0.8116841f, + 0.811684f, 0.6677839f, -0.6198869f, 0.8116839f, 0.8116838f, 0.6668782f, -0.4667901f, 0.8116842f, 0.811684f + }; + + public static readonly float[] APoseMuscles = + { + 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0.6001087f, 0f, + -0.0003306383f, 0.9999163f, 0f, 0f, 0f, 0f, 0.6001087f, 0f, -0.0003306384f, 0.9999163f, 0f, 0f, 0f, 0f, 0f, + 0f, -0.1071228f, 0.258636f, 0.1567371f, 0.9998825f, -0.0463457f, 0.002523606f, 0.0003833446f, 0f, 0f, + -0.1036742f, 0.2589961f, 0.1562322f, 0.9998825f, -0.04634446f, 0.002522176f, 0.0003835156f, -1.52127f, + 0.2634749f, 0.4322476f, 0.6443989f, 0.6669405f, -0.4663376f, 0.8116828f, 0.8116829f, 0.6678116f, + -0.6186616f, 0.8116839f, 0.8116837f, 0.6677991f, -0.6192248f, 0.8116839f, 0.8116842f, 0.6670038f, + -0.4658763f, 0.8116841f, 0.811684f, -1.520108f, 0.2612858f, 0.4322585f, 0.6444519f, 0.6668428f, -0.4670413f, + 0.8116831f, 0.8116828f, 0.6677985f, -0.6192364f, 0.8116842f, 0.8116842f, 0.667784f, -0.6198866f, 0.8116841f, + 0.8116835f, 0.6668782f, -0.4667891f, 0.8116841f, 0.811684f + }; + + public static readonly float[] IKPoseMuscles = + { + 0.00133321f, 8.195831E-06f, 8.537738E-07f, -0.002669832f, -7.651234E-06f, -0.001659694f, 0f, 0f, 0f, + 0.04213953f, 0.0003007996f, -0.008032114f, -0.03059979f, -0.0003182998f, 0.009640567f, 0f, 0f, 0f, 0f, 0f, + 0f, 0.5768794f, 0.01061097f, -0.1127839f, 0.9705755f, 0.07972051f, -0.0268422f, 0.007237188f, 0f, + 0.5768792f, 0.01056608f, -0.1127519f, 0.9705756f, 0.07971933f, -0.02682396f, 0.007229362f, 0f, + -5.651802E-06f, -3.034899E-07f, 0.4100508f, 0.3610304f, -0.0838329f, 0.9262537f, 0.1353517f, -0.03578902f, + 0.06005657f, -4.95989E-06f, -1.43007E-06f, 0.4096187f, 0.363263f, -0.08205152f, 0.9250782f, 0.1345718f, + -0.03572125f, 0.06055461f, -1.079177f, 0.2095419f, 0.6140652f, 0.6365265f, 0.6683931f, -0.4764312f, + 0.8099416f, 0.8099371f, 0.6658203f, -0.7327053f, 0.8113618f, 0.8114051f, 0.6643661f, -0.40341f, 0.8111364f, + 0.8111367f, 0.6170399f, -0.2524227f, 0.8138723f, 0.8110135f, -1.079171f, 0.2095456f, 0.6140658f, 0.6365255f, + 0.6683878f, -0.4764301f, 0.8099402f, 0.8099376f, 0.6658241f, -0.7327023f, 0.8113653f, 0.8113793f, 0.664364f, + -0.4034042f, 0.811136f, 0.8111364f, 0.6170469f, -0.2524345f, 0.8138595f, 0.8110138f + }; +} \ No newline at end of file diff --git a/DesktopVRIK/VRIKHelpers/VRIKCalibrationData.cs b/DesktopVRIK/IK/VRIKHelpers/VRIKLocomotionData.cs similarity index 63% rename from DesktopVRIK/VRIKHelpers/VRIKCalibrationData.cs rename to DesktopVRIK/IK/VRIKHelpers/VRIKLocomotionData.cs index f06ae46..95ed9fd 100644 --- a/DesktopVRIK/VRIKHelpers/VRIKCalibrationData.cs +++ b/DesktopVRIK/IK/VRIKHelpers/VRIKLocomotionData.cs @@ -1,33 +1,25 @@ using UnityEngine; -namespace NAK.DesktopVRIK.VRIKHelper; +namespace NAK.DesktopVRIK.IK.VRIKHelpers; -public struct VRIKCalibrationData +public struct VRIKLocomotionData { - public Vector3 KneeNormalLeft; - public Vector3 KneeNormalRight; public Vector3 InitialFootPosLeft; public Vector3 InitialFootPosRight; public Quaternion InitialFootRotLeft; public Quaternion InitialFootRotRight; - public float InitialHeadHeight; public float InitialFootDistance; public float InitialStepThreshold; public float InitialStepHeight; - public bool FixTransformsRequired; public void Clear() { - KneeNormalLeft = Vector3.zero; - KneeNormalRight = Vector3.zero; InitialFootPosLeft = Vector3.zero; InitialFootPosRight = Vector3.zero; InitialFootRotLeft = Quaternion.identity; InitialFootRotRight = Quaternion.identity; - InitialHeadHeight = 0f; InitialFootDistance = 0f; InitialStepThreshold = 0f; InitialStepHeight = 0f; - FixTransformsRequired = false; } } \ No newline at end of file diff --git a/DesktopVRIK/IK/VRIKHelpers/VRIKUtils.cs b/DesktopVRIK/IK/VRIKHelpers/VRIKUtils.cs new file mode 100644 index 0000000..81dc29d --- /dev/null +++ b/DesktopVRIK/IK/VRIKHelpers/VRIKUtils.cs @@ -0,0 +1,62 @@ +using RootMotion.FinalIK; +using UnityEngine; + +namespace NAK.DesktopVRIK.IK.VRIKHelpers; + +public static class VRIKUtils +{ + public static void CalculateInitialIKScaling(VRIK vrik, ref VRIKLocomotionData locomotionData) + { + // 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); + + locomotionData.InitialFootDistance = footDistance * 0.5f; + locomotionData.InitialStepThreshold = footDistance * scaleModifier; + locomotionData.InitialStepHeight = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.leftCalf.position) * 0.2f; + } + + public static void CalculateInitialFootsteps(VRIK vrik, ref VRIKLocomotionData locomotionData) + { + 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 + locomotionData.InitialFootPosLeft = root.InverseTransformPoint(leftFoot.position); + locomotionData.InitialFootPosRight = root.InverseTransformPoint(rightFoot.position); + locomotionData.InitialFootRotLeft = Quaternion.Inverse(rootWorldRot) * leftFoot.rotation; + locomotionData.InitialFootRotRight = Quaternion.Inverse(rootWorldRot) * rightFoot.rotation; + } + + public static void ResetToInitialFootsteps(VRIK vrik, VRIKLocomotionData locomotionData, float scaleModifier) + { + Transform root = vrik.references.root; + Quaternion rootWorldRot = vrik.references.root.rotation; + + // hack, use parent transform instead as setting feet position moves root (root.parent), but does not work for VR + var footsteps = vrik.solver.locomotion.footsteps; + footsteps[0].Reset(rootWorldRot, root.TransformPoint(locomotionData.InitialFootPosLeft * scaleModifier), + rootWorldRot * locomotionData.InitialFootRotLeft); + footsteps[1].Reset(rootWorldRot, root.TransformPoint(locomotionData.InitialFootPosRight * scaleModifier), + rootWorldRot * locomotionData.InitialFootRotRight); + } + + public static void ApplyScaleToVRIK(VRIK vrik, VRIKLocomotionData locomotionData, float scaleModifier) + { + IKSolverVR.Locomotion locomotionSolver = vrik.solver.locomotion; + locomotionSolver.footDistance = locomotionData.InitialFootDistance * scaleModifier; + locomotionSolver.stepThreshold = locomotionData.InitialStepThreshold * scaleModifier; + ScaleStepHeight(locomotionSolver.stepHeight, locomotionData.InitialStepHeight * scaleModifier); + } + + private static void ScaleStepHeight(AnimationCurve stepHeightCurve, float mag) + { + var keyframes = stepHeightCurve.keys; + keyframes[1].value = mag; + stepHeightCurve.keys = keyframes; + } +} \ No newline at end of file diff --git a/DesktopVRIK/Integrations/AMTAddon.cs b/DesktopVRIK/Integrations/AMTAddon.cs new file mode 100644 index 0000000..e4e0f6b --- /dev/null +++ b/DesktopVRIK/Integrations/AMTAddon.cs @@ -0,0 +1,21 @@ + +namespace NAK.DesktopVRIK.Integrations; + +public static class AMTAddon +{ + #region Variables + + public static bool integration_AMT = false; + + #endregion + + #region Initialization + + public static void Initialize() + { + integration_AMT = true; + DesktopVRIK.Logger.Msg("AvatarMotionTweaker was found. Relying on it to handle VRIK locomotion."); + } + + #endregion +} \ No newline at end of file diff --git a/DesktopVRIK/Integrations/BTKUIAddon.cs b/DesktopVRIK/Integrations/BTKUIAddon.cs index d2e0b45..e3a6c4d 100644 --- a/DesktopVRIK/Integrations/BTKUIAddon.cs +++ b/DesktopVRIK/Integrations/BTKUIAddon.cs @@ -6,45 +6,58 @@ namespace NAK.DesktopVRIK.Integrations; public static class BTKUIAddon { + #region Initialization + [MethodImpl(MethodImplOptions.NoInlining)] - public static void Init() + public static void Initialize() { - //Add myself to the Misc Menu - + // Add mod to the Misc Menu Page miscPage = QuickMenuAPI.MiscTabPage; - Category miscCategory = miscPage.AddCategory(DesktopVRIK.SettingsCategory); + Category miscCategory = miscPage.AddCategory(ModSettings.SettingsCategory); - AddMelonToggle(ref miscCategory, DesktopVRIK.EntryEnabled); + AddMelonToggle(ref miscCategory, ModSettings.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(DesktopVRIK.SettingsCategory); + SetupDesktopIKConfigurationPage(ref miscCategory); + } + + #endregion + + #region Pages Setup + + private static void SetupDesktopIKConfigurationPage(ref Category parentCategory) + { + Page desktopIKPage = parentCategory.AddPage("DesktopVRIK Settings", "", "Configure the settings for DesktopVRIK.", ModSettings.SettingsCategory); + desktopIKPage.MenuTitle = "DesktopVRIK Settings"; + Category desktopIKCategory = desktopIKPage.AddCategory(desktopIKPage.MenuTitle); // General Settings - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIK.EntryPlantFeet); + AddMelonToggle(ref desktopIKCategory, ModSettings.EntryPlantFeet); // Calibration Settings - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIK.EntryUseVRIKToes); + AddMelonToggle(ref desktopIKCategory, ModSettings.EntryUseToesForVRIK); // Fine-tuning Settings - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIK.EntryResetFootstepsOnIdle); + AddMelonToggle(ref desktopIKCategory, ModSettings.EntryResetFootstepsOnIdle); // Funny Settings - AddMelonToggle(ref desktopVRIKCategory, DesktopVRIK.EntryProneThrusting); + AddMelonToggle(ref desktopIKCategory, ModSettings.EntryProneThrusting); // Body Leaning Weight - AddMelonSlider(ref desktopVRIKPage, DesktopVRIK.EntryBodyLeanWeight, 0, 1f, 1); + AddMelonSlider(ref desktopIKPage, ModSettings.EntryBodyLeanWeight, 0, 1f, 1); // Max Root Heading Limit & Weights - AddMelonSlider(ref desktopVRIKPage, DesktopVRIK.EntryBodyHeadingLimit, 0, 90f, 0); - AddMelonSlider(ref desktopVRIKPage, DesktopVRIK.EntryPelvisHeadingWeight, 0, 1f, 1); - AddMelonSlider(ref desktopVRIKPage, DesktopVRIK.EntryChestHeadingWeight, 0, 1f, 1); + AddMelonSlider(ref desktopIKPage, ModSettings.EntryBodyHeadingLimit, 0, 90f, 0); + AddMelonSlider(ref desktopIKPage, ModSettings.EntryPelvisHeadingWeight, 0, 1f, 1); + AddMelonSlider(ref desktopIKPage, ModSettings.EntryChestHeadingWeight, 0, 1f, 1); // Lerp Speed - AddMelonSlider(ref desktopVRIKPage, DesktopVRIK.EntryIKLerpSpeed, 0, 20f, 0); + AddMelonSlider(ref desktopIKPage, ModSettings.EntryIKLerpSpeed, 0, 20f, 0); } + #endregion + + #region Melon Pref Helpers + private static void AddMelonToggle(ref Category category, MelonLoader.MelonPreferences_Entry entry) { category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b; @@ -54,4 +67,6 @@ public static class BTKUIAddon { page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max, decimalPlaces).OnValueUpdated += f => entry.Value = f; } + + #endregion } \ No newline at end of file diff --git a/DesktopVRIK/Main.cs b/DesktopVRIK/Main.cs index 309ab33..41d27d3 100644 --- a/DesktopVRIK/Main.cs +++ b/DesktopVRIK/Main.cs @@ -5,75 +5,27 @@ namespace NAK.DesktopVRIK; public class DesktopVRIK : MelonMod { internal static MelonLogger.Instance Logger; - internal const string SettingsCategory = nameof(DesktopVRIK); - - public static readonly MelonPreferences_Category Category = - MelonPreferences.CreateCategory(SettingsCategory); - - public static readonly MelonPreferences_Entry EntryEnabled = - Category.CreateEntry("Enabled", true, description: "Toggle DesktopVRIK entirely. Requires avatar reload."); - - public static readonly MelonPreferences_Entry EntryPlantFeet = - Category.CreateEntry("Enforce Plant Feet", true, description: "Forces VRIK Plant Feet enabled to prevent hovering when stopping movement."); - - public static readonly MelonPreferences_Entry EntryResetFootstepsOnIdle = - Category.CreateEntry("Reset Footsteps on Idle", false, description: "Determins if the Locomotion Footsteps will be reset to their calibration position when entering idle."); - - public static readonly MelonPreferences_Entry EntryUseVRIKToes = - Category.CreateEntry("Use VRIK Toes", false, description: "Determines if VRIK uses humanoid toes for IK solving, which can cause feet to idle behind the avatar."); - - public static readonly MelonPreferences_Entry EntryBodyLeanWeight = - Category.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 = - Category.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 = - Category.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 = - Category.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 = - Category.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 = - Category.CreateEntry("Prone Thrusting", false, description: "Allows Body Lean Weight to take effect while crouched or prone."); - - public static readonly MelonPreferences_Entry EntryNetIKPass = - Category.CreateEntry("Network IK Pass", true, description: "Should NetIK pass be applied? This fixes a bunch of small rotation errors after VRIK is run."); - - public static readonly MelonPreferences_Entry EntryIntegrationAMT = - Category.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; - //BTKUILib Misc Tab - if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "BTKUILib")) - { - Logger.Msg("Initializing BTKUILib support."); - Integrations.BTKUIAddon.Init(); - } - - //AvatarMotionTweaker Handling - if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "AvatarMotionTweaker")) - { - integration_AMT = true; - Logger.Msg("AvatarMotionTweaker was found. Relying on it to handle VRIK locomotion."); - } - else - { - Logger.Msg("AvatarMotionTweaker was not found. Using built-in VRIK locomotion handling."); - } - ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); + + InitializeIntegration("BTKUILib", Integrations.BTKUIAddon.Initialize); + InitializeIntegration("AvatarMotionTweaker", Integrations.AMTAddon.Initialize); } - void ApplyPatches(Type type) + private static void InitializeIntegration(string modName, Action integrationAction) + { + if (RegisteredMelons.All(it => it.Info.Name != modName)) + return; + + Logger.Msg($"Initializing {modName} integration."); + integrationAction.Invoke(); + } + + private void ApplyPatches(Type type) { try { diff --git a/DesktopVRIK/ModSettings.cs b/DesktopVRIK/ModSettings.cs new file mode 100644 index 0000000..8bb9016 --- /dev/null +++ b/DesktopVRIK/ModSettings.cs @@ -0,0 +1,50 @@ +using MelonLoader; + +namespace NAK.DesktopVRIK; + +public static class ModSettings +{ + internal const string SettingsCategory = nameof(DesktopVRIK); + + public static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(SettingsCategory); + + // Desktop VRIK Settings + + public static readonly MelonPreferences_Entry EntryEnabled = + Category.CreateEntry("Enabled", true, description: "Toggle DesktopVRIK entirely. Requires avatar reload."); + + public static readonly MelonPreferences_Entry EntryPlantFeet = + Category.CreateEntry("Enforce Plant Feet", true, description: "Forces VRIK Plant Feet enabled to prevent hovering when stopping movement."); + + public static readonly MelonPreferences_Entry EntryResetFootstepsOnIdle = + Category.CreateEntry("Reset Footsteps on Idle", false, description: "Determines if the Locomotion Footsteps will be reset to their calibration position when entering idle."); + + public static readonly MelonPreferences_Entry EntryUseToesForVRIK = + Category.CreateEntry("Use VRIK Toes", false, description: "Determines if VRIK uses humanoid toes for IK solving, which can cause feet to idle behind the avatar."); + + public static readonly MelonPreferences_Entry EntryBodyLeanWeight = + Category.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 = + Category.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 = + Category.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 = + Category.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 = + Category.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 = + Category.CreateEntry("Prone Thrusting", false, description: "Allows Body Lean Weight to take effect while crouched or prone."); + + public static readonly MelonPreferences_Entry EntryNetIKPass = + Category.CreateEntry("Network IK Pass", true, description: "Should NetIK pass be applied? This fixes a bunch of small rotation errors after VRIK is run."); + + public static readonly MelonPreferences_Entry EntryIntegrationAMT = + Category.CreateEntry("AMT Integration", true, description: "Relies on AvatarMotionTweaker to handle VRIK Locomotion weights if available."); + +} diff --git a/DesktopVRIK/Properties/AssemblyInfo.cs b/DesktopVRIK/Properties/AssemblyInfo.cs index c35a721..38f297a 100644 --- a/DesktopVRIK/Properties/AssemblyInfo.cs +++ b/DesktopVRIK/Properties/AssemblyInfo.cs @@ -20,12 +20,14 @@ using System.Reflection; [assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] [assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] -[assembly: MelonOptionalDependencies("BTKUILib")] +[assembly: MelonOptionalDependencies("BTKUILib", "AvatarMotionTweaker")] +[assembly: MelonColor(255, 155, 89, 182)] +[assembly: MelonAuthorColor(255, 158, 21, 32)] [assembly: HarmonyDontPatchAll] namespace NAK.DesktopVRIK.Properties; internal static class AssemblyInfoParams { - public const string Version = "4.2.0"; + public const string Version = "4.2.1"; public const string Author = "NotAKidoS"; } \ No newline at end of file diff --git a/DesktopVRIK/README.md b/DesktopVRIK/README.md index 192da67..7f2507a 100644 --- a/DesktopVRIK/README.md +++ b/DesktopVRIK/README.md @@ -12,9 +12,5 @@ https://user-images.githubusercontent.com/37721153/221870123-fbe4f5e8-8d6e-4a43- * 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 +https://feedback.abinteractive.net/p/desktop-feet-ik-for-avatars \ No newline at end of file diff --git a/DesktopVRIK/VRIKHelpers/CachedSolver.cs b/DesktopVRIK/VRIKHelpers/CachedSolver.cs deleted file mode 100644 index 2b651f1..0000000 --- a/DesktopVRIK/VRIKHelpers/CachedSolver.cs +++ /dev/null @@ -1,28 +0,0 @@ -using RootMotion.FinalIK; - -namespace NAK.DesktopVRIK.VRIKHelper; - -// I don't think this was needed at all, but it looks fancy. -//https://github.com/knah/VRCMods/blob/master/IKTweaks/CachedSolver.cs - -public struct CachedSolver -{ - public readonly IKSolverVR Solver; - public readonly IKSolverVR.Spine Spine; - public readonly IKSolverVR.Leg LeftLeg; - public readonly IKSolverVR.Leg RightLeg; - public readonly IKSolverVR.Arm LeftArm; - public readonly IKSolverVR.Arm RightArm; - public readonly IKSolverVR.Locomotion Locomotion; - - public CachedSolver(IKSolverVR solver) - { - Solver = solver; - Spine = solver.spine; - LeftArm = solver.leftArm; - LeftLeg = solver.leftLeg; - RightArm = solver.rightArm; - RightLeg = solver.rightLeg; - Locomotion = solver.locomotion; - } -} \ No newline at end of file diff --git a/DesktopVRIK/VRIKHelpers/VRIKUtils.cs b/DesktopVRIK/VRIKHelpers/VRIKUtils.cs deleted file mode 100644 index ec3288d..0000000 --- a/DesktopVRIK/VRIKHelpers/VRIKUtils.cs +++ /dev/null @@ -1,164 +0,0 @@ -using RootMotion.FinalIK; -using UnityEngine; - -namespace NAK.DesktopVRIK.VRIKHelper; - -public static class VRIKUtils -{ - public static void ConfigureVRIKReferences(VRIK vrik, bool useVRIKToes) - { - if (!useVRIKToes) - { - vrik.references.leftToes = null; - vrik.references.rightToes = null; - } - - //bullshit fix to not cause death - FixFingerBonesError(vrik); - } - - private static void FixFingerBonesError(VRIK vrik) - { - 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; - } - } - - FixFingerBones(vrik, vrik.references.leftHand, vrik.solver.leftArm); - FixFingerBones(vrik, vrik.references.rightHand, vrik.solver.rightArm); - } - - public static void CalculateKneeBendNormals(VRIK vrik, ref VRIKCalibrationData calibrationData) - { - // Helper function to get position or default to Vector3.zero - Vector3 GetPositionOrDefault(Transform transform) => transform?.position ?? Vector3.zero; - - // Get assumed left knee normal - Vector3[] leftVectors = { - GetPositionOrDefault(vrik.references.leftThigh), - GetPositionOrDefault(vrik.references.leftCalf), - GetPositionOrDefault(vrik.references.leftFoot) - }; - calibrationData.KneeNormalLeft = Quaternion.Inverse(vrik.references.root.rotation) * GetNormalFromArray(leftVectors); - - // Get assumed right knee normal - Vector3[] rightVectors = { - GetPositionOrDefault(vrik.references.rightThigh), - GetPositionOrDefault(vrik.references.rightCalf), - GetPositionOrDefault(vrik.references.rightFoot) - }; - calibrationData.KneeNormalRight = Quaternion.Inverse(vrik.references.root.rotation) * GetNormalFromArray(rightVectors); - } - - public static void ApplyKneeBendNormals(VRIK vrik, VRIKCalibrationData calibrationData) - { - // 0 uses bendNormalRelToPelvis, 1 is bendNormalRelToTarget - // modifying pelvis normal weight is easier math - vrik.solver.leftLeg.bendToTargetWeight = 0f; - vrik.solver.rightLeg.bendToTargetWeight = 0f; - - var pelvis_localRotationInverse = Quaternion.Inverse(vrik.references.pelvis.localRotation); - vrik.solver.leftLeg.bendNormalRelToPelvis = pelvis_localRotationInverse * calibrationData.KneeNormalLeft; - vrik.solver.rightLeg.bendNormalRelToPelvis = pelvis_localRotationInverse * calibrationData.KneeNormalRight; - } - - private static Vector3 GetNormalFromArray(Vector3[] positions) - { - Vector3 centroid = Vector3.zero; - for (int i = 0; i < positions.Length; i++) - { - centroid += positions[i]; - } - centroid /= positions.Length; - - Vector3 normal = Vector3.zero; - for (int i = 0; i < positions.Length - 2; i++) - { - Vector3 side1 = positions[i] - centroid; - Vector3 side2 = positions[i + 1] - centroid; - normal += Vector3.Cross(side1, side2); - } - return normal.normalized; - } - - public static void CalculateInitialIKScaling(VRIK vrik, ref VRIKCalibrationData calibrationData) - { - // Get distance between feet and thighs - float scaleModifier = Mathf.Max(1f, vrik.references.pelvis.lossyScale.x); - float footDistance = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.rightFoot.position); - - calibrationData.InitialFootDistance = footDistance * 0.5f; - calibrationData.InitialStepThreshold = footDistance * scaleModifier; - calibrationData.InitialStepHeight = Vector3.Distance(vrik.references.leftFoot.position, vrik.references.leftCalf.position) * 0.2f; - calibrationData.InitialHeadHeight = Mathf.Abs(vrik.references.head.position.y - vrik.references.rightFoot.position.y); - } - - public static void CalculateInitialFootsteps(VRIK vrik, ref VRIKCalibrationData calibrationData) - { - Transform root = vrik.references.root; - Transform leftFoot = vrik.references.leftFoot; - Transform rightFoot = vrik.references.rightFoot; - - // Calculate the world rotation of the root bone at the current frame - Quaternion rootWorldRot = root.rotation; - - // Calculate the world rotation of the left and right feet relative to the root bone - calibrationData.InitialFootPosLeft = root.parent.InverseTransformPoint(leftFoot.position); - calibrationData.InitialFootPosRight = root.parent.InverseTransformPoint(rightFoot.position); - calibrationData.InitialFootRotLeft = Quaternion.Inverse(rootWorldRot) * leftFoot.rotation; - calibrationData.InitialFootRotRight = Quaternion.Inverse(rootWorldRot) * rightFoot.rotation; - } - - public static void ResetToInitialFootsteps(VRIK vrik, VRIKCalibrationData calibrationData, float scaleModifier) - { - var locomotionSolver = vrik.solver.locomotion; - - var footsteps = locomotionSolver.footsteps; - var footstepLeft = footsteps[0]; - var footstepRight = footsteps[1]; - - var root = vrik.references.root; - var rootWorldRot = vrik.references.root.rotation; - - // hack, use parent transform instead as setting feet position moves root - footstepLeft.Reset(rootWorldRot, root.parent.TransformPoint(calibrationData.InitialFootPosLeft * scaleModifier), rootWorldRot * calibrationData.InitialFootRotLeft); - footstepRight.Reset(rootWorldRot, root.parent.TransformPoint(calibrationData.InitialFootPosRight * scaleModifier), rootWorldRot * calibrationData.InitialFootRotRight); - } - - public static void 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, VRIKCalibrationData calibrationData, float scaleModifier) - { - var locomotionSolver = vrik.solver.locomotion; - locomotionSolver.footDistance = calibrationData.InitialFootDistance * scaleModifier; - locomotionSolver.stepThreshold = calibrationData.InitialStepThreshold * scaleModifier; - ScaleStepHeight(locomotionSolver.stepHeight, calibrationData.InitialStepHeight * scaleModifier); - } - - static void ScaleStepHeight(AnimationCurve stepHeightCurve, float mag) - { - Keyframe[] keyframes = stepHeightCurve.keys; - keyframes[1].value = mag; - stepHeightCurve.keys = keyframes; - } - - public static void InitiateVRIKSolver(VRIK vrik) - { - vrik.solver.SetToReferences(vrik.references); - vrik.solver.Initiate(vrik.transform); - } -} \ No newline at end of file diff --git a/DesktopVRIK/format.json b/DesktopVRIK/format.json index 3516c95..57eba0c 100644 --- a/DesktopVRIK/format.json +++ b/DesktopVRIK/format.json @@ -1,8 +1,8 @@ { "_id": 117, "name": "DesktopVRIK", - "modversion": "4.2.0", - "gameversion": "2022r170p1", + "modversion": "4.2.1", + "gameversion": "2023r171", "loaderversion": "0.6.1", "modtype": "Mod", "author": "NotAKidoS", @@ -17,8 +17,8 @@ "requirements": [ "BTKUILib" ], - "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r5/DesktopVRIK.dll", + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r14/DesktopVRIK.dll", "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/DesktopVRIK/", - "changelog": "- Fixed IK not being reset when teleporting/respawning/sitting.\n- Fixed IK Locomotion not being disabled while sitting.\n- Cleanup of VRIK Calibration & Configuration to make updating & debugging easier.\n- Added a temporary fix for DesktopVRSwitch BodySystem handling to ensure tracking is correctly enabled after switch.\n- Added \"NetIKPass\" after VRIK solving, which fixes many rotational errors that were only visible on the local client.\n- Removed Chest VRIK reference fix & find unmapped toe options as they didn't stream over NetIK.", - "embedcolor": "9b59b6" + "changelog": "- Fixes for 2023r171.\n- Fixed ikSimulatedRootAngle being constantly reset while on a movement parent.", + "embedcolor": "#9b59b6" } \ No newline at end of file