diff --git a/Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs b/Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs new file mode 100644 index 000000000..cc3f0557d --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs @@ -0,0 +1,26 @@ +using System; +using UnityEditor; + +namespace VRM +{ + /// + /// UndoをGroupを開始して、DisposeでUndoする。 + /// using で使うのを想定。 + /// using ブロック内で Undo されるべき操作をする。 + /// + public struct RecordDisposer : IDisposable + { + int _group; + public RecordDisposer(UnityEngine.Object[] objects, string msg) + { + Undo.IncrementCurrentGroup(); + _group = Undo.GetCurrentGroup(); + Undo.RecordObjects(objects, msg); + } + + public void Dispose() + { + Undo.RevertAllDownToGroup(_group); + } + } +} diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs.meta b/Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs.meta similarity index 69% rename from Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs.meta rename to Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs.meta index 90b6df455..5cf6bf1e3 100644 --- a/Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs.meta +++ b/Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 -guid: f8ed5cb82dd13bf43a1556b24a6d13a0 -timeCreated: 1532063757 -licenseType: Free +guid: b304ed2aeece5a54191a5a8b69d9d113 MonoImporter: + externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs b/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs new file mode 100644 index 000000000..98f7ddac9 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UniGLTF; +using UnityEditor; +using UnityEngine; + +namespace VRM +{ + public static class VRMEditorExporter + { + /// + /// Editor向けのエクスポート処理 + /// + /// 出力先 + /// エクスポート設定 + public static void Export(string path, VRMExportSettings settings) + { + List destroy = new List(); + try + { + Export(path, settings, destroy); + } + finally + { + foreach (var x in destroy) + { + Debug.LogFormat("destroy: {0}", x.name); + GameObject.DestroyImmediate(x); + } + } + } + + static bool IsPrefab(GameObject go) + { + return !go.scene.IsValid(); + } + + /// + /// DeepCopy + /// + /// + /// + static BlendShapeAvatar CopyBlendShapeAvatar(BlendShapeAvatar src, bool removeUnknown) + { + var avatar = GameObject.Instantiate(src); + avatar.Clips = new List(); + foreach (var clip in src.Clips) + { + if (removeUnknown && clip.Preset == BlendShapePreset.Unknown) + { + continue; + } + avatar.Clips.Add(GameObject.Instantiate(clip)); + } + return avatar; + } + + /// + /// 使用されない BlendShape を間引いた Mesh を作成して置き換える + /// + /// + /// + static void ReplaceMesh(GameObject target, SkinnedMeshRenderer smr, BlendShapeAvatar copyBlendShapeAvatar) + { + Mesh mesh = smr.sharedMesh; + if (mesh == null) return; + if (mesh.blendShapeCount == 0) return; + + // Mesh から BlendShapeClip からの参照がある blendShape の index を集める + var usedBlendshapeIndexArray = copyBlendShapeAvatar.Clips + .SelectMany(clip => clip.Values) + .Where(val => target.transform.Find(val.RelativePath) == smr.transform) + .Select(val => val.Index) + .Distinct() + .ToArray(); + + var copyMesh = mesh.Copy(copyBlendShape: false); + // 使われている BlendShape だけをコピーする + foreach (var i in usedBlendshapeIndexArray) + { + var name = mesh.GetBlendShapeName(i); + var vCount = mesh.vertexCount; + var vertices = new Vector3[vCount]; + var normals = new Vector3[vCount]; + var tangents = new Vector3[vCount]; + mesh.GetBlendShapeFrameVertices(i, 0, vertices, normals, tangents); + + copyMesh.AddBlendShapeFrame(name, 100f, vertices, normals, tangents); + } + + // BlendShapeClip の BlendShapeIndex を更新する(前に詰める) + var indexMapper = usedBlendshapeIndexArray + .Select((x, i) => new { x, i }) + .ToDictionary(pair => pair.x, pair => pair.i); + foreach (var clip in copyBlendShapeAvatar.Clips) + { + for (var i = 0; i < clip.Values.Length; ++i) + { + var value = clip.Values[i]; + if (target.transform.Find(value.RelativePath) != smr.transform) continue; + value.Index = indexMapper[value.Index]; + clip.Values[i] = value; + } + } + + // mesh を置き換える + smr.sharedMesh = copyMesh; + } + + /// + /// + /// + /// + /// + /// 作業が終わったらDestoryするべき一時オブジェクト + static void Export(string path, VRMExportSettings settings, List destroy) + { + var target = settings.Source; + + // 常にコピーする。シーンを変化させない + target = GameObject.Instantiate(target); + destroy.Add(target); + + // 正規化 + if (settings.PoseFreeze) + { + // BoneNormalizer.Execute は Copy を作って正規化する。UNDO無用 + target = BoneNormalizer.Execute(target, settings.ForceTPose, false); + destroy.Add(target); + } + + // 元のBlendShapeClipに変更を加えないように複製 + var proxy = target.GetComponent(); + var copyBlendShapeAvatar = CopyBlendShapeAvatar(proxy.BlendShapeAvatar, settings.ReduceBlendshapeClip); + proxy.BlendShapeAvatar = copyBlendShapeAvatar; + + // BlendShape削減 + if (settings.ReduceBlendshape) + { + foreach (SkinnedMeshRenderer smr in target.GetComponentsInChildren()) + { + // 未使用のBlendShapeを間引く + ReplaceMesh(target, smr, copyBlendShapeAvatar); + } + } + + // 出力 + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + var vrm = VRMExporter.Export(target, new VRMExporterConfiguration + { + UseSparseAccessorForBlendShape = settings.UseSparseAccessor, + ExportOnlyBlendShapePosition = settings.OnlyBlendshapePosition + }); + vrm.extensions.VRM.meta.title = settings.Title; + vrm.extensions.VRM.meta.version = settings.Version; + vrm.extensions.VRM.meta.author = settings.Author; + vrm.extensions.VRM.meta.contactInformation = settings.ContactInformation; + vrm.extensions.VRM.meta.reference = settings.Reference; + + var bytes = vrm.ToGlbBytes(settings.UseExperimentalExporter ? SerializerTypes.Generated : SerializerTypes.UniJSON); + File.WriteAllBytes(path, bytes); + Debug.LogFormat("Export elapsed {0}", sw.Elapsed); + } + + if (path.StartsWithUnityAssetPath()) + { + // 出力ファイルのインポートを発動 + AssetDatabase.ImportAsset(path.ToUnityRelativePath()); + } + } + } +} diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs.meta b/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs.meta new file mode 100644 index 000000000..f6863a957 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cea266830a7f57843bb928d0ea37bcbc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs b/Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs deleted file mode 100644 index a5c76971f..000000000 --- a/Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using UnityEditor; -using UnityEngine; - - -namespace VRM -{ - [CustomEditor(typeof(VRMExportObject))] - public class VRMExportObjectEditor : Editor - { - SerializedProperty m_settings; - VRMExportObject m_target; - - void OnEnable() - { - m_target = target as VRMExportObject; - m_settings = serializedObject.FindProperty("Settings"); - } - - public override void OnInspectorGUI() - { - // - // Editor - // - serializedObject.Update(); - - var before = m_target.Settings.Source; - - EditorGUILayout.PropertyField(m_settings, true); - serializedObject.ApplyModifiedProperties(); - - // - // - // - var after = m_target.Settings.Source; - if (before != after) - { - m_target.Settings.InitializeFrom(after as GameObject); - } - - bool canExport = m_target.Settings.Source != null; - foreach (var validation in m_target.Settings.CanExport()) - { - if (!validation.CanExport) - { - canExport = false; - } - EditorGUILayout.HelpBox(validation.Message, validation.CanExport ? MessageType.Warning : MessageType.Error); - } - - if (canExport) - { - if (GUILayout.Button("Export")) - { - var path = EditorUtility.SaveFilePanel( - "Save vrm", - null,//Dir, - m_target.Settings.Source.name + ".vrm", - "vrm"); - if (!string.IsNullOrEmpty(path)) - { - var target = m_target; - EditorApplication.delayCall += () => - { - target.Settings.Export(path); - }; - } - } - } - } - - class DisposableInstance : IDisposable - { - GameObject m_go; - public GameObject GameObject - { - get - { - return m_go; - } - } - - public DisposableInstance(GameObject prefab) - { - m_go = GameObject.Instantiate(prefab); - } - - public void Dispose() - { - if (m_go != null) - { - if (Application.isPlaying) - { - GameObject.Destroy(m_go); - } - else - { - GameObject.DestroyImmediate(m_go); - } - } - } - } - } -} diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs b/Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs new file mode 100644 index 000000000..a0f420869 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UniGLTF; +using UnityEngine; + + +namespace VRM +{ + [Serializable] + public class VRMExportSettings + { + /// + /// エクスポート対象 + /// + public GameObject Source; + + #region Meta + /// + /// エクスポート名 + /// + public string Title; + + /// + /// エクスポートバージョン(エクスポートするModelのバージョン) + /// + public string Version; + + /// + /// 作者 + /// + public string Author; + + /// + /// 作者連絡先 + /// + public string ContactInformation; + + /// + /// 作品引用 + /// + public string Reference; + #endregion + + #region Settings + /// + /// エクスポート時に強制的にT-Pose化する + /// + [Tooltip("Option")] + public bool ForceTPose = false; + + /// + /// エクスポート時にヒエラルキーの正規化を実施する + /// + [Tooltip("Require only first time")] + public bool PoseFreeze = true; + + /// + /// エクスポート時に新しいJsonSerializerを使う + /// + [Tooltip("Use new JSON serializer")] + public bool UseExperimentalExporter = false; + + /// + /// BlendShapeのシリアライズにSparseAccessorを使う + /// + [Tooltip("Use sparse accessor for blendshape. This may reduce vrm size")] + public bool UseSparseAccessor = true; + + /// + /// BlendShapeのPositionのみをエクスポートする + /// + [Tooltip("UniVRM-0.54 or later can load it. Otherwise fail to load")] + public bool OnlyBlendshapePosition = false; + + /// + /// エクスポート時にBlendShapeClipから参照されないBlendShapeを削除する + /// + [Tooltip("Remove blendshape that is not used from BlendShapeClip")] + public bool ReduceBlendshape = false; + + /// + /// skip if BlendShapeClip.Preset == Unknown + /// + [Tooltip("Remove blendShapeClip that preset is Unknown")] + public bool ReduceBlendshapeClip = false; + #endregion + + public struct Validation + { + /// + /// エクスポート可能か否か。 + /// true のメッセージは警告 + /// false のメッセージはエラー + /// + public readonly bool CanExport; + public readonly String Message; + + Validation(bool canExport, string message) + { + CanExport = canExport; + Message = message; + } + + public static Validation Error(string msg) + { + return new Validation(false, msg); + } + + public static Validation Warning(string msg) + { + return new Validation(true, msg); + } + } + + /// + /// ボーン名の重複を確認 + /// + /// + bool DuplicateBoneNameExists() + { + var bones = Source.transform.Traverse().ToArray(); + var duplicates = bones + .GroupBy(p => p.name) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + + return (duplicates.Any()); + } + + /// + /// エクスポート可能か検証する + /// + /// + public IEnumerable Validate() + { + if (Source == null) + { + yield return Validation.Error("Require source"); + yield break; + } + + var animator = Source.GetComponent(); + if (animator == null) + { + yield return Validation.Error("Require animator. "); + } + else if (animator.avatar == null) + { + yield return Validation.Error("Require animator.avatar. "); + } + else if (!animator.avatar.isValid) + { + yield return Validation.Error("Animator.avatar is not valid. "); + } + else if (!animator.avatar.isHuman) + { + yield return Validation.Error("Animator.avatar is not humanoid. Please change model's AnimationType to humanoid. "); + } + + var jaw = animator.GetBoneTransform(HumanBodyBones.Jaw); + if (jaw != null) + { + yield return Validation.Warning("Jaw bone is included. It may not be what you intended. Please check the humanoid avatar setting screen"); + } + + if (DuplicateBoneNameExists()) + { + yield return Validation.Error("Find duplicate Bone names. Please check model's bone names. "); + } + + if (string.IsNullOrEmpty(Title)) + { + yield return Validation.Error("Require Title. "); + } + if (string.IsNullOrEmpty(Version)) + { + yield return Validation.Error("Require Version. "); + } + if (string.IsNullOrEmpty(Author)) + { + yield return Validation.Error("Require Author. "); + } + + if (ReduceBlendshape && Source.GetComponent() == null) + { + yield return Validation.Error("ReduceBlendshapeSize is need VRMBlendShapeProxy, you need to convert to VRM once."); + } + + var renderers = Source.GetComponentsInChildren(); + if (renderers.All(x => !x.gameObject.activeInHierarchy)) + { + yield return Validation.Error("No active mesh"); + } + + var materials = renderers.SelectMany(x => x.sharedMaterials).Distinct(); + foreach (var material in materials) + { + if (material.shader.name == "Standard") + { + // standard + continue; + } + + if (MaterialExporter.UseUnlit(material.shader.name)) + { + // unlit + continue; + } + + if (VRMMaterialExporter.VRMExtensionShaders.Contains(material.shader.name)) + { + // VRM supported + continue; + } + + yield return Validation.Warning(string.Format("unknown material '{0}' is used. this will export as `Standard` fallback", material.shader.name)); + } + } + + /// + /// 対象のモデルからMeta情報を取得し、エクスポート設定を初期する + /// + /// + public void InitializeFrom(GameObject go) + { + if (Source == go) return; + Source = go; + + // + // initialize + // + var desc = Source == null ? null : go.GetComponent(); + if (desc == null) + { + // 初回のVRMエクスポートとみなす + ForceTPose = false; // option + PoseFreeze = true; + } + else + { + // すでに正規化済みとみなす + ForceTPose = false; + PoseFreeze = false; + } + + // + // Meta + // + var meta = Source == null ? null : go.GetComponent(); + if (meta != null && meta.Meta != null) + { + Title = meta.Meta.Title; + Version = string.IsNullOrEmpty(meta.Meta.Version) ? "0.0" : meta.Meta.Version; + Author = meta.Meta.Author; + ContactInformation = meta.Meta.ContactInformation; + Reference = meta.Meta.Reference; + } + else + { + Title = go.name; + Version = "0.0"; + } + } + } +} \ No newline at end of file diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExportSettings.cs.meta b/Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs.meta similarity index 100% rename from Assets/VRM/UniVRM/Scripts/Format/VRMExportSettings.cs.meta rename to Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs.meta diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs b/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs index 92efb5c29..38bd6da80 100644 --- a/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs +++ b/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs @@ -1,93 +1,9 @@ -using System.IO; -using System.Text; -using UnityEditor; +using UnityEditor; using UnityEngine; namespace VRM { - public class VRMExporterWizard : ScriptableWizard - { - const string EXTENSION = ".vrm"; - - VRMMeta m_meta; - - private static string m_lastExportDir; - - public VRMExportSettings m_settings = new VRMExportSettings(); - - public static void CreateWizard() - { - var wiz = ScriptableWizard.DisplayWizard( - "VRM Exporter", "Export"); - var go = Selection.activeObject as GameObject; - - // update checkbox - wiz.m_settings.InitializeFrom(go); - - wiz.OnWizardUpdate(); - } - - void OnEnable() - { - // Debug.Log("OnEnable"); - Undo.willFlushUndoRecord += OnWizardUpdate; - } - - void OnDisable() - { - // Debug.Log("OnDisable"); - Undo.willFlushUndoRecord -= OnWizardUpdate; - } - - void OnWizardCreate() - { - string directory; - if (string.IsNullOrEmpty(m_lastExportDir)) - directory = Directory.GetParent(Application.dataPath).ToString(); - else - directory = m_lastExportDir; - - // save dialog - var path = EditorUtility.SaveFilePanel( - "Save vrm", - directory, - m_settings.Source.name + EXTENSION, - EXTENSION.Substring(1)); - if (string.IsNullOrEmpty(path)) - { - return; - } - m_lastExportDir = Path.GetDirectoryName(path).Replace("\\", "/"); - - // export - m_settings.Export(path); - } - - void OnWizardUpdate() - { - isValid = true; - var helpBuilder = new StringBuilder(); - var errorBuilder = new StringBuilder(); - - foreach (var validation in m_settings.CanExport()) - { - if (!validation.CanExport) - { - isValid = false; - errorBuilder.Append(validation.Message); - } - else - { - helpBuilder.AppendLine(validation.Message); - } - } - - helpString = helpBuilder.ToString(); - errorString = errorBuilder.ToString(); - } - } - public static class VRMExporterMenu { const string CONVERT_HUMANOID_KEY = VRMVersion.MENU + "/Export humanoid"; diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs.meta b/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs.meta index ee2067f06..5100b682a 100644 --- a/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs.meta +++ b/Assets/VRM/UniVRM/Editor/Format/VRMExporterMenu.cs.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 -guid: a8e41aa30fcc76e43ad8588aef8572ea -timeCreated: 1520491195 -licenseType: Free +guid: a1429b9028f33544e94aa367c2acb7fb MonoImporter: + externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs b/Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs new file mode 100644 index 000000000..90b1faa12 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs @@ -0,0 +1,90 @@ +using System.IO; +using System.Text; +using UnityEditor; +using UnityEngine; + + +namespace VRM +{ + public class VRMExporterWizard : ScriptableWizard + { + const string EXTENSION = ".vrm"; + + VRMMeta m_meta; + + private static string m_lastExportDir; + + public VRMExportSettings m_settings = new VRMExportSettings(); + + public static void CreateWizard() + { + var wiz = ScriptableWizard.DisplayWizard( + "VRM Exporter", "Export"); + var go = Selection.activeObject as GameObject; + + // update checkbox + wiz.m_settings.InitializeFrom(go); + + wiz.OnWizardUpdate(); + } + + void OnEnable() + { + // Debug.Log("OnEnable"); + Undo.willFlushUndoRecord += OnWizardUpdate; + } + + void OnDisable() + { + // Debug.Log("OnDisable"); + Undo.willFlushUndoRecord -= OnWizardUpdate; + } + + void OnWizardCreate() + { + string directory; + if (string.IsNullOrEmpty(m_lastExportDir)) + directory = Directory.GetParent(Application.dataPath).ToString(); + else + directory = m_lastExportDir; + + // save dialog + var path = EditorUtility.SaveFilePanel( + "Save vrm", + directory, + m_settings.Source.name + EXTENSION, + EXTENSION.Substring(1)); + if (string.IsNullOrEmpty(path)) + { + return; + } + m_lastExportDir = Path.GetDirectoryName(path).Replace("\\", "/"); + + // export + VRMEditorExporter.Export(path, m_settings); + } + + void OnWizardUpdate() + { + isValid = true; + var helpBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + + foreach (var validation in m_settings.Validate()) + { + if (!validation.CanExport) + { + isValid = false; + errorBuilder.Append(validation.Message); + } + else + { + helpBuilder.AppendLine(validation.Message); + } + } + + helpString = helpBuilder.ToString(); + errorString = errorBuilder.ToString(); + } + } +} diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs.meta b/Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs.meta similarity index 76% rename from Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs.meta rename to Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs.meta index 9a7ef2be7..ee2067f06 100644 --- a/Assets/VRM/UniVRM/Editor/Format/VRMExportObjectEditor.cs.meta +++ b/Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs.meta @@ -1,6 +1,6 @@ fileFormatVersion: 2 -guid: 01708ecf1aa756948be6996d987684a7 -timeCreated: 1532063961 +guid: a8e41aa30fcc76e43ad8588aef8572ea +timeCreated: 1520491195 licenseType: Free MonoImporter: serializedVersion: 2 diff --git a/Assets/VRM/UniVRM/Editor/Format/VRMHumanoidNormalizerMenu.cs b/Assets/VRM/UniVRM/Editor/Format/VRMHumanoidNormalizerMenu.cs index 400dbb2f7..b02b6ce4e 100644 --- a/Assets/VRM/UniVRM/Editor/Format/VRMHumanoidNormalizerMenu.cs +++ b/Assets/VRM/UniVRM/Editor/Format/VRMHumanoidNormalizerMenu.cs @@ -48,14 +48,8 @@ namespace VRM { var go = Selection.activeObject as GameObject; - GameObject normalizedRoot = null; - using (new VRMExportSettings.RecordDisposer(go.transform.Traverse().ToArray(), "before normalize")) - { - var normalized = BoneNormalizer.Execute(go, true, false); - VRMExportSettings.CopyVRMComponents(go, normalized.Root, normalized.BoneMap); - normalizedRoot = normalized.Root; - } - Selection.activeGameObject = normalizedRoot; + // BoneNormalizer.Execute はコピーを正規化する。UNDO無用 + Selection.activeGameObject = BoneNormalizer.Execute(go, true, false); } } } diff --git a/Assets/VRM/UniVRM/Editor/UniVRM.Editor.asmdef b/Assets/VRM/UniVRM/Editor/UniVRM.Editor.asmdef index 35b3e6254..b0fde8cc9 100644 --- a/Assets/VRM/UniVRM/Editor/UniVRM.Editor.asmdef +++ b/Assets/VRM/UniVRM/Editor/UniVRM.Editor.asmdef @@ -2,12 +2,17 @@ "name": "UniVRM.Editor", "references": [ "VRM", - "UniJSON" + "UniJSON", + "UniHumanoid" ], "optionalUnityReferences": [], "includePlatforms": [ "Editor" ], "excludePlatforms": [], - "allowUnsafeCode": false + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] } \ No newline at end of file diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs b/Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs deleted file mode 100644 index 360c47518..000000000 --- a/Assets/VRM/UniVRM/Scripts/Format/VRMExportObject.cs +++ /dev/null @@ -1,12 +0,0 @@ -using UnityEngine; - - -namespace VRM -{ - [CreateAssetMenu(menuName = "VRM/ExportObject")] - public class VRMExportObject : ScriptableObject - { - [SerializeField] - public VRMExportSettings Settings = new VRMExportSettings(); - } -} diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExportSettings.cs b/Assets/VRM/UniVRM/Scripts/Format/VRMExportSettings.cs deleted file mode 100644 index 0416aac32..000000000 --- a/Assets/VRM/UniVRM/Scripts/Format/VRMExportSettings.cs +++ /dev/null @@ -1,482 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UniGLTF; -using System.IO; -#if UNITY_EDITOR -using UnityEditor; - -#endif - - -namespace VRM -{ - [Serializable] - public class VRMExportSettings - { - public GameObject Source; - - public string Title; - - public string Version; - - public string Author; - - public string ContactInformation; - - public string Reference; - - public bool ForceTPose = true; - - public bool PoseFreeze = true; - - public bool UseExperimentalExporter = false; - - public bool ReduceBlendshapeSize = false; - - public struct Validation - { - public readonly bool CanExport; - public readonly String Message; - - Validation(bool canExport, string message) - { - CanExport = canExport; - Message = message; - } - - public static Validation Error(string msg) - { - return new Validation(false, msg); - } - - public static Validation Warning(string msg) - { - return new Validation(true, msg); - } - } - - public IEnumerable CanExport() - { - if (Source == null) - { - yield return Validation.Error("Require source"); - yield break; - } - - var animator = Source.GetComponent(); - if (animator == null) - { - yield return Validation.Error("Require animator. "); - } - else if (animator.avatar == null) - { - yield return Validation.Error("Require animator.avatar. "); - } - else if (!animator.avatar.isValid) - { - yield return Validation.Error("Animator.avatar is not valid. "); - } - else if (!animator.avatar.isHuman) - { - yield return Validation.Error("Animator.avatar is not humanoid. Please change model's AnimationType to humanoid. "); - } - - var jaw = animator.GetBoneTransform(HumanBodyBones.Jaw); - if (jaw != null) - { - yield return Validation.Warning("Jaw bone is included. It may not be what you intended. Please check the humanoid avatar setting screen"); - } - - if (DuplicateBoneNameExists()) - { - yield return Validation.Error("Find duplicate Bone names. Please check model's bone names. "); - } - - if (string.IsNullOrEmpty(Title)) - { - yield return Validation.Error("Require Title. "); - } - if (string.IsNullOrEmpty(Version)) - { - yield return Validation.Error("Require Version. "); - } - if (string.IsNullOrEmpty(Author)) - { - yield return Validation.Error("Require Author. "); - } - - if (ReduceBlendshapeSize && Source.GetComponent() == null) - { - yield return Validation.Error("ReduceBlendshapeSize is need VRMBlendShapeProxy, you need to convert to VRM once."); - } - - var renderers = Source.GetComponentsInChildren(); - if (renderers.All(x => !x.gameObject.activeInHierarchy)) - { - yield return Validation.Error("No active mesh"); - } - - var materials = renderers.SelectMany(x => x.sharedMaterials).Distinct(); - foreach (var material in materials) - { - if (material.shader.name == "Standard") - { - // standard - continue; - } - - if (MaterialExporter.UseUnlit(material.shader.name)) - { - // unlit - continue; - } - - if (VRMMaterialExporter.VRMExtensionShaders.Contains(material.shader.name)) - { - // VRM supported - continue; - } - - yield return Validation.Warning(string.Format("unknown material '{0}' is used. this will export as `Standard` fallback", material.shader.name)); - } - } - - public void InitializeFrom(GameObject go) - { - if (Source == go) return; - Source = go; - - var desc = Source == null ? null : go.GetComponent(); - if (desc == null) - { - ForceTPose = true; - PoseFreeze = true; - } - else - { - ForceTPose = false; - PoseFreeze = false; - } - - var meta = Source == null ? null : go.GetComponent(); - if (meta != null && meta.Meta != null) - { - Title = meta.Meta.Title; - Version = string.IsNullOrEmpty(meta.Meta.Version) ? "0.0" : meta.Meta.Version; - Author = meta.Meta.Author; - ContactInformation = meta.Meta.ContactInformation; - Reference = meta.Meta.Reference; - } - else - { - Title = go.name; - Version = "0.0"; - } - } - - // - // トップレベルのMonoBehaviourを移植する - // - public static void CopyVRMComponents(GameObject go, GameObject root, - Dictionary map) - { - { - // blendshape - var src = go.GetComponent(); - if (src != null) - { - var dst = root.AddComponent(); - dst.BlendShapeAvatar = src.BlendShapeAvatar; - } - } - - { - var secondary = go.transform.Find("secondary"); - if (secondary == null) - { - secondary = go.transform; - } - - var dstSecondary = root.transform.Find("secondary"); - if (dstSecondary == null) - { - dstSecondary = new GameObject("secondary").transform; - dstSecondary.SetParent(root.transform, false); - } - - // 揺れモノ - foreach (var src in go.transform.Traverse().Select(x => x.GetComponent()).Where(x => x != null)) - { - var dst = map[src.transform]; - var dstColliderGroup = dst.gameObject.AddComponent(); - dstColliderGroup.Colliders = src.Colliders.Select(y => - { - var offset = dst.worldToLocalMatrix.MultiplyPoint(src.transform.localToWorldMatrix.MultiplyPoint(y.Offset)); - return new VRMSpringBoneColliderGroup.SphereCollider - { - Offset = offset, - Radius = y.Radius - }; - }).ToArray(); - } - - foreach (var src in go.transform.Traverse().SelectMany(x => x.GetComponents())) - { - // Copy VRMSpringBone - var dst = dstSecondary.gameObject.AddComponent(); - dst.m_comment = src.m_comment; - dst.m_stiffnessForce = src.m_stiffnessForce; - dst.m_gravityPower = src.m_gravityPower; - dst.m_gravityDir = src.m_gravityDir; - dst.m_dragForce = src.m_dragForce; - if (src.m_center != null) - { - dst.m_center = map[src.m_center]; - } - - dst.RootBones = src.RootBones.Select(x => map[x]).ToList(); - dst.m_hitRadius = src.m_hitRadius; - if (src.ColliderGroups != null) - { - dst.ColliderGroups = src.ColliderGroups - .Select(x => map[x.transform].GetComponent()).ToArray(); - } - } - } - -#pragma warning disable 0618 - { - // meta(obsolete) - var src = go.GetComponent(); - if (src != null) - { - src.CopyTo(root); - } - } -#pragma warning restore 0618 - - { - // meta - var src = go.GetComponent(); - if (src != null) - { - var dst = root.AddComponent(); - dst.Meta = src.Meta; - } - } - - { - // firstPerson - var src = go.GetComponent(); - if (src != null) - { - src.CopyTo(root, map); - } - } - - { - // humanoid - var dst = root.AddComponent(); - var src = go.GetComponent(); - if (src != null) - { - dst.Avatar = src.Avatar; - dst.Description = src.Description; - } - else - { - var animator = go.GetComponent(); - if (animator != null) - { - dst.Avatar = animator.avatar; - } - } - } - } - - public static bool IsPrefab(GameObject go) - { - return !go.scene.IsValid(); - } - -#if UNITY_EDITOR - public struct RecordDisposer : IDisposable - { - int _group; - public RecordDisposer(UnityEngine.Object[] objects, string msg) - { - Undo.IncrementCurrentGroup(); - _group = Undo.GetCurrentGroup(); - Undo.RecordObjects(objects, msg); - } - - public void Dispose() - { - Undo.RevertAllDownToGroup(_group); - } - } - - public void Export(string path) - { - List destroy = new List(); - try - { - Export(path, destroy); - } - finally - { - foreach (var x in destroy) - { - Debug.LogFormat("destroy: {0}", x.name); - GameObject.DestroyImmediate(x); - } - } - } - - void Export(string path, List destroy) - { - var target = Source; - if (IsPrefab(target)) - { - using (new RecordDisposer(Source.transform.Traverse().ToArray(), "before normalize")) - { - target = GameObject.Instantiate(target); - destroy.Add(target); - } - } - - if (PoseFreeze) - { - using (new RecordDisposer(target.transform.Traverse().ToArray(), "before normalize")) - { - var normalized = BoneNormalizer.Execute(target, ForceTPose, false); - CopyVRMComponents(target, normalized.Root, normalized.BoneMap); - target = normalized.Root; - destroy.Add(target); - } - } - - // remove unused blendShape - if (ReduceBlendshapeSize) - { - var proxy = target.GetComponent(); - - // 元のBlendShapeClipに変更を加えないように複製 - var copyBlendShapeAvatar = GameObject.Instantiate(proxy.BlendShapeAvatar); - var copyBlendShapClips = new List(); - - foreach (var clip in proxy.BlendShapeAvatar.Clips) - { - copyBlendShapClips.Add(GameObject.Instantiate(clip)); - } - - var skinnedMeshRenderers = target.GetComponentsInChildren(); - - var names = new Dictionary(); - var vs = new Dictionary(); - var ns = new Dictionary(); - var ts = new Dictionary(); - - foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers) - { - Mesh mesh = smr.sharedMesh; - if (mesh == null) continue; - if (mesh.blendShapeCount == 0) continue; - - var copyMesh = mesh.Copy(true); - var vCount = copyMesh.vertexCount; - names.Clear(); - - vs.Clear(); - ns.Clear(); - ts.Clear(); - - var usedBlendshapeIndexArray = copyBlendShapClips - .SelectMany(clip => clip.Values) - .Where(val => target.transform.Find(val.RelativePath) == smr.transform) - .Select(val => val.Index) - .Distinct() - .ToArray(); - - foreach (var i in usedBlendshapeIndexArray) - { - var name = copyMesh.GetBlendShapeName(i); - var vertices = new Vector3[vCount]; - var normals = new Vector3[vCount]; - var tangents = new Vector3[vCount]; - copyMesh.GetBlendShapeFrameVertices(i, 0, vertices, normals, tangents); - - names.Add(i, name); - vs.Add(i, vertices); - ns.Add(i, normals); - ts.Add(i, tangents); - } - - copyMesh.ClearBlendShapes(); - - foreach (var i in usedBlendshapeIndexArray) - { - copyMesh.AddBlendShapeFrame(names[i], 100f, vs[i], ns[i], ts[i]); - } - - var indexMapper = usedBlendshapeIndexArray - .Select((x, i) => new { x, i }) - .ToDictionary(pair => pair.x, pair => pair.i); - - foreach (var clip in copyBlendShapClips) - { - for (var i = 0; i < clip.Values.Length; ++i) - { - var value = clip.Values[i]; - if (target.transform.Find(value.RelativePath) != smr.transform) continue; - value.Index = indexMapper[value.Index]; - clip.Values[i] = value; - } - } - - copyBlendShapeAvatar.Clips = copyBlendShapClips; - - proxy.BlendShapeAvatar = copyBlendShapeAvatar; - - smr.sharedMesh = copyMesh; - } - } - - { - var sw = System.Diagnostics.Stopwatch.StartNew(); - var vrm = VRMExporter.Export(target, ReduceBlendshapeSize); - vrm.extensions.VRM.meta.title = Title; - vrm.extensions.VRM.meta.version = Version; - vrm.extensions.VRM.meta.author = Author; - vrm.extensions.VRM.meta.contactInformation = ContactInformation; - vrm.extensions.VRM.meta.reference = Reference; - - - var bytes = vrm.ToGlbBytes(UseExperimentalExporter ? SerializerTypes.Generated : SerializerTypes.UniJSON); - File.WriteAllBytes(path, bytes); - Debug.LogFormat("Export elapsed {0}", sw.Elapsed); - } - - - if (path.StartsWithUnityAssetPath()) - { - AssetDatabase.ImportAsset(path.ToUnityRelativePath()); - } - } -#endif - - //ここで重複ボーン名のチェックをする - bool DuplicateBoneNameExists() - { - var bones = Source.transform.Traverse().ToArray(); - var duplicates = bones - .GroupBy(p => p.name) - .Where(g => g.Count() > 1) - .Select(g => g.Key); - - return (duplicates.Any()); - } - } -} \ No newline at end of file diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExporter.cs b/Assets/VRM/UniVRM/Scripts/Format/VRMExporter.cs index 8a03bb0db..ee588e253 100644 --- a/Assets/VRM/UniVRM/Scripts/Format/VRMExporter.cs +++ b/Assets/VRM/UniVRM/Scripts/Format/VRMExporter.cs @@ -19,17 +19,21 @@ namespace VRM gltf.extensions.VRM = new glTF_VRM_extensions(); } - public new static glTF Export(GameObject go, bool exportOnlyBlendShapePosition = false) + public static glTF Export(GameObject go, bool exportOnlyBlendShapePosition = false) + { + var config = VRMExporterConfiguration.Default; + config.ExportOnlyBlendShapePosition = exportOnlyBlendShapePosition; + return Export(go, config); + } + + public static glTF Export(GameObject go, VRMExporterConfiguration configuration) { var gltf = new glTF(); using (var exporter = new VRMExporter(gltf) { -#if VRM_EXPORTER_USE_SPARSE - // experimental - UseSparseAccessorForBlendShape = true -#endif - ExportOnlyBlendShapePosition = exportOnlyBlendShapePosition + UseSparseAccessorForBlendShape = configuration.UseSparseAccessorForBlendShape, + ExportOnlyBlendShapePosition = configuration.ExportOnlyBlendShapePosition }) { _Export(gltf, exporter, go); diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs b/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs new file mode 100644 index 000000000..a416c40d4 --- /dev/null +++ b/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs @@ -0,0 +1,14 @@ +namespace VRM +{ + public struct VRMExporterConfiguration + { + public bool UseSparseAccessorForBlendShape; + public bool ExportOnlyBlendShapePosition; + + public static VRMExporterConfiguration Default => new VRMExporterConfiguration + { + UseSparseAccessorForBlendShape = true, + ExportOnlyBlendShapePosition = false, + }; + } +} diff --git a/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs.meta b/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs.meta new file mode 100644 index 000000000..8073ec287 --- /dev/null +++ b/Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 974aea88b487e5543b1c39e5a528f751 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs b/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs index 513ed38dd..295561774 100644 --- a/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs +++ b/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs @@ -181,9 +181,10 @@ namespace VRM var indexMap = srcBones .Select((x, i) => new { i, x }) - .Select(x => { + .Select(x => + { Transform dstBone; - if(boneMap.TryGetValue(x.x, out dstBone)) + if (boneMap.TryGetValue(x.x, out dstBone)) { return dstBones.IndexOf(dstBone); } @@ -191,7 +192,7 @@ namespace VRM { return -1; } - }) + }) .ToArray(); for (int i = 0; i < srcBones.Length; ++i) @@ -322,14 +323,14 @@ namespace VRM var mesh = srcMesh.Copy(false); mesh.name = srcMesh.name + ".baked"; srcRenderer.BakeMesh(mesh); - - var blendShapeValues = new Dictionary(); + + var blendShapeValues = new Dictionary(); for (int i = 0; i < srcMesh.blendShapeCount; i++) { var val = srcRenderer.GetBlendShapeWeight(i); if (val > 0) blendShapeValues.Add(i, val); } - + mesh.boneWeights = MapBoneWeight(srcMesh.boneWeights, boneMap, srcRenderer.bones, dstBones); // restore weights. clear when BakeMesh // recalc bindposes @@ -385,7 +386,7 @@ namespace VRM srcRenderer.SetBlendShapeWeight(i, value); Vector3[] vertices = blendShapeMesh.vertices; - + for (int j = 0; j < vertices.Length; ++j) { if (originalBlendShapePositions[j] == Vector3.zero) @@ -404,7 +405,7 @@ namespace VRM if (originalBlendShapeNormals[j] == Vector3.zero) { normals[j] = Vector3.zero; - + } else { @@ -428,7 +429,7 @@ namespace VRM #endif var frameCount = srcMesh.GetBlendShapeFrameCount(i); - for(int f=0; f対象モデルのルート /// 強制的にT-Pose化するか /// 正規化済みのモデル - public static NormalizedResult Execute(GameObject go, bool forceTPose, bool clearBlendShapeBeforeNormalize) + public static GameObject Execute(GameObject go, bool forceTPose, bool clearBlendShapeBeforeNormalize) { Dictionary boneMap = new Dictionary(); @@ -564,11 +565,137 @@ namespace VRM NormalizeNoneSkinnedMesh(src, dst); } - return new NormalizedResult + CopyVRMComponents(go, normalized, boneMap); + + // return new NormalizedResult + // { + // Root = normalized, + // BoneMap = boneMap + // }; + return normalized; + } + + /// + /// VRMを構成するコンポーネントをコピーする。 + /// + /// コピー元 + /// コピー先 + /// コピー元とコピー先の対応関係 + static void CopyVRMComponents(GameObject go, GameObject root, + Dictionary map) + { { - Root = normalized, - BoneMap = boneMap - }; + // blendshape + var src = go.GetComponent(); + if (src != null) + { + var dst = root.AddComponent(); + dst.BlendShapeAvatar = src.BlendShapeAvatar; + } + } + + { + var secondary = go.transform.Find("secondary"); + if (secondary == null) + { + secondary = go.transform; + } + + var dstSecondary = root.transform.Find("secondary"); + if (dstSecondary == null) + { + dstSecondary = new GameObject("secondary").transform; + dstSecondary.SetParent(root.transform, false); + } + + // 揺れモノ + foreach (var src in go.transform.GetComponentsInChildren()) + { + var dst = map[src.transform]; + var dstColliderGroup = dst.gameObject.AddComponent(); + dstColliderGroup.Colliders = src.Colliders.Select(y => + { + var offset = dst.worldToLocalMatrix.MultiplyPoint(src.transform.localToWorldMatrix.MultiplyPoint(y.Offset)); + return new VRMSpringBoneColliderGroup.SphereCollider + { + Offset = offset, + Radius = y.Radius + }; + }).ToArray(); + } + + foreach (var src in go.transform.GetComponentsInChildren()) + { + // Copy VRMSpringBone + var dst = dstSecondary.gameObject.AddComponent(); + dst.m_comment = src.m_comment; + dst.m_stiffnessForce = src.m_stiffnessForce; + dst.m_gravityPower = src.m_gravityPower; + dst.m_gravityDir = src.m_gravityDir; + dst.m_dragForce = src.m_dragForce; + if (src.m_center != null) + { + dst.m_center = map[src.m_center]; + } + + dst.RootBones = src.RootBones.Select(x => map[x]).ToList(); + dst.m_hitRadius = src.m_hitRadius; + if (src.ColliderGroups != null) + { + dst.ColliderGroups = src.ColliderGroups + .Select(x => map[x.transform].GetComponent()).ToArray(); + } + } + } + +#pragma warning disable 0618 + { + // meta(obsolete) + var src = go.GetComponent(); + if (src != null) + { + src.CopyTo(root); + } + } +#pragma warning restore 0618 + + { + // meta + var src = go.GetComponent(); + if (src != null) + { + var dst = root.AddComponent(); + dst.Meta = src.Meta; + } + } + + { + // firstPerson + var src = go.GetComponent(); + if (src != null) + { + src.CopyTo(root, map); + } + } + + { + // humanoid + var dst = root.AddComponent(); + var src = go.GetComponent(); + if (src != null) + { + dst.Avatar = src.Avatar; + dst.Description = src.Description; + } + else + { + var animator = go.GetComponent(); + if (animator != null) + { + dst.Avatar = animator.avatar; + } + } + } } } }