凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity の DX11 向け機能のサンプルを読んでみた

はじめに

以下のスレッドに Unity の中の Aras 氏による DX11 機能を利用したサンプルが上がっています。

この中には 8 つのシーンにそれぞれ異なるサンプルが格納されていて、どれもあまり(日本語の)情報がなく且つ面白いものなので、勉強も兼ねて読んでみましたのでその内容をご紹介します。本文中のコードは説明しやすいように色々と編集しているところもありますので、見比べる際はサンプルと異なる点にご注意ください。

環境

Compute Shader Average Pixels

f:id:hecomi:20160510015845p:plain

Compute Shader を使った縮小処理のサンプルです。C# 側でレンダリング結果の縦横それぞれ 1/8 にした RenderTexture を用意し、レンダリング結果とその RenderTexture をコンピュートシェーダに渡して、総和計算を並列に行うアルゴリズムの Parallel Reduction を使って縮小処理を行います。

コンピュートシェーダそのものについては、先の記事でも紹介しました ScrawkBlog がとてもわかり易いのでそちらをご参照ください。

コードは以下のようになっています。

#pragma kernel CSMain

Texture2D Input;
RWTexture2D<float4> Result;

#define blocksize 8
#define groupthreads (blocksize*blocksize)
groupshared float4 accum[groupthreads];

[numthreads(blocksize,blocksize,1)]
void CSMain( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
    accum[GI] = Input[DTid.xy];

    // Parallel reduction algorithm
    GroupMemoryBarrierWithGroupSync();
    if (GI < 32)
        accum[GI] += accum[32+GI];
    GroupMemoryBarrierWithGroupSync();
    if (GI < 16)
        accum[GI] += accum[16+GI];
    GroupMemoryBarrierWithGroupSync();
    if (GI < 8)
        accum[GI] += accum[8+GI];
    GroupMemoryBarrierWithGroupSync();
    if (GI < 4)
        accum[GI] += accum[4+GI];
    GroupMemoryBarrierWithGroupSync();
    if (GI < 2)
        accum[GI] += accum[2+GI];
    GroupMemoryBarrierWithGroupSync();
    if (GI < 1)
        accum[GI] += accum[1+GI];
        
    if (GI == 0)
    {                
        Result[uint2(Gid.x,Gid.y)] = accum[0] / groupthreads;
    }
}

まず、groupshared(共有メモリ)な float4[] の配列 accum を用意します。配列の数は 8 x 8 = 64 で、この数のスレッドを持つスレッドグループに区切って処理を回します。SV_GroupIndexSV_GroupThreadID を1次元にしたもので、0 ~ 63 の値が入ってきます。それぞれのインデックスでは該当する入力画像 Inputピクセルの色を入れておき、前半 32 スレッドへ足しあわす、その次にその中での前半 16 スレッドへ足しあわす...という処理を繰り返していき、最後に 1 スレッドになったらその値を出力画像 Result へ書き込みます。各足しあわせの間では、GroupMemoryBarrierWithGroupSync()で同期待ちを行うことで、毎回必ず同じ結果になります。

Draw Procedural

f:id:hecomi:20160514173912g:plain

Graphics.DrawProcedural() を使った描画のサンプルです。Graphics.DrawProcedural() は即時実行なので使い場所を選ぶかもしれません。Unity 5.1 からは CommandBuffer.DrawProcedural() が追加されたので、適当なタイミングで実行するにはこちらの方が良いかもしれません。

以下スクリプトのコード + コメントです。

using UnityEngine;

public class DrawInstancedFromBuffer : MonoBehaviour
{
    public Material material; // 使用しているシェーダについては後述
    public int vertexCount = 30;
    public int instanceCount = 100;

    private ComputeBuffer bufferPoints;
    private ComputeBuffer bufferPos;
    private Vector3[] origPos;
    private Vector3[] pos;

    void Start()
    {
        // 円周上の座標を作成・格納
        var verts = new Vector3[vertexCount];
        for (var i = 0; i < vertexCount; ++i) {
            float phi = i * Mathf.PI * 2.0f / (vertexCount - 1);
            verts[i] = new Vector3(Mathf.Cos(phi), Mathf.Sin(phi), 0.0f);
        }

        ReleaseBuffers();

        // 作成した円周上の座標を ComputeBuffer へ転送
        // 12 は float (4 byte x 3)
        bufferPoints = new ComputeBuffer(vertexCount, 12);
        bufferPoints.SetData(verts);
        material.SetBuffer("buf_Points", bufferPoints);

        // 各インスタンスの初期座標を作成
        // 最初は適当にランダムな点を格納
        origPos = new Vector3[instanceCount];
        for (var i = 0; i < instanceCount; ++i)
            origPos[i] = Random.insideUnitSphere * 5.0f;

        // GPU 側へ渡す座標配列を作成
        pos = new Vector3[instanceCount];

        // 位置更新用の ComputeBuffer を作成
        // 実際の位置は Update() 内で渡す
        bufferPos = new ComputeBuffer(instanceCount, 12);
        material.SetBuffer("buf_Positions", bufferPos);
    }

    private void ReleaseBuffers()
    {
        if (bufferPoints != null) bufferPoints.Release();
        bufferPoints = null;
        if (bufferPos != null) bufferPos.Release();
        bufferPos = null;
    }

    void OnDisable()
    {
        ReleaseBuffers();
    }

    void Update()
    {
        // 座標を更新して GPU へ転送
        var t = Time.timeSinceLevelLoad;
        for (var i = 0; i < instanceCount; ++i) {
            var x = Mathf.Sin((t + i) * 1.17f);
            var y = Mathf.Sin((t - i) * 1.0f);
            var z = Mathf.Cos((t + i) * 1.87f);
            pos[i] = origPos[i] + new Vector3(x, y, z);
        }
        bufferPos.SetData(pos);
    }

    void OnPostRender()
    {
        // 最後に SetPass() したマテリアルで描画を行う
        material.SetPass(0);
        // インスタンシングにより描画
        // シェーダに頂点 ID とインスタンス ID がやってくるのでそれを利用して描画する
        Graphics.DrawProcedural(MeshTopology.LineStrip, vertexCount, instanceCount);
    }
}

ここで設定されているマテリアルに使用しているシェーダのコードは以下の様な短いコードになっています。コメント中にも記載しましたが、頂点 ID(SV_VertexID)およびインスタンス ID(SV_InstanceID)がやってくるので、これらを利用してセットした ComputeBuffer から情報を取り出します。

Shader "DX11/Instanced from compute buffer" {
SubShader {
Pass {

CGPROGRAM

#pragma target 5.0
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

StructuredBuffer<float3> buf_Points;
StructuredBuffer<float3> buf_Positions;

struct ps_input 
{
    float4 pos : SV_POSITION;
};

ps_input vert (uint id : SV_VertexID, uint inst : SV_InstanceID)
{
    ps_input o;
    float3 worldPos = buf_Points[id] + buf_Positions[inst];
    o.pos = mul (UNITY_MATRIX_VP, float4(worldPos,1.0f));
    return o;
}

float4 frag (ps_input i) : COLOR
{
    return float4(1, 0.5f, 0.0f, 1);
}

ENDCG

}
}

Fallback Off
}

読み取り専用なので、StructuredBufferComputeBuffer のデータを受け取り、頂点 ID およびインデックス ID を使って配列アクセスしてスクリプト側で設定した座標を取り出して頂点座標として出力しています。メッシュを作っておき、インスタンス数を与えればその数だけ 1 回のコールで描けます。

Draw Procedural Indirect

f:id:hecomi:20160512002900g:plain

先程は固定値でインスタンス数を与えていましたが、Graphics.DrawProceduralIndirect() を使うとなんとシェーダ側でカウントを行い、そのカウントに従ったインスタンス数を描画することが出来ます。この例では、2 Pass の Image Effect として動作していて、1 Pass 目で明るいピクセルをカウントし、2 Pass 目でその明るいピクセルの位置に指定した Sprite を描画しています。動かすと影で暗くなったスペキュラによるハイライト部でスプライトが消えるのが見て取れると思います。

カウントするには Append Buffer という先ほど使用した通常のバッファとは異なるものを利用します。1 Pass 目のコードを見てみます。

Pass
{
    ...

    sampler2D _MainTex;
    AppendStructuredBuffer<float2> pointBufferOutput : register(u1);

    fixed4 frag (v2f i) : COLOR0
    {
        fixed4 c = tex2D(_MainTex, i.uv);
        fixed4 c1 = tex2D(_MainTex, i.uv + 2.0/_ScreenParams.xy);
        float lumc = Luminance(c);
        float lumc1 = Luminance(c1);
        if (lumc > 0.98 && lumc > lumc1)
        {
            pointBufferOutput.Append(i.uv);
        }
        return c * 0.6;
    }

    ENDCG
}

該当ピクセルの輝度(Luminance() 関数で算出)が閾値を超えていて且つ少しずらしたピクセルの輝度よりも大きい場合に AppendStructuredBuffer.Append() で UV 座標を書き出しています。AppendStructuredBuffer はシェーダモデル 5.0 で使える LIFO のバッファで、Append() を呼ぶことで可変長のデータ(上限はバッファの作成時に指定)を格納することが出来ます。取り出すときは Consume Buffer を使うのですが、こちらは先のエントリで使用しました。

register キーワードはシェーダー変数の割当を行う役割を担っています。

ここでは u1 を指定していますが、これは 1 番目の Unordered Access View を意味しています。この 1 番目というインデクスはスクリプト側から与えます。該当の呼び出し箇所を見てみましょう。

cbPoints = new ComputeBuffer(10000, 8, ComputeBufferType.Append);

...

Graphics.SetRandomWriteTarget(1, cbPoints);
Graphics.Blit(src, dst, mat, 0);
Graphics.ClearRandomWriteTargets();

ComputeBufferType.Append フォーマットでコンピュートバッファを作成し、これを Graphics.SetRandomWriteTarget() で 1 番目のレジスタへセットしています。これが u1 に該当します。Graphics.Blit() でこの 1 番目の Pass を使って描画を行い、その際に上記シェーダのコードで Append Buffer に高輝度ピクセルを保存しています。終わったあとは Graphics.ClearRandomWriteTargets() を呼び出していますが、ここでデータが消えているわけではありません(資料が見つからなかったですが、単に unbind しただけかな...と捉えています)。

では、この格納したピクセルを使う場所を次に見てみましょう。先にスクリプト側のコードを示します。

cbDrawArgs = new ComputeBuffer(1, 16, ComputeBufferType.IndirectArguments);
var args = new int[4] { 0, 1, 0, 0 };
cbDrawArgs.SetData(args);

...

ComputeBuffer.CopyCount(cbPoints, cbDrawArgs, 0);
mat.SetBuffer("pointBuffer", cbPoints);
mat.SetPass(1);
Graphics.DrawProceduralIndirect (MeshTopology.Points, cbDrawArgs, 0);

ComputeBuffer.CopyCount() では追加した要素数を調べることが出来ます。この際、第 2 引数に渡している ComputeBufferType.IndirectArguments フォーマットのバッファは int 4 つ分のバッファです。GetData() をして一旦 CPU 側に値を持ってきてみると、何個の要素数が追加されたか確認できます。

var args = new int[4];
cbDrawArgs.GetData(args);
Debug.LogFormat("vert: {0}, {1}, {2}, {3}", args[0], args[1], args[2], args[3]);
// --> 939, 1, 0, 0

こうして得たバッファを Graphics.DrawProceduralIndirect() に渡すとその要素数分だけ頂点シェーダが頂点 ID を伴って呼び出されます。具体的にコードを見ていきましょう。

Pass 
{
    ...
    StructuredBuffer<float2> pointBuffer;
    float _Size;
    sampler2D _Sprite;
    fixed4 _Color;

    vs_out vert (uint id : SV_VertexID)
    {
        vs_out o;
        o.pos = float4(pointBuffer[id] * 2.0 - 1.0, 0, 1);
        return o;
    }

    [maxvertexcount(4)]
    void geom (point vs_out input[1], inout TriangleStream<gs_out> outStream)
    {
        float dx = _Size;
        float dy = _Size * _ScreenParams.x / _ScreenParams.y;
        gs_out output;
        output.pos = input[0].pos + float4(-dx, dy,0,0); output.uv=float2(0,0); outStream.Append(output);
        output.pos = input[0].pos + float4( dx, dy,0,0); output.uv=float2(1,0); outStream.Append(output);
        output.pos = input[0].pos + float4(-dx,-dy,0,0); output.uv=float2(0,1); outStream.Append(output);
        output.pos = input[0].pos + float4( dx,-dy,0,0); output.uv=float2(1,1); outStream.Append(output);
        outStream.RestartStrip();
    }

    fixed4 frag (gs_out i) : COLOR0
    {
        fixed4 col = tex2D (_Sprite, i.uv);
        return _Color * col;
    }

    ENDCG
}

頂点シェーダに SV_VertexID で頂点 ID が渡ってきているので、先ほどセットした pointBuffer のインデックスとして使い格納した UV 座標を取り出してその位置に頂点を持ってきておきます。次にジオメトリシェーダでこの頂点を指定したサイズの矩形ポリゴン(ビルボード)へと変換して、最後にフラグメントシェーダでその矩形ポリゴンにスプライトを描画する、という流れになっています。

Mesh Topology

f:id:hecomi:20160514173714g:plain

メッシュトポロジとテッセレーションのサンプルです。3 つのメッシュがあり、左から、半円の円弧状の点を MeshTopology.Points で格納したもの、放射状に線を MeshTopology.Lines で格納したもの、それをテッセレーションで変形させたものになります。テッセレーション自体のサンプルは _SimpleTesselation シーンでメッシュの変形を扱っておりそちらの方が詳しいですが、最小限の構成はこちらで学べます。

左と真ん中は簡単なのでサラッと見ていきましょう。動的にメッシュを生成するには Mesh クラスのインスタンスを作成し、そこに頂点やインデックス、法線といった情報を詰めていきます。今回は点群なので頂点と頂点インデックスだけ詰めていきます。左の半円点線のコードを見てみます。

using UnityEngine;

public class ProceduralPointMesh : MonoBehaviour
{
    void Start()
    {
        var mesh = new Mesh();

        var count = 100;
        var verts = new Vector3[count];
        var indices = new int[count];

        for (var i = 0; i < count; ++i) {
            var phi = i * Mathf.PI / count;
            verts[i] = new Vector3(Mathf.Cos(phi), Mathf.Sin(phi), 0.0f);
        }

        for (var i = 0; i < count; ++i) {
            indices[i] = i;
        }

        mesh.vertices = verts;
        mesh.subMeshCount = 1;
        mesh.SetIndices(indices, MeshTopology.Points, 0);

        GetComponent<MeshFilter>().mesh = mesh;
    }
}

これを次のような固定関数シェーダで描画します。

Shader "Line Shader"
{
Properties
{
    _Color("Color", Color) = (1, 0.5, 0.5, 1)
}
SubShader
{
    Pass
    {
        BindChannels { Bind "vertex", vertex }
        SetTexture[_MainTex] { constantColor[_Color] combine constant }
    }
}
}

真ん中のものも同様で、メッシュへの詰める座標とインデックスへセットする際のトポロジが異なるだけです。

using UnityEngine;

public class ProceduralMeshesWithNonTriangles : MonoBehaviour
{
    void Start()
    {
        var mesh = new Mesh();

        var lineCount = 16;
        var verts = new Vector3[lineCount * 2];
        var indices = new int[lineCount * 2];

        for (var i = 0; i < lineCount; ++i) {
            verts[i * 2 + 0] = Vector3.zero;
            verts[i * 2 + 1] = new Vector3(i * 0.1f, 2.0f, 0.0f);
        }

        for (var i = 0; i < lineCount * 2; ++i) {
            indices[i] = i;
        }

        mesh.vertices = verts;
        mesh.subMeshCount = 1;
        mesh.SetIndices(indices, MeshTopology.Lines, 0);

        GetComponent<MeshFilter>().mesh = mesh;
    }
}

右側は C# 側は真ん中のものと同じコードを利用していますが、シェーダが異なりテッセレーションを使用しています。

テッセレーションはポリゴンを滑らかにしたりディテールを追加したりするためにポリゴン分割および操作を行う機能で、ハルシェーダ、テッセレータ、ドメインシェーダの 3 つのステージからなります。テッセレータのみ固定機能で、ハルシェーダとドメインシェーダはプログラマブルです。テッセレーションではパッチとコントロールポイントという概念があるのですが、大抵の場合はそれぞれ頂点シェーダから与えられるポリゴンと頂点データと認識しておけば良いと思います。ハルシェーダではパッチごとに実行されるシェーダと、コントロールポイントごとに実行されるシェーダの 2 つを作成する必要があります。パッチ単位で実行されるシェーダにおいてパラメタ(SV_TessFactor や本例では出てきませんが SV_InsideTessFactor など)を設定することで、テッセレータでそのパラメタにしたがって自動的に分割を行ってくれます。そしてその分割された結果をドメインシェーダで事前に計算しておいたコントロールポイントのデータ、分割結果のローカル座標を使って動かして滑らかにしたりディテールを追加したりする形です。

本例では線分を分割して分割点を Sin カーブにしています。

Shader "Lines With Tessellation"
{
Properties
{
    _Color("Color", Color) = (1,0.5,0.5,1)
    _Subdivision("Subdiv", Range(1,64)) = 16.0
    _Waves("Waves", Range(3.0,40.0)) = 15.0
}

SubShader
{
Pass 
{

CGPROGRAM
#pragma vertex VS
#pragma fragment PS
#pragma hull HS
#pragma domain DS

#include "UnityCG.cginc"

float _Subdivision;
float _Waves;
float4 _Color;

struct appdata
{
    float4 vertex : POSITION;
};

struct vs2hs
{
    float3 pos : POS;
};

struct hsConst
{
    float tessFactor[2] : SV_TessFactor;
};

struct hs2ds
{
    float3 pos : POS;
};

struct ds2ps
{
    float4 pos : SV_Position;
};

vs2hs VS(appdata v)
{
    vs2hs o;
    o.pos = v.vertex.xyz;
    return o;
}

// パッチ単位で発行されるハルシェーダ
hsConst HSConst(InputPatch<vs2hs, 2> I)
{
    hsConst o;
    o.tessFactor[0] = 1.0f; // 横分割しない
    o.tessFactor[1] = _Subdivision; // 縦は与えられたパラメタで分割
    return o;
}

// コントロールポイント単位で発行されるハルシェーダ
[domain("isoline")]              // 分割するタイプ(線分)
[partitioning("fractional_odd")] // 分割方法(奇数個に分割)
[outputtopology("line")]         // トポロジ (線分)
[patchconstantfunc("HSConst")]   // パッチごとに呼び出される関数
[outputcontrolpoints(2)]         // コントロールポイント数
hs2ds HS(InputPatch<vs2hs, 2> I, uint id : SV_OutputControlPointID)
{
    // ここでは座標を詰めているだけ
    hs2ds o;
    o.pos = I[id].pos;
    return o;
}

// ドメインシェーダ
[domain("isoline")]
ds2ps DS(hsConst IC, const OutputPatch<hs2ds, 2> I, float2 uv : SV_DomainLocation)
{
    // 原点からの距離に応じて x 方向に Sin カーブを足している
    ds2ps o;
    float3 pos = lerp(I[0].pos, I[1].pos, uv.x);
    float len = length(I[0].pos - I[1].pos);
    pos.x += sin(uv.x*_Waves) * len * 0.1;
    o.pos = mul(UNITY_MATRIX_MVP, float4(pos,1.0));
    return o;
}

float4 PS(ds2ps I) : COLOR0
{
    return _Color;
}

ENDCG

}
}

Fallback "Line Shader"
}

テッセレーションのサンプルはもう一つあるので、詳しくはまたそちらで見てみます。

Random Write In Pixel Shader

f:id:hecomi:20160528185723g:plain

RenderTextureヒストグラムを描き込むサンプルです。最初の例ではコンピュートシェーダ内で出てきましたが、RWTexture2D はフラグメントシェーダでも使用可能です(要シェーダモデル5.0)。

Shader "DX11/UAV In Pixel Shader" 
{

Properties {
    _MainTex ("Texture", 2D) = "white" {}
}

SubShader 
{
Pass 
{
Cull Off ZWrite Off ZTest Always Fog { Mode Off }

CGPROGRAM

#pragma target 5.0
#pragma vertex vert_img
#pragma fragment frag

#include "UnityCG.cginc"

sampler2D _MainTex;
RWTexture2D<float4> _OutputTex;

float4 frag (v2f_img i) : COLOR
{
    float4 col = tex2D (_MainTex, i.uv);
    
    int2 loc;
    loc.x = int(col.r*255.0); loc.y = 0;
    loc.x = int(col.g*255.0); loc.y = 1;
    loc.x = int(col.b*255.0); loc.y = 2;

    _OutputTex[loc] = float4(1,0,0,1);
    _OutputTex[loc] = float4(0,1,0,1);
    _OutputTex[loc] = float4(0,0,1,1);
    
    return lerp (col, Luminance(col), 0.5);
}

ENDCG

}
}

Fallback Off
} 

全ての画面上のピクセルにおいて色を取得し、その色の RGB 値を見て X 座標に変換し、その座標を塗りつぶすとヒストグラムが完成、というわけです。スクリプトからは最初のサンプルと同じように Graphics.SetRandomWriteTarget() を使ってセットします。

...

// ヒストグラム用の RenderTexture を用意
histogram = new RenderTexture(256, 4, 0, RenderTextureFormat.ARGB32);
histogram.hideFlags = HideFlags.HideAndDontSave;
histogram.enableRandomWrite = true;
histogram.filterMode = FilterMode.Point;
histogram.Create();

// ヒストグラムを黒で塗りつぶす
Graphics.SetRenderTarget(histogram);
GL.Clear(false, true, new Color(0, 0, 0, 1));

// ヒストグラムのテクスチャを RWTexture2D としてセットして Image Effect を適用
Graphics.ClearRandomWriteTargets();
Graphics.SetRandomWriteTarget(1, histogram);
Graphics.Blit(src, dst, mat, 0);
Graphics.ClearRandomWriteTargets();

// ヒストグラムを画面に描画
GL.LoadPixelMatrix();
Graphics.DrawTexture(new Rect(0, -8, 512, 32), histogram);

...

ちなみに GIF 画像のバーがちょっと汚れて見えるのは圧縮の関係で、実際はパッキリしています。。

Simple Compute Buffer

f:id:hecomi:20160528210413g:plain

以下の 2 つのサンプルが含まれたものになります。

  • ComputeBuffer に各ジョイント(ボーン)の Transform 行列の配列を格納、ジョイントインデックスを頂点カラーにフェイクで仕込んでおき、頂点シェーダで取り出して利用することでアニメーションさせる
  • ComputeBuffer へ ColorVector4 の配列をコピーしたものを頂点カラーとして利用する

コンピュートバッファでボーン制御

スクリプト側では適当な三角錐メッシュを適当な数だけ作成し、このトランスフォーム行列の配列を ComputeBuffer で与え毎フレーム書き換えます。この三角錐たちは一つのメッシュに結合されているのですが、それぞれの三角錐の頂点カラーに ID を仕込んでおき、シェーダ側でこの ID を取り出して、その ID に応じたトランスフォーム行列を使って頂点をワールド座標変換します。

using UnityEngine;

public class ComputeBufferFakeSkinning : MonoBehaviour 
{
    ...
    private ComputeBuffer buffer;
    private Matrix4x4[] matrices;
    
    void Start() 
    {
        var mesh = new Mesh();
        
        ....
        for (var i = 0; i < instanceCount; ++i) {
            tris[i*12+ 0] = i*4+0;
            tris[i*12+ 1] = i*4+3;
            ...
            tris[i*12+11] = i*4+2;
            verts[i*4+0] = new Vector3(-.5f,0,-.5f);
            verts[i*4+1] = new Vector3( .5f,0,-.5f);
            verts[i*4+2] = new Vector3(   0,0, 1);
            verts[i*4+3] = new Vector3(   0,1, 0);
            uvs[i*4+0] = new Vector2(0,0);
            uvs[i*4+1] = new Vector2(1,0);
            uvs[i*4+2] = new Vector2(.5f,0);
            uvs[i*4+3] = new Vector2(.5f,1);
            // 頂点カラーの R に ID を 1/255 にして格納しておく
            var col = new Color(i/255.0f,0,0,0);
            colors[i*4+0] = col;
            colors[i*4+1] = col;
            colors[i*4+2] = col;
            colors[i*4+3] = col;
        }
        mesh.vertices = verts;
        mesh.uv = uvs;
        mesh.colors = colors;
        mesh.triangles = tris;
        mesh.RecalculateNormals();
        GetComponent<MeshFilter>().mesh = mesh;
        
        ReleaseBuffers ();
        
        buffer = new ComputeBuffer (instanceCount, 64);
        GetComponent<Renderer>().material.SetBuffer ("buf_BoneMatrices", buffer);
        
        matrices = new Matrix4x4[instanceCount];
    }

    ...
    
    void Update() 
    {
        // 適当に回転させたトランスフォーム行列の配列を作成し ComputeBuffer へ詰める
        var t = Time.timeSinceLevelLoad;
        for (var i = 0; i < instanceCount; ++i) {
            t += 13.0f;
            var pos = new Vector3(i, transform.position.y, 0);
            var rot = Quaternion.Euler(t * (i*4+13.0f), t * (i*2+7.0f), t * 2.0f);
            var scale = new Vector3(1,1,1);
            matrices[i].SetTRS(pos, rot, scale);
        }
        buffer.SetData(matrices);
    }    
}

ではシェーダ側を見てみましょう。

Shader "DX11/Fake Skinning with Compute Buffer" 
{

...

StructuredBuffer<float4x4> buf_BoneMatrices;

ps_input vert(vs_input v, uint id : SV_VertexID)
{
    ps_input o;
    
    // 頂点カラーに仕込んだ ID を取り出して指定のトランスフォーム行列を取り出す
    int boneIndex = (int)(v.color.r * 255.0f);
    float4x4 worldMat = buf_BoneMatrices[boneIndex];

    // 各三角錐を ID に応じて得たトランスフォーム行列で回転させる
    float4 worldPos = mul(worldMat, v.vertex);
    o.pos = mul(UNITY_MATRIX_VP, worldPos);
    
    // 法線方向だけを見てライティングする
    float3 worldN = mul((float3x3)worldMat, v.normal);  
    float3 viewN = mul((float3x3)UNITY_MATRIX_V, worldN);
    o.color.rgb = unity_LightColor[0].rgb * max(0, dot(viewN, unity_LightPosition[0].xyz)) + UNITY_LIGHTMODEL_AMBIENT.rgb;
    o.color.a = 1.0f;
    
    o.uv = v.texcoord;
    
    return o;
}

float4 frag(ps_input i) : COLOR
{
    return tex2D(_MainTex, i.uv) * i.color * 2.0f;
}

ENDCG

}

}

Fallback Off
}

スクリプト側で 255 で割って頂点カラーに与えていたので、シェーダ側で 255 を掛けて ID を取り出しています。ちなみに前回の記事でも同じように UV に ID を仕込んで擬似インスタンシングを行っていました。

コンピュートバッファで色を変更

次は色をスクリプトからコンピュートバッファを通じて設定する例です。ComputeBuffer へ与えるデータは global フラグが立っているときは Color[] 型、立っていない時は Vector4[]SetData() しているのですが、どちらも同じようにシェーダからは StructuredBuffer<float4> 型で受け取ることが出来ます。global フラグが立っているときはマテリアルにセットする代わりに、Shader.SetGlobalBuffer() を使ってどこからでも参照できるようにしています。

using UnityEngine;

public class UseComputeBuffer : MonoBehaviour 
{
    public bool global = false;
    private ComputeBuffer buffer;

    void Start() 
    {
        var mesh = GetComponent<MeshFilter>().mesh;

        ...

        int n = mesh.vertexCount;
        buffer = new ComputeBuffer(n, 16);

        // グローバルフラグが ON の時は適当な色のテクスチャを作成して
        // Shader.SetGlobalBuffer() でグローバル変数としてセット
        if (global) {
            var cols = new Color[n];
            for (int i = 0; i < n; ++i) {
                cols[i] = new Color(Mathf.Cos(i*0.1f), Mathf.Sin(i*0.37f), 1.0f, 1.0f);
            }
            buffer.SetData(cols);
            Shader.SetGlobalBuffer("bufColors", buffer);
        // オフの時はメッシュごとに mesh.tangents を色としてセット
        // ComputeBuffer.SetData() ではこのように色々な型をセットすることが可能
        } else {
            buffer.SetData(mesh.tangents);
            GetComponent<Renderer>().material.SetBuffer("bufColors", buffer);
        }
    }
    ...
}

シェーダは次のような形です。

Shader "DX11/Use Compute Buffer" 
{
...
CGPROGRAM
...

StructuredBuffer<float4> bufColors;

ps_input vert(vs_input v, uint id : SV_VertexID)
{
    ps_input o;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    o.color = bufColors[id] * 0.5 + 0.5;
    return o;
}

float4 frag(ps_input i) : COLOR
{
    return i.color;
}

ENDCG
...

}

頂点カラーを StructuredBuffer<float4> 経由で受け取りセットしている形です。

このように、Matrix4x4 でも Color でも Vector4 でも float そのままでも ComputeBuffer にセットしてシェーダとスクリプトの間でデータのやり取りをすることが出来ます。

Simple Tesselation

f:id:hecomi:20160605145102p:plain

再度テッセレーションのサンプルです。今度はラインではなくキューブのモデル(6 面四角ポリゴン)をテッセレーションを使って変形させるサンプルになります。テッセレーションそのものの解説を行うには知識がないのと分量が長くなってしまうので割愛しますが...、日本語ではもんしょさんのエントリが大変参考になりますのでそちらをご参照ください。

本サンプルには、Simple TessSimple Tess PNSimple Tess PN Disp という 3 つのシェーダが含まれています。順番に、単純に分割を行うもの、PN Triangle を使って分割を行うもの、PN Triangle を使って分割したものにディスプレースメントマップを適用したものになります。

Simple Tess

f:id:hecomi:20160605162441p:plain

まずは頂点シェーダを見てみます。ハルシェーダへと渡すので、出力が HS_Input 型になっており、これらの値がコントロールポイント用のハルシェーダに渡されます。頂点カラーについてはドメインシェーダで計算するため、割愛されています(テッセレーション無し時の比較用頂点シェーダもサンプルコードに含まれています)。またプロジェクション行列もこの段階では適用せずに、後のドメインシェーダで適用します。

struct VS_RenderSceneInput
{
    float3 vertex   : POSITION;
    float3 normal   : NORMAL;
    float2 texcoord : TEXCOORD0;
};

struct HS_Input
{
    float4 f4Position : POS;
    float3 f3Normal   : NORMAL;
    float2 f2TexCoord : TEXCOORD;
};

HS_Input VS_RenderSceneWithTessellation(VS_RenderSceneInput I)
{
    HS_Input O;
    
    O.f4Position = mul(UNITY_MATRIX_MV, float4(I.vertex,1.0f));
    O.f3Normal = mul ((float3x3)UNITY_MATRIX_IT_MV, I.normal);
    O.f2TexCoord = I.texcoord;
    
    return O;
}

このコントロールポイントは次のコントロールポイント単位で実行されるハルシェーダで受け取ります。

struct HS_ControlPointOutput
{
    float3 f3Position : POS;
    float3 f3Normal   : NORMAL;
    float2 f2TexCoord : TEXCOORD;
};

[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[patchconstantfunc("HS_PNTrianglesConstant")]
[outputcontrolpoints(3)]
HS_ControlPointOutput HS_PNTriangles(InputPatch<HS_Input, 3> I, uint uCPID : SV_OutputControlPointID)
{
    HS_ControlPointOutput O = (HS_ControlPointOutput)0;
    O.f3Position = I[uCPID].f4Position.xyz;
    O.f3Normal = I[uCPID].f3Normal;
    O.f2TexCoord = I[uCPID].f2TexCoord;
    return O;
}

アトリビュートでは時計回り方向の三角形ポリゴンを奇数個に分割する旨が書かれていて、頂点シェーダからやってきた値は基本的にはそのまま次段へと転送されます(Simple Tess PN でもこの部分の処理は同じ)。patchconstantfunc アトリビュートで指定されている、パッチ単位で実行されるハルシェーダを見てみましょう。

struct HS_ConstantOutput
{
    float fTessFactor[3]    : SV_TessFactor;
    float fInsideTessFactor : SV_InsideTessFactor;
};

HS_ConstantOutput HS_PNTrianglesConstant(InputPatch<HS_Input, 3> I)
{
    HS_ConstantOutput O = (HS_ConstantOutput)0;
    O.fTessFactor[0] = O.fTessFactor[1] = O.fTessFactor[2] = 2.0f;
    O.fInsideTessFactor = 2.0f;
    return O;
}

値を変更すると次段のテッセレータでの分割が変わるため、ポリゴンが大きく変化します。次のように分解して色々値をためしてみると面白いです。

Properties 
{
    ...
    _TessFactor ("Tess Factor", Vector) = (2.0, 2.0, 2.0, 2.0)
}

...

float4 _TessFactor;

HS_ConstantOutput HS_PNTrianglesConstant(InputPatch<HS_Input, 3> I)
{
    HS_ConstantOutput O = (HS_ConstantOutput)0;
    O.fTessFactor[0] = _TessFactor.x;
    O.fTessFactor[1] = _TessFactor.y;
    O.fTessFactor[2] = _TessFactor.z;
    O.fInsideTessFactor = _TessFactor.w;
    return O;
}

f:id:hecomi:20160605172835g:plain

さて、テッセレータを経て分割された結果を扱うドメインシェーダを見てみます。

struct DS_Output
{
    float4 f4Position : SV_Position;
    float2 f2TexCoord : TEXCOORD0;
    float4 f4Diffuse  : COLOR0;
};

[domain("tri")]
DS_Output DS_PNTriangles(HS_ConstantOutput HSConstantData, const OutputPatch<HS_ControlPointOutput, 3> I, float3 bary : SV_DomainLocation)
{
    DS_Output O = (DS_Output)0;

    float3 f3Position = 
        bary.x * I[0].f3Position + 
        bary.y * I[1].f3Position +
        bary.z * I[2].f3Position;

    float3 f3Normal = normalize(
        bary.x * I[0].f3Normal +
        bary.y * I[1].f3Normal + 
        bary.z * I[2].f3Normal);

    O.f2TexCoord = 
        bary.x * I[0].f2TexCoord + 
        bary.y * I[1].f2TexCoord + 
        bary.z * I[2].f2TexCoord;

    // ライティングの計算
    float intensity = max(0, dot(f3Normal, unity_LightPosition[0].xyz));
    O.f4Diffuse.rgb = unity_LightColor[0].rgb * intensity + UNITY_LIGHTMODEL_AMBIENT.rgb;
    O.f4Diffuse.a = 1.0f; 

    // プロジェクション行列の適用
    O.f4Position = mul(UNITY_MATRIX_P, float4(f3Position.xyz,1.0));

    return O;
}

SV_DomainLocation セマンティクスのついた変数 bary にパッチ内の分割後の重心位置が格納されているため、これを使ってテッセレータを経由して分割された頂点の座標、法線方向、UV をパッチを構成するコントロールポイントから求めます。本例では単に平均をとっているだけなので、ポリゴンの分割はされているものの、形状は変化していません。

Simple Tess PN

PN Triangles の PN は Point-Normal の略で、コントロールポイントの座標・法線を用いてポリゴンのスムージングを行う手法です。

GDC2011 での資料も参考になります。

良い感じになめらかな形状にプロシージャルに変換してくれますが、法線が不連続なポイント(ハードエッジ)があると、そこでポリゴンの断裂が起きてしまうという欠点もあります(修正には要アーティスト作業)。これを改善した手法も PN-AEN として GDC 2011 のスライドで紹介されていますが、本例では単純な PN を扱っています。

ハルシェーダを見てみます。計算はパッチ単位で行われ、座標は 3 次のベジェ曲線、法線は 2 次のベジェ曲線で計算されます。ここでは必要な各係数を計算しています。一見複雑に見えますが、論文の数式と照らしあわせて見れば同じことをやっているのがわかると思います。

struct HS_ConstantOutput
{
    float fTessFactor[3]    : SV_TessFactor;
    float fInsideTessFactor : SV_InsideTessFactor;
    
    float3 f3B210 : POS3;
    float3 f3B120 : POS4;
    float3 f3B021 : POS5;
    float3 f3B012 : POS6;
    float3 f3B102 : POS7;
    float3 f3B201 : POS8;
    float3 f3B111 : CENTER;

    float3 f3N110 : NORMAL3;
    float3 f3N011 : NORMAL4;
    float3 f3N101 : NORMAL5;
};

HS_ConstantOutput HS_PNTrianglesConstant( InputPatch<HS_Input, 3> I )
{
    HS_ConstantOutput O = (HS_ConstantOutput)0;
    
    O.fTessFactor[0] = O.fTessFactor[1] = O.fTessFactor[2] = _TessEdge;
    O.fInsideTessFactor = _TessEdge;

    float3 f3B003 = I[0].f4Position.xyz;
    float3 f3B030 = I[1].f4Position.xyz;
    float3 f3B300 = I[2].f4Position.xyz;

    float3 f3N002 = I[0].f3Normal;
    float3 f3N020 = I[1].f3Normal;
    float3 f3N200 = I[2].f3Normal;
        
    O.f3B210 = ( ( 2.0f * f3B003 ) + f3B030 - ( dot( ( f3B030 - f3B003 ), f3N002 ) * f3N002 ) ) / 3.0f;
    O.f3B120 = ( ( 2.0f * f3B030 ) + f3B003 - ( dot( ( f3B003 - f3B030 ), f3N020 ) * f3N020 ) ) / 3.0f;
    O.f3B021 = ( ( 2.0f * f3B030 ) + f3B300 - ( dot( ( f3B300 - f3B030 ), f3N020 ) * f3N020 ) ) / 3.0f;
    O.f3B012 = ( ( 2.0f * f3B300 ) + f3B030 - ( dot( ( f3B030 - f3B300 ), f3N200 ) * f3N200 ) ) / 3.0f;
    O.f3B102 = ( ( 2.0f * f3B300 ) + f3B003 - ( dot( ( f3B003 - f3B300 ), f3N200 ) * f3N200 ) ) / 3.0f;
    O.f3B201 = ( ( 2.0f * f3B003 ) + f3B300 - ( dot( ( f3B300 - f3B003 ), f3N002 ) * f3N002 ) ) / 3.0f;

    float3 f3E = ( O.f3B210 + O.f3B120 + O.f3B021 + O.f3B012 + O.f3B102 + O.f3B201 ) / 6.0f;
    float3 f3V = ( f3B003 + f3B030 + f3B300 ) / 3.0f;
    O.f3B111 = f3E + ( ( f3E - f3V ) / 2.0f );
    
    float fV12 = 2.0f * dot( f3B030 - f3B003, f3N002 + f3N020 ) / dot( f3B030 - f3B003, f3B030 - f3B003 );
    float fV23 = 2.0f * dot( f3B300 - f3B030, f3N020 + f3N200 ) / dot( f3B300 - f3B030, f3B300 - f3B030 );
    float fV31 = 2.0f * dot( f3B003 - f3B300, f3N200 + f3N002 ) / dot( f3B003 - f3B300, f3B003 - f3B300 );
    O.f3N110 = normalize( f3N002 + f3N020 - fV12 * ( f3B030 - f3B003 ) );
    O.f3N011 = normalize( f3N020 + f3N200 - fV23 * ( f3B300 - f3B030 ) );
    O.f3N101 = normalize( f3N200 + f3N002 - fV31 * ( f3B003 - f3B300 ) );
           
    return O;
}

そしてこれらの係数がドメインシェーダへと渡されて、先ほど求めた係数を用いてベジェ曲線による補間を与えられた重心座標から求めます。

[domain("tri")]
DS_Output DS_PNTriangles( HS_ConstantOutput HSConstantData, const OutputPatch<HS_ControlPointOutput, 3> I, float3 f3BarycentricCoords : SV_DomainLocation )
{
    DS_Output O = (DS_Output)0;

    // 重心座標
    float fU = f3BarycentricCoords.x;
    float fV = f3BarycentricCoords.y;
    float fW = f3BarycentricCoords.z;

    // 使いやすい形に先にしておく
    float fUU = fU * fU;
    float fVV = fV * fV;
    float fWW = fW * fW;
    float fUU3 = fUU * 3.0f;
    float fVV3 = fVV * 3.0f;
    float fWW3 = fWW * 3.0f;
    
    // 位置を計算
    float3 f3Position = 
        I[0].f3Position * fWW * fW +
        I[1].f3Position * fUU * fU +
        I[2].f3Position * fVV * fV +
        HSConstantData.f3B210 * fWW3 * fU +
        HSConstantData.f3B120 * fW * fUU3 +
        HSConstantData.f3B201 * fWW3 * fV +
        HSConstantData.f3B021 * fUU3 * fV +
        HSConstantData.f3B102 * fW * fVV3 +
        HSConstantData.f3B012 * fU * fVV3 +
        HSConstantData.f3B111 * 6.0f * fW * fU * fV;
    
    // 法線の計算
    float3 f3Normal = normalize(
        I[0].f3Normal * fWW +
        I[1].f3Normal * fUU +
        I[2].f3Normal * fVV +
        HSConstantData.f3N110 * fW * fU +
        HSConstantData.f3N011 * fU * fV +
        HSConstantData.f3N101 * fW * fV);

    // テクスチャ座標はリニアに補間
    O.f2TexCoord = 
        I[0].f2TexCoord * fW + 
        I[1].f2TexCoord * fU + 
        I[2].f2TexCoord * fV;

    // 頂点カラー
    float intensity = max(0, dot(f3Normal, unity_LightPosition[0].xyz));
    O.f4Diffuse.rgb = unity_LightColor[0].rgb * intensity + UNITY_LIGHTMODEL_AMBIENT.rgb;
    O.f4Diffuse.a = 1.0f;

    // プロジェクション行列を適用
    O.f4Position = mul(UNITY_MATRIX_P, float4(f3Position.xyz,1.0));
        
    return O;
}

_TessFactor をエディタから動的に変化させると次のようになります。

f:id:hecomi:20160605200641g:plain

ディスプレースメントマップ

f:id:hecomi:20160605222636g:plain

ディスプレースメントマップはここから追加するのは簡単で、ドメインシェーダでテクスチャを読み込み座標を動かすだけです。

Properties 
{
    ...
    _DispTex ("Disp Texture", 2D) = "gray" {}
    _Displacement ("Displacement", Range(0, 0.3)) = 0.1
}

[domain("tri")]
DS_Output DS_PNTriangles( HS_ConstantOutput HSConstantData, const OutputPatch<HS_ControlPointOutput, 3> I, float3 f3BarycentricCoords : SV_DomainLocation )
{
    DS_Output O = (DS_Output)0;

    ...
    float disp = _DispTex.SampleLevel(
        sampler_DispTex, O.f2TexCoord, 0).r * _Displacement;
    f3Position += f3Normal * disp;
    ...
        
    return O;
}

与えているテクスチャは次のようなタイル模様です。

f:id:hecomi:20160605233219p:plain

これに従って PN で滑らかなるようした面をディスプレースメントテクスチャを使って盛り上げている形になります。凹凸を完全にテクスチャで制御するだけなら Simple Tess のテクスチャにこの処理を追加するだけでも出来ます。

f:id:hecomi:20160605233650p:plain

ここでは均等に全体を分割しましたが、カメラの向きと法線が垂直に近い場所だけ細かく分割し、シルエットを綺麗に見せるといったことや、テレインのディテールを距離に応じて付加したりといったことも可能です。

Volume Textures

f:id:hecomi:20160605235835g:plain

通常のテクスチャは 2 次元ですが、ボリュームテクスチャは 3 次元のデータ構造になっています。tex2D() の代わりに tex3D() で UV が 3 次元になっている、というとわかりやすいかもしれません。ボリュームレンダリングなどの用途に使えます。

本サンプルでは、C# 及びコンピュートシェーダからボリュームテクスチャを生成する方法が紹介されています。シェーダは共通で以下のようになっています。

Shader "DX11/Sample 3D Texture" 
{

Properties 
{
    _Volume ("Texture", 3D) = "" {}
}

SubShader 
{
Pass 
{

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma exclude_renderers flash gles

#include "UnityCG.cginc"

struct vs_input 
{
    float4 vertex : POSITION;
};

struct ps_input 
{
    float4 pos : SV_POSITION;
    float3 uv  : TEXCOORD0;
};


ps_input vert (vs_input v)
{
    ps_input o;
    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    o.uv = v.vertex.xyz*0.5+0.5;
    return o;
}

sampler3D _Volume;

float4 frag (ps_input i) : COLOR
{
    return tex3D (_Volume, i.uv);
}

ENDCG

}
}

Fallback "VertexLit"
}

注目すべきところは先程触れたように、ps_input.uvfloat3 になっており、これをフラグメントシェーダで tex3D() を使って取り出しているところです。

C# から生成

Texture3D を作成し、サイズ分確保した Color[] 変数の cols を for 文で適当に計算してセットし、Texture3D.SetPixels(Color[]) でセットしてシェーダ側に渡すだけです。

using UnityEngine;

public class Create3DTex : MonoBehaviour 
{
    public Texture3D tex;
    public int size = 16;

    void Start()
    {
        tex = new Texture3D(size, size, size, TextureFormat.ARGB32, true);
        var cols = new Color[size*size*size];
        float mul = 1.0f / (size-1);
        int idx = 0;
        Color c = Color.white;
        for (int z = 0; z < size; ++z) {
            for (int y = 0; y < size; ++y) {
                for (int x = 0; x < size; ++x, ++idx) {
                    c.r = ((x&1)!=0) ? x*mul : 1-x*mul;
                    c.g = ((y&1)!=0) ? y*mul : 1-y*mul;
                    c.b = ((z&1)!=0) ? z*mul : 1-z*mul;
                    cols[idx] = c;
                }
            }
        }
        tex.SetPixels(cols);
        tex.Apply();
        GetComponent<Renderer>().material.SetTexture("_Volume", tex);
    }    
}

コンピュートシェーダで生成

次のようなコンピュートシェーダを回すスクリプトを書きます。テクスチャは RenderTexture で作成するのですが、RenderTexture.depth は深度バッファのことなので、コンストラクタの第 3 引数はセットせずに、直接 RenderTexture.volumeDepth でセットします。また isVolume プロパティは obsolete になったので、UnityEngine.Rendering.TextureDimention 型の dimention プロパティを代わりに使ってボリュームテクスチャとして設定します。

using UnityEngine;

public class RenderInto3DCS : MonoBehaviour
{
    public ComputeShader cs;
    public int size = 16;    
    private RenderTexture volume;
        
    ...

    private void CreateResources()
    {
        if (!volume) {
            volume = new RenderTexture(size, size, 0, RenderTextureFormat.ARGB32);
            volume.volumeDepth = size;
            // volume.isVolume = true; <-- obsolete
            volume.dimension = UnityEngine.Rendering.TextureDimension.Tex3D;
            volume.enableRandomWrite = true;
            volume.Create();
            GetComponent<Renderer>().material.SetTexture ("_Volume", volume);
        }
    }
    
    void Update()
    {
        if (!SystemInfo.supportsComputeShaders) return;

        CreateResources();
        cs.SetVector("g_Params", new Vector4(Time.timeSinceLevelLoad, size, 1.0f/size, 1.0f));
        cs.SetTexture(0, "Result", volume);
        cs.Dispatch(0, size, size, size);
    }
}

コンピュートシェーダは次のようになります。

#pragma kernel CSMain

RWTexture3D<float4> Result;
float4 g_Params;

[numthreads(8,8,8)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    float4 c;
    c.r = (id.x & id.y & id.z) ? 1.0 : 0.0;
    c.g = sin((id.y * g_Params.z - g_Params.x) * 3.0) * 0.5 + 0.5;
    c.b = id.z * g_Params.z;
    c.a = 1.0;
    Result[id] = c;
}

R 成分で位置のビット演算をしているので変な模様ができています。BG 成分で適当に時間でアニメーションさせているのでモワンモワンとなっています。

おわりに

だいぶ長くなってしまいましたが、一通りサンプルを見てみました。触ったことがなかったので良い勉強になりました。