diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs b/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs new file mode 100644 index 00000000..4de068ea --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs @@ -0,0 +1,9 @@ +namespace net.fushizen.modular_avatar.core.editor +{ + internal static class HookSequence + { + public const int SEQ_RESETTERS = -90000; + public const int SEQ_MERGE_ARMATURE = -80001; + public const int SEQ_RETARGET_MESH = -80000; + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs.meta b/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs.meta new file mode 100644 index 00000000..ec3e754e --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/HookSequence.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9e6b3680d07242d38d5b2c6b00951ca0 +timeCreated: 1661632859 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/MergeArmatureHook.cs b/Packages/net.fushizen.modular-avatar.core/Editor/MergeArmatureHook.cs index 2b77bfb3..2064343a 100644 --- a/Packages/net.fushizen.modular-avatar.core/Editor/MergeArmatureHook.cs +++ b/Packages/net.fushizen.modular-avatar.core/Editor/MergeArmatureHook.cs @@ -7,11 +7,11 @@ namespace net.fushizen.modular_avatar.core.editor { public class MergeArmatureHook : IVRCSDKPreprocessAvatarCallback { - public int callbackOrder => -3000; + public int callbackOrder => HookSequence.SEQ_MERGE_ARMATURE; public bool OnPreprocessAvatar(GameObject avatarGameObject) { - var mergeArmatures = avatarGameObject.transform.GetComponentsInChildren(); + var mergeArmatures = avatarGameObject.transform.GetComponentsInChildren(true); foreach (var mergeArmature in mergeArmatures) { @@ -32,6 +32,7 @@ namespace net.fushizen.modular_avatar.core.editor private void RecursiveMerge(ModularAvatarMergeArmature config, GameObject src, GameObject target) { + BoneDatabase.AddMergedBone(src.transform); src.transform.SetParent(target.transform, true); List children = new List(); foreach (Transform child in src.transform) diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs b/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs new file mode 100644 index 00000000..0517b3ee --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs @@ -0,0 +1,176 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.SDKBase.Editor.BuildPipeline; + +namespace net.fushizen.modular_avatar.core.editor +{ + internal class MeshRetargeterResetHook : IVRCSDKPreprocessAvatarCallback + { + public int callbackOrder => HookSequence.SEQ_RESETTERS; + public bool OnPreprocessAvatar(GameObject avatarGameObject) + { + BoneDatabase.ResetBones(); + return true; + } + } + + internal static class BoneDatabase + { + private static Dictionary IsRetargetable = new Dictionary(); + + internal static void ResetBones() + { + IsRetargetable.Clear(); + } + + internal static void AddMergedBone(Transform bone) + { + IsRetargetable[bone] = true; + } + + internal static void MarkNonRetargetable(Transform bone) + { + if (IsRetargetable.ContainsKey(bone)) IsRetargetable[bone] = false; + } + + internal static Transform GetRetargetedBone(Transform bone) + { + if (!IsRetargetable.ContainsKey(bone)) return null; + + while (bone != null && IsRetargetable.ContainsKey(bone) && IsRetargetable[bone]) bone = bone.parent; + + if (IsRetargetable.ContainsKey(bone)) return null; + return bone; + } + + internal static IEnumerable> GetRetargetedBones() + { + return IsRetargetable.Where((kvp) => kvp.Value) + .Select(kvp => new KeyValuePair(kvp.Key, GetRetargetedBone(kvp.Key))) + .Where(kvp => kvp.Value != null); + } + } + + internal class RetargetMeshes : IVRCSDKPreprocessAvatarCallback + { + public int callbackOrder => HookSequence.SEQ_RETARGET_MESH; + public bool OnPreprocessAvatar(GameObject avatarGameObject) + { + foreach (var renderer in avatarGameObject.GetComponentsInChildren(true)) + { + bool isRetargetable = false; + foreach (var bone in renderer.bones) + { + if (BoneDatabase.GetRetargetedBone(bone) != null) + { + isRetargetable = true; + break; + } + } + + if (isRetargetable) + { + new MeshRetargeter(renderer).Retarget(); + } + } + + // Now remove retargeted bones + foreach (var bonePair in BoneDatabase.GetRetargetedBones()) + { + var sourceBone = bonePair.Key; + var destBone = bonePair.Value; + + foreach (Transform child in sourceBone) + { + child.SetParent(destBone, true); + } + + UnityEngine.Object.DestroyImmediate(sourceBone.gameObject); + } + + return true; + } + } + + /** + * This class processes a given mesh, adjusting the bind poses for any bones that are to be merged to instead match + * the bind pose of the original avatar's bone. + */ + public class MeshRetargeter + { + private readonly SkinnedMeshRenderer renderer; + private Mesh src, dst; + + struct BindInfo + { + public Matrix4x4 priorLocalToBone; + public Matrix4x4 localToBone; + public Matrix4x4 priorToNew; + } + + public MeshRetargeter(SkinnedMeshRenderer renderer) + { + this.renderer = renderer; + } + + public void Retarget() + { + + var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform); + if (avatar == null) throw new System.Exception("Could not find avatar in parents of " + renderer.name); + var avatarTransform = avatar.transform; + + var avPos = avatarTransform.position; + var avRot = avatarTransform.rotation; + var avScale = avatarTransform.lossyScale; + + avatarTransform.position = Vector3.zero; + avatarTransform.rotation = Quaternion.identity; + avatarTransform.localScale = Vector3.one; + + src = renderer.sharedMesh; + dst = Mesh.Instantiate(src); + dst.name = "RETARGETED: " + src.name; + + RetargetBones(); + AdjustShapeKeys(); + + avatarTransform.position = avPos; + avatarTransform.rotation = avRot; + avatarTransform.localScale = avScale; + + AssetDatabase.CreateAsset(dst, Util.GenerateAssetPath()); + } + + private void AdjustShapeKeys() + { + // TODO + } + + private void RetargetBones() + { + var originalBindPoses = src.bindposes; + var originalBones = renderer.bones; + + var newBones = (Transform[]) originalBones.Clone(); + var newBindPoses = (Matrix4x4[]) originalBindPoses.Clone(); + + for (int i = 0; i < originalBones.Length; i++) + { + Transform newBindTarget = BoneDatabase.GetRetargetedBone(originalBones[i]); + if (newBindTarget == null) continue; + + Matrix4x4 Bp = newBindTarget.worldToLocalMatrix * originalBones[i].localToWorldMatrix * originalBindPoses[i]; + + newBones[i] = newBindTarget; + newBindPoses[i] = Bp; + } + + dst.bindposes = newBindPoses; + renderer.bones = newBones; + renderer.sharedMesh = dst; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs.meta b/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs.meta new file mode 100644 index 00000000..04b8eed6 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/MeshRetargeter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 71c5610d7a73420da0a5b40e315e2500 +timeCreated: 1661632791 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs b/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs new file mode 100644 index 00000000..e138bfba --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs @@ -0,0 +1,47 @@ +using UnityEditor; +using UnityEditor.Animations; + +namespace net.fushizen.modular_avatar.core.editor +{ + public static class Util + { + internal const string generatedAssetsSubdirectory = "999_Modular_Avatar_Generated"; + internal const string generatedAssetsPath = "Assets/" + generatedAssetsSubdirectory; + + static internal AnimatorController CreateContainer() + { + var container = new AnimatorController(); + AssetDatabase.CreateAsset(container, GenerateAssetPath()); + + return container; + } + + internal static string GenerateAssetPath() + { + return GetGeneratedAssetsFolder() + "/" + GUID.Generate() + ".asset"; + } + + internal static string GetGeneratedAssetsFolder() + { + if (!AssetDatabase.IsValidFolder(generatedAssetsPath)) + { + AssetDatabase.CreateFolder("Assets", generatedAssetsSubdirectory); + } + + return generatedAssetsPath; + } + + static internal void DeleteTemporaryAssets() + { + EditorApplication.delayCall += () => + { + AssetDatabase.SaveAssets(); + + var subdir = generatedAssetsPath; + + AssetDatabase.DeleteAsset(subdir); + //FileUtil.DeleteFileOrDirectory(subdir); + }; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs.meta b/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs.meta new file mode 100644 index 00000000..06d2548a --- /dev/null +++ b/Packages/net.fushizen.modular-avatar.core/Editor/Util.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 287488046f2f4b898159f7cbe91b3771 +timeCreated: 1661635336 \ No newline at end of file