UnityでGraphicsやGLを使ってレンダリングしてみる

はじめに

以前に書いた記事では独自のシェーダを書いてレンダリングをしてみました。 シェーダの処理自体は自作しましたが、描画命令の発行はMeshRenderer任せでした。 今回はMeshRendererを使わずにレンダリングする方法として、Graphics.DrawMeshGraphics.DrawMeshNow、そしてGLの3種類試してみます。

実行結果

Unityのバージョン:2018.3.0b1

Graphics.DrawMesh

まずはGraphics.DrawMeshを試してみます。

次のコードを適当なGameObjectにアタッチします。

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

public class DrawMesh : MonoBehaviour {

  Mesh mesh;
  Material material;

  void Start () {
    mesh = new Mesh ();
    mesh.name = "TestMesh";
    var vertices = new List<Vector3> {
      new Vector3 (0.5f, 0.5f, 0),
      new Vector3 (-0.5f, 0.5f, 0),
      new Vector3 (0.5f, -0.5f, 0),
      new Vector3 (-0.5f, -0.5f, 0),
    };
    var triangles = new List<int> {
      1,
      0,
      2,
      1,
      2,
      3
    };
    mesh.SetVertices (vertices);
    mesh.SetTriangles (triangles, 0);

    material = new Material (Shader.Find ("Standard"));
  }

  void Update () {
    Graphics.DrawMesh (
      mesh,
      transform.position,
      transform.rotation,
      material,
      LayerMask.NameToLayer ("Default")
    );
  }
}

Start内で正方形のMeshを作成しマテリアルの取得を行っています。

UpdateではGraphics.DrawMeshを呼び出しています。 アタッチしたGameObjectのtransformを反映するようにしています。

実行結果は次のとおりです。

Graphics.DrawMeshで描画した正方形

無事にレンダリングされました。

この方法でレンダリングを行うと、MeshFilterMeshRendererをつけた GameObjectを用意せずにMeshを描画できます。 大量のMeshを描画したい場合にはGameObjectのオーバーヘッドが馬鹿にならなくなるので スクリプトから描画処理を走らせるのが有効だそうです。

Graphics.DrawMeshNow

次はGraphics.DrawMeshNowを試してみます。

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

public class DrawMeshNow : MonoBehaviour {

  Mesh mesh;
  Material material;

  void Start () {
    mesh = new Mesh ();
    mesh.name = "TestMesh";
    var vertices = new List<Vector3> {
      new Vector3 (0.5f, 0.5f, 0),
      new Vector3 (-0.5f, 0.5f, 0),
      new Vector3 (0.5f, -0.5f, 0),
      new Vector3 (-0.5f, -0.5f, 0),
    };
    var triangles = new List<int> {
      1,
      0,
      2,
      1,
      2,
      3
    };
    mesh.SetVertices (vertices);
    mesh.SetTriangles (triangles, 0);

    material = new Material (Shader.Find ("Unlit/TestShader"));
  }

  void OnRenderObject () {
    material.SetPass (0);
    Graphics.DrawMeshNow (mesh, transform.position, transform.rotation);
  }
}

Graphics.DrawMeshと比べるとUpdateではなくOnRenderObjectで 呼び出している点が異なります。 OnRenderObjectは通常のシーンのレンダリングが終わったあとに呼ばれるものです。

また、Graphics.DrawMeshNowGraphics.DrawMeshと違って マテリアルを引数に渡していません。 Graphics.DrawMeshNowを呼び出す前に、使うマテリアルでmaterial.SetPassを 呼び出しておく必要があります。 SetPassではマテリアルの使うパスのインデックス番号を0始まりで指定します。 今回は次のようなシェーダを用意しました。

Shader "Unlit/TestShader"
{
  SubShader
  {
    Tags { "RenderType"="Opaque" }

    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
      };

      struct v2f
      {
        float4 vertex : SV_POSITION;
      };

      v2f vert (appdata v)
      {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
      }

      fixed4 frag (v2f i) : SV_Target
      {
        return fixed4(1, 0, 0, 1);
      }
      ENDCG
    }
  }
}

Passは1つで単純に赤で塗りつぶすものです。

これのスクリプトを適当なGameObjectにつけて実行してみます。

Graphics.DrawMeshNowで描画した正方形

Graphics.DrawMeshと比べてみると影が描画されていないのがわかります。

Graphics.DrawMeshNowの描画処理をOnRenderObjectではなくUpdateで描画すると、 その後の通常のレンダリング処理でクリアされて消えてしまいます。 フレームデバッガを確認してみます。

フレームデバッガの確認

これがOnRenderObjectGraphics.DrawMeshNowを呼び出した場合です。 他のオブジェクトが描画されたあとでOnRenderObjectのタイミングで描画されてます。

Updateで呼び出すと一番最初に描画処理が走る

こちらがUpdateで呼び出した場合です。 一番最初に描画処理が走っています。

クリアされている

その後の通常の描画処理でクリアされてしまいます。

GL

次はGLを使って描画してみます。 GLはOpenGLのような非常に低レベルなAPIです。

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

public class DrawWithGL : MonoBehaviour {

  Material material;

  void Start () {
    material = new Material (Shader.Find ("Unlit/TestShader"));
  }

  void OnRenderObject () {
    GL.PushMatrix ();

    GL.MultMatrix (transform.localToWorldMatrix);

    material.SetPass (0);

    GL.Begin (GL.TRIANGLES);

    GL.Vertex (new Vector3 (-0.5f, 0.5f, 0));
    GL.Vertex (new Vector3 (0.5f, 0.5f, 0));
    GL.Vertex (new Vector3 (0.5f, -0.5f, 0));

    GL.Vertex (new Vector3 (-0.5f, 0.5f, 0));
    GL.Vertex (new Vector3 (0.5f, -0.5f, 0));
    GL.Vertex (new Vector3 (-0.5f, -0.5f, 0));

    GL.End ();

    GL.PopMatrix ();
  }
}

Graphics.DrawMeshNowと同じ理由でUpdateではなくOnRenderObjectで呼び出しています。

GL.BegineからGL.Endの間でモデルの頂点を組み立てていきます。 三角形を作る順番でGL.Vertexを呼び出しています。 こちらもGraphics.DrawMeshNowと同じで、 使うマテリアルは事前にmaterial.SetPassを呼び出しておきます。

これを適当なGameObjectにつけて実行してみます。

GLでのレンダリング

比較

3種類の描画方法を比較してみます。

Graphics.DrawMeshは即座にメッシュを描画するわけではありません。 DrawMeshに渡されたメッシュは通常のレンダリングパイプラインの中で呼び出されます。 フレームデバッガを確認してみると次のようになっています。

レンダリングパイプラインで呼ばれている

Shadowパスでもきちんと呼び出されています。

Shadowもきちんと呼ばれている

レンダリングパイプラインで適切に呼び出されるおかげで影なども適切にレンダリングされます。

一方で、Graphics.DrawMeshNowGLは呼び出したときに直接レンダリングを行います。 そのためレンダリングパイプラインから外れてしまいます。 先のGraphics.DrawMeshNowのプログラムのフレームデバッガは次のとおりです。

描画処理は最後に付け加えられる

OnRenderObjectで描画しているため通常の描画処理がすべて終わったあとで呼び出されています。 レンダリングパイプラインから外れるため影の描画などがきちんとおこなわれません。

おわりに

MeshRendererに頼らない描画方法について3つ試してみました。 次回はこの記事では試さなかったCommandBufferを試してみたいと思います。

この記事のプログラムのソースコードはこちらのリポジトリに置いてあります。

  • Unity