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.Begin
やGL.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を いじってみたいと思います。
この記事のプログラムのソースコードはこちらのリポジトリに置いてあります。