はじめに
実装方法について紹介したいと思います。
注意点
今回の頂点カラーペイントツールでは、メッシュにペイントした結果は保存されません (Unityを再起動するとリセットされます)
環境
Unity2021.1.6f1
実装の流れ
- Sceneビュークリック時、カメラからモデルへ向けてRayを飛ばす
- Rayが当たった位置の近くの頂点を選択
- 選択頂点の頂点カラーを上書きする
3Dモデルには頂点カラーを表示するシェーダーを適用しておきます。
Tips01. Sceneビューのマウスクリックイベント取得
マウスクリック時に実行したい処理をOnSceneGUI()に実装します
[CustomEditor(typeof(VertexPaintTool))]
public class VertexPaintToolEditor : Editor
{
private void OnSceneGUI()
{
var evt = Event.current;
// マウスクリックで頂点選択
if (evt.type == EventType.MouseDown && Event.current.button == 0)
{
// マウスクリック時の処理をここに書く
}
}
}
Tips02. ColliderいらずのRaycast
SceneViewのマウスクリック位置からRayを飛ばし、モデルにぶつかった位置を取得する方法を紹介します。
以下の記事を参考に、EditorRaycastHelper.cs
というクラスを作成しました。
https://forum.unity.com/threads/editor-raycast-against-scene-meshes-without-collider-editor-select-object-using-gui-coordinate.485502/
using System.Reflection;
using UnityEditor;
using UnityEngine;
public static class EditorRaycastHelper
{
private static readonly MethodInfo intersectRayMeshMethod = typeof(HandleUtility).GetMethod("IntersectRayMesh",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
private static GameObject lastGameObjectUnderCursor;
public static bool RaycastAgainstScene(out RaycastHit hit)
{
Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
// First, try raycasting against scene geometry with or without colliders (it doesn't matter)
// Credit: https://forum.unity.com/threads/editor-raycast-against-scene-meshes-without-collider-editor-select-object-using-gui-coordinate.485502
GameObject gameObjectUnderCursor;
switch (Event.current.type)
{
// HandleUtility.PickGameObject doesn't work with some EventTypes in OnSceneGUI
case EventType.Layout:
case EventType.Repaint:
case EventType.ExecuteCommand:
gameObjectUnderCursor = lastGameObjectUnderCursor;
break;
default:
gameObjectUnderCursor = HandleUtility.PickGameObject(Event.current.mousePosition, false);
break;
}
if (gameObjectUnderCursor)
{
Mesh meshUnderCursor = null;
if (gameObjectUnderCursor.TryGetComponent(out MeshFilter meshFilter))
meshUnderCursor = meshFilter.sharedMesh;
if (!meshUnderCursor &&
gameObjectUnderCursor.TryGetComponent(out SkinnedMeshRenderer skinnedMeshRenderer))
meshUnderCursor = skinnedMeshRenderer.sharedMesh;
if (meshUnderCursor)
{
// Remember this GameObject so that it can be used inside problematic EventTypes, as well
lastGameObjectUnderCursor = gameObjectUnderCursor;
object[] rayMeshParameters = new object[]
{ray, meshUnderCursor, gameObjectUnderCursor.transform.localToWorldMatrix, null};
if ((bool) intersectRayMeshMethod.Invoke(null, rayMeshParameters))
{
hit = (RaycastHit) rayMeshParameters[3];
return true;
}
}
else
lastGameObjectUnderCursor = null;
}
// Raycast against scene geometry with colliders
object raycastResult = HandleUtility.RaySnap(ray);
if (raycastResult != null && raycastResult is RaycastHit)
{
hit = (RaycastHit) raycastResult;
return true;
}
hit = new RaycastHit();
return false;
}
}
使い方
上記のクラスを導入することで、SceneViewにてRaycastできるようになります。
RaycastHit hit;
if (EditorRaycastHelper.RaycastAgainstScene(out hit))
{
Vector3 point = hit.point; // Rayがぶつかった位置を取得
}
Tips03. 頂点データの取得・設定
メッシュが持つすべての頂点は以下のようなコードで取得可能です。
List<Vector3> vertices = new List<Vector3>(sharedMesh.vertexCount);
sharedMesh.GetVertices(vertices); // 頂点の取得
List<Color> colors = new List<Vector3>(sharedMesh.vertexCount);
sharedMesh.GetColors(colors); // 頂点カラーの取得
sharedMesh.SetColors(colors); // 頂点カラーの設定
以下のようなコードでも頂点データは取得できますが、アクセスするたびに配列がAllocateされてしまうので、避けたほうが良いでしょう。
Vector3[] vertices = mesh.vertices;
Color[] colors = mesh.colors ;
参考:
https://docs.unity3d.com/ScriptReference/Mesh.GetVertices.html
https://docs.unity3d.com/ScriptReference/Mesh.GetColors.html
Tips04. 距離を利用した頂点ペイント
オブジェクトSpaceにて、Rayのヒット位置に近い頂点の頂点カラーを書き換えます。
(ワールドSpaceで距離を判定してしまうと、オブジェクトのスケールが変わったときに塗り替わる範囲が変わってしまうため
オブジェクトSpaceで距離を判定します)
// Rayのヒット位置(オブジェクトSpace)
var hitPositionOS = transform.InverseTransformPoint(hit.point);
// 頂点カラー 取得
var colors = new List<Color>(sharedMesh.vertexCount);
sharedMesh.GetColors(colors);
// 頂点カラーの書き換え
for (int i = 0; i < vertices.Count; i++)
{
var v = vertices[i]; // 頂点座標(オブジェクトSpace)
float sqrDistanceOS = (hitPositionOS - v).sqrMagnitude; // Rayヒットと頂点の距離 (オブジェクトSpace)
if (sqrDistanceOS > component.BrushSize * component.BrushSize) // 距離がある程度離れている頂点は除外する
continue;
colors[i] = Color.Lerp(colors[i], component.PaintColor, component.PaintColor.a);
}
// 頂点カラー 設定
sharedMesh.SetColors(colors);
頂点ペイントツールの実装
VertexPaintTool.cs
頂点カラーペイントしたいMeshのオブジェクトに以下のコンポーネントをアタッチします。
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
public class VertexPaintTool : MonoBehaviour
{
[SerializeField] private MeshFilter meshFilter;
[SerializeField] private Color paintColor = new Color(0f, 0f, 0f, 1f); // ブラシカラー
[SerializeField, Range(0f, 1f)] private float brushSize = 0.3f; // ブラシサイズ
public Color PaintColor => paintColor;
public float BrushSize => brushSize;
/// <summary>
/// メッシュ
/// </summary>
public Mesh SharedMesh
{
get
{
if (meshFilter == null)
meshFilter = GetComponent<MeshFilter>();
return meshFilter.sharedMesh;
}
}
/// <summary>
/// 色のリセット
/// </summary>
public void ResetColor()
{
Color[] colors;
if (SharedMesh.colors.Length == 0)
{
colors = new Color[SharedMesh.vertexCount];
}
else
{
colors = SharedMesh.colors;
}
for (int i = 0; i < colors.Length; i++)
{
colors[i] = Color.white;
}
SharedMesh.colors = colors;
}
}
VertexPaintToolEditor.cs
以下のスクリプトをEditorフォルダの中に入れます
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(VertexPaintTool))]
public class VertexPaintToolEditor : Editor
{
private List<Vector3> vertices = new List<Vector3>();
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Reset Color"))
{
var component = target as VertexPaintTool;
component.ResetColor();
}
}
private void OnSceneGUI()
{
// マウスクリックで頂点カラーペイント
if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
{
var component = target as VertexPaintTool;
if (EditorRaycastHelper.RaycastAgainstScene(out var hit))
{
// ヒット位置(オブジェクトSpace)
var hitPositionOS = component.transform.InverseTransformPoint(hit.point);
Mesh sharedMesh = component.SharedMesh;
// 頂点の取得
if (vertices.Count != sharedMesh.vertexCount)
component.SharedMesh.GetVertices(vertices);
if (sharedMesh.colors.Length == 0)
component.ResetColor();
// 頂点カラーを取得
var colors = new List<Color>(sharedMesh.vertexCount);
sharedMesh.GetColors(colors);
// 頂点カラーの書き換え
for (var i = 0; i < vertices.Count; i++)
{
var v = vertices[i]; // 頂点座標(オブジェクトSpace)
float sqrDistanceOS = (hitPositionOS - v).sqrMagnitude; // Rayヒットと頂点の距離 (オブジェクトSpace)
if (sqrDistanceOS > component.BrushSize * component.BrushSize) // 距離がある程度離れている頂点は除外する
continue;
// 色の書き換え
colors[i] = Color.Lerp(colors[i], component.PaintColor, component.PaintColor.a);
}
// 頂点カラー設定
sharedMesh.SetColors(colors);
}
}
}
}