4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unityエディタ拡張】頂点カラーペイントツールを作ってみた

Last updated at Posted at 2021-08-14

はじめに

頂点カラーをペイントするツールを作ってみました。
0.gif

実装方法について紹介したいと思います。

注意点

今回の頂点カラーペイントツールでは、メッシュにペイントした結果は保存されません (Unityを再起動するとリセットされます)

環境

Unity2021.1.6f1

実装の流れ

  1. Sceneビュークリック時、カメラからモデルへ向けてRayを飛ばす
  2. Rayが当たった位置の近くの頂点を選択
  3. 選択頂点の頂点カラーを上書きする

3Dモデルには頂点カラーを表示するシェーダーを適用しておきます。

Tips01. Sceneビューのマウスクリックイベント取得

マウスクリック時に実行したい処理をOnSceneGUI()に実装します

VertexPaintToolEditor.cs
[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を飛ばし、モデルにぶつかった位置を取得する方法を紹介します。
image.png

以下の記事を参考に、EditorRaycastHelper.cs というクラスを作成しました。
https://forum.unity.com/threads/editor-raycast-against-scene-meshes-without-collider-editor-select-object-using-gui-coordinate.485502/

EditorRaycastHelper.cs
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できるようになります。

Sceneビューのマウス位置で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のオブジェクトに以下のコンポーネントをアタッチします。

VertexPaintTool.cs
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フォルダの中に入れます

VertexPaintToolEditor.cs
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);
            }
        }
    }
}
4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?