LoginSignup
23
16

More than 5 years have passed since last update.

[Unity] UnityでComputeShaderを使う解説をしているページを訳してみた その3

Last updated at Posted at 2015-04-30

前回の続きです。
だんだん内容が難しくなり、翻訳も怪しくなってきています。
元の記事はこちら

一応、このパートでは、

  • テクスチャへのバッファの書き出しについて
  • サンプリングについて

あたりの紹介をしています。


DirectCompute tutorial for Unity: Textures

このチュートリアルで焦点を当てるのはテクスチャだ。DirectComputeを使う際に、おそらく最も重要な特徴だと思う。ほとんど全てのシェーダが少なくとも一つはテクスチャを使っているのである。
Unityでのテクスチャのレンダリングは不幸にもProしか使えない。なのでこのチュートリアルはPro向けのものになるだろう(実はUnity5のPersonalEditionから使えるからそんなことはないんですが。)
DirectComputeでのテクスチャはとても単純に使えるのだけども、いくつかのハマりやすいトラップがある。とにかくシンプルに始めよう。コンピュートシェーダファイルを作って以下のファイルをコピペしよう。

#pragma kernel CSMain

RWTexture2D<float4> tex;

float w, h;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    tex[id] = float4(id.x / w, id.y / h,  0.0, 1.0);
}

そして、C#スクリプトであるTextureExampleには以下のコードを貼ろう。

TextureExample.cs
using UnityEngine;
using System.Collections;

public class TextureExample : MonoBehaviour
{
    public ComputeShader shader;

    RenderTexture tex;

    void Start ()
    {

        tex = new RenderTexture(64, 64, 0);
        tex.enableRandomWrite = true;
        tex.Create();

        shader.SetFloat("w", tex.width);
        shader.SetFloat("h", tex.height);
        shader.SetTexture(0, "tex", tex);
        shader.Dispatch(0, tex.width/8, tex.height/8, 1);
    }

    void OnGUI()
    {
        int w = Screen.width/2;
        int h = Screen.height/2;
        int s = 512;

        GUI.DrawTexture(new Rect(w-s/2,h-s/2,s,s), tex);
    }

    void OnDestroy()
    {
        tex.Release();
    }
}

スクリプトをアタッチして、シェーダをバインドしてシーンを実行しよう。UV座標で色付けられたテクスチャが見えるはずだ。これが、コンピュートシェーダを使ってテクスチャに書きだした内容だ。

カーネル引数の”uint2 id : SV_DispatchThreadID”は一つ前のチュートリアルで知っているよね。これはスレッドグループ内でのスレッド位置を表して、バッファと同じように、テクスチャに結果を書き出す際の位置を知るために必要となる。
そして、"tex[id]=resultのように使用する。しかし、このときバッファのように"flattened"なインデックスである必要はない。バッファテクスチャと違って、多次元になりうる。2Dのスレッドグループと2Dテクスチャを持つのだ。

テクスチャの宣言である"RWTexture2Dtex;"を見てほしい。”RWTexture2D”の部分が特に重要だ。明らかにテクスチャであると分かるが、RWはなんだろうか?この宣言は、テクスチャが"unordered access view"のタイプであることを宣言している。この意味は、テクスチャ内のどの座標でもシェーダから書き込めるということである。ただし、書き込むことができる代わりに読み込むことはできない。RW部分を取り除くとただのテクスチャとなるが、書き込むことはできなくなる。覚えておくべきことは、Texture2Dは読み込めるだけ、RWTexture2Dは書き出せるだけという点である。

それではスクリプトを見てみよう。どうやってテクスチャが作られるか分かるはずだ。

TextureExample.cs
tex = new RenderTexture(64, 64, 0);
tex.enableRandomWrite = true;
tex.Create();

ここでは重要なことが2つある。一つ目は、”enableRandomWrite”だ。この値をtrueにしなければテクスチャに書き込むことができない。これは基本的にテクスチャはunordered access viewになれるということを示している。
この設定をせずにいるとシェーダを動かした際に何も起こらず、Unityはエラーを返してくるはずだ。そして、それはただ失敗したとしか言わず明確な理由を返してこない。
2つ目は"Create"関数についてだ。テクスチャ上でcreateをコールするのは書き込む前にやる必要がある。これもやらなければエラーが返ってくる。テクスチャの書き込みをグラフィックスブリットを使ってやるなら、createを呼ばなくてもよいことに気づくはずだ。
これは、グラフィックスブリットがテクスチャは生成されているかどうかを確認し、なければ生成するからである。dispatch関数はそれができない、なぜなら、呼び出しを受けた時に何のテクスチャが書かれたかが分からないからだ。

また、テクスチャはOnDestroy()関数で開放される。レンダーテクスチャの処理が終わった時にこれを解放させる必要がある。

それではDispatch関数を見てみよう

TextureExample.cs
shader.Dispatch(0, tex.width/8, tex.height/8, 1);

前回のチュートリアルを思い出してほしい。いくつのグループを実行するかを設定していたね。ではなぜテクスチャサイズを8で割った数のグループを走らせるのだろうか?
シェーダを見てほしい。8個のスレッドがグループごとに走っていることが、”[numthreads(8,8,1)]”の行から分かるはずだ。そのため、テクスチャの各ピクセルのためにスレッドを必要としている。
つまり、64ピクセル幅のテクスチャを持ち、グループごとのスレッド数でピクセルを割るならば、必要になるグループの数を取得できる。
X方向において、8スレッドを持つ8グループからなり、全体で64スレッドからなる構成となり、Y方向についても同様である。その結果、全体で4096スレッド(64*64)が実行され、それがテクスチャ中のピクセル数となっている。
この図を見ると個人的にはわかりやすかった。(https://msdn.microsoft.com/ja-jp/library/ee422447(v=vs.85).aspx)

続いて、スクリプト中の以下のパートを見てみよう。

TextureExample.cs
shader.SetFloat("w", tex.width);
shader.SetFloat("h", tex.height);
shader.SetTexture(0, "tex", tex);

シェーダ用のuniformsを設定している。書き込み対象のテクスチャを設定しなければならないが、テクスチャの幅と高さも必要とする。この値によってdispatchIdからUVを計算できるようになる。
変数をシェーダに渡す一般的な方法はこのようなやり方だが、この場合では、幅と高さを必要とはしない。シェーダ内で取得できるからだ。シェーダを以下のように変更してみよう。

#pragma kernel CSMain

RWTexture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float w, h;
    tex.GetDimensions(w, h);

    float2 uv = float2(id.x/w, id.y/h);

    tex[id] = float4(uv, 0.0, 1.0);
}

そして、以下の2つのラインを削除しよう。

TextureExample.cs
shader.SetFloat("w", tex.width);
shader.SetFloat("h", tex.height);

シーンを実行してみると、同じ結果が得られるはずだ。"tex.Dimensions(w,h);"の行に気づいてる?テクスチャもオブジェクトなんだ。これは、呼び出せる関数を盛っていることを意味している。この場合だと、テクスチャの次元数を教えてくれるように頼んでいる。
テクスチャは呼び出し可能なオーバーロードされた関数をいくつか持っている。最も一般的なものとその使い方については調査しようと思うけども、シーンをその前に少し変える必要がある。
今やりたいことはテクスチャの内容を他のテクスチャにコピーして、その結果を表示させることだ。

最初に新しいコンピュートシェーダを作って、以下のコードをコピーしてね。

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float4 t = tex[id];

    texCopy[id] = t;
}

そしてスクリプトを次のように変更しよう。

TextureExample.cs
using UnityEngine;
using System.Collections;

public class TextureExample : MonoBehaviour
{
    public ComputeShader shader, shaderCopy;

    RenderTexture tex, texCopy;

    void Start ()
    {

        tex = new RenderTexture(64, 64, 0);
        tex.enableRandomWrite = true;
        tex.Create();

        texCopy = new RenderTexture(64, 64, 0);
        texCopy.enableRandomWrite = true;
        texCopy.Create();

        shader.SetTexture(0, "tex", tex);
        shader.Dispatch(0, tex.width/8, tex.height/8, 1);

        shaderCopy.SetTexture(0, "tex", tex);
        shaderCopy.SetTexture(0, "texCopy", texCopy);
        shaderCopy.Dispatch(0, texCopy.width/8, texCopy.height/8, 1);
    }

    void OnGUI()
    {
        int w = Screen.width/2;
        int h = Screen.height/2;
        int s = 512;

        GUI.DrawTexture(new Rect(w-s/2,h-s/2,s,s), texCopy);
    }

    void OnDestroy()
    {
        tex.Release();
        texCopy.Release();
    }
}

"shaderCopy"を役割として作成された新しいシェーダを割り当てて、シーンを実行しよう。シーンは以前と何も変わらないように見える。ここでやることは、1つ目のテクスチャのUVを以前のように色として埋めることで、他のテクスチャにその内容をコピーすることだ。
シェーダ内でテクスチャからサンプリングするいろいろな方法を試すことができる。”float4 t = tex[id];”という行がコピーシェーダにあることに気づいているかな?これが最も単純なテクスチャからのサンプリング方法だ。dispatchIdは書き込みたい場所であるが、読み出したい場所にとして使うこともできる。
つまり、配列のように扱ってテクスチャからサンプリングするやり方ができる。他のやり方もある。例えば・・・

float4 t = tex.mips[0][id];

ここでは代わりにテクスチャのミップマップにアクセスしている。レベル0は最初のミップマップで、テクスチャと同じサイズである。ミップマップ配列の次の次元は、サンプル場所を指していて、dipatchIdが使われている。テクスチャ上でミップマップを有効にしていなければ、サンプリングレベルが0以外の場合での結果は何も得られない。
同じことがテクスチャの読み込み関数においても使ってできる。

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

SamplerState _LinearClamp;
SamplerState _LinearRepeat;
SamplerState _PointClamp;
SamplerState _PointRepeat;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float w, h;
    texCopy.GetDimensions(w, h);
    float2 uv = float2(id.x/w, id.y/h);

    float4 t = tex.SampleLevel(_LinearClamp, uv, 0);
    texCopy[id] = t;
}

このケースでは、uint3のXとYの値がサンプリングの位置であり、Zの値(0)がミップマップのレベルとなる。どちらも同じように動作する。

多用するであろうテクスチャの特徴がある。テクスチャの機能はフィルターとワープだ。これらを使うためにはサンプラステートが必要となる。HLSLをUnity以外で使っていたのであれば、使用するサンプラオブジェクトを生成する必要が有ることを知っているだろう。これはUnityでは少し違う。
基本的に2つの選択肢がフィルタリングにはある(LinearとPoint)、ワープにも2種類(Clamp、Repeat)ある。やるためには、サンプラステートをシェーダで宣言して、その際に、LinearかPointという名前とRepeatかClampという名前から構成する
例えば、myLinearClampやaPointRepeatなどの名前が使える。私個人としてはアンダースコアを使うのが好きだ。ということで、シェーダを以下のように変える。

float4 t = tex.Sample(_LinearClamp, uv);

シーンを実行すると同じようにみえるはずだ。”float4 t = tex.SampleLevel(_LinearClamp, uv, 0);”という行に気づいてほしい。ここで、テクスチャのSampleLevel関数を使っている。この関数はサンプラステート、UV、ミップマップレベルを使っている。UVは正規化に必要となり、0から1の範囲にする。
サンプラステート変数も見てほしい。もし一般的に使われているバイリニアフィルタリングをサンプラステートとして使いたいのならば、_LinearClampか_LinearRepeatを使えばよい。

フラグメントシェーダ(コンピュートシェーダではなく)でHLSLを使っていたならば、次の関数がフィルタリングに使えることを知っているだろう。

float4 t = tex.Sample(_LinearClamp, uv);

SampleLevelではなくSampleと呼ばれ、ミップマップパラメータは存在しないことが分かる。コンピュートシェーダでこれを使おうとした場合、この関数が存在しないためエラーになる。理由は非常に複雑で、GPUがどうやって動くかを理解していないと難しい。
シーンの裏で、フラグメントシェーダ(もしくは他のシェーダ)は沢山のサンプルで動いていて、それはコンピュートシェーダと同様に同じGPU構造を共有しているからである。
それらをスレッド内で実行し、スレッドはスレッドグループに並べられる。グループ内のスレッドはメモリを共有することを思い出してほしい。フラグメントシェーダはいつも、最低でも2x2のスレッドのグループ内で実行されている。テクスチャからサンプルするときは、フラグメントシェーダは近傍のUVについての検証を行う。このため、UVの導関数?が算出される。導関数は変化の割合で、高い割合で変化するテクスチャを持つ場所ではより高いミップマップが使われ、より変化の少ない場所では低いミップマップが使われる。これはGPUがエイリアシングの問題を減らすための方法で、メモリのバンド幅を節約する(高いレベルのミップマップを小さくする)という嬉しい副産物も持ち合わせている。

では、コンピュートシェーダではどうやっているのか?コンピュートシェーダは普通のGPUパイプラインの上では動作しない。より自由度があり、そのために自分自身でやるべきことが沢山ある。
サンプリング関数はサポートされないのは、GPUは自動的に導関数を計算しないからである。しかしこれはできないことを意味していない。SampleGrad関数を使えばできる。

float4 t = tex.SampleGrad(_LinearClamp, uv, dx, dy);

ここで掴みたいことは、導関数(dxとdy)を計算しなければならないということである。できない理由はない。スレッドはいつもスレッドグループ内で動いていてメモリを共有できるのだ。(メモリ上で共有する話はそのうち語るつもり。)
もし混乱してしまっても心配ない。ほとんど使うことがなく、バイリニアフィルタリングがほとんどの場合で有効であるからである。

ここで示したサンプル全てが2Dだが、原理は3Dも同じように当てはまる。テクスチャを少し違う形で生成しなければならない。

TextureExample.cs
tex = new RenderTexture(64, 64, 0);
tex.volumeDepth = 64;
tex.isVolume = true;
tex.enableRandomWrite = true;
tex.Create();

3Dテクスチャを生成し、それは64x64x64のピクセルで構成される。"volumeDepth"が64になっており、"isVolume"がtrueになっていることが分かる。
スレッドとグループの数を正しい値で設定することを忘れずに行い、dispatchIdはuint3かint3でなければならない。

TextureExample.cs
shader.Dispatch(0, tex.width/8, tex.height/8, tex.depth/8);

そして、

[numthreads(8,8,8)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    tex[id] = anotherTex[id];
}

これにてテクスチャの話は終わり。次はバッファについて話をする。デフォルトバッファの使い方、他のバッファタイプの話、どうやってバッファからデータを書くか、バッファの設定/取得方法などについて話をする。


訳してみて

やっぱり長い。訳も変になってきてる。

23
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
16