BITFALL/Assets/Plugins/MagicaCloth2/Scripts/Editor/Cloth/ClothPainter.cs

792 lines
29 KiB
C#
Raw Normal View History

2023-06-08 14:09:50 +08:00
// 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
{
/// <summary>
/// 頂点ペイントウインドウ
/// </summary>
[InitializeOnLoad]
public class ClothPainter
{
public enum PaintMode
{
None,
/// <summary>
/// Move/Fixed/Ignore/Invalid
/// </summary>
Attribute,
/// <summary>
/// Max Distance/Backstop
/// </summary>
Motion,
}
static PaintMode paintMode = PaintMode.None;
/// <summary>
/// 編集対象のクロスコンポーネント
/// </summary>
static MagicaCloth cloth = null;
/// <summary>
/// 編集対象のクロスEditorクラス
/// </summary>
static MagicaClothEditor clothEditor = null;
/// <summary>
/// 編集対象のエディットメッシュ
/// </summary>
static VirtualMesh editMesh = null;
/// <summary>
/// 編集対象のセレクションデータ
/// </summary>
static SelectionData selectionData = null;
/// <summary>
/// 編集開始時のセレクションデータ(コピー)
/// </summary>
static SelectionData initSelectionData = null;
//=========================================================================================
const int PointFlag_Selecting = 1; // 選択中
internal struct Point : IComparable<Point>
{
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<Point> dispPointList;
static NativeArray<float3> pointWorldPositions;
static VirtualMeshRaycastHit rayhit = default;
static bool oldShowAll = false;
static bool forceUpdate = false;
//=========================================================================================
static ClothPainter()
{
// シーンビューにGUIを描画するためのコールバック
SceneView.duringSceneGui += OnGUI;
}
/// <summary>
/// ペイント開始
/// </summary>
/// <param name="clothComponent"></param>
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<Point>(vmesh.VertexCount, Allocator.Persistent);
pointWorldPositions = new NativeArray<float3>(vmesh.VertexCount, Allocator.Persistent);
// UndoRedoコールバック
Undo.undoRedoPerformed += UndoRedoCallback;
}
/// <summary>
/// ペイント終了
/// </summary>
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;
}
/// <summary>
/// Undo/Redo実行後のコールバック
/// </summary>
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;
}
//=========================================================================================
/// <summary>
/// 指定クロスコンポーネントを編集中かどうか
/// </summary>
/// <param name="clothComponent"></param>
/// <returns></returns>
public static bool HasEditCloth(MagicaCloth clothComponent)
{
return cloth == clothComponent;
}
/// <summary>
/// 編集中かどうか
/// </summary>
/// <returns></returns>
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<ClothPainterWindowData>.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();
}
/// <summary>
/// ポイントデータを更新する
/// </summary>
/// <param name="ct"></param>
/// <param name="cam"></param>
/// <param name="ray"></param>
/// <param name="showAll"></param>
/// <param name="brushSize"></param>
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<float3> localPositions;
[Unity.Collections.ReadOnly]
public NativeArray<float3> localNormals;
[Unity.Collections.ReadOnly]
public NativeArray<FixedList32Bytes<int>> vertexToTriangles;
[Unity.Collections.WriteOnly]
public NativeArray<float3> pointWorldPositions;
[Unity.Collections.WriteOnly]
public NativeList<Point>.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<Point> dispPointList;
public void Execute()
{
if (dispPointList.Length > 1)
dispPointList.Sort();
}
}
/// <summary>
/// 渡されたpointListをカメラからの距離の降順にソートして返す
/// </summary>
/// <param name="positions"></param>
/// <param name="positonOffset"></param>
/// <param name="LtoW">positionをワールド座標変換するマトリックス</param>
/// <param name="camPos">カメラワールド座標</param>
/// <param name="pointList"></param>
internal static void CalcPointCameraDistance(NativeArray<float3> positions, int positonOffset, float4x4 LtoW, float3 camPos, NativeList<Point> 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<float3> positions;
[NativeDisableParallelForRestriction]
public NativeList<Point> 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;
}
}
/// <summary>
/// 選択中ポイントに属性を付与する
/// </summary>
/// <param name="attribute"></param>
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;
}
}
/// <summary>
/// セレクションデータをシリアライズする
/// </summary>
static void ApplySelectionData()
{
// セレクションデータデータをシリアライズ化する
selectionData.userEdit = true; // ユーザー編集フラグを立てる
clothEditor?.ApplyClothPainter(selectionData);
// Undo/Redoの状態を切り替えるためセレクションデータのクローンを作成して切り替える
selectionData = selectionData.Clone();
}
static void DoWindow(int unusedWindowID)
{
var windata = ScriptableSingleton<ClothPainterWindowData>.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();
}
}
//=============================================================================================
/// <summary>
/// ペイントウインドウの保持データ
/// ScriptableSingletonを利用することによりエディタ終了時までデータを保持できる
/// </summary>
public class ClothPainterWindowData : ScriptableSingleton<ClothPainterWindowData>
{
/// <summary>
/// ウインドウの位置とサイズ
/// </summary>
public Rect windowRect = new Rect(100, 100, 400, 205);
/// <summary>
/// 表示ポイントサイズ
/// </summary>
public float drawPointSize = 0.02f;
/// <summary>
/// ブラシサイズ
/// </summary>
public float brushSize = 0.05f;
/// <summary>
/// 裏面カリング
/// </summary>
public bool BackFaceCulling = true;
/// <summary>
/// 形状を表示
/// </summary>
public bool showShape = true;
/// <summary>
/// Zテスト
/// </summary>
public bool zTest = false;
/// <summary>
/// 現在アクティブなポイント属性(0=Move/1=Fixed/2=Ignore/3=Invalid)
/// VertexAttributeとは異なるので注意
/// </summary>
public int editAttribute = 0;
/// <summary>
/// 現在アクティブなモーション制約(0=Valid, 1=Invalid)
/// </summary>
public int editMotion = 0;
}
}