[NAK_CVR_Mods] Unfucked for 2026r182

This commit is contained in:
NotAKid 2026-06-18 22:20:56 -05:00
parent c13dc8375a
commit 281403d68b
209 changed files with 3936 additions and 1122 deletions

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>ASTExtension</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,109 @@
using UnityEngine;
namespace NAK.DehumanizePlayers;
public static class HumanoidAvatarRebinder
{
private static readonly HashSet<Transform> s_BoneTransforms = new();
private static readonly HashSet<string> s_HumanBoneNames = new();
private static readonly List<(Transform t, string original)> s_Renames = new();
private static readonly List<Transform> s_AllTransforms = new();
public static Animator RebindToParentHumanoidAnimator(GameObject avatarGameObject)
{
if (avatarGameObject == null) return null;
var parent = avatarGameObject.transform.parent;
if (parent == null) return null;
var innerAnimator = avatarGameObject.GetComponent<Animator>();
if (innerAnimator == null || innerAnimator.avatar == null || !innerAnimator.avatar.isHuman)
{
DehumanizePlayersMod.Logger.Error("[HumanoidAvatarRebinder] avatarGameObject must have an Animator with a humanoid Avatar.");
return null;
}
s_BoneTransforms.Clear();
s_HumanBoneNames.Clear();
s_Renames.Clear();
s_AllTransforms.Clear();
var humanDescription = innerAnimator.avatar.humanDescription;
var sourceAvatarName = innerAnimator.avatar.name;
var outerAnimator = parent.GetComponent<Animator>();
if (outerAnimator) UnityEngine.Object.DestroyImmediate(outerAnimator);
//if (outerAnimator == null)
outerAnimator = parent.gameObject.AddComponent<Animator>();
outerAnimator.enabled = false;
outerAnimator.applyRootMotion = false;
outerAnimator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
outerAnimator.runtimeAnimatorController = null;
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)
{
var bone = innerAnimator.GetBoneTransform((HumanBodyBones)i);
if (bone != null) s_BoneTransforms.Add(bone);
}
// As we cannot rebind the inner animator, unassign now ...?
// Cannot undo renames without breaking things, assuming need to wait a frame.
innerAnimator.avatar = null;
// innerAnimator.Rebind();
if (humanDescription.skeleton != null && humanDescription.skeleton.Length > 0)
{
var expectedRootName = humanDescription.skeleton[0].name;
if (!string.IsNullOrEmpty(expectedRootName) && avatarGameObject.name != expectedRootName)
{
s_Renames.Add((avatarGameObject.transform, avatarGameObject.name));
avatarGameObject.name = expectedRootName;
}
}
foreach (var bone in humanDescription.human)
if (!string.IsNullOrEmpty(bone.boneName)) s_HumanBoneNames.Add(bone.boneName);
parent.GetComponentsInChildren(true, s_AllTransforms);
int renamed = 0;
foreach (var t in s_AllTransforms)
{
if (s_HumanBoneNames.Contains(t.name) && !s_BoneTransforms.Contains(t))
{
s_Renames.Add((t, t.name));
t.name = t.name + "__nb";
renamed++;
}
}
if (renamed > 0)
DehumanizePlayersMod.Logger.Msg($"[HumanoidAvatarRebinder] Renamed {renamed} non-bone transforms.");
var rebuiltAvatar = AvatarBuilder.BuildHumanAvatar(parent.gameObject, humanDescription);
if (!rebuiltAvatar.isValid)
{
DehumanizePlayersMod.Logger.Error("[HumanoidAvatarRebinder] Rebuilt Avatar is invalid.");
RevertRenames();
return null;
}
rebuiltAvatar.name = sourceAvatarName + "_Rebound";
outerAnimator.avatar = rebuiltAvatar;
outerAnimator.Rebind();
// RevertRenames();
return outerAnimator;
}
private static void RevertRenames()
{
for (int i = s_Renames.Count - 1; i >= 0; i--)
{
var (t, original) = s_Renames[i];
if (t != null) t.name = original;
}
s_Renames.Clear();
}
}

37
DehumanizePlayers/Main.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Reflection;
using ABI_RC.Core.Player;
using HarmonyLib;
using MelonLoader;
using UnityEngine;
namespace NAK.DehumanizePlayers;
public class DehumanizePlayersMod : MelonMod
{
internal static MelonLogger.Instance Logger;
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(DehumanizePlayers));
private static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("enabled", true,
"Dehumanize Players", description: "When enabled creates a dummy animator above the avatar root which handles muscle application.");
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
HarmonyInstance.Patch(
typeof(NetIKController).GetMethod(nameof(NetIKController.SetupAvatar),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(DehumanizePlayersMod).GetMethod(nameof(OnPreNetIKControllerSetupAvatar),
BindingFlags.NonPublic | BindingFlags.Static))
);
}
// ReSharper disable once RedundantAssignment
private static void OnPreNetIKControllerSetupAvatar(GameObject remoteAvatar, ref Animator remoteAnimator)
{
if (!EntryEnabled.Value) return;
remoteAnimator = HumanoidAvatarRebinder.RebindToParentHumanoidAnimator(remoteAvatar);
}
}

View file

@ -0,0 +1,32 @@
using MelonLoader;
using NAK.DehumanizePlayers.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.DehumanizePlayers))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.DehumanizePlayers))]
[assembly: MelonInfo(
typeof(NAK.DehumanizePlayers.DehumanizePlayersMod),
nameof(NAK.DehumanizePlayers),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/DehumanizePlayers"
)]
[assembly: MelonGame("ChilloutVR", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll]
namespace NAK.DehumanizePlayers.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,33 @@
# DehumanizePlayers
Humanoid animators are needlessly more expensive than generic ones due to scheduling twist resolving for each layer, even when empty or unnecessary.
You can see some profiler data gathered by a VRChat community member here:
https://docs.google.com/document/d/1SpG7O30O0Cb5tQCEgRro8BixO0lRkrlV2o9Cbq-rzJU/edit?tab=t.0
This was my attempt at addressing the issue when it was first linked to me. It works, albeit really jank. Remote avatars do not need to be humanoid as muscle data is streamed in over the network (unless playing an emote).
VRChat has recently addressed this issue as well, but with an engine modification to skip this when IK Pass is disabled on the evaluated layer, which we are unable to do via mod or native. I have no way to test if the two fixes are comparable, but their fix likely applies to all animators, unlike this fix which only applies to remote player avatars.
Surprisingly, Unity lets you nest animators. This mod creates a dummy disabled animator on the parent object of the avatar and directs the netikcontroller to apply muscles through that (which is very similar to how Basis drives muscles). The avatars original animator is then **dehumanized** to avoid the humanoid layer cost.
This setup also would allow CVR to utilize Playable Animation Jobs on existing avatars to drive muscles, which makes netik cost free when the avatar is occlusion culled:
https://bsky.app/profile/nak.koneko.cat/post/3mlgptv7ttu2o
(playable netik was dropped native due to breaking existing animator setups)
Alternatively, this problem can also be addressed by driving the transforms of remote avatars directly, like Zettai's FastNetIK mod:
https://github.com/ZettaiVR/CVR-Mods/tree/main/FastNetIK
The two approaches to addressing this low-hanging perf-issue are likely to be considered and tested after the GS2 update.
---
Here is the block of text where I tell you this mod is not affiliated with or endorsed by ChilloutVR.
https://docs.chilloutvr.net/official/legal/tos/#6-modding-our-game
> This mod is an independent creation not affiliated with, supported by, or approved by ChilloutVR.
> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
> To the best of my knowledge, I have adhered to the Modding Guidelines established by ChilloutVR.

View file

@ -0,0 +1,24 @@
{
"_id": 223,
"name": "ASTExtension",
"modversion": "1.0.5",
"gameversion": "2025r181",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool):\n- VR Gesture to scale\n- Persistent height\n- Copy height from others\n\nBest used with Avatar Scale Tool, but will attempt to work with found scaling setups.\nRequires already having Avatar Scaling on the avatar. This is **not** Universal Scaling.",
"searchtags": [
"tool",
"scaling",
"height",
"extension",
"avatar"
],
"requirements": [
"BTKUILib"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ASTExtension.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ASTExtension/",
"changelog": "- Rebuilt for CVR 2025r181",
"embedcolor": "#f61963"
}