UnityのDeferredでCommandBufferを利用してGBufferをいじってみる

はじめに

前回はCommandBufferを触ってみました。 今回も引き続きCommandBufferを触ってみます。 今回はコマンドをレンダリングパイプラインに差し込めることを利用して DeferredでGBufferをいじってみます。

RT2

Unityのバージョン:2018.3.0b1

Deferred Shading

まずは今回扱うDeferred Shadingについて軽く説明します。

Deferred Shadingはシェーディング手法の一種です。 Deferredシェーディングでは大まかに言って2つのパスで構成されます。 1つめのパスで位置や法線、マテリアルの情報をテクスチャに書き出します。 2つめのパスでそのテクスチャを利用してシェーディングを行います。 最初のポリゴン描画時にはシェーディングを行わないため 「Deferred(遅延)」シェーディングと呼ばれます。

最初のパスで描画されるテクスチャはGeometry BufferやGBufferと呼ばれます。 GBufferのイメージを次に上げます。

色のバッファ

Zバッファ

ワールド法線のバッファ

上から順番に色とZバッファ、そしてワールドスペースの法線のバッファです。 第1パスで書き出したこれらのGBufferをもとに 第2パスでシェーディングを行います。

シェーディングの結果のイメージ

テクスチャに色ではなくて法線などのジオメトリ情報を入れるのは不思議な感じがしますね。

このDeferredシェーディングはライティングがジオメトリの計算のあとから行われるので、 ライトを追加するごとにジオメトリを計算する必要がなくなります。 そのため動的なライトなどのコストが低くなるという利点があります。 ゲームなどではこのレンダリング手法が選ばれることも多いようです。

一方で半透明の物体の扱いに難がある、ジオメトリバッファの分メモリを使う、 アンチエイリアスが辛いなどデメリットもそれなりにあるレンダリング方法ではあります。

Deferredシェーディングはその過程でGBufferが生成されるので、 これをいじることで面白いことができたりします。 今回はこのGBufferをいじるのをUnity上で試してみます。

UnityのDeferred Shading

UnityでDeferredのレンダリングにするにはプロジェクト設定をいじります。 「Edit > Settings > Graphics」から「Rendering Path」の設定で「Deferred」を選びます。

Rendering Pathの設定

プロジェクト設定を利用しなくてもカメラ単位でDeferredを設定することもできるようです。

カメラの設定

UnityのDeferredでのGBufferのレイアウトはドキュメントに記載されています。

  • RT0, ARGB32 format: Diffuse color (RGB), occlusion (A).
  • RT1, ARGB32 format: Specular color (RGB), roughness (A).
  • RT2, ARGB2101010 format: World space normal (RGB), unused (A).
  • RT3, ARGB2101010 (non-HDR) or ARGBHalf (HDR) format: Emission + lighting + lightmaps + reflection probes buffer.
  • Depth+Stencil buffer.

RT0とRT1にマテリアルの情報を格納し、RT2にワールド空間のノーマルを書き出します。 RT3にはいろいろと詰め込まれていますね。 UnityのDeferredでは動的なライトは遅延されGBuffer生成後に計算されますが、 環境光やライトプローブ、GIなどはフォワードと同じようにモデルごとに計算されます。

UnityのDeferredを詳しく追いかけたい場合には凹みさんの記事が参考になります。


GBufferを弄る

GBufferに書き加えることでシーンにオブジェクトを描画してみます。 まずは簡単な板をGBufferに描画してみます。

スクリプトを全文のせます。

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

#if UNITY_EDITOR
using UnityEditor;
#endif

public class GBufferTest : MonoBehaviour {

  private Mesh quad;
  private Material material;
  private int gBuffer0ColorPropertyID;
  private int gBuffer1ColorPropertyID;
  private int gBuffer3ColorPropertyID;
  private CommandBuffer buf;

  [ColorUsage (showAlpha: false)]
  public Color albedo;
  [Range (0, 1)]
  public float occlusion;
  [ColorUsage (showAlpha: false)]
  public Color specular;
  [Range (0, 1)]
  public float smoothness;
  [ColorUsage (showAlpha: false, hdr: true)]
  public Color emission;

  private void Start () {
    quad = new Mesh ();
    var vertices = new List<Vector3> {
      new Vector3 (1f, 1f, 0),
      new Vector3 (-1f, 1f, 0),
      new Vector3 (1f, -1f, 0),
      new Vector3 (-1f, -1f, 0),
    };
    var triangles = new List<int> {
      1,
      0,
      2,
      1,
      2,
      3
    };
    quad.SetVertices (vertices);
    quad.SetTriangles (triangles, 0);
		quad.RecalculateNormals ();

    material = new Material (Shader.Find ("GBuffer/GBufferTest"));

    gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
    gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
    gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");

    buf = new CommandBuffer ();
    buf.name = "GBuffer Test";
    foreach (var cam in Camera.allCameras) {
      if (!cam) {
        break;
      }
      cam.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
    }

#if UNITY_EDITOR
    var sceneViewCameras = SceneView.GetAllSceneCameras ();
    foreach (var cam in sceneViewCameras) {
      if (!cam) {
        break;
      }
      cam.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
    }
#endif
  }

  private void Update () {
    buf.Clear ();

    var gBufferTarget = new [] {
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
        new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
        new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
        new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
    };

    buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);

    material.SetVector (
      gBuffer0ColorPropertyID,
      new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
    );
    material.SetVector (
      gBuffer1ColorPropertyID,
      new Vector4 (specular.r, specular.g, specular.b, smoothness)
    );
    material.SetVector (
      gBuffer3ColorPropertyID,
      new Vector4 (emission.r, emission.g, emission.b, 0)
    );

    var matrix = Matrix4x4.TRS (
      transform.position,
      transform.rotation,
      transform.localScale
    );

    buf.DrawMesh (
      quad,
      matrix,
      material,
      submeshIndex : 0,
      shaderPass : 0,
      properties : null
    );
  }
}

順番に解説をしていきます。

Start

最初にStartの処理について見てみます。

Meshの作成

Startで最初にメッシュの作成をしています。

...
private Mesh quad;
...

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

シンプルな正方形のメッシュを作っています。

マテリアル関連

次にマテリアル関連の初期化を行っています。

...
private Material material;
private int gBuffer0ColorPropertyID;
private int gBuffer1ColorPropertyID;
private int gBuffer3ColorPropertyID;
...

void Start() {

  ...

  material = new Material (Shader.Find ("GBuffer/GBufferTest"));

  gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
  gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
  gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");

  ...

マテリアルを作成し、シェーダのプロパティのIDを取得し保持します。 マテリアルのシェーダについては後で説明します。 シェーダのプロパティIDはマテリアルに値を受け渡すときに必要になるものです。

カメラへのコマンドバッファの登録

Startの最後にコマンドバッファの登録も行っています。

...
private CommandBuffer buf;

void Start(){

  ...

  buf = new CommandBuffer ();
  buf.name = "GBuffer Test";
  foreach (var cam in Camera.allCameras) {
    if (!cam) {
      break;
    }
    cam.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
  }

#if UNITY_EDITOR
  var sceneViewCameras = SceneView.GetAllSceneCameras ();
  foreach (var cam in sceneViewCameras) {
    if (!cam) {
      break;
    }
    cam.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
  }
#endif
}

シーン上のすべてのカメラに対して登録を行っています。 登録するのは前回とは違ってAfterGBufferのタイミングです。 エディタの場合にはエディタ上のカメラも反映するようにしました。

Update

次にUpdateでの処理を見ていきます。 Updateで毎フレームコマンドの更新を行っています。

private void Update () {
  buf.Clear ();
  ...
}

最初にbuf.Clear ();でコマンドバッファをクリアした後に、 コマンドバッファの構築を行っていきます。

private void Update () {
  buf.Clear ();

  var gBufferTarget = new [] {
    new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
      new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
  };

  buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);

  ...

  var matrix = Matrix4x4.TRS (
    transform.position,
    transform.rotation,
    transform.localScale
  );

  buf.DrawMesh (
    quad,
    matrix,
    material,
    submeshIndex : 0,
    shaderPass : 0,
    properties : null
  );
}

ポイントはbuf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);です。 これでbuf.DrawMeshの書き出す先をGBufferにしています。 いわゆるマルチレンダーターゲットを利用してGBufferを一度で描画します。

GBufferのレンダーターゲットはnew RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0)のようにして取得できるようです。

今回はカメラのHDRレンダリングをオンにしています。

HDRオン

HDRのときは4つ目のGBufferのRT3にはカメラのターゲットが利用されるようです。

Unity - Manual: Deferred shading rendering path

Note that when the Camera is using HDR rendering, there’s no separate rendertarget being created for Emission+lighting buffer (RT3); instead the rendertarget that the Camera renders into (that is, the one that is passed to the image effects) is used as RT3.

そのため、GBufferのRT3として BuiltinRenderTextureType.CameraTargetを指定しています。

SetVectorでマテリアルの情報をシェーダに渡しています。 この値はスクリプトのインスペクタから設定できるようにしてあります。

...
[ColorUsage (showAlpha: false)]
public Color albedo;
[Range (0, 1)]
public float occlusion;
[ColorUsage (showAlpha: false)]
public Color specular;
[Range (0, 1)]
public float smoothness;
[ColorUsage (showAlpha: false, hdr: true)]
public Color emission;
...

private void Update () {

  ...

  material.SetVector (
    gBuffer0ColorPropertyID,
    new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
  );
  material.SetVector (
    gBuffer1ColorPropertyID,
    new Vector4 (specular.r, specular.g, specular.b, smoothness)
  );
  material.SetVector (
    gBuffer3ColorPropertyID,
    new Vector4 (emission.r, emission.g, emission.b, 0)
  );

  ...
}

RT3はemissionと名付けましたがEmission以外もGIなどいろいろはいっているようですね。

GBuffer/GBufferTest

次にGBuffer/GBufferTestシェーダについて解説していきます。 最初にコード全文をのせます。

Shader "GBuffer/GBufferTest"
{
  Properties
  {
    _GBuffer0Color ("GBuffer0 Color", Color) = (0, 0, 0, 0)
    _GBuffer1Color ("GBuffer1 Color", Color) = (0, 0, 0, 0)
    _GBuffer3Color ("GBuffer3 Color", Color) = (0, 0, 0, 0)
  }
  SubShader
  {
    Pass
    {
      Stencil
      {
        Comp Always
        Pass Replace
        Ref 128
      }

      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag

      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
      };

      struct v2f
      {
        float4 position : SV_POSITION;
        float3 normal : NORMAL;
      };

      struct flagout
      {
        float4 gBuffer0 : SV_TARGET0;
        float4 gBuffer1 : SV_TARGET1;
        float4 gBuffer2 : SV_TARGET2;
        float4 gBuffer3 : SV_TARGET3;
        float depth: SV_DEPTH;
      };

      uniform float4 _GBuffer0Color;
      uniform float4 _GBuffer1Color;
      uniform float4 _GBuffer3Color;

      void vert (in appdata v, out v2f o)
      {
        o.position = UnityObjectToClipPos(v.vertex);
        o.normal = UnityObjectToWorldNormal(v.normal);
      }

      void frag (in v2f i, out flagout o)
      {
        o.gBuffer0 = _GBuffer0Color;
        o.gBuffer1 = _GBuffer1Color;
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer3 = _GBuffer3Color;
        o.depth = i.position.z;
      }
      ENDCG
    }
  }
}

順番に解説していきます。

プロパティ

プロパティではノーマル以外のGBufferの値を受け取るようにしています。 受け取った値はフラグメントシェーダで書き出すときに利用しています。

Shader "GBuffer/GBufferTest"
{
  Properties
  {
    _GBuffer0Color ("GBuffer0 Color", Color) = (0, 0, 0, 0)
    _GBuffer1Color ("GBuffer1 Color", Color) = (0, 0, 0, 0)
    _GBuffer3Color ("GBuffer3 Color", Color) = (0, 0, 0, 0)
  }
  SubShader
  {
    Pass
    {
      ...

      uniform float4 _GBuffer0Color;
      uniform float4 _GBuffer1Color;
      uniform float4 _GBuffer3Color;

      ...
    }
  }
}

ステンシル

Passにはステンシルを設定しています。

Stencil
{
  Comp Always
  Pass Replace
  Ref 128
}

ステンシルの128がUnityのDeferred Shadingでシェーディングを行うフラグになっているようです。 128を立てておかないとシェーディングが行われなくなります。

flagout構造体

flagoutで複数のターゲットを指定しています。

struct flagout
{
  float4 gBuffer0 : SV_TARGET0;
  float4 gBuffer1 : SV_TARGET1;
  float4 gBuffer2 : SV_TARGET2;
  float4 gBuffer3 : SV_TARGET3;
  float depth: SV_DEPTH;
};

それぞれスクリプトのbuf.SetRenderTargetで指定したGBufferに対応しています。

頂点シェーダ

頂点シェーダでは頂点をクリッピング座標に変換し、 法線をワールド座標へ変換しフラグメントシェーダに渡します。

void vert (in appdata v, out v2f o)
{
  o.position = UnityObjectToClipPos(v.vertex);
  o.normal = UnityObjectToWorldNormal(v.normal);
}

フラグメントシェーダ

フラグメントシェーダではプロパティで受け取ったGBufferの値を書き込んでいます。

void frag (in v2f i, out flagout o)
{
  o.gBuffer0 = _GBuffer0Color;
  o.gBuffer1 = _GBuffer1Color;
  o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
  o.gBuffer3 = _GBuffer3Color;
  o.depth = i.position.z;
}

法線は-1~1の範囲を0~1の範囲にするために、各要素に対して0.5を掛けて0.5足しています。

depthにはクリッピングスペースのzを渡しています。

実行結果

スクリプトを適当なGameObjectにアタッチして実行すると次のようになります。

PostProcessingでSSRを加えてみます。

GBufferにマテリアルの値を書き込むことできちんと描画に反映されることが確認できました。

フレームデバッガでGBufferの中身を確認してみます。

RT0

RT0です。

RT1

RT1です。

RT2

RT2です。

RT3

RT3です。 本来は単色で塗りつぶすのではなく ちゃんと処理をしないとならないようです。 詳しくは凹みさんの記事に書かれています。

Depth

Depthです。

レンダリング結果

レンダリング結果です。


GBufferを弄って球を描画する

次は板ポリ一枚で球をGBufferに書き込み描画してみます。

最初にスクリプトを全文載せます。

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

public class GBufferSphere : MonoBehaviour {

  private Mesh quad;
  private Material material;
  private int gBuffer0ColorPropertyID;
  private int gBuffer1ColorPropertyID;
  private int gBuffer3ColorPropertyID;
  private Dictionary<Camera, CommandBuffer> dict = new Dictionary<Camera, CommandBuffer> ();

  [ColorUsage (showAlpha: false)]
  public Color albedo;
  [Range (0, 1)]
  public float occlusion;
  [ColorUsage (showAlpha: false)]
  public Color specular;
  [Range (0, 1)]
  public float smoothness;
  [ColorUsage (showAlpha: false, hdr: true)]
  public Color emission;

  private void Start () {
    quad = new Mesh ();
    var vertices = new List<Vector3> {
      new Vector3 (1f, 1f, 0),
      new Vector3 (-1f, 1f, 0),
      new Vector3 (1f, -1f, 0),
      new Vector3 (-1f, -1f, 0),
    };
    var triangles = new List<int> {
      1,
      0,
      2,
      1,
      2,
      3
    };
    var uvs = new List<Vector2> {
      new Vector2 (1, 1),
      new Vector2 (0, 1),
      new Vector2 (1, 0),
      new Vector2 (0, 0),
    };
    quad.SetVertices (vertices);
    quad.SetTriangles (triangles, 0);
    quad.SetUVs (0, uvs);

    material = new Material (Shader.Find ("GBuffer/GBufferSphere"));

    gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
    gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
    gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");
  }

  private void OnWillRenderObject () {
    var camera = Camera.current;
    if (camera == null) return;

    CommandBuffer buf;
    if (dict.ContainsKey (camera)) {
      buf = dict[camera];
    } else {
      buf = new CommandBuffer ();
      buf.name = "GBuffer Sphere";
      camera.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
      dict[camera] = buf;
    }

    buf.Clear ();

    var gBufferTarget = new [] {
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
        new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
        new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
        new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
    };

    buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);

    material.SetVector (
      gBuffer0ColorPropertyID,
      new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
    );
    material.SetVector (
      gBuffer1ColorPropertyID,
      new Vector4 (specular.r, specular.g, specular.b, smoothness)
    );
    material.SetVector (
      gBuffer3ColorPropertyID,
      new Vector4 (emission.r, emission.g, emission.b, 0)
    );

    buf.DrawMesh (
      quad,
      Matrix4x4.Rotate (camera.transform.rotation),
      material,
      submeshIndex : 0,
      shaderPass : 0,
      properties : null
    );
  }
}

順に説明していきます。

Start

private void Start () {
  quad = new Mesh ();
  var vertices = new List<Vector3> {
    new Vector3 (1f, 1f, 0),
    new Vector3 (-1f, 1f, 0),
    new Vector3 (1f, -1f, 0),
    new Vector3 (-1f, -1f, 0),
  };
  var triangles = new List<int> {
    1,
    0,
    2,
    1,
    2,
    3
  };
  var uvs = new List<Vector2> {
    new Vector2 (1, 1),
    new Vector2 (0, 1),
    new Vector2 (1, 0),
    new Vector2 (0, 0),
  };
  quad.SetVertices (vertices);
  quad.SetTriangles (triangles, 0);
  quad.SetUVs (0, uvs);

  material = new Material (Shader.Find ("GBuffer/GBufferSphere"));

  gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
  gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
  gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");
}

Startではメッシュの作成とマテリアル関連の初期化をしています。

今回は次のようなUVをもつMeshを作成しています。

MeshのUV

マテリアルは"GBuffer/GBufferSphere"というシェーダから作っています。 このシェーダについては後で解説します。

OnWillRenderObject

さきほどとは違い、UpdateではなくOnWillRenderObjectで処理を書いています。 OnWillRenderObjectは描画時にカメラごとに呼ばれる、1フレームに複数回呼ばれるメソッドです。 OnWillRenderObjectの中でCamera.currentにアクセスすると 現在描画しようとしているカメラを取得できます。

さきの例ではStartでシーン上の全カメラにコマンドバッファを適用していました。 この方法だとカメラが途中で追加されたりしたときに対応できません。 OnWillRenderObjectでコマンドバッファの登録を行うことで これから描画する複数のカメラすべてに対応します。

OnWillRenderObjectはカメラに写っている場合しか呼び出されないので、 このスクリプトは地面などの常に画面に映るGameObjectにアタッチする必要があります。

コマンドバッファの登録

辞書にカメラとコマンドバッファのペアを登録しています。

private Dictionary<Camera, CommandBuffer> dict = new Dictionary<Camera, CommandBuffer> ();

private void OnWillRenderObject () {
  ...

  CommandBuffer buf;
  if (dict.ContainsKey (camera)) {
    buf = dict[camera];
  } else {
    buf = new CommandBuffer ();
    buf.name = "GBuffer Sphere";
    camera.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
    dict[camera] = buf;
  }

  ...
}

すでにコマンドバッファが登録してある場合は新しくコマンドバッファを作成しないために 辞書を確認します。 カメラにコマンドバッファが登録されていないときだけ コマンドバッファを作成してCameraEvent.AfterGBufferのタイミングで登録しています。

今回は省略していますが、カメラからコマンドバッファを取り除く処理を書くときにも この辞書を利用できます。

コマンドバッファの構築

次にコマンドバッファを毎フレーム構築し直しています。

private void OnWillRenderObject () {
  ...

  buf.Clear ();

  ...
}

最初にbuf.Clear()でコマンドをクリアしています。

その後、レンダーターゲットにGBufferを設定します。

private void OnWillRenderObject () {
  ...

  var gBufferTarget = new [] {
    new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
      new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
      new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
  };

  buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);

  ...
}

最後にDrawMeshコマンドをコマンドバッファに積んでいます。

private void OnWillRenderObject () {
  ...

  buf.DrawMesh (
    quad,
    Matrix4x4.Rotate (camera.transform.rotation),
    material,
    submeshIndex : 0,
    shaderPass : 0,
    properties : null
  );
}

Meshをカメラと同じ回転をさせてから描画しています。 これでこのメッシュは常にカメラの方を向く いわゆるビルボードになります。

マテリアル情報の受け渡し

マテリアル情報の受け渡しもOnWillRenderObjectで行っています。

private void OnWillRenderObject () {
  ...

  material.SetVector (
    gBuffer0ColorPropertyID,
    new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
  );
  material.SetVector (
    gBuffer1ColorPropertyID,
    new Vector4 (specular.r, specular.g, specular.b, smoothness)
  );
  material.SetVector (
    gBuffer3ColorPropertyID,
    new Vector4 (emission.r, emission.g, emission.b, 0)
  );

  ...
}

これについてはさきほどのGBufferに板を描画したときと変わりませんね。

GBuffer/GBufferSphere

次にGBufferへ書き込みを行うシェーダを説明します。

最初にシェーダ全文載せます。

Shader "GBuffer/GBufferSphere"
{
  Properties
  {
    _GBuffer0Color ("GBuffer0 Color", Color) = (0, 0, 0, 0)
    _GBuffer1Color ("GBuffer1 Color", Color) = (0, 0, 0, 0)
    _GBuffer3Color ("GBuffer3 Color", Color) = (0, 0, 0, 0)
    _Radius ("Radius", float) = 1.0
  }
  SubShader
  {
    Pass
    {
      Stencil
      {
        Comp Always
        Pass Replace
        Ref 128
      }

      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag

      #include "UnityCG.cginc"

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

      struct v2f
      {
        float4 position : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 eyeSpacePos : TEXCOORD1;
      };

      struct flagout
      {
        float4 gBuffer0 : SV_TARGET0;
        float4 gBuffer1 : SV_TARGET1;
        float4 gBuffer2 : SV_TARGET2;
        float4 gBuffer3 : SV_TARGET3;
        float depth: SV_DEPTH;
      };

      uniform float4 _GBuffer0Color;
      uniform float4 _GBuffer1Color;
      uniform float4 _GBuffer3Color;
      uniform float _Radius;

      void vert (in appdata v, out v2f o)
      {
        o.position = UnityObjectToClipPos(v.vertex);
        o.normal = UnityObjectToWorldNormal(v.normal);
        o.uv = v.uv;
        o.eyeSpacePos = UnityObjectToViewPos(v.vertex);
      }

      void frag (in v2f i, out flagout o)
      {
        float3 eyeSpaceNormal;
        eyeSpaceNormal.xy = i.uv * 2 - 1;
        float r2 = dot(eyeSpaceNormal.xy, eyeSpaceNormal.xy);
        if (r2 > 1.0) discard;
        eyeSpaceNormal.z = sqrt(1.0 - r2);

        float4 pixelPos = float4(i.eyeSpacePos + eyeSpaceNormal * _Radius, 1);
        float4 clipSpacePos = mul(UNITY_MATRIX_P, pixelPos);
        o.depth = clipSpacePos.z / clipSpacePos.w;

        float4 worldSpaceNormal = mul(
          transpose(UNITY_MATRIX_V),
          float4(eyeSpaceNormal.xyz, 0)
        );

        o.gBuffer0 = _GBuffer0Color;
        o.gBuffer1 = _GBuffer1Color;
        o.gBuffer2 = worldSpaceNormal * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer3 = _GBuffer3Color;
      }
      ENDCG
    }
  }
}

プロパティ

シェーダのプロパティを定義している部分を抜き出すと次のとおりです。

Shader "GBuffer/GBufferSphere"
{
  Properties
  {
    _GBuffer0Color ("GBuffer0 Color", Color) = (0, 0, 0, 0)
    _GBuffer1Color ("GBuffer1 Color", Color) = (0, 0, 0, 0)
    _GBuffer3Color ("GBuffer3 Color", Color) = (0, 0, 0, 0)
    _Radius ("Radius", float) = 1.0
  }
  SubShader
  {
    Pass
    {
      ...

      uniform float4 _GBuffer0Color;
      uniform float4 _GBuffer1Color;
      uniform float4 _GBuffer3Color;
      uniform float _Radius;

      ...
    }
  }
}

Normal以外のGBufferの色をプロパティとして定義しています。

また、_Radiusという球の半径の値も定義しています。 今回は半径1の球を描画するので、プロパティにしましたがデフォルト値をそのまま使っています。

シェーディングのためにステンシルの設定

Passにはステンシルを設定しています。

Stencil
{
  Comp Always
  Pass Replace
  Ref 128
}

128を立ててシェーディングを行うようにします。

頂点シェーダ

void vert (in appdata v, out v2f o)
{
  o.position = UnityObjectToClipPos(v.vertex);
  o.normal = UnityObjectToWorldNormal(v.normal);
  o.uv = v.uv;
  o.eyeSpacePos = UnityObjectToViewPos(v.vertex);
}

頂点シェーダでは通常どおり位置と法線、そしてUVを渡しています。

それに加えてeyeスペースの位置も渡しています。

フラグメントシェーダ

フラグメントシェーダは少し複雑です。 順番に説明をしていきます。

void frag (in v2f i, out flagout o)
{
  float3 eyeSpaceNormal;
  eyeSpaceNormal.xy = i.uv * 2 - 1;

  ...
}

uvを変換して0~1から-1~1の範囲にしています。

UVの変換

void frag (in v2f i, out flagout o)
{
  ...

  float r2 = dot(eyeSpaceNormal.xy, eyeSpaceNormal.xy);
  if (r2 > 1.0) discard;

  ...
}

uvを-1~1に変換したものについてxyの内積が1より大きかったら そのピクセルについて捨てています。 これによって正方形いっぱいの円が描画され、その外側は捨てられます。

x*x + y*y > 1の領域を捨てる

円の内側のピクセルについてはzを次のようにして計算しています。

void frag (in v2f i, out flagout o)
{
  ...

  eyeSpaceNormal.z = sqrt(1.0 - r2);

  ...
}

半径1の球上の点を考えてxyから残りのzの大きさを計算しています。 このxyzのベクトルがそのままeyeスペースでの法線になります。

計算したeyeスペースの法線をつかってピクセルのdepthを計算します。

void frag (in v2f i, out flagout o)
{
  ...

  float4 pixelPos = float4(i.eyeSpacePos + eyeSpaceNormal * _Radius, 1);
  float4 clipSpacePos = mul(UNITY_MATRIX_P, pixelPos);
  o.depth = clipSpacePos.z / clipSpacePos.w;

  ...
}

ビルボードから球として飛び出ている分のzを加えて eyeスペースでのピクセルのz位置を計算しています。 depthだけ考えるのでeyeスペースでのxy成分については無視できます。 その後、P行列を掛けることでクリップスペースにしてdepthを書き出しています。

次にeyeスペースの法線をもとにワールドスペースの法線を計算します。

void frag (in v2f i, out flagout o)
{
  ...

  float4 worldSpaceNormal = mul(
    transpose(UNITY_MATRIX_V),
    float4(eyeSpaceNormal.xyz, 0)
  );

  ...
}

eyeスペースの法線にV行列の回転成分の逆行列をかけてワールドスペースにします。 wを0にすることで平行移動成分を無視して回転成分だけの計算にしています。 回転の逆行列は転置と等しくなることを利用しています。

最後にGBufferを書き出しています。

void frag (in v2f i, out flagout o)
{
  ...

  o.gBuffer0 = _GBuffer0Color;
  o.gBuffer1 = _GBuffer1Color;
  o.gBuffer2 = worldSpaceNormal * 0.5 + float4(0.5, 0.5, 0.5, 0);
  o.gBuffer3 = _GBuffer3Color;
}

実行結果

スクリプトは地面などの常に画面に映るGameObjectにアタッチして実行してみます。 カメラにはPostProcessingをつけてあります。

実行してみると板ポリに球体がレンダリングされているのが確認できます。 ちなみに隣にあるのは本物のSphereです。

フレームデバッガで確認してみます。

RT0

RT0です。

RT1

RT1です。

RT2

RT2です。 ちゃんと自分で計算したワールドスペースのノーマルが書き込まれていますね。

RT3

RT3です。 本来ならばちゃんと処理をしないとならないようです。

depth

depthです。

それぞれ適切にGBufferが書き出されています。

実行結果

おわりに

今回はUnityでCommandBufferを利用してGBufferをいじってみるのをやってみました。 板ポリで見せかけの球を描画するのを試してみました。

今回のソースコードはこちらのリポジトリに置いてあります。

  • Unity