ちょび日記

明日は明日の風が吹く

2016-11-11

MeshRenderer.additionalVertexStreamsを使って頂点ペイント


Unityの5系統からあんまり説明されてないけど入っていたadditionaVertexStreamという機能があります。

https://docs.unity3d.com/ScriptReference/MeshRenderer-additionalVertexStreams.html

Vertex attributes in this mesh will override or add attributes of the primary mesh in the MeshRenderer.
This is used for the UVs for realtime lightmaps, but could also be used for vertex painting tools, etc.

という実際にどういうデータが対象になるのか、という詳細が殆どなかったりするのですが…

割愛するとMeshRendererがこれからrenderingしようとするmeshとは別に、各種属性を上書きするための別Meshを用意しておくと 別Mesh側の頂点属性が使われるようになるという機能で、リアルタイムなライトマップ向けのuvや、頂点ペイント等に応用できたりする機能です。

使い方

var origMesh = filter.mesh;
mesh = new Mesh();
mesh.vertices = origMesh.vertices;
mesh.normals = origMesh.normals;
var colors = new Color[origMesh.vertexCount];
for (var i = 0; i < origMesh.vertexCount; i++)
{
    colors[i] = Color.black;
}
mesh.SetColors(colors.ToList());
renderer.additionalVertexStreams = mesh;

と、いうようにrenderer.additionalVertexStreamsにレンダリング対象のmesh以上の頂点を持っている 新しいmeshを割り当ててあげればOKです。

スクリプトの実行順にもよりますが、Awakeで設定していまうとうまくいかない事があるようです。

また、version間の差異により若干割当周りの挙動が異なることがあるのでそこらへんは自分のUnityの versionで実際に試してみて確認をしてください(もうちょっとドキュメント書いてくれてもいいんじゃよ・・・)

どうやら(すくなくとも5.4.0系等では)indicesは元のメッシュのものを使うのでセットする必要はないみたいです。 UploadMeshDataは暗黙的にやってくれるようなので特に指定する必要はないようでした。

簡易頂点ペイントサンプル

それでは簡易的なadditionalVertexStreamsを使った頂点ペイントのサンプルを作ってみたいと思います。 additionalVertexStreamsを使うことで元のmeshは変更しないところがポイントです。

Editor上でPlaneの位置で左クリックすると半径1.0以内の頂点に青色を塗る、というサンプルです。 消したりするのは実装していないのでPlayボタン押して止めて戻してください。 止めたタイミングですぐ塗るとセットアップ処理がうまくできないので1~2病待ってからSceneViewのPlaneを クリックしてください。

プロジェクトファイルは下記URLとなります。

https://github.com/chobie/VertexPaintExample

Unityでvertexpaintのシーンを開くとplaneがScene上においてあるのでマウスでクリックすると色が塗れます。

実装的にはコレぐらい。コメントに書いてあるくらいの内容しかありませんが、これで頂点ペイントができます。 (UnityのShaderは頂点カラーを出すものがデフォルトではないので、この記事では書いてありませんが頂点カラーだけを 出すShaderを追加して割り当てています。)

VertexPaint.cs

using UnityEngine;
using System.Collections;
using UnityEditor;

[ExecuteInEditMode]
public class VertexPaint : MonoBehaviour
{
    private MeshRenderer renderer;
    private MeshFilter filter;
    private Mesh mesh;
    private Mesh stream;
    private Color[] colors;
    private bool init = false;

    void OnEnable()
    {
        SceneView.onSceneGUIDelegate += OnSceneGUI;
    }

    void OnDisable()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
    }

    void setup()
    {
        renderer = GetComponent<MeshRenderer>();
        filter = GetComponent<MeshFilter>();
        mesh = filter.sharedMesh;

        stream = new Mesh();
        stream.MarkDynamic();

        stream.vertices = mesh.vertices;
        colors = new Color[mesh.vertexCount];
        for (var i = 0; i < colors.Length; i++)
        {
            colors[i] = Color.white;
        }
        stream.triangles = mesh.triangles;
        stream.colors = colors;
        renderer.additionalVertexStreams = stream;

        Debug.Log("Called VertexPaint.setup");
    }


    void OnSceneGUI(SceneView sceneView)
    {
        RaycastHit hit;
        Vector3 mousePosition = Event.current.mousePosition;

        // ScreenPointToRayで期待しているyの位置を補正する。
        mousePosition.y = sceneView.camera.pixelHeight - mousePosition.y;
        var ray = sceneView.camera.ScreenPointToRay(mousePosition);

        if (Event.current.isMouse && Event.current.type == EventType.mouseDown)
        {
            if (!init)
            {
                setup();
                init = true;
            }

            // 現状meshが一個しかないので判定を省きつつ、meshに対してrayを打つ。
            if (RXLookingGlass.IntersectRayMesh(ray, mesh, transform.localToWorldMatrix, out hit))
            {
                // hit.pointはこの場合meshでのローカル座標になる。
                Debug.LogFormat("hit at {0}(local position)", hit.point);
                // あとは頂点を塗るだけ
                paint(hit.point);
            }
        }
    }

    private void paint(Vector3 position)
    {
        var radius = 1.0f;
        for (var i = 0; i < stream.vertexCount; i++)
        {
            var dist = Vector3.Distance(stream.vertices[i], position);
            if (dist < radius)
            {
                colors[i] = Color.blue;
            }
        }

        // colorsはセットしてあげないと動かないみたい。
        stream.colors = colors;
    }

}

RXLookingGlass.IntersectRayMeshは、UnityEditor.dllで未公開のtrisに対するRayを撃つラッパです。 詳細のコードはここらへん。 https://gist.github.com/MattRix/9205bc62d558fef98045

実際に頂点ペイントを作り込む場合はもっともっといろいろな処理が必要ですが、大まかには

  • trisに対してRayを打ってlocal座標を得る
  • 得た座標と頂点の位置を比較して塗りの係数を決める
  • 格納したい属性(colorsとか)に実際のデータを置く
  • meshを更新する

という流れです。InGameでやる場合は自前で交差判定を作ったり、判定を高速化したりする必要がでてきます。

そのほか

https://github.com/slipster216/VertexPaint

まさにadditionalVertexStreamを使った頂点ペイント機能が公開されています。 本人記載の通り

Warning: This package can take a long time to import into Unity due to a large number of shader variants

とあって実際にimportしようとするとコンビニいって帰ってこれるレベルで時間がかかるので抜くか 余裕がある時に見てみると面白いと思います。

でわでわ。



Copyright© 2016, chobie All rights reserved.