// Magica Cloth 2. // Copyright (c) 2023 MagicaSoft. // https://magicasoft.jp using System; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEditor; using UnityEngine; namespace MagicaCloth2 { /// /// 頂点ペイントウインドウ /// [InitializeOnLoad] public class ClothPainter { public enum PaintMode { None, /// /// Move/Fixed/Ignore/Invalid /// Attribute, /// /// Max Distance/Backstop /// Motion, } static PaintMode paintMode = PaintMode.None; /// /// 編集対象のクロスコンポーネント /// static MagicaCloth cloth = null; /// /// 編集対象のクロスEditorクラス /// static MagicaClothEditor clothEditor = null; /// /// 編集対象のエディットメッシュ /// static VirtualMesh editMesh = null; /// /// 編集対象のセレクションデータ /// static SelectionData selectionData = null; /// /// 編集開始時のセレクションデータ(コピー) /// static SelectionData initSelectionData = null; //========================================================================================= const int PointFlag_Selecting = 1; // 選択中 internal struct Point : IComparable { public int vindex; public float distance; public BitField32 flag; public int CompareTo(Point other) { if (distance != other.distance) return distance < other.distance ? 1 : -1; else if (vindex != other.vindex) return vindex < other.vindex ? 1 : -1; else return 0; } } static NativeList dispPointList; static NativeArray pointWorldPositions; static VirtualMeshRaycastHit rayhit = default; static bool oldShowAll = false; static bool forceUpdate = false; //========================================================================================= static ClothPainter() { // シーンビューにGUIを描画するためのコールバック SceneView.duringSceneGui += OnGUI; } /// /// ペイント開始 /// /// public static void EnterPaint(PaintMode mode, MagicaClothEditor editor, MagicaCloth clothComponent, VirtualMesh vmesh, SelectionData sdata) { Develop.DebugLog($"EnterPaint"); paintMode = mode; cloth = clothComponent; clothEditor = editor; editMesh = vmesh; selectionData = sdata; initSelectionData = sdata.Clone(); rayhit = default; forceUpdate = true; // ポイントバッファ dispPointList = new NativeList(vmesh.VertexCount, Allocator.Persistent); pointWorldPositions = new NativeArray(vmesh.VertexCount, Allocator.Persistent); // UndoRedoコールバック Undo.undoRedoPerformed += UndoRedoCallback; } /// /// ペイント終了 /// public static void ExitPaint() { Develop.DebugLog($"ExitPaint"); cloth = null; clothEditor = null; editMesh = null; selectionData = null; initSelectionData = null; rayhit = default; if (dispPointList.IsCreated) dispPointList.Dispose(); if (pointWorldPositions.IsCreated) pointWorldPositions.Dispose(); // UndoRedoコールバック Undo.undoRedoPerformed -= UndoRedoCallback; } /// /// Undo/Redo実行後のコールバック /// static void UndoRedoCallback() { //Develop.DebugLog($"Undo Redo!"); if (EditorApplication.isPlaying) return; if (cloth == null || editMesh == null || selectionData == null || editMesh.IsSuccess == false) return; // セレクションデータを取り直す selectionData = clothEditor.GetSelectionData(cloth, editMesh); forceUpdate = true; } //========================================================================================= /// /// 指定クロスコンポーネントを編集中かどうか /// /// /// public static bool HasEditCloth(MagicaCloth clothComponent) { return cloth == clothComponent; } /// /// 編集中かどうか /// /// public static bool IsPainting() { return cloth != null; } //========================================================================================= static void OnGUI(SceneView sceneView) { if (EditorApplication.isPlaying) return; // アクティブシーンビュー判定 if (SceneView.lastActiveSceneView != sceneView) return; if (cloth == null || editMesh == null || selectionData == null) return; if (editMesh.IsSuccess == false || selectionData.IsValid() == false) return; var windata = ScriptableSingleton.instance; // シーンビューカメラ Camera cam = SceneView.currentDrawingSceneView.camera; Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); Vector3 spos = ray.origin; Vector3 epos = spos + ray.direction * 1000.0f; // カメラ座標をローカル空間に変換する var t = cloth.ClothTransform; // コンポーネント空間 bool repaint = false; bool updatePoint = false; // ポイントサイズ float pointSize = windata.drawPointSize; bool showAll = windata.BackFaceCulling == false; // マウス移動中は常にメッシュとの交差判定 if (Event.current.type == EventType.MouseMove || Event.current.type == EventType.MouseDrag || oldShowAll != showAll || forceUpdate) { oldShowAll = showAll; forceUpdate = false; updatePoint = true; } // マウス選択 if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && !Event.current.alt) { // ペイント適用 ApplyPaint(windata); // シーンビューのエリア選択を出さないために、とりあえずこうするらしい int controlId = GUIUtility.GetControlID(FocusType.Passive); GUIUtility.hotControl = controlId; Event.current.Use(); // ? } if (Event.current.type == EventType.MouseDrag && Event.current.button == 0 && !Event.current.alt) { // ペイント適用 ApplyPaint(windata); Event.current.Use(); } if (Event.current.type == EventType.MouseUp && Event.current.button == 0 && !Event.current.alt) { // ペイント結果を適用する ApplySelectionData(); // マウスボタンUPでコントロールロックを解除するらしい GUIUtility.hotControl = 0; Event.current.Use(); // ? } // Handlesの描画はGUIの前でなければならない.理由は不明 if (Event.current.type == EventType.Repaint) { // ディスクの描画 if (rayhit.IsValid()) { Handles.matrix = Matrix4x4.identity; Handles.zTest = UnityEngine.Rendering.CompareFunction.Always; var dpos = t.TransformPoint(rayhit.position); var dnor = t.TransformDirection(rayhit.normal); Handles.color = new Color(0.3f, 0.5f, 1.0f, 0.4f); Handles.DrawSolidDisc(dpos, dnor, windata.brushSize); } // ポイント表示 Handles.lighting = true; //Handles.zTest = showAll ? UnityEngine.Rendering.CompareFunction.Always : UnityEngine.Rendering.CompareFunction.LessEqual; //Handles.zTest = UnityEngine.Rendering.CompareFunction.Always; Handles.zTest = windata.zTest ? UnityEngine.Rendering.CompareFunction.LessEqual : UnityEngine.Rendering.CompareFunction.Always; Handles.matrix = Matrix4x4.identity; int cnt = dispPointList.Length; for (int i = 0; i < cnt; i++) { var point = dispPointList[i]; Color col = Color.black; var attr = selectionData.attributes[point.vindex]; switch (paintMode) { case PaintMode.Attribute: if (attr.IsMove()) col = Color.green; else if (attr.IsFixed()) col = Color.red; //else if (attr.IsIgnore()) col = Color.blue; else col = Color.gray; break; case PaintMode.Motion: if (attr.IsMotion()) col = Color.cyan; else col = Color.gray; break; } // 選択中 if (point.flag.IsSet(PointFlag_Selecting)) { col = Color.yellow; } var pos = pointWorldPositions[point.vindex]; Handles.color = col; Handles.SphereHandleCap(0, pos, Quaternion.identity, windata.drawPointSize, EventType.Repaint); } // ペイント中のギズモ表示 if (windata.showShape) ClothEditorUtility.DrawClothEditor(editMesh, ClothEditorUtility.PaintSettings, cloth.SerializeData, true, true, true); } // GUI sceneView.BeginWindows(); string title = string.Empty; switch (paintMode) { case PaintMode.Attribute: title = "Movement attribute paint"; break; case PaintMode.Motion: title = "Max Distance/Backstop paint"; break; } var rect = GUILayout.Window(1, windata.windowRect, DoWindow, title); // ウインドウをSceneViewの領域内にクランプする // sceneView.positionでrectが取れる rect.x = Mathf.Clamp(rect.x, 0, Mathf.Max(sceneView.position.width - rect.width, 0)); rect.y = Mathf.Clamp(rect.y, 0, Mathf.Max(sceneView.position.height - rect.height - 25, 0)); rect.width = 300; rect.height = 205; windata.windowRect = rect; sceneView.EndWindows(); // ポイントデータ更新 if (updatePoint) { UpdatePoint(t, cam, ray, showAll, windata.brushSize, windata.drawPointSize); repaint = true; } // 画面リフレッシュ if (repaint) sceneView.Repaint(); } /// /// ポイントデータを更新する /// /// /// /// /// /// static void UpdatePoint(Transform ct, Camera cam, Ray ray, bool showAll, float brushSize, float pointSize) { // 表示頂点選別 CreateDispPointList(ct, cam.transform.position, cam.transform.forward, showAll, brushSize).Complete(); // サーフェース交差判定 rayhit = editMesh.IntersectRayMesh(ray.origin, ray.direction, showAll, pointSize); } static JobHandle CreateDispPointList(Transform ct, float3 camPos, float3 camDir, bool showAll, float brushSize, JobHandle jobHandle = default) { dispPointList.Clear(); int vcnt = editMesh.VertexCount; if (vcnt == 0) return jobHandle; // ローカルカメラ var localCameraDirection = ct.InverseTransformDirection(camDir); // 頂点のワールド変換と表示頂点の選別 var job = new CreateDispPointListJob() { LtoW = ct.localToWorldMatrix, showAll = showAll, cameraPosition = camPos, cameraDirection = localCameraDirection, useBrush = rayhit.IsValid(), brushPosition = ct.TransformPoint(rayhit.position), brushSize = brushSize, localPositions = editMesh.localPositions.GetNativeArray(), localNormals = editMesh.localNormals.GetNativeArray(), vertexToTriangles = editMesh.vertexToTriangles, pointWorldPositions = pointWorldPositions, dispPointList = dispPointList.AsParallelWriter(), }; jobHandle = job.Schedule(vcnt, 16, jobHandle); // 距離ソート var job2 = new SortDispPointJob() { dispPointList = dispPointList, }; jobHandle = job2.Schedule(jobHandle); return jobHandle; } [BurstCompile] struct CreateDispPointListJob : IJobParallelFor { public float4x4 LtoW; public bool showAll; public float3 cameraPosition; public float3 cameraDirection; public bool useBrush; public float3 brushPosition; public float brushSize; [Unity.Collections.ReadOnly] public NativeArray localPositions; [Unity.Collections.ReadOnly] public NativeArray localNormals; [Unity.Collections.ReadOnly] public NativeArray> vertexToTriangles; [Unity.Collections.WriteOnly] public NativeArray pointWorldPositions; [Unity.Collections.WriteOnly] public NativeList.ParallelWriter dispPointList; public void Execute(int vindex) { // ワールド座標 var lpos = localPositions[vindex]; var wpos = math.transform(LtoW, lpos); pointWorldPositions[vindex] = wpos; // カメラ距離 float dist = math.distance(cameraPosition, wpos); // 表示選別 bool show; if (showAll) { show = true; } else { // 頂点がトライアングルに属さない場合は無条件で表示する if (vertexToTriangles[vindex].Length == 0) show = true; else { // トライアングルに属する頂点は法線が画面に向いているもののみ表示 show = math.dot(cameraDirection, localNormals[vindex]) < 0.0f; } } if (show) { var flag = new BitField32(); // ブラシ範囲内に存在するか判定 if (useBrush) { var bdist = math.distance(brushPosition, wpos); if (bdist <= brushSize) flag.SetBits(PointFlag_Selecting, true); } dispPointList.AddNoResize(new Point() { vindex = vindex, distance = dist, flag = flag }); } } } [BurstCompile] struct SortDispPointJob : IJob { public NativeList dispPointList; public void Execute() { if (dispPointList.Length > 1) dispPointList.Sort(); } } /// /// 渡されたpointListをカメラからの距離の降順にソートして返す /// /// /// /// positionをワールド座標変換するマトリックス /// カメラワールド座標 /// internal static void CalcPointCameraDistance(NativeArray positions, int positonOffset, float4x4 LtoW, float3 camPos, NativeList pointList) { // ポイントのカメラからの距離を求める int cnt = pointList.Length; if (cnt <= 1) return; var job = new CalcCameraDistanceJob() { LtoW = LtoW, cameraPosition = camPos, positions = positions, pointList = pointList, positionOffset = positonOffset, }; job.Run(cnt); // 距離ソート var job2 = new SortDispPointJob() { dispPointList = pointList, }; job2.Run(); } [BurstCompile] struct CalcCameraDistanceJob : IJobParallelFor { public float4x4 LtoW; public float3 cameraPosition; [Unity.Collections.ReadOnly] public NativeArray positions; [NativeDisableParallelForRestriction] public NativeList pointList; public int positionOffset; public void Execute(int index) { var point = pointList[index]; // ワールド座標 var lpos = positions[positionOffset + point.vindex]; var wpos = math.transform(LtoW, lpos); // カメラ距離 float dist = math.distance(cameraPosition, wpos); point.distance = dist; pointList[index] = point; } } /// /// 選択中ポイントに属性を付与する /// /// static void ApplyPaint(ClothPainterWindowData windata) { int cnt = dispPointList.Length; for (int i = 0; i < cnt; i++) { var point = dispPointList[i]; if (point.flag.IsSet(PointFlag_Selecting) == false) continue; var attr = selectionData.attributes[point.vindex]; bool change = false; switch (paintMode) { case PaintMode.Attribute: // 移動/固定 if (windata.editAttribute == 0 && attr.IsMove() == false) { attr.SetFlag(VertexAttribute.Flag_Move, true); attr.SetFlag(VertexAttribute.Flag_Fixed, false); //attr.SetFlag(VertexAttribute.Flag_Ignore, false); change = true; } else if (windata.editAttribute == 1 && attr.IsFixed() == false) { attr.SetFlag(VertexAttribute.Flag_Move, false); attr.SetFlag(VertexAttribute.Flag_Fixed, true); //attr.SetFlag(VertexAttribute.Flag_Ignore, false); change = true; } //else if (windata.editAttribute == 2 && attr.IsIgnore() == false) //{ // attr.SetFlag(VertexAttribute.Flag_Move, false); // attr.SetFlag(VertexAttribute.Flag_Fixed, false); // attr.SetFlag(VertexAttribute.Flag_Ignore, true); // change = true; //} else if (windata.editAttribute == 3 && attr.IsInvalid() == false) { attr.SetFlag(VertexAttribute.Flag_Move, false); attr.SetFlag(VertexAttribute.Flag_Fixed, false); //attr.SetFlag(VertexAttribute.Flag_Ignore, false); change = true; } break; case PaintMode.Motion: // MaxDistance/Backstop if (windata.editMotion == 0 && attr.IsMotion() == false) { attr.SetFlag(VertexAttribute.Flag_InvalidMotion, false); change = true; } else if (windata.editMotion == 1 && attr.IsMotion()) { attr.SetFlag(VertexAttribute.Flag_InvalidMotion, true); change = true; } break; } if (change) selectionData.attributes[point.vindex] = attr; } } /// /// セレクションデータをシリアライズする /// static void ApplySelectionData() { // セレクションデータデータをシリアライズ化する selectionData.userEdit = true; // ユーザー編集フラグを立てる clothEditor?.ApplyClothPainter(selectionData); // Undo/Redoの状態を切り替えるためセレクションデータのクローンを作成して切り替える selectionData = selectionData.Clone(); } static void DoWindow(int unusedWindowID) { var windata = ScriptableSingleton.instance; // ポイントサイズスライダー windata.drawPointSize = EditorGUILayout.Slider("Draw Point Size", windata.drawPointSize, 0.001f, 0.1f); // ブラシサイズスライダー windata.brushSize = EditorGUILayout.Slider("Brush Size", windata.brushSize, 0.001f, 0.2f); // 形状表示 windata.showShape = EditorGUILayout.Toggle("Show Shape", windata.showShape); // 全表示 windata.BackFaceCulling = EditorGUILayout.Toggle("Back-face Culling", windata.BackFaceCulling); // Zテスト windata.zTest = EditorGUILayout.Toggle("Z Test", windata.zTest); // 属性ボタン EditorGUILayout.Space(); EditorGUILayout.LabelField("Paint Attribute"); Color bcol = GUI.backgroundColor; using (new EditorGUILayout.HorizontalScope()) { if (paintMode == PaintMode.Attribute) { // Move/Fixed int nowAttr = windata.editAttribute; for (int i = 0; i < 4; i++) { // !現在Ignoreはオミット if (i == 2) continue; Color col = Color.black; string title = string.Empty; switch (i) { case 0: // move col = Color.green; title = "Move"; break; case 1: // fixed col = Color.red; title = "Fixed"; break; case 2: // ignore col = Color.blue; title = "Ignore"; break; case 3: // invalid col = Color.gray; title = "Invalid"; break; } GUI.backgroundColor = i == nowAttr ? col : Color.gray; bool ret = GUILayout.Toggle(i == nowAttr, title, EditorStyles.miniButton); if (ret) { nowAttr = i; } } windata.editAttribute = nowAttr; } else if (paintMode == PaintMode.Motion) { // MaxDistance/Backstop int nowAttr = windata.editMotion; for (int i = 0; i < 2; i++) { // カラー var col = i == 0 ? Color.cyan : Color.red; GUI.backgroundColor = i == nowAttr ? col : Color.gray; bool ret = GUILayout.Toggle(i == nowAttr, i == 0 ? "Valid" : "Invalid", EditorStyles.miniButton); if (ret) { nowAttr = i; } } windata.editMotion = nowAttr; } } GUI.backgroundColor = bcol; // 適用/キャンセルボタン EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.Space(); if (GUILayout.Button("Exit")) { // ペイント終了フラグ cloth = null; // この編集によりセレクションデータに変更があった場合はProxyMeshをリビルドする if (initSelectionData.Compare(selectionData) == false) { Develop.DebugLog($"Change selection data!"); clothEditor?.UpdateEditMesh(); } ExitPaint(); } EditorGUILayout.Space(); } GUI.DragWindow(); } } //============================================================================================= /// /// ペイントウインドウの保持データ /// ScriptableSingletonを利用することによりエディタ終了時までデータを保持できる /// public class ClothPainterWindowData : ScriptableSingleton { /// /// ウインドウの位置とサイズ /// public Rect windowRect = new Rect(100, 100, 400, 205); /// /// 表示ポイントサイズ /// public float drawPointSize = 0.02f; /// /// ブラシサイズ /// public float brushSize = 0.05f; /// /// 裏面カリング /// public bool BackFaceCulling = true; /// /// 形状を表示 /// public bool showShape = true; /// /// Zテスト /// public bool zTest = false; /// /// 現在アクティブなポイント属性(0=Move/1=Fixed/2=Ignore/3=Invalid) /// VertexAttributeとは異なるので注意! /// public int editAttribute = 0; /// /// 現在アクティブなモーション制約(0=Valid, 1=Invalid) /// public int editMotion = 0; } }