【ProBuilder拡張】頂点、エッジ、面の選択情報を明確にする方法

2024/08/22に公開

はじめに

UnityのProBuilderは、強力なメッシュ編集ツールですが、選択した頂点、エッジ、面の情報がわかりにくいという課題があります。特に複雑なメッシュを編集する際、選択モードが直感的でないため、意図しない選択や操作ミスが発生しやすくなります。

リポジトリ

GitHub
https://github.com/dsgarage/ProBuilderModeLogger

問題の背景

1. 選択したメッシュ情報の把握が難しい

ProBuilderでは、選択された頂点やエッジ、面の情報が視覚的にわかりにくく、ユーザーはどの部分を選択したのかを正確に把握するのが難しいことがあります。

2. 選択時に保持されているクラスの情報が分かりにくい

ProBuilderのメッシュ情報はProBuilderMeshProBuilderMeshFilterといったクラスによって管理されていますが、これらのクラスがどのようにデータを保持し、選択状態を管理しているかがわかりにくいため、開発者が意図通りに操作するのが難しい場合があります。

3. 基本モードの切り替えがUnity Editor上で操作しづらい

頂点、エッジ、面の基本モードの切り替えがUnity Editor上で直感的に操作できず、特に複雑な編集作業の際に意図しないモード切替が発生しやすく、作業効率が低下する可能性があります。

解決策の概要

選択モードを明確に管理し、選択した要素に関する情報をリアルタイムで把握できるようにすることで、作業の効率化を図ります。具体的には、選択時にモードを決定し、次に選択し直すまでモードを固定することで、意図しないモード切替を防ぎます。

実装の詳細

1. 選択モードのロック

最初の選択でモードを決定し、それ以降はユーザーが明示的に選択し直すまでモードを変更しないようにします。これにより、誤操作によるモード切替を防ぎます。

2. 配列とリストの正しい比較

Face[]配列とList<Face>の比較には注意が必要です。選択された要素が同一かどうかを正しく判断し、不要な重複ログを避けるために、配列をリストに変換して正しく比較します。

3. デバッグ情報のリアルタイム表示

選択内容が変わった場合のみ、最新の選択情報をデバッグログに出力することで、常に正確な情報を把握できるようにします。

必要なEditor拡張の知識

ProBuilderModeLoggerを実装するためには、Unity Editorの拡張に関するいくつかの重要な知識が必要です。まず、Editorスクリプトの基礎知識として、EditorWindowEditorクラスの使い方を理解することが不可欠です。次に、EditorApplication.updateSelection.selectionChangedなどのイベント処理の仕組みを理解し、Unity Editor内での適切なタイミングでスクリプトが実行されるようにする必要があります。

また、ProBuilderのAPIについての知識も重要です。ProBuilderMeshProBuilderMeshFilterクラスを使用してメッシュ情報を取得し、操作する方法を習得することが必要です。これに加えて、SelectModeや選択状態に基づくモードの切り替え、状態の保持に関する知識も不可欠です。

最後に、選択状態に応じた情報をリアルタイムでデバッグログに出力するための手法も理解しておく必要があります。これらの知識が揃うことで、ProBuilderModeLoggerを効果的に実装し、選択情報を明確に管理できるようになります。

UnityEditorの機能

1. EditorWindow

EditorWindowは、Unity Editor内で独自のカスタムウィンドウを作成するためのクラスです。例えば、特定のツールやデバッグ用のウィンドウを作成して表示できます。

2. Editor

Editorクラスは、Unityのインスペクターをカスタマイズするために使用されます。これにより、特定のコンポーネントやスクリプタブルオブジェクトのカスタムインスペクターを作成できます。

3. EditorApplication.update

Unity Editorの毎フレームの更新時に呼び出されるイベントです。リアルタイムでエディタの状態を監視し、定期的な処理を実行するために使用します。

4. Selection.selectionChanged

Unity Editorで選択が変更されたときに呼び出されるイベントです。選択されたオブジェクトに応じて特定の処理を実行できます。

ProBuilderの機能

1. ProBuilderMesh

ProBuilderでメッシュの頂点、エッジ、面などを管理するクラスです。このクラスを使用して、メッシュデータの取得や操作を行います。

2. ProBuilderMeshFilter

ProBuilderMeshとUnityの標準的なMeshFilterコンポーネントをリンクするクラスです。これにより、ProBuilderで編集したメッシュがシーン内で正しく描画されます。

3. SelectMode

ProBuilderの選択モード(頂点、エッジ、面など)を管理する列挙型です。選択モードを判別し、対応する編集操作を適用するために使用されます。

これらのAPIを理解し活用することで、ProBuilderModeLoggerの実装が可能となり、選択情報を明確に管理できるようになります。

ProBuilderModeLoggerの機能と処理内容

機能:

ProBuilderModeLoggerは、ProBuilderで選択した頂点、エッジ、面の情報を管理し、それぞれの選択モードに応じてデバッグ情報をリアルタイムで出力するクラスです。選択内容に基づき、初回選択時にモードを決定し、次の選択が行われるまでそのモードを固定します。

処理内容:

  1. モードの判定とロック:

    • 初めて選択された要素に応じて、頂点、エッジ、面のいずれかのモードを決定し、モードをロックします。
  2. デバッグ出力:

    • 現在の選択モードに基づき、選択された要素の詳細な情報をデバッグログに出力します。
  3. 状態の比較:

    • 過去の選択内容と現在の選択内容を比較し、変化があった場合のみ情報を更新してログに出力します。これにより、不要なログ出力を防ぎます。

このように、ProBuilderModeLoggerは選択されたメッシュ情報を明確にし、開発者が選択内容を把握しやすくするためのサポートを行います。

実装と説明

ProBuilderModeLoggerは、ProBuilderで選択した頂点、エッジ、面の情報を管理し、各モードごとに適切なデバッグ情報を出力するためのクラスです。
以下では、モード判定からデバッグ出力までの全体的な流れを説明します。

1. モードの初期化とロック

最初に、選択された要素に基づいてモードを判定し、そのモードをロックします。これにより、選択が変更されるまでモードが切り替わらないようにします。具体的には、OnEditorUpdateメソッドが毎フレーム呼ばれ、選択されたオブジェクトの状態を確認します。選択モードがまだ決定されていない場合、DetermineInitialModeメソッドが呼ばれ、最初に選択された要素に基づいてモードが設定され、その後モードがロックされます。

private static void OnEditorUpdate()
{
    var pbMesh = Selection.activeGameObject?.GetComponent<ProBuilderMesh>();
    if (pbMesh == null)
    {
        modeLocked = false;
        return;
    }

    if (!modeLocked)
    {
        DetermineInitialMode(pbMesh);
        modeLocked = true;
    }

    switch (currentMode)
    {
        case SelectMode.Vertex:
            HandleVertexMode(pbMesh);
            break;
        case SelectMode.Edge:
            HandleEdgeMode(pbMesh);
            break;
        case SelectMode.Face:
            HandleFaceMode(pbMesh);
            break;
    }
}

2. モードの判定

DetermineInitialModeメソッドは、最初に選択された要素が頂点、エッジ、面のどれであるかを判定します。このメソッドによってcurrentModeが設定され、その後の選択モードが確定されます。この設定は一度決定されると、次に選択がリセットされるまで変更されません。

private static void DetermineInitialMode(ProBuilderMesh pbMesh)
{
    if (pbMesh.selectedVertices.Count > 0)
    {
        currentMode = SelectMode.Vertex;
        HandleVertexMode(pbMesh);
    }
    else if (pbMesh.selectedEdges.Count > 0)
    {
        currentMode = SelectMode.Edge;
        HandleEdgeMode(pbMesh);
    }
    else if (pbMesh.GetSelectedFaces().Length > 0)
    {
        currentMode = SelectMode.Face;
        HandleFaceMode(pbMesh);
    }
}

3. モードごとの処理

各モードに対応する処理が実行され、選択された要素(頂点、エッジ、面)の詳細な情報がデバッグログに出力されます。この処理は、現在の選択内容が前回のものと異なる場合にのみ行われます。

private static void HandleVertexMode(ProBuilderMesh pbMesh)
{
    var currentSelectedVertices = new List<int>(pbMesh.selectedVertices);
    if (currentSelectedVertices.Count > 0 && !AreVerticesEqual(lastSelectedVertices, currentSelectedVertices))
    {
        Debug.Log("Vertex Mode Selected");
        lastSelectedVertices = new List<int>(currentSelectedVertices);
        lastSelectedEdges.Clear();
        lastSelectedFaces.Clear();
        foreach (var vertex in currentSelectedVertices)
        {
            Debug.Log($"Vertex Position: {pbMesh.positions[vertex]}");
        }
    }
}

private static void HandleEdgeMode(ProBuilderMesh pbMesh)
{
    var currentSelectedEdges = new List<Edge>(pbMesh.selectedEdges);
    if (currentSelectedEdges.Count > 0 && !AreEdgesEqual(lastSelectedEdges, currentSelectedEdges))
    {
        Debug.Log("Edge Mode Selected");
        lastSelectedEdges = new List<Edge>(currentSelectedEdges);
        lastSelectedVertices.Clear();
        lastSelectedFaces.Clear();
        foreach (var edge in currentSelectedEdges)
        {
            var startVertex = pbMesh.positions[edge.a];
            var endVertex = pbMesh.positions[edge.b];
            Debug.Log($"Edge: Start {startVertex}, End {endVertex}");
        }
    }
}

private static void HandleFaceMode(ProBuilderMesh pbMesh)
{
    var currentSelectedFaces = pbMesh.GetSelectedFaces();
    var currentSelectedFacesList = currentSelectedFaces.ToList(); 
    if (currentSelectedFaces.Length > 0 && !AreFacesEqual(lastSelectedFaces, currentSelectedFacesList))
    {
        Debug.Log("Face Mode Selected");
        lastSelectedFaces = currentSelectedFacesList;
        lastSelectedVertices.Clear();
        lastSelectedEdges.Clear();
        foreach (var face in currentSelectedFaces)
        {
            Debug.Log("Face vertices:");
            foreach (var index in face.indexes)
            {
                Debug.Log($"Vertex: {pbMesh.positions[index]}");
            }
        }
    }
}

4. 状態の比較

最後に、状態の比較が行われます。これにより、選択内容が前回の選択内容と異なる場合にのみ新しいデバッグ情報が出力され、同じ選択が繰り返された場合には不要なログ出力を防ぐことができます。

private static bool AreVerticesEqual(List<int> list1, List<int> list2)
{
    if (list1.Count != list2.Count)
        return false;

    for (int i = 0; i < list1.Count; i++)
    {
        if (list1[i] != list2[i])
            return false;
    }
    return true;
}

private static bool AreEdgesEqual(List<Edge> list1, List<Edge> list2)
{
    if (list1.Count != list2.Count)
        return false;

    for (int i = 0; i < list1.Count; i++)
    {
        if (!list1[i].Equals(list2[i]))
            return false;
    }
    return true;
}

private static bool AreFacesEqual(List<Face> list1, List<Face> list2)
{
    if (list1.Count != list2.Count)
        return false;

    for (int i = 0; i < list1.Count; i++)
    {
        if (!list1[i].Equals(list2[i]))
            return false;
    }
    return true;
}

コード全体

using UnityEditor;
using UnityEngine;
using UnityEngine.ProBuilder;
using System.Collections.Generic;
using System.Linq;

[InitializeOnLoad]
public class ProBuilderModeLogger
{
    private static SelectMode currentMode = SelectMode.Object; // デフォルトはオブジェクトモード
    private static bool modeLocked = false;
    private static List<int> lastSelectedVertices = new List<int>();
    private static List<Edge> lastSelectedEdges = new List<Edge>();
    private static List<Face> lastSelectedFaces = new List<Face>();

    static ProBuilderModeLogger()
    {
        EditorApplication.update += OnEditorUpdate;
    }

    private static void OnEditorUpdate()
    {
        var pbMesh = Selection.activeGameObject?.GetComponent<ProBuilderMesh>();
        if (pbMesh == null)
        {
            modeLocked = false; // オブジェクトが選択されていない場合はロックを解除
            return;
        }

        if (!modeLocked)
        {
            DetermineInitialMode(pbMesh);
            modeLocked = true; // モードを決定した後はロック
        }

        switch (currentMode)
        {
            case SelectMode.Vertex:
                HandleVertexMode(pbMesh);
                break;
            case SelectMode.Edge:
                HandleEdgeMode(pbMesh);
                break;
            case SelectMode.Face:
                HandleFaceMode(pbMesh);
                break;
        }
    }

    private static void DetermineInitialMode(ProBuilderMesh pbMesh)
    {
        if (pbMesh.selectedVertices.Count > 0)
        {
            currentMode = SelectMode.Vertex;
            HandleVertexMode(pbMesh);
        }
        else if (pbMesh.selectedEdges.Count > 0)
        {
            currentMode = SelectMode.Edge;
            HandleEdgeMode(pbMesh);
        }
        else if (pbMesh.GetSelectedFaces().Length > 0)
        {
            currentMode = SelectMode.Face;
            HandleFaceMode(pbMesh);
        }
    }

    private static void HandleVertexMode(ProBuilderMesh pbMesh)
    {
        var currentSelectedVertices = new List<int>(pbMesh.selectedVertices);
        if (currentSelectedVertices.Count > 0 && !AreVerticesEqual(lastSelectedVertices, currentSelectedVertices))
        {
            Debug.Log("Vertex Mode Selected");
            lastSelectedVertices = new List<int>(currentSelectedVertices);
            lastSelectedEdges.Clear();
            lastSelectedFaces.Clear();
            foreach (var vertex in currentSelectedVertices)
            {
                Debug.Log($"Vertex Position: {pbMesh.positions[vertex]}");
            }
        }
    }

    private static void HandleEdgeMode(ProBuilderMesh pbMesh)
    {
        var currentSelectedEdges = new List<Edge>(pbMesh.selectedEdges);
        if (currentSelectedEdges.Count > 0 && !AreEdgesEqual(lastSelectedEdges, currentSelectedEdges))
        {
            Debug.Log("Edge Mode Selected");
            lastSelectedEdges = new List<Edge>(currentSelectedEdges);
            lastSelectedVertices.Clear();
            lastSelectedFaces.Clear();
            foreach (var edge in currentSelectedEdges)
            {
                var startVertex = pbMesh.positions[edge.a];
                var endVertex = pbMesh.positions[edge.b];
                Debug.Log($"Edge: Start {startVertex}, End {endVertex}");
            }
        }
    }

    private static void HandleFaceMode(ProBuilderMesh pbMesh)
    {
        var currentSelectedFaces = pbMesh.GetSelectedFaces();
        var currentSelectedFacesList = currentSelectedFaces.ToList(); 
        if (currentSelectedFaces.Length > 0 && !AreFacesEqual(lastSelectedFaces, currentSelectedFacesList))
        {
            Debug.Log("Face Mode Selected");
            lastSelectedFaces = currentSelectedFacesList;
            lastSelectedVertices.Clear();
            lastSelectedEdges.Clear();
            foreach (var face in currentSelectedFaces)
            {
                Debug.Log("Face vertices:");
                foreach (var index in face.indexes)
                {
                    Debug.Log($"Vertex: {pbMesh.positions[index]}");
                }
            }
        }
    }

    private static bool AreVerticesEqual(List<int> list1, List<int> list2)
    {
        if (list1.Count != list2.Count)
            return false;

        for (int i = 0; i < list1.Count; i++)
        {
            if (list1[i] != list2[i])
                return false;
        }
        return true;
    }

    private static bool AreEdgesEqual(List<Edge> list1, List<Edge> list2)
    {
        if (list1.Count != list2.Count)
            return false;

        for (int i = 0; i < list1.Count; i++)
        {
            if (!list1[i].Equals(list2[i]))
                return false;
        }
        return true;
    }

    private static bool AreFacesEqual(List<Face> list1, List<Face> list2)
    {
        if (list1.Count != list2.Count)
            return false;

        for (int i = 0; i < list1.Count; i++)
        {
            if (!list1[i].Equals(list2[i]))
                return false;
        }
        return true;
    }
}


実際の挙動

以上の実装で、このようなログが出てきます。

頂点

エッジ


これでProBuilderでのメッシュ編集時に選択モードを適切に管理し、選択された要素に関する情報をリアルタイムでデバッグ出力するためのクラスです。

モードの判定とロックにより、選択モードの不意な切り替えを防ぎ、効率的なデバッグをサポートします。

また、状態の比較を通じて、不要なログ出力を抑えることで、開発者が選択内容を効率的に把握できるようにします。

Discussion