// Magica Cloth 2. // Copyright (c) 2023 MagicaSoft. // https://magicasoft.jp using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Unity.Mathematics; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.Compilation; #if UNITY_2020 using UnityEditor.Experimental.SceneManagement; #else using UnityEditor.SceneManagement; #endif using UnityEngine; namespace MagicaCloth2 { /// /// エディタ編集時のコンポーネント管理 /// [InitializeOnLoad] public class ClothEditorManager { /// /// コンポーネント情報 /// class ClothInfo { public ResultCode result = ResultCode.Empty; public bool building; public GizmoType gizmoType; public ClothBehaviour component; public int componentHash; public VirtualMesh editMesh; public int nextBuildHash; public int importCount; } static Dictionary editClothDict = new Dictionary(); static List destroyList = new List(); static List drawList = new List(); static CancellationTokenSource cancelToken = new CancellationTokenSource(); static bool isValid = false; static internal Action OnEditMeshBuildComplete; //========================================================================================= static ClothEditorManager() { Develop.DebugLog($"ClothEditorManager Initialize!"); Dispose(); // スクリプトコンパイルコールバック CompilationPipeline.compilationStarted -= OnStartCompile; CompilationPipeline.compilationStarted += OnStartCompile; // シーンビューにGUIを描画するためのコールバック SceneView.duringSceneGui -= OnSceneGUI; SceneView.duringSceneGui += OnSceneGUI; // プレハブステージ PrefabStage.prefabStageClosing -= OnPrefabStageClosing; PrefabStage.prefabStageClosing += OnPrefabStageClosing; // Undo/Redo Undo.undoRedoPerformed -= OnUndoRedoPerformed; Undo.undoRedoPerformed += OnUndoRedoPerformed; isValid = true; } /// /// エディタの実行状態が変更された場合に呼び出される /// [InitializeOnLoadMethod] static void PlayModeStateChange() { EditorApplication.playModeStateChanged += (mode) => { //Develop.DebugLog($"PlayModeStateChanged:{mode}"); if (mode == UnityEditor.PlayModeStateChange.ExitingEditMode || mode == UnityEditor.PlayModeStateChange.ExitingPlayMode) { //Develop.DebugLog($"PlayModeStateChange. Exit Editor mode!"); Dispose(); isValid = false; } if (mode == UnityEditor.PlayModeStateChange.EnteredEditMode || mode == UnityEditor.PlayModeStateChange.EnteredPlayMode) { //Develop.DebugLog($"PlayModeStateChange. Enter Editor mode!"); isValid = true; } }; } /// /// スクリプトコンパイル開始 /// /// static void OnStartCompile(object obj) { Develop.DebugLog($"start compile."); Dispose(); isValid = false; } /// /// ビルド完了時 /// /// /// [PostProcessBuildAttribute(1)] static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) { Develop.DebugLog($"build compile."); isValid = true; } /// /// プレハブステージが閉じる時 /// /// static void OnPrefabStageClosing(PrefabStage pstage) { ForceUpdateAllComponents(); } /// /// Undo/Redo実行時 /// static void OnUndoRedoPerformed() { ForceUpdateAllComponents(); } /// /// MagidaClothコンポーネントの登録および編集メッシュの作成/更新 /// /// public static void RegisterComponent(ClothBehaviour component, GizmoType gizmoType, bool forceUpdate = false) { //Develop.DebugLog($"RegisterComponent:{component.name}, isValid:{isValid}"); if (isValid == false) return; if (component == null) return; // ペイント中は無効 if (ClothPainter.IsPainting()) return; int id = component.GetInstanceID(); MagicaCloth cloth = component as MagicaCloth; //if (cloth) // Develop.DebugLog($"Register Cloth:{component.name}, gizmoType:{gizmoType}"); // ギズモ表示判定 if (gizmoType.HasFlag(GizmoType.Active)) { gizmoType = GizmoType.Active; } else { // ★何故かGizmoType.InSelectionHierarchyが正常に判定できないので手動で解決する!(2022/10/14) // ★GizmoType.InSelectionHierarchyがバグっている? gizmoType = 0; var t = component.transform.parent; var activeT = Selection.activeTransform; if (activeT) { while (t) { if (t == activeT) { gizmoType = GizmoType.Selected; break; } else t = t.parent; } } // 常に表示 if (cloth && cloth.GizmoSerializeData.IsAlways()) gizmoType = GizmoType.Active; } // クロス指定のコライダー表示 if (cloth && cloth.GizmoSerializeData.clothDebugSettings.collider && gizmoType != 0) { foreach (var col in cloth.SerializeData.colliderCollisionConstraint.colliderList) { RegisterComponent(col, gizmoType); } } lock (editClothDict) { if (editClothDict.ContainsKey(id) == false) { //Debug.Log($"登録:{component.name}"); var info = new ClothInfo(); info.building = false; info.result = ResultCode.Empty; info.component = component; info.editMesh = null; info.gizmoType = gizmoType; info.componentHash = 0; info.nextBuildHash = 0; info.importCount = 1; editClothDict.Add(id, info); } else { if (gizmoType > editClothDict[id].gizmoType) editClothDict[id].gizmoType = gizmoType; } } // EditMesh作成(MagicaClothコンポーネントのみ) if (cloth && EditorApplication.isPlaying == false) { int hash = component.GetHashCode(); lock (editClothDict) { if (editClothDict.ContainsKey(id)) { var info = editClothDict[id]; // ハッシュにはインポート回数を乗算する hash *= (info.importCount + 1); // ハッシュが異なる場合のみ再構築/もしくは強制更新 if (info.componentHash != hash || forceUpdate) { // 現在作成中ならば次の作成候補として登録 if (info.building) { info.nextBuildHash = hash; } // そうでなければ作成を開始する else { info.componentHash = hash; info.nextBuildHash = 0; if (cloth.isActiveAndEnabled) { // スレッドで作成 info.building = true; var _ = CreateOrUpdateEditMesh(id, cloth, cancelToken.Token); } } } } } } } public static VirtualMesh GetEditMesh(ClothBehaviour comp) { if (isValid == false) return null; if (comp == null) return null; int id = comp.GetInstanceID(); VirtualMesh vmesh = null; lock (editClothDict) { vmesh = editClothDict.ContainsKey(id) ? editClothDict[id].editMesh : null; } return vmesh; } /// /// 現在のコンポーネント状態を返す /// /// /// public static ResultCode GetResultCode(ClothBehaviour comp) { if (comp == null) return ResultCode.Empty; int id = comp.GetInstanceID(); lock (editClothDict) { if (editClothDict.ContainsKey(id)) return editClothDict[id].result; else return ResultCode.Empty; } } static void Dispose() { cancelToken.Cancel(); cancelToken.Dispose(); cancelToken = new CancellationTokenSource(); destroyList.Clear(); drawList.Clear(); lock (editClothDict) { foreach (var info in editClothDict.Values) { info?.editMesh?.Dispose(); } editClothDict.Clear(); } } /// /// コンポーネントの削除チェック /// static void DestroyCheck() { lock (editClothDict) { destroyList.Clear(); foreach (var kv in editClothDict) { if (kv.Value.component == null) { destroyList.Add(kv.Key); } } foreach (var id in destroyList) { //Debug.Log($"削除"); editClothDict[id].editMesh?.Dispose(); editClothDict.Remove(id); } destroyList.Clear(); } } /// /// アセット更新にともなう編集用メッシュの更新 /// /// public static void UpdateFromAssetImport(string[] importedAssets) { if (importedAssets == null || importedAssets.Length == 0) return; var importedAssetSet = new HashSet(importedAssets); // クロスコンポーネントがインポートされたアセットを参照している場合は再構築フラグを立てる var updateDict = new Dictionary(); lock (editClothDict) { foreach (var cinfo in editClothDict.Values) { var cloth = cinfo.component as MagicaCloth; if (cloth) { bool update = false; var sdata = cloth.SerializeData; // source renderes if (sdata.clothType == ClothProcess.ClothType.MeshCloth) { foreach (var ren in sdata.sourceRenderers) { if (ren && importedAssetSet.Contains(AssetDatabase.GetAssetPath(ren))) update = true; } } // paint maps if (sdata.clothType == ClothProcess.ClothType.MeshCloth && sdata.paintMode != ClothSerializeData.PaintMode.Manual) { foreach (var map in sdata.paintMaps) { if (map && importedAssetSet.Contains(AssetDatabase.GetAssetPath(map))) update = true; } } if (update) { // 再構築フラグ // インポートカウントを増加させることでハッシュ値を変化させる cinfo.importCount++; updateDict.Add(cinfo.component, cinfo.gizmoType); } } } } // 再構築開始 foreach (var kv in updateDict) { RegisterComponent(kv.Key, kv.Value); } } /// /// 強制的にすべてのコンポーネントの更新フラグを立てる /// static void ForceUpdateAllComponents() { lock (editClothDict) { foreach (var cinfo in editClothDict.Values) { var cloth = cinfo.component as MagicaCloth; if (cloth) { // 再構築フラグ // インポートカウントを増加させることでハッシュ値を変化させる // 次のギズモ表示もしくはペイント時に再構築される cinfo.importCount++; } } } } //========================================================================================= /// /// 編集用メッシュの作成/更新 /// /// /// /// /// /// static async Task CreateOrUpdateEditMesh(int id, MagicaCloth cloth, CancellationToken ct) { // ■メインスレッド Develop.DebugLog($"Create and update edit meshes: {cloth.name}"); var sdata = cloth.SerializeData; var sdata2 = cloth.GetSerializeData2(); var setupList = new List(); VirtualMesh editMesh = null; ResultCode result = new ResultCode(); try { // ■メインスレッド if (cloth == null || isValid == false) { result.SetError(Define.Result.CreateCloth_InvalidCloth); throw new MagicaClothProcessingException(); } // メッシュを構築するための最低限のデータが揃っているか確認 if (sdata.IsValid() == false) { result.SetResult(Define.Result.Empty); throw new MagicaClothProcessingException(); } // セットアップデータの作成 if (sdata.clothType == ClothProcess.ClothType.MeshCloth) { foreach (var ren in sdata.sourceRenderers) { if (ren) { var setup = new RenderSetupData(ren); if (setup.IsFaild()) { setup.Dispose(); result.Set(setup.result); throw new MagicaClothProcessingException(); } setupList.Add(setup); } else { result.SetError(Define.Result.CreateCloth_NoRenderer); throw new MagicaClothProcessingException(); } } } else if (sdata.clothType == ClothProcess.ClothType.BoneCloth) { var setup = new RenderSetupData(cloth.ClothTransform, sdata.rootBones, sdata.connectionMode, cloth.name); setupList.Add(setup); } if (setupList.Count == 0) { result.SetError(Define.Result.CreateCloth_InvalidSetupList); throw new MagicaClothProcessingException(); } // クロスコンポーネントトランスフォーム情報 var clothTransformRecord = new TransformRecord(cloth.ClothTransform); // 法線調整用トランスフォーム var normalAdjustmentTransformRecored = new TransformRecord( sdata.normalAlignmentSetting.adjustmentTransform ? sdata.normalAlignmentSetting.adjustmentTransform : cloth.ClothTransform ); // ペイントマップデータの作成(これはメインスレッドでのみ作成可能) var paintMapDataList = new List(); if (sdata.clothType == ClothProcess.ClothType.MeshCloth && sdata.paintMode != ClothSerializeData.PaintMode.Manual) { var ret = cloth.Process.GeneratePaintMapDataList(paintMapDataList); if (ret.IsError()) { result = ret; throw new MagicaClothProcessingException(); } if (paintMapDataList.Count != setupList.Count) { result.SetError(Define.Result.CreateCloth_PaintMapCountMismatch); throw new MagicaClothProcessingException(); } } // エディットメッシュ作成 // メッシュはセンター空間で作成される ct.ThrowIfCancellationRequested(); editMesh = new VirtualMesh("EditMesh"); if (sdata.clothType == ClothProcess.ClothType.MeshCloth) { // MeshClothではクロストランスフォームを追加しておく editMesh.SetTransform(cloth.ClothTransform); } editMesh.result.SetProcess(); // ■スレッド await Task.Run(() => { try { // MeshCloth/BoneClothで処理が一部異なる ct.ThrowIfCancellationRequested(); if (sdata.clothType == ClothProcess.ClothType.MeshCloth) { foreach (var setup in setupList) { // レンダーメッシュ作成 using var renderMesh = new VirtualMesh($"[{setup.name}]"); renderMesh.result.SetProcess(); // インポート renderMesh.ImportFrom(setup); //Debug.Log($"(IMPORT) {renderMesh}"); if (renderMesh.IsError) continue; renderMesh.result.SetSuccess(); // マージ editMesh.AddMesh(renderMesh); } //Debug.Log($"(MERGE) {editMesh}"); // リダクション if (editMesh.VertexCount > 1 && sdata.reductionSetting.IsEnabled) { editMesh.Reduction(sdata.reductionSetting, ct); if (editMesh.IsError) { result = editMesh.result; throw new MagicaClothProcessingException(); } //Debug.Log($"(REDUCTION) {editMesh}"); } } else if (sdata.clothType == ClothProcess.ClothType.BoneCloth) { // import editMesh.ImportFrom(setupList[0]); if (editMesh.IsError) { result = editMesh.result; throw new MagicaClothProcessingException(); } //Debug.Log($"(IMPORT) {editMesh}"); } // 元の頂点から結合頂点へのインデックスを初期化 if (editMesh.joinIndices.IsCreated == false) { editMesh.joinIndices = new Unity.Collections.NativeArray(editMesh.VertexCount, Unity.Collections.Allocator.Persistent); JobUtility.SerialNumberRun(editMesh.joinIndices, editMesh.VertexCount); // 連番をつける } // 最適化 ct.ThrowIfCancellationRequested(); editMesh.Optimization(); if (editMesh.IsError) { result = editMesh.result; throw new MagicaClothProcessingException(); } //Debug.Log($"(OPTIMIZE) {editMesh}"); } catch (OperationCanceledException) { throw; } catch (MagicaClothProcessingException) { throw; } catch (Exception exception) { Debug.LogException(exception); result.SetError(Define.Result.CreateCloth_Exception); throw; } }, ct); // ■メインスレッド // セレクションデータの作成 // まだセレクションデータが未編集の場合は作り直す ct.ThrowIfCancellationRequested(); { if (sdata2.selectionData == null || sdata2.selectionData.IsValid() == false || sdata2.selectionData.userEdit == false) { // 新規作成 var selectionData = CreateAutoSelectionData(cloth, sdata, editMesh); // 格納 //ApplySelectionData(cloth, selectionData); sdata2.selectionData = selectionData; } } // ■スレッド await Task.Run(() => { try { // MeshClothでペイントテクスチャ指定の場合はセレクションデータを生成する ct.ThrowIfCancellationRequested(); var selectionData = sdata2.selectionData; if (sdata.clothType == ClothProcess.ClothType.MeshCloth && sdata.paintMode != ClothSerializeData.PaintMode.Manual) { // ペイントマップからセレクションデータを生成する var ret = cloth.Process.GenerateSelectionDataFromPaintMap(editMesh, clothTransformRecord, setupList, paintMapDataList, out selectionData); if (ret.IsError()) { result = ret; throw new MagicaClothProcessingException(); } } // セレクションデータから頂点属性を付与する ct.ThrowIfCancellationRequested(); if (selectionData.IsValid()) { editMesh.ApplySelectionAttribute(selectionData); } // ProxyMeshへの変換(属性決定後に実行) ct.ThrowIfCancellationRequested(); editMesh.ConvertProxyMesh(cloth.SerializeData, null, null, normalAdjustmentTransformRecored); if (editMesh.IsError) { result = editMesh.result; throw new MagicaClothProcessingException(); } //Debug.Log($"(PROXY) {editMesh}"); #if false // pitch/yaw個別制限はv1.0では実装しないので一旦停止 // 角度制限計算用回転を作成 ct.ThrowIfCancellationRequested(); editMesh.CreateAngleCalcLocalRotation(sdata.normalCalculation, sdata.normalCalculationCenter); if (editMesh.IsError) throw new InvalidOperationException(); #endif // 完了 ct.ThrowIfCancellationRequested(); editMesh.result.SetSuccess(); } catch (OperationCanceledException) { throw; } catch (MagicaClothProcessingException) { throw; } catch (Exception exception) { Debug.LogException(exception); result.SetError(Define.Result.CreateCloth_Exception); throw; } }, ct); // ■メインスレッド if (cloth == null) { //result.SetError(Define.Result.CreateCloth_InvalidCloth); //throw new MagicaClothProcessingException(); // キャンセル扱いにする throw new OperationCanceledException(); } // 成功 Develop.DebugLog($"(FINAL PROXY) {editMesh}"); result.SetSuccess(); } catch (MagicaClothProcessingException) { if (result.IsNone()) result.SetError(Define.Result.CreateCloth_UnknownError); result.DebugLog(); } catch (OperationCanceledException) { Develop.DebugLog($"Editor mesh build canceled!"); result.SetCancel(); } catch (Exception exception) { Debug.LogException(exception); result.SetError(Define.Result.CreateCloth_Exception); } finally { // 状態変更 lock (editClothDict) { if (editClothDict.ContainsKey(id)) { var info = editClothDict[id]; info.building = false; info.result = result; info.editMesh?.Dispose(); if (result.IsSuccess()) { info.editMesh = editMesh; editMesh = null; Develop.DebugLog($"Registration Complete : {cloth.name}"); } else { info.editMesh = null; } } } // dispose foreach (var setup in setupList) { setup?.Dispose(); } editMesh?.Dispose(); //span.DebugLog(); // 引き続き再構築の実行判定 lock (editClothDict) { if (editClothDict.ContainsKey(id)) { var info = editClothDict[id]; info.building = false; if (info.nextBuildHash != 0 && cloth) { if (info.nextBuildHash == info.componentHash) { info.nextBuildHash = 0; } else { info.componentHash = info.nextBuildHash; info.nextBuildHash = 0; info.building = true; info.result.Clear(); var _ = CreateOrUpdateEditMesh(id, cloth, ct); } } } } SceneView.RepaintAll(); // ビルド完了通知 OnEditMeshBuildComplete?.Invoke(); } } /// /// セレクションデータをシリアライズ化する /// /// /// public static void ApplySelectionData(MagicaCloth cloth, SelectionData selectionData) { if (cloth == null || selectionData == null || selectionData.IsValid() == false) return; if (cloth.GetSerializeData2().selectionData != null && cloth.GetSerializeData2().selectionData.Compare(selectionData)) return; // 変更なし //Debug.Log($"セレクションデータ格納!"); Undo.RecordObject(cloth, "Paint"); // 最終的に[SerializeReference]から[SerializeField]に変更(2022/12/18) cloth.GetSerializeData2().selectionData = selectionData; EditorUtility.SetDirty(cloth); } /// /// 編集メッシュから自動でセレクションデータを生成する(メインスレッドのみ) /// /// /// /// /// public static SelectionData CreateAutoSelectionData(MagicaCloth cloth, ClothSerializeData sdata, VirtualMesh emesh) { var selectionData = new SelectionData(emesh, float4x4.identity); int cnt = selectionData.Count; if (cnt == 0) return selectionData; // BoneClothはRootをFixedに定義する、それ以外はMove if (sdata.clothType == ClothProcess.ClothType.BoneCloth) { selectionData.Fill(VertexAttribute.Move); // BoneClothではセットアップデータのrootのみ固定に設定する using var setup = new RenderSetupData(cloth.ClothTransform, sdata.rootBones, sdata.connectionMode, cloth.name); foreach (int id in setup.rootTransformIdList) { int rootIndex = setup.GetTransformIndexFromId(id); selectionData.attributes[rootIndex] = VertexAttribute.Fixed; } } // MeshClothではすべて固定で定義する else { selectionData.Fill(VertexAttribute.Invalid); } return selectionData; } //========================================================================================= /// /// シーンビューへのギズモ描画 /// /// static void OnSceneGUI(SceneView sceneView) { if (isValid == false) return; // コンポーネント削除チェック DestroyCheck(); // コンポーネントギズモ描画 if (Event.current.type == EventType.Repaint) { //Develop.DebugLog($"Repaint. F:{Time.frameCount}"); drawList.Clear(); bool isPainting = ClothPainter.IsPainting(); bool isPlaying = EditorApplication.isPlaying; var camPos = sceneView.camera.transform.position; Quaternion camRot = sceneView.camera.transform.rotation; lock (editClothDict) { foreach (var info in editClothDict.Values) { if (info == null || info.component == null) continue; if (info.component.isActiveAndEnabled == false) continue; // 描画フラグ if (info.gizmoType.HasFlag(GizmoType.Active) == false && info.gizmoType.HasFlag(GizmoType.Selected) == false) continue; if (info.component is MagicaCloth && info.gizmoType.HasFlag(GizmoType.Active) == false) continue; // ペイント中は表示しない if (isPainting) continue; // コンポーネント距離(ただし非選択時のみ) bool selected = Selection.Contains(info.component.gameObject); if (selected == false) { float dist = Vector3.Distance(camPos, info.component.transform.position); if (dist >= 20.0f) // 20m continue; } // 選択状態 selected = info.gizmoType.HasFlag(GizmoType.Active); // Collider if (info.component is ColliderComponent) { GizmoUtility.DrawCollider(info.component as ColliderComponent, camRot, true, selected); } // MagicaCloth else if (info.component is MagicaCloth) { var cloth = info.component as MagicaCloth; // Cloth if (cloth.GizmoSerializeData.clothDebugSettings.enable) { //Debug.Log($"ペイントくろす"); if (isPlaying) ClothEditorUtility.DrawClothRuntime(cloth.Process, cloth.GizmoSerializeData.clothDebugSettings, selected); else ClothEditorUtility.DrawClothEditor(info.editMesh, cloth.GizmoSerializeData.clothDebugSettings, cloth.SerializeData, selected, false, false); } #if MC2_DEBUG // Proxy Mesh if (cloth.GizmoSerializeData.proxyDebugSettings.enable) { if (isPlaying) VirtualMeshEditorUtility.DrawRuntimeGizmos(cloth.Process, false, cloth.Process.ProxyMesh, cloth.GizmoSerializeData.proxyDebugSettings, selected, true); else VirtualMeshEditorUtility.DrawGizmos(info.editMesh, cloth.GizmoSerializeData.proxyDebugSettings, selected, true); } // Mapping Mesh if (cloth.GizmoSerializeData.mappingDebugSettings.enable) { if (isPlaying) { var renderMeshInfo = cloth.Process.GetRenderMeshInfo(cloth.GizmoSerializeData.debugMappingIndex); if (renderMeshInfo != null && renderMeshInfo.renderMesh != null) VirtualMeshEditorUtility.DrawRuntimeGizmos(cloth.Process, true, renderMeshInfo.renderMesh, cloth.GizmoSerializeData.mappingDebugSettings, selected, true); } } #endif // MC2_DEBUG } // 描画フラグoff info.gizmoType = 0; } } } } } }