avatar/Assets/VRCFury/Scripts/Editor/VF/Feature/ArmatureLinkBuilder.cs

463 lines
24 KiB
C#

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<ArmatureLink> {
[FeatureBuilderAction(FeatureOrder.ArmatureLinkBuilder)]
public void Apply() {
if (model.propBone == null) {
Debug.LogWarning("Root bone is null on armature link.");
return;
}
var mover = allBuildersInRun.OfType<ObjectMoveBuilder>().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<SkinnedMeshRenderer>(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<Transform>(true)
.ToImmutableHashSet();
var skinsUsingBonesInProp = avatarObject
.GetComponentsInChildren<SkinnedMeshRenderer>(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<Transform, Transform>();
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<SkinnedMeshRenderer>(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<ParentConstraint>();
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<ParentConstraint>();
if (p != null) Object.DestroyImmediate(p);
p = propBone.AddComponent<ParentConstraint>();
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<Component>()) {
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<IConstraint>(true)) {
UpdateConstraint(propBone, avatarBone, c);
}
}
private void UpdateConstraint(GameObject propBone, GameObject avatarBone, IConstraint constraint) {
List<ConstraintSource> sources = new List<ConstraintSource>();
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<VRCPhysBone>(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<VRCPhysBone>(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<Tuple<GameObject, GameObject>> mergeBones
= new Stack<Tuple<GameObject, GameObject>>();
// left=object to move | right=new parent
public readonly Stack<Tuple<GameObject, GameObject>> reparent
= new Stack<Tuple<GameObject, GameObject>>();
}
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<Tuple<GameObject, GameObject>>();
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<ScaleConstraint>() == null) return null;
var pConstraint = orig.GetComponent<ParentConstraint>();
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;
}
}
}