【Unity】選択したメッシュの頂点をSceneビューで可視化する

f:id:halya_11:20180201233308p:plain:w300

Unityって頂点情報をみたいときに地味に結構困るんですよね。
頂点カラーとか。

ということで、今回はとりあえず頂点を表示してみます。

実装の方針

頂点情報はギズモ的な考え方で、Sceneビューにのみ表示することにします。
また、オブジェクト選択時にのみ表示します。

実装としてはさすがにギズモで1個1個頂点を描いていてはパフォーマンス的に使い物にならないので、 その辺りはGPUに任せます。
つまりシェーダを使います。

頂点を表示する

Shader "MeshInfo"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry"}

        Pass
        {
            ZWrite Off
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma target 3.0

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                half size: PSIZE;
                half faceSign: TEXCOORD0;
            };

            half _PointSize;
            half _Alpha;
            float4x4 _TransformMatrix;
            
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex = mul(_TransformMatrix, v.vertex);
                o.vertex = UnityObjectToClipPos(v.vertex);
                half3 viewDir = ObjSpaceViewDir(v.vertex);
                o.faceSign = dot(viewDir, v.normal);
                o.size = _PointSize;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half alpha = step(0, i.faceSign);
                half4 color = 1;
                color.a = alpha * _Alpha;
                return color;
            }
            ENDCG
        }
    }
}

まずシェーダから。

PSIZE セマンティクスをつけた変数を v2f構造体に定義することで描画される点の大きさを変えます。

また、視点方向へのベクトルと法線ベクトルとの内積をとって、
見えない位置にある頂点を非表示(透明)にしています。

次にスクリプトです。

#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[ExecuteInEditMode]
public class MeshInfo : MonoBehaviour {

    [SerializeField, Range(0.1f, 100.0f)]
    private float _pointSize = 5.0f;
    
    private Mesh _originalMesh;
    private Mesh _pointMesh;
    private Material _material;

    /// <summary>
    /// 初期化する
    /// </summary>
    private void Setup(){
        // MeshFilterからメッシュを取得
        _originalMesh = GetComponent<MeshFilter>().sharedMesh;
        // 頂点を表示するためのメッシュを生成
        _pointMesh = Instantiate(_originalMesh);
        _pointMesh.SetIndices(_originalMesh.triangles, MeshTopology.Points, 0);
        // マテリアルを生成
        _material = new Material(Shader.Find("MeshInfo"));
        var transformMatrix = Matrix4x4.TRS(Vector3.zero, transform.rotation, transform.lossyScale);
        _material.SetMatrix("_TransformMatrix", transformMatrix);
    }

    /// <summary>
    /// リセットする
    /// </summary>
    private void Reset(){
        _originalMesh = null;
        _pointMesh = null;
    }

    private void OnEnable(){
        Setup();
    }

    private void OnDisable(){
        Reset();
    }

    private void Update(){
        if (transform.hasChanged) {
            Reset();
            Setup();
        }

        // 頂点用のメッシュを描画
        _material.SetFloat("_PointSize", _pointSize);
        Graphics.DrawMesh(_pointMesh, transform.position, Quaternion.identity, _material, 0);
    }

    private void OnWillRenderObject(){
        
        if (Selection.activeObject != gameObject) {
            _material.SetFloat("_Alpha", 0.0f);
            return;
        }
        else if (Camera.current.name == "SceneCamera") {
            // シーンビューではメッシュ情報を表示する
            _material.SetFloat("_Alpha", 1.0f);
        } 
        else {
            // シーンビュー以外ではメッシュ情報を非表示にする
            _material.SetFloat("_Alpha", 0.0f);
        }
    }
}
#endif

処理はコメントの通りですが、ポイントとしては、
まずメッシュを複製して、頂点表示用のメッシュを生成しています。
頂点を表示するためには Mesh.SetIndices() の第2引数に MeshTopology.Points を与える必要があります。

これを Update() で DrawMesh() することにより描画しています。
transform が変わったらリセットしているのは、変換行列を生成し直してマテリアルに渡すためです。

また、OnWillRenderObject() では

  • 選択中のオブジェクトのみメッシュ情報を描画
  • シーンビューにのみ描画

ということをやっています。

このコンポーネントをMeshFilterを持つオブジェクトにアタッチすると

f:id:halya_11:20180202000948p:plain:w300

頂点が表示されました。

ラインも表示する

頂点だけだといまいち格好がつかないので頂点を結ぶ線も描画してみます。

スクリプトだけ次のように変更します。

#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[ExecuteInEditMode]
public class MeshInfo : MonoBehaviour {

    [SerializeField, Range(0.1f, 100.0f)]
    private float _pointSize = 5.0f;
    
    private Mesh _originalMesh;
    private Mesh _pointMesh;
    private Mesh _lineMesh;
    private Material _material;

    /// <summary>
    /// 初期化する
    /// </summary>
    private void Setup(){
        // MeshFilterからメッシュを取得
        _originalMesh = GetComponent<MeshFilter>().sharedMesh;
        // 頂点を表示するためのメッシュを生成
        _pointMesh = Instantiate(_originalMesh);
        _pointMesh.SetIndices(_originalMesh.triangles, MeshTopology.Points, 0);
        // ラインを表示するためのメッシュを生成
        _lineMesh = Instantiate(_pointMesh);
        _lineMesh.SetIndices(MakeIndicesForLine(_originalMesh.triangles), MeshTopology.Lines, 0);
        // マテリアルを生成
        _material = new Material(Shader.Find("MeshInfo"));
        var transformMatrix = Matrix4x4.TRS(Vector3.zero, transform.rotation, transform.lossyScale);
        _material.SetMatrix("_TransformMatrix", transformMatrix);
    }

    /// <summary>
    /// リセットする
    /// </summary>
    private void Reset(){
        _originalMesh = null;
        _pointMesh = null;
        _lineMesh = null;
    }

    private void OnEnable(){
        Setup();
    }

    private void OnDisable(){
        Reset();
    }

    private void Update(){
        if (transform.hasChanged) {
            Reset();
            Setup();
        }

        // 頂点用のメッシュとライン用のメッシュを描画
        _material.SetFloat("_PointSize", _pointSize);
        Graphics.DrawMesh(_pointMesh, transform.position, Quaternion.identity, _material, 0);
        Graphics.DrawMesh(_lineMesh, transform.position, Quaternion.identity, _material, 0);
    }

    private void OnWillRenderObject(){
        
        if (Selection.activeObject != gameObject) {
            _material.SetFloat("_Alpha", 0.0f);
            return;
        }
        else if (Camera.current.name == "SceneCamera") {
            // シーンビューではメッシュ情報を表示する
            _material.SetFloat("_Alpha", 1.0f);
        } 
        else {
            // シーンビュー以外ではメッシュ情報を非表示にする
            _material.SetFloat("_Alpha", 0.0f);
        }
    }

    /// <summary>
    /// Lineが綺麗に描画されるように頂点情報を組み直したものを得る
    /// </summary>
    private int[] MakeIndicesForLine(int[] triangles){
        int[] indices = new int[2 * triangles.Length];
        int i = 0;
        for( int t = 0; t < triangles.Length; t+=3 )
        {
            indices[i++] = triangles[t];
            indices[i++] = triangles[t + 1];
            indices[i++] = triangles[t + 1];
            indices[i++] = triangles[t + 2];
            indices[i++] = triangles[t + 2];
            indices[i++] = triangles[t];
        }
        return indices;
    }
}
#endif

要領は頂点の描画とほぼ同じです。
ラインの描画の仕方はこちらを参考にさせていただきました。

izmiz.hateblo.jp

これを先ほどのオブジェクトにアタッチして

f:id:halya_11:20180201233308p:plain:w300

ラインと頂点が描画されるようになりました。

関連

法線情報を可視化 light11.hatenadiary.com