using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Animations; using UnityEngine.UIElements; using VF.Builder; using VF.Builder.Exceptions; using VF.Feature.Base; using VF.Inspector; using VF.Model.Feature; using VRC.SDK3.Dynamics.PhysBone.Components; using Object = UnityEngine.Object; namespace VF.Feature { public class ArmatureLinkBuilder : FeatureBuilder { [FeatureBuilderAction(FeatureOrder.ArmatureLinkBuilder)] public void Apply() { if (model.propBone == null) { Debug.LogWarning("Root bone is null on armature link."); return; } var mover = allBuildersInRun.OfType().First(); var links = GetLinks(); if (links == null) { return; } var linkMode = model.linkMode; if (linkMode == ArmatureLink.ArmatureLinkMode.Auto) { var usesBonesFromProp = false; foreach (var skin in avatarObject.GetComponentsInChildren(true)) { if (skin.transform.IsChildOf(links.propMain.transform)) continue; usesBonesFromProp |= skin.rootBone && skin.rootBone.IsChildOf(links.propMain.transform); usesBonesFromProp |= skin.bones.Any(bone => bone && bone.IsChildOf(links.propMain.transform)); } linkMode = usesBonesFromProp ? ArmatureLink.ArmatureLinkMode.SkinRewrite : ArmatureLink.ArmatureLinkMode.ReparentRoot; } var keepBoneOffsets = model.keepBoneOffsets2 == ArmatureLink.KeepBoneOffsets.Yes; if (model.keepBoneOffsets2 == ArmatureLink.KeepBoneOffsets.Auto) { keepBoneOffsets = linkMode == ArmatureLink.ArmatureLinkMode.ReparentRoot; } if (linkMode == ArmatureLink.ArmatureLinkMode.SkinRewrite) { var scalingFactor = model.skinRewriteScalingFactor; if (scalingFactor <= 0) { var avatarMainScale = Math.Abs(links.avatarMain.transform.lossyScale.x); var propMainScale = Math.Abs(links.propMain.transform.lossyScale.x); double GetError(int pow) => Math.Abs(propMainScale / Math.Pow(10, pow) - avatarMainScale); var scalingPowerWithLeastError = Enumerable.Range(-10, 21) .OrderBy(GetError) .First(); scalingFactor = (float)Math.Pow(10, scalingPowerWithLeastError); } Debug.Log("Detected scaling factor: " + scalingFactor); var scalingRequired = scalingFactor < 0.999 || scalingFactor > 1.001; if (scalingRequired) { var bonesInProp = links.propMain .GetComponentsInChildren(true) .ToImmutableHashSet(); var skinsUsingBonesInProp = avatarObject .GetComponentsInChildren(true) .Where(skin => skin.sharedMesh) .Where(skin => bonesInProp.Contains(skin.rootBone) || skin.bones.Any(b => bonesInProp.Contains(b))); foreach (var skin in skinsUsingBonesInProp) { var meshCopy = Object.Instantiate(skin.sharedMesh); VRCFuryAssetDatabase.SaveAsset(meshCopy, tmpDir, meshCopy.name); meshCopy.bindposes = Enumerable.Zip(skin.bones, meshCopy.bindposes, (a,b) => (a,b)) .Select(boneAndBindPose => { var bone = boneAndBindPose.a; var bindPose = boneAndBindPose.b; var isBoneMerged = links.mergeBones.Any(m => m.Item1 == bone.gameObject); if (!isBoneMerged) return bindPose; var rescaledBindPose = Matrix4x4.Scale(new Vector3(scalingFactor, scalingFactor, scalingFactor)) * bindPose; return rescaledBindPose; }) .ToArray(); EditorUtility.SetDirty(meshCopy); skin.sharedMesh = meshCopy; EditorUtility.SetDirty(skin); } } // First, move over all the "new children objects" that aren't bones foreach (var reparent in links.reparent) { var objectToMove = reparent.Item1; var newParent = reparent.Item2; // Move the object mover.Move( objectToMove, newParent, "vrcf_" + uniqueModelNum + "_" + objectToMove.name ); // Because we're adding new children, we need to ensure they are ignored by any existing physbones on the avatar. RemoveFromPhysbones(objectToMove); } // Now, update all the skinned meshes in the prop to use the avatar's bone objects var boneMapping = new Dictionary(); foreach (var mergeBone in links.mergeBones) { var propBone = mergeBone.Item1; var avatarBone = mergeBone.Item2; FailIfComponents(propBone); UpdatePhysbones(propBone, avatarBone); UpdateConstraints(propBone, avatarBone); boneMapping[propBone.transform] = avatarBone.transform; mover.AddDirectRewrite(propBone, avatarBone); } foreach (var skin in avatarObject.GetComponentsInChildren(true)) { if (skin.rootBone != null) { if (boneMapping.TryGetValue(skin.rootBone, out var newRootBone)) { skin.rootBone = newRootBone; } } var bones = skin.bones; for (var i = 0; i < bones.Length; i++) { if (bones[i] != null) { if (boneMapping.TryGetValue(bones[i], out var newBone)) { bones[i] = newBone; } } } skin.bones = bones; } foreach (var mergeBone in links.mergeBones) { var propBone = mergeBone.Item1; Object.DestroyImmediate(propBone); } } else if (linkMode == ArmatureLink.ArmatureLinkMode.MergeAsChildren || linkMode == ArmatureLink.ArmatureLinkMode.ReparentRoot) { var rootOnly = linkMode == ArmatureLink.ArmatureLinkMode.ReparentRoot; // Otherwise, we move all the prop bones into their matching avatar bones (as children) foreach (var mergeBone in links.mergeBones) { var propBone = mergeBone.Item1; var avatarBone = mergeBone.Item2; if (rootOnly) { if (propBone != model.propBone) { continue; } } else { FailIfComponents(propBone); UpdatePhysbones(propBone, avatarBone); } // Move the object var p = propBone.GetComponent(); if (p != null) Object.DestroyImmediate(p); mover.Move( propBone, avatarBone, "vrcf_" + uniqueModelNum + "_" + propBone.name ); if (!keepBoneOffsets) { propBone.transform.localPosition = Vector3.zero; propBone.transform.localRotation = Quaternion.identity; } // Because we're adding new children, we need to ensure they are ignored by any existing physbones on the avatar. RemoveFromPhysbones(propBone); } } else if (linkMode == ArmatureLink.ArmatureLinkMode.ParentConstraint) { foreach (var mergeBone in links.mergeBones) { var propBone = mergeBone.Item1; var avatarBone = mergeBone.Item2; var p = propBone.GetComponent(); if (p != null) Object.DestroyImmediate(p); p = propBone.AddComponent(); p.AddSource(new ConstraintSource() { sourceTransform = avatarBone.transform, weight = 1 }); p.weight = 1; p.constraintActive = true; p.locked = true; if (keepBoneOffsets) { Matrix4x4 inverse = Matrix4x4.TRS(avatarBone.transform.position, avatarBone.transform.rotation, new Vector3(1,1,1)).inverse; p.SetTranslationOffset(0, inverse.MultiplyPoint3x4(p.transform.position)); p.SetRotationOffset(0, (Quaternion.Inverse(avatarBone.transform.rotation) * p.transform.rotation).eulerAngles); } } } } private void FailIfComponents(GameObject propBone) { foreach (var c in propBone.GetComponents()) { if (c is Transform) { } else if (c is ParentConstraint) { Object.DestroyImmediate(c); } else { var path = clipBuilder.GetPath(propBone); throw new VRCFBuilderException( "Prop bone " + path + " contains a " + c.GetType().Name + " component" + " which would be lost during Armature Link because the bone is being merged." + " If this component needs to be kept, it should be moved to a child object."); } } } private void UpdateConstraints(GameObject propBone, GameObject avatarBone) { foreach (var c in avatarObject.GetComponentsInChildren(true)) { UpdateConstraint(propBone, avatarBone, c); } } private void UpdateConstraint(GameObject propBone, GameObject avatarBone, IConstraint constraint) { List sources = new List(); constraint.GetSources(sources); var changed = false; for (var i = 0; i < sources.Count; i++) { if (sources[i].sourceTransform == propBone.transform) { var newSource = sources[i]; newSource.sourceTransform = avatarBone.transform; sources[i] = newSource; changed = true; } } if (changed) { constraint.SetSources(sources); } // TODO: Update the rest offsets if the bone moved as a result of the merge } private void UpdatePhysbones(GameObject propBone, GameObject avatarBone) { foreach (var physbone in avatarObject.GetComponentsInChildren(true)) { var root = physbone.GetRootTransform(); if (propBone.transform == root) { if (model.physbonesOnAvatarBones) { physbone.rootTransform = avatarBone.transform; } else { var physbonePath = clipBuilder.GetPath(physbone.gameObject); throw new VRCFBuilderException( "Physbone " + physbonePath + " points to a bone that is going to" + " stop existing because it is being merged into the avatar using Armature Link." + " If this physbone needs to exist, it should be placed on a new child object of the linked bone."); } } } } private void RemoveFromPhysbones(GameObject obj) { foreach (var physbone in avatarObject.GetComponentsInChildren(true)) { var root = physbone.GetRootTransform(); if (obj.transform != root && obj.transform.IsChildOf(root)) { physbone.ignoreTransforms.Add(obj.transform); } } } private class Links { // These are stacks, because it's convenient, and we want to iterate over them in reverse order anyways // because when operating on the vrc clone, we delete game objects as we process them, and we want to // delete the children first. public GameObject propMain; public GameObject avatarMain; // left=bone in prop | right=bone in avatar public readonly Stack> mergeBones = new Stack>(); // left=object to move | right=new parent public readonly Stack> reparent = new Stack>(); } private Links GetLinks() { var propBone = model.propBone; if (propBone == null) return null; GameObject avatarBone = null; if (string.IsNullOrWhiteSpace(model.bonePathOnAvatar)) { avatarBone = VRCFArmatureUtils.FindBoneOnArmature(avatarObject, model.boneOnAvatar); if (!avatarBone) { foreach (var fallback in model.fallbackBones) { avatarBone = VRCFArmatureUtils.FindBoneOnArmature(avatarObject, fallback); if (avatarBone) break; } } if (!avatarBone) { throw new VRCFBuilderException( "ArmatureLink failed to find " + model.boneOnAvatar + " bone on avatar."); } } else { avatarBone = avatarObject.transform.Find(model.bonePathOnAvatar)?.gameObject; if (avatarBone == null) { Debug.LogError("Failed to find " + model.bonePathOnAvatar + " bone on avatar. Skipping armature link."); return null; } } var removeBoneSuffix = model.removeBoneSuffix; if (string.IsNullOrWhiteSpace(model.removeBoneSuffix)) { if (propBone.name.Contains(avatarBone.name) && propBone.name != avatarBone.name) { removeBoneSuffix = propBone.name.Replace(avatarBone.name, ""); } } var links = new Links(); var checkStack = new Stack>(); checkStack.Push(Tuple.Create(propBone, avatarBone)); links.mergeBones.Push(Tuple.Create(propBone, avatarBone)); while (checkStack.Count > 0) { var check = checkStack.Pop(); foreach (Transform child in check.Item1.transform) { var childPropBone = child.gameObject; var searchName = childPropBone.name; if (!string.IsNullOrWhiteSpace(removeBoneSuffix)) { searchName = searchName.Replace(removeBoneSuffix, ""); } var childAvatarBone = check.Item2.transform.Find(searchName)?.gameObject; // Hack for Rexouium model, which added ChestUp bone at some point and broke a ton of old props if (childAvatarBone == null) { childAvatarBone = check.Item2.transform.Find("ChestUp/" + searchName)?.gameObject; } if (childAvatarBone != null) { var marshmallowChild = GetMarshmallowChild(childAvatarBone); if (marshmallowChild != null) childAvatarBone = marshmallowChild; } if (childAvatarBone != null) { links.mergeBones.Push(Tuple.Create(childPropBone, childAvatarBone)); checkStack.Push(Tuple.Create(childPropBone, childAvatarBone)); } else { links.reparent.Push(Tuple.Create(childPropBone, check.Item2)); } } } links.propMain = propBone; links.avatarMain = avatarBone; return links; } // Marshmallow PB unity package inserts fake bones in the armature, breaking our link. // Detect if this happens, and return the proper child bone instead. private static GameObject GetMarshmallowChild(GameObject orig) { if (orig.GetComponent() == null) return null; var pConstraint = orig.GetComponent(); if (pConstraint == null) return null; if (pConstraint.sourceCount != 1) return null; var source = pConstraint.GetSource(0); if (source.sourceTransform == null) return null; if (!source.sourceTransform.name.Contains("Constraint")) return null; var child = orig.transform.Find(orig.name); if (!child) return null; return child.gameObject; } public override string GetEditorTitle() { return "Armature Link"; } public override VisualElement CreateEditor(SerializedProperty prop) { var container = new VisualElement(); container.Add(VRCFuryEditorUtils.Info( "This feature will link an armature in a prop to the armature on the avatar base." + " It can also be used to link a single object in the prop to a certain bone on the avatar's armature.")); container.Add(VRCFuryEditorUtils.WrappedLabel("Root bone/object in the prop:")); container.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("propBone"))); container.Add(new VisualElement { style = { paddingTop = 10 } }); container.Add(VRCFuryEditorUtils.WrappedLabel("Corresponding root bone on avatar:")); container.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("boneOnAvatar"))); container.Add(new VisualElement { style = { paddingTop = 10 } }); var adv = new Foldout { text = "Advanced Options", value = false }; container.Add(adv); adv.Add(VRCFuryEditorUtils.WrappedLabel("Link Mode:")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Skin Rewrite) Rewrites skinned meshes to use avatar's own bones. Excellent performance, but breaks some clothing.")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Merge as Children) Makes prop bones into children of the avatar's bones. Medium performance, but often works when Skin Rewrite doesn't.")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Reparent Root) The prop object is moved into the avatar's bone. No other merging takes place.")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Bone Constraint) Adds a parent constraint to every prop bone, linking it to the avatar bone. Awful performance, pretty much never use this.")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Auto) Selects Skin Rewrite if a mesh uses bones from the prop armature, or Reparent Root otherwise.")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("linkMode"), formatEnum: str => { if (str == ArmatureLink.ArmatureLinkMode.SkinRewrite.ToString()) { return "Skin Rewrite"; } else if (str == ArmatureLink.ArmatureLinkMode.MergeAsChildren.ToString()) { return "Merge as Children"; } else if (str == ArmatureLink.ArmatureLinkMode.ReparentRoot.ToString()) { return "Reparent Root"; } else if (str == ArmatureLink.ArmatureLinkMode.ParentConstraint.ToString()) { return "Bone Constraint"; } else if (str == ArmatureLink.ArmatureLinkMode.Auto.ToString()) { return "Auto"; } return str; })); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("Remove bone suffix/prefix:")); adv.Add(VRCFuryEditorUtils.WrappedLabel("If set, this substring will be removed from all bone names in the prop. This is useful for props where the artist added " + "something like _PropName to the end of every bone, breaking AvatarLink in the process. If empty, the suffix will be predicted " + "based on the difference between the name of the given root bones.")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("removeBoneSuffix"))); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("String path to bone on avatar:")); adv.Add(VRCFuryEditorUtils.WrappedLabel("If provided, humanoid bone dropdown will be ignored.")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("bonePathOnAvatar"))); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("Keep bone offsets:")); adv.Add(VRCFuryEditorUtils.WrappedLabel( "If no, linked bones will be rigidly locked to the transform of the corresponding avatar bone.")); adv.Add(VRCFuryEditorUtils.WrappedLabel( "If yes, prop bones will maintain their initial offset to the corresponding avatar bone. This is unusual.")); adv.Add(VRCFuryEditorUtils.WrappedLabel( "If auto, offsets will be kept only if Reparent Root link mode is used.")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("keepBoneOffsets2"))); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("Allow prop physbones to target avatar bone transforms (unusual):")); adv.Add(VRCFuryEditorUtils.WrappedLabel("If checked, physbones in the prop pointing to bones on the avatar will be updated " + "to point to the corresponding bone on the base armature. This is extremely unusual. Don't use this " + "unless you know what you are doing.")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("physbonesOnAvatarBones"))); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("Fallback bones:")); adv.Add(VRCFuryEditorUtils.WrappedLabel("If the given bone cannot be found on the avatar, these bones will also be attempted before failing.")); adv.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("fallbackBones"))); adv.Add(new VisualElement { style = { paddingTop = 10 } }); adv.Add(VRCFuryEditorUtils.WrappedLabel("Skin rewrite scaling factor:")); adv.Add(VRCFuryEditorUtils.WrappedLabel("(Will automatically detect scaling factor if 0)")); adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("skinRewriteScalingFactor"))); return container; } } }