Last active
February 4, 2024 21:49
-
-
Save Ooseykins/0e037914360e33cde0327979a2c3dfdc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Collections.Generic; | |
using System.Linq; | |
using BepInEx; | |
using UniVRM10; | |
using GameNetcodeStuff; | |
using UniGLTF; | |
using UnityEngine; | |
using UnityEngine.Rendering; | |
using UnityEngine.SceneManagement; | |
namespace LethalVRM | |
{ | |
public class LethalVRMManager : MonoBehaviour | |
{ | |
private const string MODEL_PATH = "VRMs"; | |
private HashSet<PlayerControllerB> players = new(); | |
private Dictionary<PlayerControllerB, ulong> playerIds = new(); | |
private HashSet<LethalVRMInstance> instances = new (); | |
private bool requirePlayerUpdate; | |
private GameObject playerPrefab; | |
private float playerPrefabHeight; | |
private Material HDRP_BaseMaterial; | |
private void Awake() | |
{ | |
string baseBundlePath = Path.Combine(Paths.PluginPath, "lethalvrm"); | |
string pluginBundlePath = Path.Combine(Paths.PluginPath, "Ooseykins-LethalVRM/lethalvrm"); | |
AssetBundle bundle = null; | |
if (File.Exists(baseBundlePath)) | |
{ | |
bundle = AssetBundle.LoadFromFile(baseBundlePath); | |
} | |
else if (File.Exists(pluginBundlePath)) | |
{ | |
bundle = AssetBundle.LoadFromFile(pluginBundlePath); | |
} | |
if (bundle == null) | |
{ | |
enabled = false; | |
Debug.LogError("LethalVRM failed to load it's asset bundle, this mod will not function"); | |
return; | |
} | |
// The MToon replacement shader has all the same texture properties as the regular MToon shader, but they are unused. | |
HDRPVrm10MToonMaterialImporter.MToonReplacementShader = (Shader)bundle.LoadAsset("MToonParameterShader"); | |
if (HDRPVrm10MToonMaterialImporter.MToonReplacementShader == null) | |
{ | |
enabled = false; | |
Debug.LogError("LethalVRM failed to load the MToon replacement shader, this mod will not function"); | |
return; | |
} | |
// Trigger to check for new player controllers on scene load | |
SceneManager.sceneLoaded += (_,_) => SceneLoad(); | |
// Keep this object alive forever, through scene unloads and more | |
gameObject.hideFlags = HideFlags.HideAndDontSave; | |
// This mod requires this path to be accessible | |
if (!Directory.Exists(MODEL_PATH)) | |
{ | |
Directory.CreateDirectory(MODEL_PATH); | |
} | |
if (!Directory.Exists(MODEL_PATH)) | |
{ | |
enabled = false; | |
Debug.LogError("LethalVRM failed to create directory for models, this mod will not function"); | |
return; | |
} | |
} | |
private void LateUpdate() | |
{ | |
FindUpdatedIDs(); | |
FindMaskEnemies(); | |
AnimateBonePairs(); | |
if (requirePlayerUpdate) | |
{ | |
FindPlayerControllers(); | |
requirePlayerUpdate = false; | |
} | |
} | |
void SceneLoad() | |
{ | |
FindPlayerControllers(); | |
PreparePrefabs(); | |
requirePlayerUpdate = true; | |
} | |
void PreparePrefabs() | |
{ | |
// Prepare the T-pose player prefab, this will live until the application quits | |
if (playerPrefab == null) | |
{ | |
var basePlayer = players.FirstOrDefault(x => x.name == "Player"); | |
if (basePlayer != null) | |
{ | |
basePlayer.gameObject.SetActive(false); | |
playerPrefab = Instantiate(basePlayer.gameObject); | |
playerPrefab.name = "VRM T Pose Match"; | |
playerPrefab.hideFlags = HideFlags.HideAndDontSave; | |
var HDRP_CutoffMaterial = GameObject.Find("CatwalkShip").GetComponent<MeshRenderer>().material; | |
HDRP_BaseMaterial = new Material(HDRP_CutoffMaterial); | |
HDRP_BaseMaterial.SetFloat("_Smoothness", 0f); | |
HDRP_BaseMaterial.SetFloat("_Metallic", 0f); | |
HDRP_BaseMaterial.SetTextureScale("_BaseColorMap",Vector2.one); | |
basePlayer.gameObject.SetActive(true); | |
playerPrefab.GetComponentInChildren<Animator>().enabled = false; | |
playerPrefab.transform.FindDescendant("arm.L_upper").localRotation = Quaternion.Euler(0,90,-10); | |
playerPrefab.transform.FindDescendant("arm.R_upper").localRotation = Quaternion.Euler(-10,0,0); | |
playerPrefab.transform.FindDescendant("thigh.L").localRotation = Quaternion.Euler(20,180,180); | |
playerPrefab.transform.FindDescendant("thigh.R").localRotation = Quaternion.Euler(20,180,180); | |
playerPrefab.transform.FindDescendant("hand.R").localRotation = Quaternion.Euler(0,90,0); | |
playerPrefab.transform.FindDescendant("hand.L").localRotation = Quaternion.Euler(0,270,0); | |
var footL = playerPrefab.transform.FindDescendant("foot.L"); | |
var footR = playerPrefab.transform.FindDescendant("foot.R"); | |
var head = playerPrefab.transform.FindDescendant("spine.004_end"); | |
var p1 = new Vector3(0, ((footL.position+footR.position)/2).y, 0); | |
var p2 = new Vector3(0, head.position.y, 0); | |
playerPrefabHeight = Vector3.Distance(p1, p2); | |
playerPrefab.transform.rotation = Quaternion.identity; | |
Debug.Log($"LethalVRM base prefab set to {playerPrefab.name}, has a height of {playerPrefabHeight:0.###}"); | |
} | |
} | |
} | |
void FindPlayerControllers() | |
{ | |
players = FindObjectsOfType<PlayerControllerB>(true).ToHashSet(); | |
Debug.Log($"LethalVRM found {players.Count} player controllers"); | |
} | |
class LethalVRMInstance | |
{ | |
private const int firstPersonLayer = 23; | |
private const int thirdPersonLayer = 0; | |
public Vrm10Instance Vrm10Instance; | |
public PlayerControllerB PlayerControllerB; | |
public HashSet<(Transform target, Transform source, Quaternion localRotation)> boneTranslation = new(); | |
public HashSet<Renderer> renderers = new(); | |
public Dictionary<Transform, Transform> deadMap; | |
public Transform deadBodyRoot; | |
public float hipOffset; | |
public void SetSkeletonMimic(Transform root) | |
{ | |
deadBodyRoot = root; | |
deadMap = new(); | |
if (PlayerControllerB.deadBody != null && PlayerControllerB.deadBody.transform == root) | |
{ | |
root.name = "spine"; | |
} | |
foreach (var t in boneTranslation) | |
{ | |
var srcT = root.FindDescendant(t.source.parent.name); | |
var newBone = new GameObject("VRM Rotation Bone").transform; | |
newBone.parent = srcT; | |
newBone.position = srcT.position; | |
newBone.localRotation = t.localRotation; | |
deadMap[t.source] = newBone; | |
} | |
foreach (var r in root.GetComponentsInChildren<Renderer>()) | |
{ | |
if ((PlayerControllerB.deadBody != null && PlayerControllerB.deadBody.transform == root) || r.name is "LOD1" or "LOD2" or "LOD3" or "LevelSticker" or "BetaBadge") | |
{ | |
r.enabled = false; | |
} | |
} | |
} | |
public void UpdateVisibility() | |
{ | |
var deadShouldRender = !PlayerControllerB.isPlayerDead || (deadBodyRoot != null && PlayerControllerB.deadBody != null); | |
var localShouldRender = !PlayerControllerB.gameplayCamera.enabled; | |
foreach (var r in renderers) | |
{ | |
r.gameObject.layer = localShouldRender ? thirdPersonLayer : firstPersonLayer; | |
r.enabled = deadShouldRender; | |
} | |
} | |
} | |
void FindUpdatedIDs() | |
{ | |
foreach (var p in players) | |
{ | |
if (p.playerSteamId == 0) | |
{ | |
continue; | |
} | |
// Remove instances and dict entries for disconnected players | |
if (playerIds.ContainsKey(p) && (p.disconnectedMidGame || (p.NetworkObject.OwnerClientId == 0 && p.name != "Player"))) | |
{ | |
foreach (var i in instances) | |
{ | |
if (i.PlayerControllerB == p) | |
{ | |
Destroy(i.Vrm10Instance.gameObject); | |
} | |
} | |
instances.RemoveWhere(x => x.PlayerControllerB == p); | |
playerIds.Remove(p); | |
continue; | |
} | |
if (p.NetworkObject.OwnerClientId == 0 && p.name != "Player") | |
{ | |
continue; | |
} | |
// Add new players to the Id dict. Try and load models for the steamId | |
if (!playerIds.ContainsKey(p) || playerIds[p] != p.playerSteamId) | |
{ | |
playerIds[p] = p.playerSteamId; | |
File.WriteAllText($"{MODEL_PATH}/{p.playerSteamId}_{p.playerUsername}.txt",$"{p.playerSteamId} seen as {p.playerUsername}"); | |
if (instances.Any(x => x.PlayerControllerB.playerSteamId == p.playerSteamId)) | |
{ | |
continue; | |
} | |
string path = $"{MODEL_PATH}/{p.playerSteamId}.vrm"; | |
if (File.Exists(path)) | |
{ | |
Debug.Log($"LethalVRM trying to load model for path {path}"); | |
LoadModelToPlayer(path, p); | |
} | |
else | |
{ | |
Debug.Log($"LethalVRM trying to load model for path {path}, no such file exists"); | |
} | |
} | |
} | |
} | |
void FindMaskEnemies() | |
{ | |
var masks = FindObjectsByType<MaskedPlayerEnemy>(sortMode: FindObjectsSortMode.None); | |
foreach (var m in masks) | |
{ | |
if (m.transform == null || m.mimickingPlayer == null) | |
{ | |
continue; | |
} | |
foreach (var i in instances) | |
{ | |
if (m.mimickingPlayer == i.PlayerControllerB && i.deadBodyRoot != m.transform) | |
{ | |
i.SetSkeletonMimic(m.transform); | |
} | |
} | |
} | |
} | |
async void LoadModelToPlayer(string path, PlayerControllerB player) | |
{ | |
// Let VRM do it's thing, it spits all kinds of errors if things go wrong | |
var instance = await Vrm10.LoadPathAsync(path); | |
if (instance == null) | |
{ | |
enabled = false; | |
Debug.LogError($"LethalVRM had an error loading the VRM at {path}, this mod will not function"); | |
return; | |
} | |
instance.name = $"LethalVRM Character Model {player.playerUsername} {player.playerSteamId}"; | |
instance.transform.position = player.transform.position; | |
// Create instance for LethalVRM | |
LethalVRMInstance newInstance = new LethalVRMInstance(); | |
// Replace VRM materials with Lethal Company shader materials | |
if (HDRP_BaseMaterial == null) | |
{ | |
enabled = false; | |
Debug.LogError("LethalVRM had some error loading the Lethal Company shader material, this mod will not function"); | |
return; | |
} | |
foreach (var r in instance.GetComponentsInChildren<Renderer>(true)) | |
{ | |
newInstance.renderers.Add(r); | |
r.receiveShadows = true; | |
r.shadowCastingMode = ShadowCastingMode.TwoSided; | |
if (r is SkinnedMeshRenderer skinnedMeshRenderer) | |
{ | |
skinnedMeshRenderer.updateWhenOffscreen = true; | |
} | |
Material[] newMaterials = new Material[r.materials.Length]; | |
for(int i = 0; i < r.materials.Length; i++) | |
{ | |
var m = r.materials[i]; | |
Material newM = new Material(HDRP_BaseMaterial); | |
newM.name = m.name; | |
newM.mainTexture = m.mainTexture; | |
if (m.HasProperty("_M_CullMode")) | |
{ | |
newM.SetFloat("_CullMode", m.GetFloat("_M_CullMode")); | |
} | |
if (m.HasProperty("_BumpMap")) | |
{ | |
newM.SetTexture("_NormalMap", m.GetTexture("_BumpMap")); | |
} | |
newMaterials[i] = newM; | |
} | |
r.materials = newMaterials; | |
} | |
// Disable the VRM animators | |
Animator a = instance.Runtime.ControlRig.ControlRigAnimator; | |
a.enabled = false; | |
instance.Runtime.VrmAnimation = null; | |
// Transform names -> Unity bone names | |
(string name, HumanBodyBones bone)[] boneNames = new[] | |
{ | |
("spine", HumanBodyBones.Hips), | |
("spine.001", HumanBodyBones.Spine), | |
("spine.002", HumanBodyBones.Chest), | |
("spine.003", HumanBodyBones.UpperChest), | |
("spine.004", HumanBodyBones.Neck), | |
("shoulder.R", HumanBodyBones.RightShoulder), | |
("arm.R_upper", HumanBodyBones.RightUpperArm), | |
("arm.R_lower", HumanBodyBones.RightLowerArm), | |
("hand.R", HumanBodyBones.RightHand), | |
("shoulder.L", HumanBodyBones.LeftShoulder), | |
("arm.L_upper", HumanBodyBones.LeftUpperArm), | |
("arm.L_lower", HumanBodyBones.LeftLowerArm), | |
("hand.L", HumanBodyBones.LeftHand), | |
("thigh.R", HumanBodyBones.RightUpperLeg), | |
("shin.R", HumanBodyBones.RightLowerLeg), | |
("foot.R", HumanBodyBones.RightFoot), | |
("thigh.L", HumanBodyBones.LeftUpperLeg), | |
("shin.L", HumanBodyBones.LeftLowerLeg), | |
("foot.L", HumanBodyBones.LeftFoot), | |
///////////////////// | |
("finger1.L", HumanBodyBones.LeftThumbProximal), | |
("finger1.L.001", HumanBodyBones.LeftThumbIntermediate), | |
("finger1.L.001_end", HumanBodyBones.LeftThumbDistal), | |
("finger2.L", HumanBodyBones.LeftIndexProximal), | |
("finger2.L.001", HumanBodyBones.LeftIndexIntermediate), | |
("finger2.L.001_end", HumanBodyBones.LeftIndexDistal), | |
("finger3.L", HumanBodyBones.LeftMiddleProximal), | |
("finger3.L.001", HumanBodyBones.LeftMiddleIntermediate), | |
("finger3.L.001_end", HumanBodyBones.LeftMiddleDistal), | |
("finger4.L", HumanBodyBones.LeftRingProximal), | |
("finger4.L.001", HumanBodyBones.LeftRingIntermediate), | |
("finger4.L.001_end", HumanBodyBones.LeftRingDistal), | |
("finger5.L", HumanBodyBones.LeftLittleProximal), | |
("finger5.L.001", HumanBodyBones.LeftLittleIntermediate), | |
("finger5.L.001_end", HumanBodyBones.LeftLittleDistal), | |
///////////////////// | |
("finger1.R", HumanBodyBones.RightThumbProximal), | |
("finger1.R.001", HumanBodyBones.RightThumbIntermediate), | |
("finger1.R.001_end", HumanBodyBones.RightThumbDistal), | |
("finger2.R", HumanBodyBones.RightIndexProximal), | |
("finger2.R.001", HumanBodyBones.RightIndexIntermediate), | |
("finger2.R.001_end", HumanBodyBones.RightIndexDistal), | |
("finger3.R", HumanBodyBones.RightMiddleProximal), | |
("finger3.R.001", HumanBodyBones.RightMiddleIntermediate), | |
("finger3.R.001_end", HumanBodyBones.RightMiddleDistal), | |
("finger4.R", HumanBodyBones.RightRingProximal), | |
("finger4.R.001", HumanBodyBones.RightRingIntermediate), | |
("finger4.R.001_end", HumanBodyBones.RightRingDistal), | |
("finger5.R", HumanBodyBones.RightLittleProximal), | |
("finger5.R.001", HumanBodyBones.RightLittleIntermediate), | |
("finger5.R.001_end", HumanBodyBones.RightLittleDistal), | |
}; | |
// Add extra bones to each player bone to use as reference for world angles | |
// Better way to do this? Probably but I'm not good at math | |
if (playerPrefab == null) | |
{ | |
enabled = false; | |
Debug.LogError("LethalVRM failed to find the player prefab, this mod will not function"); | |
return; | |
} | |
HashSet<(Transform target, Transform source, Quaternion localRotation)> boneTranslation = new(); | |
foreach (var p in boneNames) | |
{ | |
var targetT = a.GetBoneTransform(p.bone); | |
if (targetT == null) | |
{ | |
Debug.Log($"LethalVRM {path} missing bone {p.bone} ({p.name})"); | |
continue; | |
} | |
var srcT = player.transform.FindDescendant(p.name); | |
var poseT = playerPrefab.transform.FindDescendant(p.name); | |
var newBone = new GameObject("VRM Rotation Bone").transform; | |
newBone.parent = poseT; | |
newBone.position = poseT.position; | |
newBone.rotation = targetT.rotation; | |
var localRotation = newBone.localRotation; | |
newBone.parent = srcT; | |
newBone.position = srcT.position; | |
newBone.localRotation = localRotation; | |
boneTranslation.Add((targetT,newBone, localRotation)); | |
} | |
// Calculate VRM height for scaling the player to the correct size | |
Vector3 p1 = new Vector3(0, instance.Humanoid.Head.position.y, 0); | |
Vector3 p2 = new Vector3(0, instance.transform.position.y, 0); | |
float height = Vector3.Distance(p1,p2); | |
float playerScale = playerPrefabHeight/height; | |
instance.transform.localScale = new Vector3(playerScale,playerScale,playerScale); | |
Debug.Log($"LethalVRM {path} has a height of: {height:0.###}, scaling to {playerScale:0.###}"); | |
// Calculate distance from feet to hips to offset the player hips for different leg lengths | |
float vrmHipHeight = Vector3.Distance(instance.Humanoid.Hips.position, | |
(instance.Humanoid.LeftFoot.position + instance.Humanoid.RightFoot.position) / 2f); | |
var head = playerPrefab.transform.FindDescendant("spine"); | |
var leftFoot = playerPrefab.transform.FindDescendant("foot.L"); | |
var rightFoot = playerPrefab.transform.FindDescendant("foot.R"); | |
float lethalHipHeight = Vector3.Distance(head.position, (leftFoot.position+rightFoot.position)/2f); | |
// Set player renderer visibility, done by name to prevent hiding special renderers like first-person arms etc | |
player.transform.FindDescendant("LOD1").gameObject.SetActive(false); | |
player.transform.FindDescendant("LOD2").gameObject.SetActive(false); | |
player.transform.FindDescendant("LOD3").gameObject.SetActive(false); | |
player.transform.FindDescendant("LevelSticker").gameObject.SetActive(false); | |
player.transform.FindDescendant("BetaBadge").gameObject.SetActive(false); | |
// Add new player instance to the set | |
newInstance.Vrm10Instance = instance; | |
newInstance.PlayerControllerB = player; | |
newInstance.hipOffset = vrmHipHeight - lethalHipHeight; | |
newInstance.boneTranslation = boneTranslation; | |
instances.Add(newInstance); | |
Debug.Log($"LethalVRM finished loading {path}"); | |
} | |
void AnimateBonePairs() | |
{ | |
List<LethalVRMInstance> toRemove = new List<LethalVRMInstance>(); | |
foreach (var i in instances) | |
{ | |
// Remove instance if the player is destroyed | |
if (i.Vrm10Instance == null || i.PlayerControllerB == null) | |
{ | |
toRemove.Add(i); | |
continue; | |
} | |
// Remove the dead body if root is destroyed | |
if (i.deadBodyRoot == null) | |
{ | |
i.deadMap = null; | |
i.deadBodyRoot = null; | |
} | |
// Remove instance if the player lost some bones | |
bool exit = false; | |
foreach (var t in i.boneTranslation) | |
{ | |
if (t.target == null || t.source == null) | |
{ | |
toRemove.Add(i); | |
exit = true; | |
break; | |
} | |
// If a dead body bone is missing, clear the dead body | |
if (i.deadMap != null && i.deadMap[t.source] == null) | |
{ | |
i.deadMap = null; | |
i.deadBodyRoot = null; | |
} | |
} | |
if (exit) | |
{ | |
continue; | |
} | |
// Clear the dead body if the player is alive or the dead body no longer exists | |
if (!i.PlayerControllerB.isPlayerDead || i.deadBodyRoot == null) | |
{ | |
i.deadMap = null; | |
i.deadBodyRoot = null; | |
} | |
// Prepare dead body if it changed | |
if (i.PlayerControllerB.isPlayerDead && i.deadBodyRoot == null) | |
{ | |
if (i.PlayerControllerB.deadBody != null) | |
{ | |
i.SetSkeletonMimic(i.PlayerControllerB.deadBody.transform); | |
} | |
} | |
// Position bones | |
foreach (var t in i.boneTranslation) | |
{ | |
t.target.rotation = i.deadMap != null ? i.deadMap[t.source].rotation : t.source.rotation; | |
if (t.source.parent.name == "spine") | |
{ | |
t.target.position = i.deadMap != null ? i.deadMap[t.source].position : t.source.position; | |
t.target.position += t.target.up * i.hipOffset; | |
} | |
} | |
// Set layer and renderer visibility | |
i.UpdateVisibility(); | |
} | |
// Remove instances flagged for deletion | |
foreach (var i in toRemove) | |
{ | |
instances.Remove(i); | |
} | |
} | |
} | |
} |
In response to the comments from lines 397 & 398.
Here's the math way to do it.
Assuming that we are using this Quaternion extensions class :
public static class QuaternionExtensions
{
public static Quaternion Diff(this Quaternion fromSource, Quaternion toTarget)
{
return Quaternion.Inverse(fromSource) * toTarget;
}
public static Quaternion Add(this Quaternion fromSource, Quaternion diff)
{
return fromSource * diff;
}
}
It can be used as such (sca = Scavenger Model & vrm = VRM Model & diff = Offset) :
// Calculate the offset.
Transform scaTPoseBone;
Transform vrmTPoseBone;
Quaternion diff = scaTPoseBone.rotation.Diff(vrmBoneTPose.rotation);
// Apply the the offset
Transform scaSourceBone;
Transform vrmTargetBone;
vrmTargetBone.rotation = scaSourceBone.rotation.Add(diff);
This is where I found the information from the user luisfinke's post : https://forum.unity.com/threads/get-the-difference-between-two-quaternions-and-add-it-to-another-quaternion.513187/#post-8187513
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey! Would it be possible to add these 2 things?
-Adding an overall scale modifier using the generated .txt file (or an in-game setting) instead of re-exporting the VRM itself
-Adding support for the Company Issued Protogen mod, where it deletes the protogen model if the player has a VRM model (currently it displays both the VRM and protogen model on the same player, and looks very jank)