final touches

completely migrated to new setup using a helper monobehavior, which makes code a lot cleaner
This commit is contained in:
NotAKidoS 2022-12-25 22:27:12 -06:00
parent 50c5016cc6
commit 22391d1fcc
5 changed files with 148 additions and 296 deletions

View file

@ -1,62 +1,65 @@
using ABI.CCK.Components; using ABI.CCK.Components;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.MovementSystem;
using ABI_RC.Systems.IK; using ABI_RC.Systems.IK;
using ABI_RC.Systems.IK.SubSystems; using ABI_RC.Systems.IK.SubSystems;
using MelonLoader; using ABI_RC.Systems.MovementSystem;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK; using RootMotion.FinalIK;
using UnityEngine;
using UnityEngine.Events;
namespace DesktopVRIK; namespace DesktopVRIK;
public class NAKDesktopVRIK : MonoBehaviour public class DesktopVRIK : MonoBehaviour
{ {
public static NAKDesktopVRIK Instance; public static DesktopVRIK Instance;
public VRIK vrik;
public bool Setting_Enabled;
public bool Setting_EmulateVRChatHipMovement;
public bool Setting_EmoteVRIK;
public bool Setting_EmoteLookAtIK;
void Start() void Start()
{ {
Instance = this; Instance = this;
} }
void LateUpdate() public void OnPreSolverUpdate()
{ {
//pretty much zero out VRIK trying to locomote us using autofootstep //Reset avatar offset (VRIK will literally make you walk away from root otherwise)
transform.localPosition = Vector3.zero; IKSystem.vrik.transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity; IKSystem.vrik.transform.localRotation = Quaternion.identity;
//VRChat hip movement emulation
if (Setting_EmulateVRChatHipMovement)
{
float angle = PlayerSetup.Instance.desktopCamera.transform.localEulerAngles.x;
angle = (angle > 180) ? angle - 360 : angle;
float weight = (1 - MovementSystem.Instance.movementVector.magnitude);
Quaternion rotation = Quaternion.AngleAxis(angle * weight, IKSystem.Instance.avatar.transform.right);
IKSystem.vrik.solver.AddRotationOffset(IKSolverVR.RotationOffset.Head, rotation);
}
} }
public void CalibrateAvatarVRIK(CVRAvatar avatar) public void CalibrateAvatarVRIK(CVRAvatar avatar)
{ {
//check if VRIK already exists, as it is an allowed component
vrik = avatar.gameObject.GetComponent<VRIK>();
if (vrik == null)
{
vrik = avatar.gameObject.AddComponent<VRIK>();
}
//Generic VRIK calibration shit //Generic VRIK calibration shit
vrik.fixTransforms = false; IKSystem.vrik.fixTransforms = false;
vrik.solver.plantFeet = false; IKSystem.vrik.solver.plantFeet = false;
vrik.solver.locomotion.weight = 1f; IKSystem.vrik.solver.locomotion.weight = 1f;
vrik.solver.locomotion.angleThreshold = 30f; IKSystem.vrik.solver.locomotion.angleThreshold = 30f;
vrik.solver.locomotion.maxLegStretch = 0.75f; IKSystem.vrik.solver.locomotion.maxLegStretch = 0.75f;
//nuke weights //nuke weights
vrik.solver.spine.headClampWeight = 0f; IKSystem.vrik.solver.spine.headClampWeight = 0f;
vrik.solver.spine.minHeadHeight = 0f; IKSystem.vrik.solver.spine.minHeadHeight = 0f;
vrik.solver.leftArm.positionWeight = 0f; IKSystem.vrik.solver.leftArm.positionWeight = 0f;
vrik.solver.leftArm.rotationWeight = 0f; IKSystem.vrik.solver.leftArm.rotationWeight = 0f;
vrik.solver.rightArm.positionWeight = 0f; IKSystem.vrik.solver.rightArm.positionWeight = 0f;
vrik.solver.rightArm.rotationWeight = 0f; IKSystem.vrik.solver.rightArm.rotationWeight = 0f;
vrik.solver.leftLeg.positionWeight = 0f; IKSystem.vrik.solver.leftLeg.positionWeight = 0f;
vrik.solver.leftLeg.rotationWeight = 0f; IKSystem.vrik.solver.leftLeg.rotationWeight = 0f;
vrik.solver.rightLeg.positionWeight = 0f; IKSystem.vrik.solver.rightLeg.positionWeight = 0f;
vrik.solver.rightLeg.rotationWeight = 0f; IKSystem.vrik.solver.rightLeg.rotationWeight = 0f;
//ChilloutVR specific stuff //ChilloutVR specific stuff
@ -69,15 +72,20 @@ public class NAKDesktopVRIK : MonoBehaviour
BodySystem.TrackingRightArmEnabled = false; BodySystem.TrackingRightArmEnabled = false;
BodySystem.TrackingLeftLegEnabled = false; BodySystem.TrackingLeftLegEnabled = false;
BodySystem.TrackingRightLegEnabled = false; BodySystem.TrackingRightLegEnabled = false;
vrik.solver.IKPositionWeight = 0f; IKSystem.vrik.solver.IKPositionWeight = 0f;
vrik.enabled = false; IKSystem.vrik.enabled = false;
//Calibrate HeadIKOffset //Calibrate HeadIKOffset
VRIKCalibrator.CalibrateHead(vrik, headAnchor, IKSystem.Instance.headAnchorPositionOffset, IKSystem.Instance.headAnchorRotationOffset); VRIKCalibrator.CalibrateHead(IKSystem.vrik, headAnchor, IKSystem.Instance.headAnchorPositionOffset, IKSystem.Instance.headAnchorRotationOffset);
vrik.enabled = true; IKSystem.vrik.enabled = true;
vrik.solver.IKPositionWeight = 1f; IKSystem.vrik.solver.IKPositionWeight = 1f;
vrik.solver.spine.maintainPelvisPosition = 0f; IKSystem.vrik.solver.spine.maintainPelvisPosition = 0f;
if (IKSystem.vrik != null)
{
IKSystem.vrik.onPreSolverUpdate.AddListener(new UnityAction(this.OnPreSolverUpdate));
}
} }
//This is built because original build placed IK Targets on all joints.
private static Transform FindIKTarget(Transform targetParent) private static Transform FindIKTarget(Transform targetParent)
{ {
/** /**

View file

@ -1,10 +1,9 @@
using ABI_RC.Core.Player; using ABI.CCK.Components;
using ABI.CCK.Components; using ABI_RC.Core.Player;
using ABI_RC.Core.Savior; using ABI_RC.Core.Savior;
using ABI_RC.Systems.IK; using ABI_RC.Systems.IK;
using ABI_RC.Systems.IK.SubSystems; using ABI_RC.Systems.IK.SubSystems;
using HarmonyLib; using HarmonyLib;
using MelonLoader;
using RootMotion.FinalIK; using RootMotion.FinalIK;
namespace DesktopVRIK; namespace DesktopVRIK;
@ -14,22 +13,59 @@ internal class HarmonyPatches
{ {
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), "SetupAvatarGeneral")] [HarmonyPatch(typeof(PlayerSetup), "SetupAvatarGeneral")]
private static void InitializeDesktopIKSystem(ref CVRAvatar ____avatarDescriptor) private static void SetupDesktopIKSystem(ref CVRAvatar ____avatarDescriptor)
{ {
if (MetaPort.Instance.isUsingVr) return; if (!MetaPort.Instance.isUsingVr && DesktopVRIK.Instance.Setting_Enabled)
{
//this will stop at the useless isVr return (the function is only ever called by vr anyways...) //this will stop at the useless isVr return (the function is only ever called by vr anyways...)
IKSystem.Instance.InitializeAvatar(____avatarDescriptor); IKSystem.Instance.InitializeAvatar(____avatarDescriptor);
}
} }
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(IKSystem), "InitializeAvatar")] [HarmonyPatch(typeof(IKSystem), "InitializeAvatar")]
private static void InitializeDesktopAvatar(CVRAvatar avatar, ref VRIK ____vrik) private static void InitializeDesktopAvatarVRIK(CVRAvatar avatar, ref VRIK ____vrik)
{ {
//need IKSystem to see VRIK component for setup if (!MetaPort.Instance.isUsingVr && DesktopVRIK.Instance.Setting_Enabled)
____vrik = avatar.gameObject.AddComponent<VRIK>(); {
//now i add my own VRIK stuff //need IKSystem to see VRIK component for setup
NAKDesktopVRIK NAKVRIK = avatar.gameObject.AddComponent<NAKDesktopVRIK>(); ____vrik = avatar.gameObject.AddComponent<VRIK>();
NAKVRIK.CalibrateAvatarVRIK(avatar); //now I calibrate DesktopVRIK
DesktopVRIK.Instance.CalibrateAvatarVRIK(avatar);
}
}
private static bool emotePlayed = false;
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), "Update")]
private static void CorrectVRIK(ref bool ____emotePlaying, ref LookAtIK ___lookIK)
{
if (MetaPort.Instance.isUsingVr || DesktopVRIK.Instance == null) return;
//might need to rework this in the future
if (____emotePlaying && !emotePlayed)
{
emotePlayed = true;
if (DesktopVRIK.Instance.Setting_EmoteVRIK)
{
BodySystem.TrackingEnabled = false;
//IKSystem.vrik.solver.Reset();
}
if (DesktopVRIK.Instance.Setting_EmoteLookAtIK && ___lookIK != null)
{
___lookIK.enabled = false;
}
}
else if (!____emotePlaying && emotePlayed)
{
emotePlayed = false;
IKSystem.vrik.solver.Reset();
BodySystem.TrackingEnabled = true;
if (___lookIK != null)
{
___lookIK.enabled = true;
}
}
} }
} }

View file

@ -1,240 +1,48 @@
using ABI.CCK.Components; using ABI_RC.Core.Player;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.MovementSystem;
using ABI_RC.Systems.IK;
using ABI_RC.Systems.IK.SubSystems;
using HarmonyLib;
using MelonLoader; using MelonLoader;
using RootMotion.FinalIK;
using UnityEngine;
using UnityEngine.Events;
namespace DesktopVRIK; namespace DesktopVRIK;
public class DesktopVRIK : MelonMod public class DesktopVRIKMod : MelonMod
{ {
internal const string SettingsCategory = "DesktopVRIK";
private static MelonPreferences_Category m_categoryDesktopVRIK; private static MelonPreferences_Category m_categoryDesktopVRIK;
private static MelonPreferences_Entry<bool> m_entryEnabled; private static MelonPreferences_Entry<bool> m_entryEnabled, m_entryEmulateHipMovement, m_entryEmoteVRIK, m_entryEmoteLookAtIK;
private static MelonPreferences_Entry<bool> m_entryEmulateHipMovement; public override void OnInitializeMelon()
private static MelonPreferences_Entry<bool> m_entryEmoteVRIK;
private static MelonPreferences_Entry<bool> m_entryEmoteLookAtIK;
public override void OnApplicationStart()
{ {
m_categoryDesktopVRIK = MelonPreferences.CreateCategory(nameof(DesktopVRIK)); m_categoryDesktopVRIK = MelonPreferences.CreateCategory(SettingsCategory);
m_entryEnabled = m_categoryDesktopVRIK.CreateEntry<bool>("Enabled", true, description: "Attempt to give Desktop VRIK."); m_entryEnabled = m_categoryDesktopVRIK.CreateEntry<bool>("Enabled", true, description: "Attempt to give Desktop VRIK on avatar load.");
m_entryEmulateHipMovement = m_categoryDesktopVRIK.CreateEntry<bool>("Emulate Hip Movement", true, description: "Emulates VRChat-like hip movement when moving head up/down on desktop."); m_entryEmulateHipMovement = m_categoryDesktopVRIK.CreateEntry<bool>("Emulate Hip Movement", true, description: "Emulates VRChat-like hip movement when moving head up/down on desktop.");
m_entryEmoteVRIK = m_categoryDesktopVRIK.CreateEntry<bool>("Disable Emote VRIK", true, description: "Disable VRIK while emoting."); m_entryEmoteVRIK = m_categoryDesktopVRIK.CreateEntry<bool>("Disable Emote VRIK", true, description: "Disable VRIK while emoting. Only disable if you are ok with looking dumb.");
m_entryEmoteLookAtIK = m_categoryDesktopVRIK.CreateEntry<bool>("Disable Emote LookAtIK", true, description: "Disable LookAtIK while emoting."); m_entryEmoteLookAtIK = m_categoryDesktopVRIK.CreateEntry<bool>("Disable Emote LookAtIK", true, description: "Disable LookAtIK while emoting. This setting doesn't really matter, as LookAtIK isn't networked while doing an emote.");
m_categoryDesktopVRIK.SaveToFile(false);
foreach (var setting in m_categoryDesktopVRIK.Entries)
{
setting.OnEntryValueChangedUntyped.Subscribe(OnUpdateSettings);
}
MelonLoader.MelonCoroutines.Start(WaitForLocalPlayer());
} }
[HarmonyPatch] System.Collections.IEnumerator WaitForLocalPlayer()
private class HarmonyPatches
{ {
private static bool emotePlayed = false; while (PlayerSetup.Instance == null)
yield return null;
[HarmonyPostfix] PlayerSetup.Instance.gameObject.AddComponent<DesktopVRIK>();
[HarmonyPatch(typeof(PlayerSetup), "Update")] while (DesktopVRIK.Instance == null)
private static void CorrectVRIK(ref bool ____emotePlaying, ref LookAtIK ___lookIK) yield return null;
{ UpdateAllSettings();
if (MetaPort.Instance.isUsingVr) return;
if (IKSystem.vrik == null) return;
//pretty much zero out VRIK trying to locomote us using autofootstep
IKSystem.Instance.avatar.transform.localPosition = Vector3.zero;
IKSystem.Instance.avatar.transform.localRotation = Quaternion.identity;
//TODO: Smooth out offset when walking/running
if ( m_entryEmulateHipMovement.Value )
{
float angle = PlayerSetup.Instance.desktopCamera.transform.localEulerAngles.x;
angle = (angle > 180) ? angle - 360 : angle;
float weight = (1 - MovementSystem.Instance.movementVector.magnitude);
Quaternion rotation = Quaternion.AngleAxis(angle * weight, IKSystem.Instance.avatar.transform.right);
IKSystem.vrik.solver.AddRotationOffset(IKSolverVR.RotationOffset.Head, rotation);
}
//Avatar Motion Tweaker has custom emote detection to disable VRIK via state tags
if (____emotePlaying && !emotePlayed)
{
emotePlayed = true;
if (m_entryEmoteVRIK.Value)
{
BodySystem.TrackingEnabled = false;
IKSystem.vrik.solver.Reset();
}
if (m_entryEmoteLookAtIK.Value && ___lookIK != null)
{
___lookIK.enabled = false;
}
}
else if (!____emotePlaying && emotePlayed)
{
emotePlayed = false;
BodySystem.TrackingEnabled = true;
IKSystem.vrik.solver.Reset();
if (___lookIK != null)
{
___lookIK.enabled = true;
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(IKSystem), "InitializeAvatar")]
private static void InitializeDesktopAvatar(CVRAvatar avatar, ref VRIK ____vrik, ref HumanPoseHandler ____poseHandler, ref Vector3 ____referenceRootPosition, ref Quaternion ____referenceRootRotation, ref float[] ___HandCalibrationPoseMuscles)
{
if (!m_entryEnabled.Value) return;
if (MetaPort.Instance.isUsingVr) return;
//set avatar to
Quaternion initialRotation = avatar.transform.rotation;
avatar.transform.rotation = Quaternion.identity;
____vrik = avatar.gameObject.AddComponent<VRIK>();
____vrik.fixTransforms = false;
____vrik.solver.locomotion.weight = 1f;
IKSystem.Instance.ApplyAvatarScaleToIk(avatar.viewPosition.y);
____vrik.solver.locomotion.angleThreshold = 30f;
____vrik.solver.locomotion.maxLegStretch = 0.75f;
____vrik.solver.spine.headClampWeight = 0f;
____vrik.solver.spine.minHeadHeight = 0f;
if (____vrik != null)
{
____vrik.onPreSolverUpdate.AddListener(new UnityAction(IKSystem.Instance.OnPreSolverUpdate));
}
if (____poseHandler == null)
{
____poseHandler = new HumanPoseHandler(IKSystem.Instance.animator.avatar, IKSystem.Instance.animator.transform);
}
____poseHandler.GetHumanPose(ref IKSystem.Instance.humanPose);
____referenceRootPosition = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.Hips).position;
____referenceRootRotation = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.Hips).rotation;
for (int i = 0; i < ___HandCalibrationPoseMuscles.Length; i++)
{
IKSystem.Instance.ApplyMuscleValue((MuscleIndex)i, ___HandCalibrationPoseMuscles[i], ref IKSystem.Instance.humanPose.muscles);
}
____poseHandler.SetHumanPose(ref IKSystem.Instance.humanPose);
if (IKSystem.Instance.applyOriginalHipPosition)
{
IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.Hips).position = ____referenceRootPosition;
}
if (IKSystem.Instance.applyOriginalHipRotation)
{
IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.Hips).rotation = ____referenceRootRotation;
}
BodySystem.isCalibratedAsFullBody = false;
BodySystem.isCalibrating = false;
BodySystem.TrackingPositionWeight = 1f;
BodySystem.isCalibratedAsFullBody = false;
//InitializeDesktopIK
//centerEyeAnchor now is head bone
Transform headAnchor = FindIKTarget(IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.Head));
IKSystem.Instance.headAnchorPositionOffset = Vector3.zero;
IKSystem.Instance.headAnchorRotationOffset = Vector3.zero;
IKSystem.Instance.leftHandModel.SetActive(false);
IKSystem.Instance.rightHandModel.SetActive(false);
IKSystem.Instance.animator = IKSystem.Instance.avatar.GetComponent<Animator>();
IKSystem.Instance.animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
//tell game to not track limbs
BodySystem.TrackingLeftArmEnabled = false;
BodySystem.TrackingRightArmEnabled = false;
BodySystem.TrackingLeftLegEnabled = false;
BodySystem.TrackingRightLegEnabled = false;
//create ik targets for avatars to utilize
//____vrik.solver.leftLeg.target = FindIKTarget(IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.LeftFoot));
//____vrik.solver.rightLeg.target = FindIKTarget(IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.RightFoot));
____vrik.solver.IKPositionWeight = 0f;
____vrik.enabled = false;
VRIKCalibrator.CalibrateHead(____vrik, headAnchor, IKSystem.Instance.headAnchorPositionOffset, IKSystem.Instance.headAnchorRotationOffset);
//IKSystem.Instance.leftHandPose.transform.position = ____vrik.references.leftHand.position;
//IKSystem.Instance.rightHandPose.transform.position = ____vrik.references.rightHand.position;
//____vrik.solver.leftArm.target = IKSystem.Instance.leftHandAnchor;
//____vrik.solver.rightArm.target = IKSystem.Instance.rightHandAnchor;
//Transform boneTransform = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
//Transform boneTransform2 = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.RightLowerArm);
//if (boneTransform.GetComponent<TwistRelaxer>() == null)
//{
// TwistRelaxer twistRelaxer = boneTransform.gameObject.AddComponent<TwistRelaxer>();
// twistRelaxer.ik = ____vrik;
// twistRelaxer.child = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.LeftHand);
// twistRelaxer.weight = 0.5f;
// twistRelaxer.parentChildCrossfade = 0.9f;
// twistRelaxer.twistAngleOffset = 0f;
//}
//if (boneTransform2.GetComponent<TwistRelaxer>() == null)
//{
// TwistRelaxer twistRelaxer2 = boneTransform2.gameObject.AddComponent<TwistRelaxer>();
// twistRelaxer2.ik = ____vrik;
// twistRelaxer2.child = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.RightHand);
// twistRelaxer2.weight = 0.5f;
// twistRelaxer2.parentChildCrossfade = 0.9f;
// twistRelaxer2.twistAngleOffset = 0f;
//}
//if (IKSystem.Instance.animator != null && IKSystem.Instance.animator.avatar != null && IKSystem.Instance.animator.avatar.isHuman)
//{
// Transform boneTransform3 = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.LeftThumbProximal);
// if (boneTransform3 != null)
// {
// ____vrik.solver.leftArm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis(____vrik.references.leftHand, ____vrik.references.leftForearm, boneTransform3);
// }
// Transform boneTransform4 = IKSystem.Instance.animator.GetBoneTransform(HumanBodyBones.RightThumbProximal);
// if (boneTransform4 != null)
// {
// ____vrik.solver.rightArm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis(____vrik.references.rightHand, ____vrik.references.rightForearm, boneTransform4);
// }
//}
____vrik.enabled = true;
____vrik.solver.IKPositionWeight = 1f;
____vrik.solver.spine.maintainPelvisPosition = 0f;
//prevent the IK from walking away
____vrik.solver.locomotion.maxVelocity = 0f;
avatar.transform.rotation = initialRotation;
}
private static Transform FindIKTarget(Transform targetParent)
{
/**
I want creators to be able to specify their own custom IK Targets, so they can move them around with animations if they want.
We check for existing target objects, and if none are found we make our own.
Naming scheme is parentobject name + " IK Target".
**/
Transform parentTransform = targetParent.transform;
string targetName = parentTransform.name + " IK Target";
//check for existing target
foreach (object obj in parentTransform)
{
Transform childTransform = (Transform)obj;
if (childTransform.name == targetName)
{
return childTransform;
}
}
//create new target if none are found
GameObject newTarget = new GameObject(targetName);
newTarget.transform.parent = parentTransform;
newTarget.transform.localPosition = Vector3.zero;
newTarget.transform.localRotation = Quaternion.identity;
return newTarget.transform;
}
} }
private void UpdateAllSettings()
{
if (!DesktopVRIK.Instance) return;
DesktopVRIK.Instance.Setting_Enabled = m_entryEnabled.Value;
DesktopVRIK.Instance.Setting_EmulateVRChatHipMovement = m_entryEmulateHipMovement.Value;
DesktopVRIK.Instance.Setting_EmoteVRIK = m_entryEmoteVRIK.Value;
DesktopVRIK.Instance.Setting_EmoteLookAtIK = m_entryEmoteLookAtIK.Value;
}
private void OnUpdateSettings(object arg1, object arg2) => UpdateAllSettings();
} }

View file

@ -11,7 +11,7 @@ using DesktopVRIK.Properties;
[assembly: AssemblyProduct(nameof(DesktopVRIK))] [assembly: AssemblyProduct(nameof(DesktopVRIK))]
[assembly: MelonInfo( [assembly: MelonInfo(
typeof(DesktopVRIK.DesktopVRIK), typeof(DesktopVRIK.DesktopVRIKMod),
nameof(DesktopVRIK), nameof(DesktopVRIK),
AssemblyInfoParams.Version, AssemblyInfoParams.Version,
AssemblyInfoParams.Author, AssemblyInfoParams.Author,

View file

@ -1,23 +1,23 @@
{ {
"_id": 95, "_id": -1,
"name": "DesktopVRIK", "name": "DesktopVRIK",
"modversion": "1.1.0", "modversion": "1.0.0",
"gameversion": "2022r168", "gameversion": "2022r170",
"loaderversion": "0.5.4", "loaderversion": "0.5.7",
"modtype": "Mod", "modtype": "Mod",
"author": "NotAKidoS", "author": "NotAKidoS",
"description": "Corrects MM and QM position when avatar is scaled.\nAdditional option to scale player collision.", "description": "Adds VRIK to Desktop avatars. No longer will you be a liveless sliding statue~!\nAdds the small feet stepping when looking around on Desktop.\n\nAdditional option to emulate VRChat-like hip movement if you like becoming a fish.",
"searchtags": [ "searchtags": [
"menu", "desktop",
"scale", "vrik",
"avatarscale", "ik",
"slider" "feet"
], ],
"requirements": [ "requirements": [
"None" "None"
], ],
"downloadlink": "https://github.com/NotAKidOnSteam/DesktopVRIK/releases/download/r2/DesktopVRIK.dll", "downloadlink": "https://github.com/NotAKidOnSteam/DesktopVRIK/releases/download/r1/DesktopVRIK.dll",
"sourcelink": "https://github.com/NotAKidOnSteam/DesktopVRIK/", "sourcelink": "https://github.com/NotAKidOnSteam/DesktopVRIK/",
"changelog": "Added option to scale player collision. Fixed some VR specific issues.", "changelog": "Simplified VRIK calibration to avoid low heel issue.",
"embedcolor": "804221" "embedcolor": "9b59b6"
} }