UnityでCommandBufferを利用してMeshをレンダリングしてみる

はじめに

前回はGraphicsやGLを使ってMeshRendererに頼らない描画を試してみました。 今回はもうひとつのMeshRendererに頼らない描画方法のCommandBufferを試してみます。

実行結果

Unityのバージョン:2018.3.0b1

CommandBufferについて

CommandBufferはDirectX 12やVulkan、Metalなどの最近の低レベルグラフィックスAPIで 使われている概念です。 呼び名はいろいろでCommandBufferの他にCommandList、DisplayListなどと呼ばれています。

CommandBuffer以前のグラフィックスAPIはステートフルなものでした。 前回のGLを使った描画のソースコードを見てみます。

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 ();
}

GL.BeginGL.Vertexなどの呼び出しは内部の状態として保持されていきます。

このようなステートフルなAPIはマルチスレッドのプログラムを書く際に問題となります。 API呼び出しのタイミングに強く依存するため 排他制御しなければまともにモデルひとつ描画することもできなくなります。 排他制御してしまうということはマルチスレッドの恩恵を活かすことができません。

近年はマルチパスレンダリングを多用したりVR向けに右目左目それぞれレンダリングを行うなど、 CPUからのグラフィックAPIの呼び出しの数は増えています。 CPUでの処理が増えている中で GPUの性能向上に比べてCPUの性能向上は緩やかで、 さらに近年のCPUはクロック数を上げるよりもコア数を増やすことで性能を向上させています。 マルチスレッドなプログラムを書かなければCPUを有効活用することはできません。

こういった状況を背景に、 グラフィックスAPIをマルチスレッド向けに作り直して生まれた概念が CommandBufferです。

CommandBufferはGPUの描画命令をいくつか積み上げたようなものです。 CommandBufferを構築し、そのCommandBufferをコマンドキューに流し込みます。 GPUはコマンドキューに積まれたコマンドを実行していきます。 CommandBufferの構築はマルチスレッドでもそれぞれのスレッドで実行できます。 また、コマンドの再利用も行えるため効率的な描画が可能になりました。

CommandBufferでMeshのレンダリングを行う

UnityにもCommandBufferというクラスがあります。 Unityのものは低レベルなグラフィクスAPIのものよりは もう少し高レベルなものになっているようですが。

CommandBufferを利用して簡単なものを描画してみましょう。 最初にスクリプトのコード全文を載せます。

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

[RequireComponent (typeof (Camera))]
public class DrawWithCommandBuffer : MonoBehaviour {

  Mesh CreateMesh () {
    var 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);
    return mesh;
  }

  CommandBuffer CreateCommandBuffer (Mesh mesh, Material material) {
    var buf = new CommandBuffer ();

    buf.name = "My Command Buffer";

    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (0, 0, 0), Quaternion.identity, Vector3.one),
      material
    );
    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (2f, 0, 0), Quaternion.identity, Vector3.one),
      material
    );
    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (-2f, 0, 0), Quaternion.identity, Vector3.one),
      material
    );

    return buf;
  }

  void Start () {
    var mesh = CreateMesh ();
    var material = new Material (Shader.Find ("Unlit/TestShader"));
    var buf = CreateCommandBuffer (mesh, material);

    var camera = GetComponent<Camera> ();
    camera.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
  }

}

Start

Startメソッドにメインの処理を書いています。

void Start () {
  var mesh = CreateMesh ();
  var material = new Material (Shader.Find ("Unlit/TestShader"));
  var buf = CreateCommandBuffer (mesh, material);

  var camera = GetComponent<Camera> ();
  camera.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
}

最初にメッシュとマテリアルを作成し、それを使ってコマンドバッファを作成しています。 CreateMeshは上で定義しているメッシュを作成するメソッドです。 マテリアルのシェーダについては前回作成したのと同じものです。 赤色で塗りつぶすだけの単純なシェーダです。 CreateCommandBufferメソッドは上で定義しているコマンドバッファを構築して返すメソッドです。

次に、このスクリプトがアタッチされたカメラをvar camera = GetComponent<Camera> ();で 取得しています。 このスクリプトはメインカメラにアタッチする前提で書いています。 CameraコンポーネントのないGameObjectにアタッチできないよう RequireComponent属性を付けています。

[RequireComponent (typeof (Camera))]
public class DrawWithCommandBuffer : MonoBehaviour {
  ...
}

CreateCommandBufferの戻り値のCommandBufferをcamera.AddCommandBufferで カメラに登録しています。

カメラにコマンドバッファを登録するときにCameraEvent.AfterSkyboxを指定しています。 camera.AddCommandBufferよってレンダリングパイプラインの特定の場所に コマンドを差し込むことができます。 差し込めるタイミングについてはドキュメントに記載されています。

CreateMesh

CreateMeshメソッドについて見てみます。

Mesh CreateMesh () {
  var 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);
  return mesh;
}

1辺の大きさ1の正方形のメッシュを作って返しています。

CreateCommandBuffer

次にCreateCommandBufferメソッドの中身を見ていきます。

CommandBuffer CreateCommandBuffer (Mesh mesh, Material material) {
  var buf = new CommandBuffer ();

  buf.name = "My Command Buffer";

  buf.DrawMesh (
    mesh,
    Matrix4x4.TRS (new Vector3 (0, 0, 0), Quaternion.identity, Vector3.one),
    material
  );
  buf.DrawMesh (
    mesh,
    Matrix4x4.TRS (new Vector3 (2f, 0, 0), Quaternion.identity, Vector3.one),
    material
  );
  buf.DrawMesh (
    mesh,
    Matrix4x4.TRS (new Vector3 (-2f, 0, 0), Quaternion.identity, Vector3.one),
    material
  );

  return buf;
}

var buf = new CommandBuffer ();でコマンドバッファを作成します。 ここからコマンドを構築していきます。

buf.name = "My Command Buffer";は識別のための名前を設定しています。 名前を設定しておくとフレームデバッガなどで表示されます。

作成したbufに対してDrawMeshコマンドを追加しています。 今回は渡されたメッシュを渡されたマテリアルで3回描画しています。

実行結果

このスクリプトをメインカメラにアタッチして実行してみます。 実行してみた結果は次のとおりです。

実行結果

フレームデバッガを確認してみます。 「My Command Buffer」が「AfterSkybox」のタイミングで実行されているのが分かります。

フレームデバッガ

CommandBufferを追加したカメラはこのようになります。

追加されたコマンドバッファが表示される

追加されたCommandBufferが表示されています。

CommandBufferの別スレッドからの呼び出しについて

CommandBufferはマルチスレッドへグラフィクスAPIを対応させるものでしたが、 UnityのCommandBufferはマルチスレッドに対応していません。 Unityの他のAPIと同じく、メインスレッドから呼び出さなければならないようです。 次のコードはエラーになります。

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Rendering;

public class CreateACommandBufferInASeparateThread : MonoBehaviour {

  Mesh CreateMesh () {
    var 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);
    return mesh;
  }

  CommandBuffer CreateCommandBuffer (Mesh mesh, Material material) {
    Debug.Log (Thread.CurrentThread.ManagedThreadId);
    var buf = new CommandBuffer ();

    buf.name = "My Command Buffer";

    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (0, 0, 0), Quaternion.identity, Vector3.one),
      material
    );
    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (2f, 0, 0), Quaternion.identity, Vector3.one),
      material
    );
    buf.DrawMesh (
      mesh,
      Matrix4x4.TRS (new Vector3 (-2f, 0, 0), Quaternion.identity, Vector3.one),
      material
    );

    return buf;
  }

  void Start () {
    Debug.Log (Thread.CurrentThread.ManagedThreadId);

    var mesh = CreateMesh ();
    var material = new Material (Shader.Find ("Unlit/TestShader"));

    var task = Task.Run (() => CreateCommandBuffer (mesh, material));
    task.Wait ();
    var buf = task.Result;

    var camera = GetComponent<Camera> ();
    camera.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
  }
}

別スレッドから呼び出すとエラーになる

おわりに

今回はCommandBufferを試してみました。 今回は単純なメッシュを表示するだけでしたが、 次回はコマンドをレンダリングパイプラインに差し込めることを利用してG-Bufferを いじってみたいと思います。

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

  • Unity
  • Shader