using System.Collections.Generic; using UnityEngine; using UniGLTF; using UniVRM10; using UnityEditor; using System.Linq; using System.Reflection; using UniGLTF.M17N; using UniGLTF.MeshUtility; using VRMShaders; using System.IO; using System; using System.Net.Http.Headers; using System.Threading.Tasks; using COSXML; using COSXML.Model.Object; namespace MetaCity.BundleKit.Editor.VRM { public class MetacityAvatorVRMBuildWindow : EditorWindow { [MenuItem("Metacity/AvatarVRMBuildWindow")] public static void Open() { var window =GetWindow(); window.titleContent = new GUIContent("Metacity Avator VRM Exporter"); window.Show(); } enum Tabs { Meta, Mesh, ExportSettings, } Tabs _tab; VRM10ExportSettings m_settings; UnityEditor.Editor m_settingsInspector; MetaCityMeshExportValidator m_meshes; UnityEditor.Editor m_meshesInspector; VRM10Object m_meta; VRM10Object Vrm { get { return m_meta; } set { if (value != null && AssetDatabase.IsSubAsset(value)) { // SubAsset is readonly. copy Debug.Log("copy VRM10ObjectMeta"); value.Meta.CopyTo(m_tmpObject.Meta); return; } if (m_meta == value) { return; } m_metaEditor = default; m_meta = value; } } VRM10Object m_tmpObject; VRM10MetaEditor m_metaEditor; ExporterDialogState m_state; /// /// Delate bundles in the folder /// private void PrepareBuild() { var buildPath = Constants.AvatarBundleFolderPath; if (Directory.Exists(buildPath)) { if(m_settings.incrementalExport)return; DirectoryInfo di = new DirectoryInfo(buildPath); foreach (FileInfo file in di.GetFiles()) { file.Delete(); } foreach (DirectoryInfo dir in di.GetDirectories()) { dir.Delete(true); } Directory.Delete(buildPath); } Directory.CreateDirectory(buildPath); } private void OnEnable() { Undo.willFlushUndoRecord += Repaint; Selection.selectionChanged += Repaint; m_state = new ExporterDialogState(); Initialize(); m_state.ExportRootChanged += (root) => { Repaint(); }; m_state.ExportRoot = Selection.activeObject as GameObject; } private void Initialize() { m_tmpObject = ScriptableObject.CreateInstance(); m_tmpObject.name = "_vrm1_"; m_tmpObject.Meta.Authors = new List { "" }; m_settings = ScriptableObject.CreateInstance(); m_settingsInspector = UnityEditor.Editor.CreateEditor(m_settings); m_meshes = ScriptableObject.CreateInstance(); m_meshes.UseCustomShaderGetter=()=>m_settings.useCustomShader; m_meshesInspector = UnityEditor.Editor.CreateEditor(m_meshes); m_state.ExportRootChanged += (root) => { // update meta if (root == null) { Vrm = null; } else { var controller = root.GetComponent(); if (controller != null) { Vrm = controller.Vrm; } else { Vrm = null; } } }; } void OnDisable() { Clear(); m_state.Dispose(); Selection.selectionChanged -= Repaint; Undo.willFlushUndoRecord -= Repaint; } protected void Clear() { UnityEditor.Editor.DestroyImmediate(m_settingsInspector); m_settingsInspector = null; // m_meshesInspector UnityEditor.Editor.DestroyImmediate(m_meshesInspector); m_meshesInspector = null; // Meta Vrm = null; ScriptableObject.DestroyImmediate(m_tmpObject); m_tmpObject = null; // m_settings ScriptableObject.DestroyImmediate(m_settings); m_settings = null; // m_meshes ScriptableObject.DestroyImmediate(m_meshes); m_meshes = null; } // // scroll // public delegate Vector2 BeginVerticalScrollViewFunc(Vector2 scrollPosition, bool alwaysShowVertical, GUIStyle verticalScrollbar, GUIStyle background, params GUILayoutOption[] options); static BeginVerticalScrollViewFunc s_func; static BeginVerticalScrollViewFunc BeginVerticalScrollView { get { if (s_func == null) { var methods = typeof(EditorGUILayout).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.Name == "BeginVerticalScrollView").ToArray(); var method = methods.First(x => x.GetParameters()[1].ParameterType == typeof(bool)); s_func = (BeginVerticalScrollViewFunc)method.CreateDelegate(typeof(BeginVerticalScrollViewFunc)); } return s_func; } } private Vector2 m_ScrollPosition; // // validation // protected IEnumerable ValidatorFactory() { HumanoidValidator.MeshInformations = m_meshes.Meshes; // HumanoidValidator.EnableFreeze = m_settings.PoseFreeze; yield return HierarchyValidator.Validate; if (!m_state.ExportRoot) { yield break; } // Mesh/Renderer のチェック m_meshes.MaterialValidator = new MetacityVRMMaterialValidator(); yield return m_meshes.Validate; yield return HumanoidValidator.Validate_TPose; var vrm = Vrm ? Vrm : m_tmpObject; yield return vrm.Meta.Validate; } void OnGUI() { var modified = false; var isValid = BeginGUI(); modified = DoVRMGUI(isValid); EndGUI(); if (modified) { m_state.Invalidate(); } } private bool DoVRMGUI(bool isValid) { if (m_state.ExportRoot == null) { return false; } if (m_state.ExportRoot.GetComponent() != null) { var backup = GUI.enabled; GUI.enabled = m_state.ExportRoot.scene.IsValid(); if (GUI.enabled) { EditorGUILayout.HelpBox(EnableTPose.ENALBE_TPOSE_BUTTON.Msg(), MessageType.Info); } else { EditorGUILayout.HelpBox(EnableTPose.DISABLE_TPOSE_BUTTON.Msg(), MessageType.Warning); } if (GUILayout.Button("T-Pose" + "(unity internal)")) { if (m_state.ExportRoot != null) { Undo.RecordObjects(m_state.ExportRoot.GetComponentsInChildren(), "tpose.internal"); if (InternalTPose.TryMakePoseValid(m_state.ExportRoot)) { // done Repaint(); } else { Debug.LogWarning("not found"); } } } GUI.enabled = backup; } if (!isValid) { return false; } if (m_tmpObject == null) { // disabled return false; } // tabbar _tab = TabBar.OnGUI(_tab); switch (_tab) { case Tabs.Meta: if (m_metaEditor == null) { SerializedObject so; if (m_meta != null) { so = new SerializedObject(Vrm); } else { so = new SerializedObject(m_tmpObject); } m_metaEditor = VRM10MetaEditor.Create(so); } m_metaEditor.OnInspectorGUI(); break; case Tabs.Mesh: m_meshesInspector.OnInspectorGUI(); break; case Tabs.ExportSettings: m_settingsInspector.OnInspectorGUI(); break; } return true; } private void OnLayout() { m_meshes.SetRoot(m_state.ExportRoot, m_settings.MeshExportSettings, new DefualtBlendShapeExportFilter()); } const string LANG_KEY = "VRM_LANG"; const string English ="en"; bool BeginGUI() { // ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint Aborting // Validation により GUI の表示項目が変わる場合があるので、 // EventType.Layout と EventType.Repaint 間で内容が変わらないようしている。 if (Event.current.type == EventType.Layout) { OnLayout(); m_state.Validate(ValidatorFactory()); } EditorGUIUtility.labelWidth = 150; // lang EditorPrefs.SetString(LANG_KEY, English); if(m_state.ExportRoot is null) { EditorGUILayout.HelpBox("You can convert existing vrm file to avator catalog", MessageType.Info); m_settings.incrementalExport=EditorGUILayout.ToggleLeft("Use Incremental Export",m_settings.incrementalExport); if(GUILayout.Button("Convert", GUILayout.MinWidth(100))) { var path=EditorUtility.OpenFilePanel("Manuallly add VRM File to catalog",Application.dataPath,"vrm"); if(string.IsNullOrEmpty(path))return false; PrepareBuild(); ConvertVRM(path); } } EditorGUILayout.LabelField("ExportRoot"); { m_state.ExportRoot = (GameObject)EditorGUILayout.ObjectField(m_state.ExportRoot, typeof(GameObject), true); } // Render contents using Generic Inspector GUI m_ScrollPosition = BeginVerticalScrollView(m_ScrollPosition, false, GUI.skin.verticalScrollbar, "OL Box"); GUIUtility.GetControlID(645789, FocusType.Passive); // validation foreach (var v in m_state.Validations) { v.DrawGUI(); if (v.ErrorLevel == ErrorLevels.Critical) { // Export UI を表示しない return false; } } return true; } string m_logLabel; void EndGUI() { EditorGUILayout.EndScrollView(); // // export button // // Create and Other Buttons { // errors GUILayout.BeginVertical(); // GUILayout.FlexibleSpace(); { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUI.enabled = m_state.Validations.All(x => x.CanExport); if (GUILayout.Button("Export", GUILayout.MinWidth(100))) { PrepareBuild(); ExportPath($"{Constants.AvatarBundleFolderPath}/{m_state.ExportRoot.name}.vrm"); GUIUtility.ExitGUI(); } GUI.enabled = true; // if (GUILayout.Button("Publish",GUILayout.MinWidth(100))) // { // EditorApplication.delayCall += DoPublish; // } GUILayout.EndHorizontal(); } GUILayout.EndVertical(); } GUILayout.Space(8); } private void ConvertVRM(string path) { List catalogs; try { //增量式创建Catalogs catalogs=AvatarBundleExporter.LoadLastAvatarBuiltCatalog(); } catch { catalogs=new List(); } var objectPath=path.Replace(Application.dataPath,string.Empty); var root=AssetDatabase.LoadAssetAtPath($"Assets/{objectPath}"); var vrmHash=System.Guid.NewGuid().ToString().Replace("-",string.Empty); catalogs.RemoveAll(x=>x.bundleName==root.name); catalogs.Add(CatalogUtilities.CreateVRMAvatarCatalog(EditorUserBuildSettings.activeBuildTarget,root.name,vrmHash,null,path)); var catalogCollection = new BuildCatalogCollection { catalogs = catalogs.ToArray() }; var writer = new StreamWriter(Constants.AvatarBundleLocalCatalogPath, false); writer.WriteLine(JsonUtility.ToJson(catalogCollection)); writer.Close(); } /// /// Exporting VRM to the path /// /// private void ExportPath(string path) { m_logLabel = ""; m_logLabel += $"export...\n"; var root = m_state.ExportRoot; try { using (var arrayManager = new NativeArrayManager()) { List catalogs; try { //增量式创建Catalogs catalogs=AvatarBundleExporter.LoadLastAvatarBuiltCatalog(); } catch { catalogs=new List(); } var converter = new UniVRM10.ModelExporter(); var model = converter.Export(arrayManager, root); // 右手系に変換 m_logLabel += $"convert to right handed coordinate...\n"; model.ConvertCoordinate(VrmLib.Coordinates.Vrm1, ignoreVrm: false); // export vrm-1.0 var exporter = new UniVRM10.Vrm10Exporter(new EditorTextureSerializer(), m_settings.MeshExportSettings); //Build VRM Custom Material Bundle if(m_settings.useCustomShader) { (catalogs,exporter.BundleManifest)=VRMMaterialBundleExporter.BuildMaterialBundle(root.name,model.Materials,catalogs); } var option = new VrmLib.ExportArgs { sparse = m_settings.MorphTargetUseSparse, }; exporter.Export(root, model, converter, option, Vrm ? Vrm.Meta : m_tmpObject.Meta); var exportedBytes = exporter.Storage.ToGlbBytes(); m_logLabel += $"VRM write to {path}...\n"; File.WriteAllBytes(path, exportedBytes); Debug.Log("VRM ExportedBytes: " + exportedBytes.Length); var platformTarget=EditorUserBuildSettings.activeBuildTarget; var vrmHash=System.Guid.NewGuid().ToString().Replace("-",string.Empty); string dependency=null; if(exporter.BundleManifest!=null) { string bundleName=exporter.BundleManifest.GetAllAssetBundles()[0]; // Since we need to download dependecy bundle from remote when user load vrm model, // we need to write the remote bundle key into catolog instead of just name dependency=$"{bundleName}-{exporter.BundleManifest.GetAssetBundleHash(bundleName)}"; Debug.Log($"Include dependency bundle: {dependency}"); } catalogs.RemoveAll(x=>x.bundleName==root.name); catalogs.Add(CatalogUtilities.CreateVRMAvatarCatalog(platformTarget,root.name,vrmHash,dependency,path)); var catalogCollection = new BuildCatalogCollection { catalogs = catalogs.ToArray() }; var writer = new StreamWriter(Constants.AvatarBundleLocalCatalogPath, false); writer.WriteLine(JsonUtility.ToJson(catalogCollection)); writer.Close(); } } catch (Exception ex) { m_logLabel += ex.ToString(); // rethrow throw; } } private float _process = 0; private string _processNote = ""; private PutObjectRequest _request; private async Task UploadBundles(List assetBundles, FailedCallBack onFailed) { CosXmlConfig config = new CosXmlConfig.Builder() .SetRegion("ap-shanghai") .SetDebugLog(true) .Build(); var cosXml = new CosXmlServer(config, null); int totalCount = assetBundles.Count; int count = 1; foreach (var assetBundle in assetBundles) { if (!File.Exists(assetBundle.bundlePath)) { onFailed.Invoke(); return false; } try { var cosPath = $"{assetBundle.bundleName}-{assetBundle.bundleHash}"; string requestSignURL = await MetacityClient.GetUploadUrlAsync(cosPath); _request = new PutObjectRequest(null, null, assetBundle.bundlePath); _request.RequestURLWithSign = requestSignURL; _request.SetCosProgressCallback(delegate(long completed, long total) { _process = (float) completed / total; _processNote = string.Format("Upload({0}/{1})\n" + "uploaded {2} of {3} bytes. {4:##.##} % complete...", count, totalCount, completed, total, completed * 100.0 / total); }); PutObjectResult result = cosXml.PutObject(_request); if (result.IsSuccessful()) { count++; } else { return false; } } catch (COSXML.CosException.CosClientException clientEx) { Debug.LogException(clientEx); } catch (COSXML.CosException.CosServerException serverEx) { Debug.LogException(serverEx); } } return true; } private void _failedCallBack() { Debug.Log("Upload failed >>>"); } // private async void DoPublish() // { // // MetacityClient.client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", MetacityTokenWindow.AccessToken); // bool isValid = await MetacityClient.CheckAccessToken(); // if (!isValid) // { // Debug.LogError("Token is not valid, please get your access token from metacity website and enter it in the token window"); // return; // } // var catalogs = AvatarBundleExporter.LoadLastAvatarBuiltCatalog(); // var assetBundles = new List(); // if (catalogs != null) // { // for (var i = 0; i < catalogs.Count; i++) // { // assetBundles.Add(new MetaCity.BundleKit.Editor.AssetBundle() // { // bundlePath = catalogs[i].bundleLocalPath, // bundleName = catalogs[i].bundleName, // bundleType = catalogs[i].bundleType, // bundleHash = catalogs[i].bundleHash, // bundleDependencies = catalogs[i].bundleDependencies, // platform = catalogs[i].bundlePlatform // }); // } // // var isSuccess = await UploadBundles(assetBundles, _failedCallBack); // if (!isSuccess) // { // return; // } // // isSuccess = await MetacityClient.PublishAssetBundles(assetBundles); // if (!isSuccess) // { // Debug.LogError("fail to publish avatar bundles to COS"); // } // else // { // Debug.Log("Publish succeed"); // } // } // } } }