【Unity】SV_VertexIDを、Meshオブジェクトの、uv2プロパティで代用するやり方


SV_VertexIDを、Meshオブジェクトの、uv2プロパティで、代用するやり方です。
頂点テクスチャフェッチが苦手と、なんだか、操作がめんどくさいなという感じがあったので、この方法を試しました。
SV_VertexIDは、openGLES3以上、DX11以上という、条件もあったので、それ以下も、対応したく、uv2を、SV_VertexIDを、以下のブログで知り、方法を記載します。

Meshオブジェクトには、このブログにも記載がありますが、tangentプロパティも用意されているのですが、Androidだと、正規化されてしまうようなので、uv2で、試しました。

これがあると、1つのMeshで、大量に、モデルを描画したあと、たくさんのモデルの中の、特定のモデルを、特定の位置に動かしたいということを、CPU側で、頂点情報の一部を更新するだけで、移動/回転などが、できる感じです。同時に、特定の動きを実現したい場合は、頂点シェーダー側で対応すれば良いですし、割と、操作しやすいかなと、全てを、GPUでというと、頑張って頂点テクスチャフェッチを使うしかないんですけどね。

サンプルソースコード

https://github.com/yasuohasegawa/UnityVertexIDAndUV2Sample
SV_VertexIDのサンプルもあります。

テスト環境

Unity5.6以上

コード例

C#側:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UV2Main : MonoBehaviour {
    private int m_faceCount = 0;
    private List<Vector3> m_vertices = new List<Vector3>();
    private List<Color> m_faceColors = new List<Color>();
    private List<int> m_indices = new List<int>();
    private List<Vector2> m_uvs = new List<Vector2>();
    private List<Vector4> m_posList = new List<Vector4> ();
    private List<Vector2> m_uv2s = new List<Vector2> ();

    private MeshRenderer m_renderer;

    // Use this for initialization
    void Start () {
        m_renderer = gameObject.GetComponent<MeshRenderer> ();

        // 1つ目のプレーンの位置情報
        m_posList.Add (new Vector4(0f,2f,0f,1f));
        m_posList.Add (new Vector4(0f,2f,0f,1f));
        m_posList.Add (new Vector4(0f,2f,0f,1f));
        m_posList.Add (new Vector4(0f,2f,0f,1f));

        // 2つ目のプレーンの位置情報
        m_posList.Add (new Vector4(0f,0f,2f,1f));
        m_posList.Add (new Vector4(0f,0f,2f,1f));
        m_posList.Add (new Vector4(0f,0f,2f,1f));
        m_posList.Add (new Vector4(0f,0f,2f,1f));

        // 頂点indexの作成
        for(int i = 0; i<m_posList.Count; i++) {
            m_uv2s.Add(new Vector2 ((float)i, 0f));
        }

        m_renderer.materials [0].SetVectorArray ("_posList", m_posList);
        CreatePlane (new Vector3(0f,0f,0f),new Vector3(1f,0f,0f));
        CreatePlane (new Vector3(3f,0f,0f),new Vector3(1f,0f,0f));

        UpdateMesh ();
    }

    // Update is called once per frame
    void Update () {

    }

    public void CreatePlane(Vector3 pos, Vector3 col) {
        float x = pos.x * 0.5f;
        float y = pos.y * 0.5f;
        float z = pos.z * 0.5f;

        m_vertices.Add (new Vector3 (x - 0.5f, y - 0.5f, z));
        m_vertices.Add (new Vector3 (x - 0.5f, y + 0.5f, z));
        m_vertices.Add (new Vector3 (x + 0.5f, y + 0.5f, z));
        m_vertices.Add (new Vector3 (x + 0.5f, y - 0.5f, z));

        m_faceColors.Add (new Color (col.x, col.y, col.z));
        m_faceColors.Add (new Color (col.x, col.y, col.z));
        m_faceColors.Add (new Color (col.x, col.y, col.z));
        m_faceColors.Add (new Color (col.x, col.y, col.z));

        m_indices.Add (m_faceCount * 4); //1
        m_indices.Add (m_faceCount * 4 + 1); //2
        m_indices.Add (m_faceCount * 4 + 2); //3
        m_indices.Add (m_faceCount * 4); //1
        m_indices.Add (m_faceCount * 4 + 2); //3
        m_indices.Add (m_faceCount * 4 + 3); //4

        m_uvs.Add (new Vector2 (0f, 0f));
        m_uvs.Add (new Vector2 (1f, 0f));
        m_uvs.Add (new Vector2 (1f, 1f));
        m_uvs.Add (new Vector2 (0f, 1f));

        m_faceCount++;
    }

    private void UpdateMesh() {
        Mesh mesh = gameObject.GetComponent<MeshFilter> ().mesh;
        mesh.Clear ();
        mesh.vertices = m_vertices.ToArray();
        mesh.triangles = m_indices.ToArray();
        mesh.uv = m_uvs.ToArray ();
        mesh.colors = m_faceColors.ToArray();
        mesh.uv2 = m_uv2s.ToArray ();

        mesh.RecalculateNormals ();
        mesh.RecalculateBounds ();
    }

    public void OnUpdateVertices() {
        Vector4 pos = m_posList [4];
        pos.x = 0f;
        pos.y = 0f;
        pos.z = 0f;
        m_posList [4] = pos;
        m_posList [5] = pos;
        m_posList [6] = pos;
        m_posList [7] = pos;
        m_renderer.materials [0].SetVectorArray ("_posList", m_posList);
    }
}

gameObjectには、あらかじめ、MeshFilterと、MeshRendererを、addComponentしておいてください。
このC#側のソースコードは、1つの、Meshで、2つのPlaneを「CreatePlane」メソッドで作成しています。2つPlaneの頂点情報と、この、頂点に対する、頂点indexを、Vector2の、x座標に、

for(int i = 0; i<m_posList.Count; i++) {
    m_uv2s.Add(new Vector2 ((float)i, 0f));
}

で、格納し、

mesh.uv2 = m_uv2s.ToArray ();

で、Meshオブジェクトに頂点indexを渡します。

shader側:

Shader "Unlit/UV2"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Cull off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            #define POS_SIZE 8

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                fixed4 color : TEXCOORD1;
            };


            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _posList[POS_SIZE];

            v2f vert (appdata v) // uint vid : SV_VertexID you can do this way as well but it needs openGLES3 above
            {
                v2f o;

                int vid = v.uv2.x; // replace for SV_VertexID

                v.vertex.x += _posList[vid].x;
                v.vertex.y += _posList[vid].y;
                v.vertex.z += _posList[vid].z;

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                float f = (float)vid;
                o.color = half4(sin(f/10),sin(f/100),sin(f/1000),0) * 0.5 + 0.5;

                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target{
                return i.color;
            }
            ENDCG
        }
    }
}

頂点シェーダーの入力に、

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float2 uv2 : TEXCOORD1; // 追加
};
float4 _posList[POS_SIZE];
:
:

uv2を追加。

v2f vert (appdata v)
{
    v2f o;

    int vid = v.uv2.x;

    v.vertex.x += _posList[vid].x;
    v.vertex.y += _posList[vid].y;
    v.vertex.z += _posList[vid].z;
:
:

頂点シェーダー側で、Meshオブジェクトで、付与した、頂点indexを、

int vid = v.uv2.x

で、受け取ります。
SetVectorArrayで、流し込んだ、頂点情報を、「_posList」に紐づけて、
個々の頂点情報を、更新することができるようになります。
コード例では、2つ目に作成した、Planeの頂点情報を、以下のように、CPU側で、更新しています。

public void OnUpdateVertices() {
    Vector4 pos = m_posList [4];
    pos.x = 0f;
    pos.y = 0f;
    pos.z = 0f;
    m_posList [4] = pos;
    m_posList [5] = pos;
    m_posList [6] = pos;
    m_posList [7] = pos;
    m_renderer.materials [0].SetVectorArray ("_posList", m_posList);
}

今後は、移動/回転/拡大の処理は、頂点シェーダー側で計算して、頂点情報を更新できるようにし、新しいサンプルをgitにPUSHしようかと思います。ご参考になればとです。

追記

今回の環境でですが、SetVectorArrayなどで設定した、Shader側で、扱える配列の要素数の最大が、253でした。。。最新のUnityでは、もう少しいけるのかもですが、用途は、要検討でね。。。