// 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 VirtualMeshContainer editMeshContainer = null; /// /// 編集対象のセレクションデータ /// static SelectionData selectionData = null; /// /// 編集開始時のセレクションデータ(コピー) /// static SelectionData initSelectionData = null; internal const int WindowWidth = 200; internal const int WindowHeight = 200; //========================================================================================= 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, VirtualMeshContainer cmesh, SelectionData sdata) { Develop.DebugLog($"EnterPaint"); paintMode = mode; cloth = clothComponent; clothEditor = editor; editMeshContainer = cmesh; selectionData = sdata; initSelectionData = sdata.Clone(); rayhit = default; forceUpdate = true; // ポイントバッファ dispPointList = new NativeList(cmesh.shareVirtualMesh.VertexCount, Allocator.Persistent); pointWorldPositions = new NativeArray(cmesh.shareVirtualMesh.VertexCount, Allocator.Persistent); // UndoRedoコールバック Undo.undoRedoPerformed += UndoRedoCallback; } /// /// ペイント終了 /// public static void ExitPaint() { Develop.DebugLog($"ExitPaint"); cloth = null; clothEditor = null; editMeshContainer = 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 || editMeshContainer == null || selectionData == null || editMeshContainer.shareVirtualMesh.IsSuccess == false) return; // セレクションデータを取り直す selectionData = clothEditor.GetSelectionData(cloth, editMeshContainer.shareVirtualMesh); 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 || editMeshContainer == null || editMeshContainer.shareVirtualMesh == null || selectionData == null) return; if (editMeshContainer.shareVirtualMesh.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; // 透過カーソル用 float rayAngle = Vector3.Angle(cam.transform.forward, ray.direction); float nearPlaneDistance = cam.orthographic ? cam.nearClipPlane : cam.nearClipPlane / math.cos(math.radians(rayAngle)); float nearPlaneVSize = cam.orthographic ? cam.orthographicSize : math.tan(math.radians(cam.fieldOfView * 0.5f)) * cam.nearClipPlane * 2; float brushSizeThrough = nearPlaneVSize * windata.brushSizeThrough; // カーソルのviewportポイント var sp = HandleUtility.GUIPointToScreenPixelCoordinate(Event.current.mousePosition); var cursorViewpos = cam.ScreenToViewportPoint(sp); cursorViewpos = math.remap(new float3(0), new float3(1), new float3(-1), new float3(1), cursorViewpos); // 射影行列(-1 ~ +1)に変換 // カメラ座標をローカル空間に変換する 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, dispPointList); // シーンビューのエリア選択を出さないために、とりあえずこうするらしい 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, dispPointList); 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 (windata.through) { // 透過モード Handles.matrix = Matrix4x4.identity; Handles.zTest = UnityEngine.Rendering.CompareFunction.Always; var dpos = spos + ray.direction * nearPlaneDistance; var dnor = cam.transform.forward; Handles.color = new Color(0.6f, 0.2f, 1.0f, 0.3f); Handles.DrawSolidDisc(dpos, dnor, brushSizeThrough); } else 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(editMeshContainer, ClothEditorUtility.PaintSettings, cloth.SerializeData, true, windata.backFaceCulling, 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(71903439, windata.windowRect, DoWindow, title); //Debug.Log(windata.windowRect); // ウインドウを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 = WindowWidth; rect.height = WindowHeight; windata.windowRect = rect; //sceneView.EndWindows(); // ポイントデータ更新 if (updatePoint) { UpdatePoint( windata.through, t, cam, ray, showAll, windata.through ? windata.brushSizeThrough : windata.brushSize, windata.through ? cursorViewpos : t.TransformPoint(rayhit.position), windata.drawPointSize ); repaint = true; } // 画面リフレッシュ if (repaint) sceneView.Repaint(); } /// /// ポイントデータを更新する /// /// /// /// /// /// static void UpdatePoint(bool through, Transform ct, Camera cam, Ray ray, bool showAll, float brushSize, float3 brushPos, float pointSize) { // 表示頂点選別 CreateDispPointList(through, ct, cam, showAll, brushSize, brushPos).Complete(); // サーフェース交差判定 //rayhit = editMesh.IntersectRayMesh(ray.origin, ray.direction, showAll, pointSize); rayhit = editMeshContainer.shareVirtualMesh.IntersectRayMesh(ray.origin, ray.direction, showAll, pointSize); } static JobHandle CreateDispPointList( bool through, Transform ct, Camera cam, bool showAll, float brushSize, float3 brushPosition, JobHandle jobHandle = default ) { dispPointList.Clear(); var editMesh = editMeshContainer.shareVirtualMesh; int vcnt = editMesh.VertexCount; if (vcnt == 0) return jobHandle; // ローカルカメラ var localCameraDirection = ct.InverseTransformDirection(cam.transform.forward); // 頂点のワールド変換と表示頂点の選別 var job = new CreateDispPointListJob() { through = through, LtoW = ct.localToWorldMatrix, showAll = showAll, cameraPosition = cam.transform.position, cameraDirection = localCameraDirection, cameraProjectionMatrix = cam.projectionMatrix, worldToCameraMatrix = cam.worldToCameraMatrix, cameraAspectRatio = cam.aspect, useBrush = through || rayhit.IsValid(), brushPosition = brushPosition, 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 bool through; public float4x4 LtoW; public bool showAll; public float3 cameraPosition; public float3 cameraDirection; public float4x4 cameraProjectionMatrix; public float4x4 worldToCameraMatrix; public float cameraAspectRatio; 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) { if (through) { // 透過モード // 射影変換 var cpos = math.mul(worldToCameraMatrix, new float4(wpos, 1)); var vpos = math.mul(cameraProjectionMatrix, new float4(cpos.xyz, 1)); vpos /= vpos.w; // wで割る必要あり // ブラシはアスペクト比を考慮した楕円で判定する必要あり // brushPositionはviewport座標を指す float2 v = vpos.xy - brushPosition.xy; v.x *= cameraAspectRatio; var bdist = math.length(v); if (bdist <= brushSize) flag.SetBits(PointFlag_Selecting, true); } else { // サーフェス選択モード 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, NativeList applyPointList) { int cnt = applyPointList.Length; for (int i = 0; i < cnt; i++) { var point = applyPointList[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 Fill(ClothPainterWindowData windata) { // 塗りつぶし用のポイントデータを作成する //int vcnt = editMesh.VertexCount; int vcnt = editMeshContainer.shareVirtualMesh.VertexCount; using var fillDispPointList = new NativeList(vcnt, Allocator.TempJob); BitField32 flag = new(); flag.SetBits(PointFlag_Selecting, true); for (int i = 0; i < vcnt; i++) { var p = new Point() { vindex = i, distance = 0, flag = flag, }; fillDispPointList.Add(p); } // セレクションデータへ反映 ApplyPaint(windata, fillDispPointList); // シリアライズ ApplySelectionData(); } static void DoWindow(int unusedWindowID) { var windata = ScriptableSingleton.instance; //Debug.Log(windata.windowRect); //float lineHight = EditorGUIUtility.singleLineHeight; // ポイントサイズスライダー const float sliderLableWidth = 80; const float sliderWidth = 180; using (var h = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("Point Size", GUILayout.Width(sliderLableWidth)); windata.drawPointSize = EditorGUILayout.Slider(GUIContent.none, windata.drawPointSize, 0.001f, 0.1f, GUILayout.Width(sliderWidth)); } // ブラシサイズスライダー using (var h = new EditorGUILayout.HorizontalScope()) { if (windata.through) { EditorGUILayout.LabelField("Brush Size T", GUILayout.Width(sliderLableWidth)); windata.brushSizeThrough = EditorGUILayout.Slider(GUIContent.none, windata.brushSizeThrough, 0.01f, 0.3f, GUILayout.Width(sliderWidth)); } else { EditorGUILayout.LabelField("Brush Size", GUILayout.Width(sliderLableWidth)); windata.brushSize = EditorGUILayout.Slider(GUIContent.none, windata.brushSize, 0.001f, 0.2f, GUILayout.Width(sliderWidth)); } } EditorGUILayout.Space(); const float toggleLableWidth = 80; const float toggleButtonWidth = 30; using (var h = new EditorGUILayout.HorizontalScope()) { // 形状表示 EditorGUILayout.LabelField("Shape", GUILayout.Width(toggleLableWidth)); windata.showShape = EditorGUILayout.Toggle(GUIContent.none, windata.showShape, GUILayout.Width(toggleButtonWidth)); // 全表示 EditorGUILayout.LabelField("Culling", GUILayout.Width(toggleLableWidth)); windata.backFaceCulling = EditorGUILayout.Toggle(GUIContent.none, windata.backFaceCulling, GUILayout.Width(toggleButtonWidth)); } using (var h = new EditorGUILayout.HorizontalScope()) { // Zテスト EditorGUILayout.LabelField("Z-Test", GUILayout.Width(toggleLableWidth)); windata.zTest = EditorGUILayout.Toggle(GUIContent.none, windata.zTest, GUILayout.Width(toggleButtonWidth)); // 透過 EditorGUILayout.LabelField("Through", GUILayout.Width(toggleLableWidth)); windata.through = EditorGUILayout.Toggle(GUIContent.none, windata.through, GUILayout.Width(toggleButtonWidth)); } // 属性ボタン 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()) { // 塗りつぶしボタン if (GUILayout.Button("Fill", GUILayout.Width(70))) { Fill(windata); } GUILayout.FlexibleSpace(); // ペイント終了ボタン 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, ClothPainter.WindowWidth, ClothPainter.WindowHeight); /// /// 表示ポイントサイズ /// public float drawPointSize = 0.02f; /// /// ブラシサイズ /// public float brushSize = 0.05f; /// /// ブラシサイズ(透過モード時) /// シーンカメラ垂直サイズの(%) /// public float brushSizeThrough = 0.1f; /// /// 裏面カリング /// public bool backFaceCulling = true; /// /// 形状を表示 /// public bool showShape = true; /// /// Zテスト /// public bool zTest = false; /// /// 透過モード /// public bool through = false; /// /// 現在アクティブなポイント属性(0=Move/1=Fixed/2=Ignore/3=Invalid) /// VertexAttributeとは異なるので注意! /// public int editAttribute = 0; /// /// 現在アクティブなモーション制約(0=Valid, 1=Invalid) /// public int editMotion = 0; } }