忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。


C#でGPGPUチュートリアル その3 演算シェーダー

ベクトルを2倍にする

 前回と前々回は、
GPUにあるデータをCPUに
持ってくるというのがテーマでした。

GPGPUのチュートリアルでありながら、
やっているのはデータの移動だけで
GPUに計算をさせていません。

今回こそ計算させてみましょう。

今回行う計算は、あるバッファの中身を2倍にするというものにします。

doubleTheContentsOfGpuBuffer.jpg

各要素にそれぞれ、同じ計算(2倍する)をするので、
並列処理っぽい、CPUよりもGPUが得意とする計算なはずです。


演算シェーダー

GPGPUな計算に使うシェーダーを
演算シェーダーといいます。
演算シェーダーはDirectX11で初めて導入された機能です。

SlimDXで演算シェーダーを使うには、
DeviceContextクラスのComputeShaderプロパティを通すことになります。
public ComputeShaderWrapper ComputeShader { get; }
SlimDX.Direct3D11.ComputeShaderWrapperクラスは
演算シェーダーの機能をまとめたクラスです。
演算シェーダーの実行に必要な設定用メソッドが用意されています。
(ただし、演算シェーダーの実行そのものにはこのクラスは関与しません。
実行にはDeviceContextクラスのメソッドを使います。後述)


計算結果を格納するバッファ

今回演算シェーダーの計算結果を格納するバッファを指定する方法は、
ComputeShaderWrapper.SetUnorderedAccessView()メソッドを使うというものです。
public void SetUnorderedAccessView(UnorderedAccessView unorderedAccessView, int slot);
unorderedAccessViewは演算シェーダーにセットするUnorderedAccessViewです。
slotはデバイスに存在するUnorderedAccessView配列のインデックスです。つまり配列のどこにセットするのかということですね。

バッファを指定するメソッドと言っておきながらこれの引数は
(バッファではなく)「なんとかかんとかView」です。
どういう事かというと、この「なんとかかんとかView」は
バッファをコンストラクタ引数に取るクラスで、
バッファの使われ方のようなものを表すのです。
バッファはデータの塊、Viewは機能の塊といったところでしょうか。

しかしなぜViewとバッファを別物として考えるのでしょうか?
DirectX9世代では少なくともViewというようなものはなかったはずです。
バッファに相当するものだけで何とかちゃんとやりくりできていたのです。
その疑問の答えは、
「現在のDirectXではリソースは一般的な形で存在し、色々な使い方ができるため、
その都度Viewで使い方を指定してやる必要がある」、
ということだと思います。(鵜呑みにしないでください)

たとえば、float2をテクスチャ座標としてSample()で色を獲得する一般的なテクスチャ
(DirectX9でピクセルシェーダー内で使っていたようなテクスチャ、Texture2Dです)
より厳密にuint2をインデックスとして色を獲得できるテクスチャ
(演算シェーダーの登場によってこういうテクスチャが現れました。RWTexture2Dです)
を考えてみましょう。
ある一つのテクスチャをある局面ではfloat2のテクスチャ、
別の局面ではuint2のテクスチャとして使いたくなった場合、
バッファとViewが同じものだった場合インターフェースがやや面倒なことになるかもしれません。
クラスの凝集度が下がりそうです。

一方、BufferとViewを別のものにしていた場合、
1つのバッファに複数のViewを割り当てる」ことで上記のような場合も柔軟に対応できるでしょう。
そういったわけで、データそのものを意味するBufferと、
そのデータに対する操作を表すViewに分離しているようです。
(概念的にはobjectを様々なインターフェースにキャストすることに似ているかもしれません。
こっちはあまり褒められたものではありませんが)

なお、3DCGのカメラのViewとはなんの関係もありません。

ですから、「なんとかかんとかViewを作る」と言った場合それが意味するのは
「このバッファはなんとかかんとかな役割を果たす」ということです。
ここではSlimDX.Direct3D11.UnorderedAccessViewですが、これは
「このバッファは一度に複数のスレッドからのランダムアクセスな読み書きが出来る」という意味です。
public class UnorderedAccessView : ResourceView

なぜバッファをUnorderedAccessViewとして扱う必要があるのでしょう?
それは今回のようなベクトルの各要素にアクセスするような並列計算をする場合、
配列の"[]"演算子を使うのが手っ取り早いからです。
UnorderedAccessViewでないということは、
C#で喩えるならT[]ではなくIEnumerable<T>を使うようなものです
(ただしUnorderedAccessViewの場合は書き込みの話です。
読み込みはUnorderedAccessViewでなくてもランダムアクセスっぽく[]でアクセスできるようです)。
IEnumerable<T>はそのままでは並列的な計算に向きませんよね。
Parallel.Forの中でアクセスするのはIEnumerable<T>の要素ではなくT[]の要素にしたいものです。

別の言い方をするなら、ピクセルシェーダーのように書込み先の位置が決まっているのではなく、
[]演算子で好きな位置に書き込めるということです。
ピクセルシェーダーでは計算した結果を格納する先は決まっていて、
プログラマが出来ることといったら書きこむデータを計算することだけでした。
UnorderedAccessViewなら計算した結果を自由な位置に書き込めます。

コンストラクタは4つ用意されていますが
今回使うのは一番簡単なこのコンストラクタです。
public UnorderedAccessView(Device device, Resource resource);
deviceはこのViewを持つデバイスです。
resourceはこのビューがその役割を指定するリソース(この場合はバッファ)です。

なお、~ViewクラスはSlimDXに4つ存在するようです。
~Viewクラス 説明
DepthStencilView 深度ステンシルテストでテクスチャリソースとして使います。
RenderTargetView レンダーターゲットなテクスチャーリソースとして使います。画面に映る描画先のことですね。
ShaderResourceView シェーダーリソースとして使います。たとえば定数バッファ、テクスチャバッファ、テクスチャ、サンプラーなどです。演算シェーダーでも計算に必要なデータを格納したバッファをこれに指定することがあります。
UnorderedAccessView ピクセルシェーダーか演算シェーダーで、複数のスレッドからのランダムアクセスっぽい読み書きを受け付ける、ということを意味します。


さて、生成したバッファをUnorderedAccessとして使う場合、
Bufferのコンストラクタで指定しなければならないオプションがあります。
コンストラクタに入れるBufferDescriptionのBindFlagsプロパティを、
以下のようにBindFlags.UnorderedAccessにしなくてはいけないのです。

BindFlags = BindFlags.UnorderedAccess

これは、生成するバッファを引数にUnorderedAccessViewを生成できるということです。

ちなみに、BindFlagsには他にも以下のようなオプションがあります。
BindFlagsというのはバッファの使われ方を意味する、
SlimDX.Direct3D11.BindFlags型のプロパティです。

BindFlagsの値 数値 説明
None 0 特に何も指定しません。
VertexBuffer 1 生成されるリソースが、Input Assemblerステージで、頂点バッファとして使えることを意味します。
IndexBuffer 2 生成されるリソースが、Input Assemblerステージで、インデックスバッファとして使えることを意味します。
ConstantBuffer 4 生成されるリソースが、シェーダーステージで、定数バッファとして使えることを意味します。
ShaderResource 8 生成されるリソースが、シェーダーステージで、バッファやテクスチャとして使えるとうことを意味します。
StreamOutput 16 生成されるリソースが、Stream Outputステージで、出力バッファとして使用できるということを意味します。Stream Outputステージとは頂点シェーダーとピクセルシェーダーステージの間に位置するステージの一つです。どうやらジオメトリーシェーダーとセットになっているらしく、新たな頂点データをメモリに書き出すことができます。
RenderTarget 32 生成されるリソースが、Output Mergerステージで、レンダーターゲットとして使えることを意味します。つまり普通のDirectXアプリケーションで画面に表示される画像のことですね。
DepthStencil 64 生成されるリソースが、Output Mergerステージで、深度ステンシルとして使えることを意味します。
UnorderedAccess 128 生成されるリソースが、ランダムアクセス風に、複数のスレッドから同時に読み書きできることを意味します。

数値が0,1,2,4…となっていることからわかるように、
これは複数のフラグを"|"で一緒に指定することができます。


演算シェーダーのセット

演算シェーダーで行う具体的な計算は、
C#ではなくHLSLのファイルに書きます。

C#側で行うのは、HLSLファイルをロードしてコンパイルしてセットすることです。

HLSLファイルをロードしてコンパイルするには、
ShaderBytecode.CompileFromFile()メソッドを使います。
このメソッドには6種類オーバーロードがあるのですが、
今回使うのは2番目に簡単なこのメソッドです。
(といっても引数が結構多いです。
思うにこれはC#の名前付き引数機能を使えば簡単になりそうですね)

public static ShaderBytecode CompileFromFile(
    string fileName, 
    string entryPoint, 
    string profile, 
    ShaderFlags shaderFlags, 
    EffectFlags effectFlags
    );
これはDirectX SDK付属のHLSLコンパイルツール、fxc.exeに似ています。

fileNameはシェーダー(今回の場合は演算シェーダー)を記述したHLSLファイルの名前。
entryPointはHLSLファイルの中の、シェーダーとして使う関数の名前。
profileはシェーダーのターゲットです。演算シェーダーなら"cs_4_0"とか"cs_5_0"などというふうな文字列です。頂点シェーダーなら"vs_5_0"、ピクセルシェーダーなら"ps_5_0"という感じになるでしょう。
shaderFlagsはシェーダーコンパイルオプションです。今回はとくにどうでもいいのでNoneを選びます。
effectFlagsはエフェクトコンパイルオプションです。今回は特にどうでもいいのでNoneを選びます。

こうしてコンパイルされると、戻り値としてShaderBytecodeが帰ってきます。
このShaderBytecodeを使って
SlimDX.Direct3D11.ComputeShaderクラスのインスタンスを生成します。

public class ComputeShader : DeviceChild

public ComputeShader(Device device, ShaderBytecode shaderBytecode);

deviceは演算シェーダーを作るデバイス。
shaderBytecodeは演算シェーダーのバイトコードです。

このコンストラクタを使ってできたComputeShaderを
実行出来るようセットする必要があります。
それにはComputeShaderWrapper.Set()メソッドを使います。

public void Set(ComputeShader shader);
shaderはデバイスにセットする演算シェーダーです。

演算シェーダーの実行

演算シェーダーに関するセッティングが全て終わったら、
いよいよ演算シェーダーを実行できます。
実行にはComputeShaderWrapper.Dispatch()メソッドを使います。

public void Dispatch(
    int threadGroupCountX, 
    int threadGroupCountY, 
    int threadGroupCountZ
    );

さてここには3つの引数がありますがなんでしょうか?
それを説明するにはまず演算シェーダーのスレッドの簡単な説明が必要です。

演算シェーダーは多数のスレッドを同時に実行できます。

gpgpuThreads3D.png

そしてそれらのスレッドはグループ分けされているのです。

gpgpuThreadsGrouped.png
Dispatch()メソッドの引数はこのグループの数を指定するのです。
(全スレッドの数ではありません。念のため)


numthreads属性 (HLSL側)

では1グループあたりのスレッドの数はどうやって指定するのでしょうか?
それにはHLSL側のnumthreads属性を使います。

numthreads(X, Y, Z)

X, Y, Zは1グループあたりのスレッドサイズ(?いい表現が思いつきませんでした)です。
1グループあたりのスレッドの数はX * Y * Z個になります。
これは全スレッドの数を指定するのではないことに注意してください。
あくまでも1グループにいくつスレッドがあるかを指定するものです。
全スレッドの数はC#側のDispatchの引数とこのX, Y, Zをかけ合わせたもので決まります。

なお、X, Y, Zには範囲に制限があります。
MSDNの表をそのまま持ってきます:

演算シェーダーバージョン Zの最大値 スレッドの最大個数(X * Y * Z)
cs_4_x 1 768
cs_5_0 64 1024


SV_DispatchThreadID (HLSL側)

さて、このように演算シェーダーでは
多数のスレッドで計算することができます。
こういうことをしていると、
「今演算シェーダーの関数がどのスレッドで実行されているのか」
ということが知りたくなることが良くあります
たとえば今回行う「ベクトルのすべての要素を2倍にする」というようなものだと、
要素一つ一つにそれぞれスレッドを1つ割り当てることになりますが、
どの要素を操作するのかというキーとしてスレッドのIDを知りたくなるのです。

それにはSV_DispatchThreadIDセマンティックを使います。
こいつを演算シェーダーのuint3型の引数に付けてやると、それに
現在実行しているスレッドのIDが入っているのです。


RWStructuredBuffer<T> (HLSL側)

今回HLSLでの計算結果の読み書きはHLSLの
RWStructureBuffer<T>の変数に対して行います。
この型は、読み書きが出来る上(RWとはRead/Writeのことです)、
C#のジェネリクスのついたコレクションのように、
Tの中身を色々な構造体に指定することができます。
例えば今回の場合はRWStructureBuffer<int>というような具合です。

この型には(C#noジェネリクスのついたコレクションのように)
[]演算子も付いています。
T Operator[]( in uint indexPosition );

これは、引数にインデックスをとって、その位置の要素を返します。


コード

今回のコードは2つのファイルからなります。
C#のコードと、HLSLのコードです。

HLSLのコードは
Visual C#のプロパティ「Copy to Output Directory」を
「Copy if newer」にしましょう。

Program.cs
using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

class Program
{
    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);
        Buffer onlyGpuBuffer = createStructuredBuffer(device, Enumerable.Range(0, 10).ToArray());

        initComputeShader(device, onlyGpuBuffer);
        device.ImmediateContext.Dispatch(10, 1, 1);
        writeBuffer(device, onlyGpuBuffer);
    }

    static void initComputeShader(Device device, Buffer onlyGpuBuffer)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(new UnorderedAccessView(device, onlyGpuBuffer), 0);

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_4_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, Enumerable.Range(0, 10).ToArray());
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

        foreach (var number in readBack)
        {
            System.Console.WriteLine(number);
        }
    }

    static Buffer createStructuredBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                BindFlags = BindFlags.UnorderedAccess,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    static Buffer createCpuAccessibleBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }

    static int[] readBackFromGpu(Buffer from)
    {
        DataBox data = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        return getArrayInt32(data.Data);
    }

    static int[] getArrayInt32(DataStream stream)
    {
        int[] buffer = new int[stream.Length / sizeof(int)];
        stream.ReadRange(buffer, 0, buffer.Length);
        return buffer;
    }
}


MyShader.fx
RWStructuredBuffer<int> MyBuffer;

[numthreads(1, 1, 1)]
void MyComputeShader( uint3 threadID : SV_DispatchThreadID )
{
    MyBuffer[threadID.x] *= 2;
}



実行結果
0
2
4
6
8
10
12
14
16
18

2倍になりました!

このプログラムがしていることは
GPU中に「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というStructuredBufferをつくり、
演算シェーダーでそれぞれ2倍し「0, 2, 4, 6, 8, 10, 12, 14, 16, 18」にした、ということです。


10個のスレッドで、演算シェーダーの関数
MyComputeShader()を実行しているのです。
(10個のスレッドグループを作り、
1つのスレッドグループに1つのスレッドがあるので全部で合計10個のスレッドです)

MyComputeShaderの引数threadIDの中には、
(0, 0, 0)
(1, 0, 0)
(2, 0, 0)
(3, 0, 0)
(4, 0, 0)
(5, 0, 0)
(6, 0, 0)
(7, 0, 0)
(8, 0, 0)
(9, 0, 0)
のいずれかが入っているはずです。

そしてのXの値をインデックスにして、
バッファにアクセスし数を二倍にしています。
つまりバッファの中の全てのintは2倍されるのです。


拍手[1回]

PR

C#でGPGPUチュートリアル その2バッファのコピー

バッファからバッファへコピー

前回
、GPUにあるデータをCPU側に持ってきました。
図で表すと以下のようなことをしたのです。

howTutorial01Worked.jpg



しかし実際にGPGPUをやろうとすると、
これだけでは計算結果をCPU側に持ってこれないことがわかります。

どうやら「CPUからアクセス可能なバッファ」は計算結果を入れられないようなのです。
計算結果を入れることが可能であり、
かつCPUからアクセスできるバッファをつくろうとすると例外をスローします。

ではどうすればいいのか?
と言いますと、間にひとつバッファをはさんでやればいいのです。

howSlimDXComputeShaderTutorial02Works.jpg

「計算結果を入れることができるけれどCPUからはアクセス出来ないバッファ」
に計算結果を入れます。
そこから
「CPUからアクセスはできるけど計算結果を入れることができないバッファ」
にデータをコピーしてやるのです。
そうすれば後は前回と同じようにCPU側に持ってこれます。

ですから、今回しなくてはならないのはバッファからバッファへのコピーです。
それとついでに、「計算結果を入れることができるけれどCPUからはアクセス出来ないバッファ」の生成ですね。

バッファからバッファへのコピーには
DeviceContext.CopyResource()メソッドを使います。
public void CopyResource(Resource source, Resource destination);
これは珍しく簡単なメソッドで、解説の必要は無いかもしれません。
ですが念のため言っておくと、
sourceはコピー元で、
destinationがコピー先です。


計算結果を入れるバッファの生成

計算結果を入れるバッファは
StructuredBufferというものにしてやります。
StructuredBufferとは何かというとGPU側の配列のようなものです。
つまり、ある一定のサイズの構造体がズラッと列になって並んでいるようなものです。

では「そうでないバッファなどあるのだろうか、
頂点バッファは頂点のサイズは一定だし、
インデックスバッファはインデックスのサイズが一定で
ズラッとならんでいるのに」
と思われるかもしれません。

ですが、実はDirectX11にはそういうものがあって、
たとえば定数バッファです。
定数バッファの中身はひとつのマトリックスとひとつのベクトルという可能性がありえます。
(Projectionマトリックスとライトの向きとかでしょうか)
マトリックスとベクトルは当然サイズが違います。
つまり定数バッファは要素のサイズが一定でない、
言い換えるとStructuredBufferではない、ということなのでしょうね。(要素数が1でない限り)

生成時にStructuredBufferにしてなんの得があるのか、ということですが、
HLSL側で配列のようにカッコ"[ ]"を使ってアクセスすることができます。
StructuredBufferを使わない場合は(それは可能です)、
"[ ]"ではなく「バッファの~バイト目にアクセスしそれをintと解釈して読み込む」
というような感じの面倒な使い方をしなくてはなりません。
ちなみにHLSLのコードでは変数の型としてStructuredBuffer<int>というような書き方をします。
(より正確にはStructuredBuffer<構造体名>です。
そうすると配列というよりはジェネリクスのコレクションっぽい物といった感じでしょうか)

C#側ではSlimDXにStructuredBufferという型そのものがあるわけでは無いようで、
普通のBufferクラスのコンストラクタ引数BufferDescriptionの内容を
OptionFlags = ResourceOptionFlags.StructuredBuffer,
StructureByteStride = 配列要素一つのサイズ
としてやることでStructuredBufferにすることができます。

 OptionFlagsはバッファのその他の設定を表す、
SlimDX.Direct3D11.ResourceOptionFlags型のプロパティです。

ResourceOptionFlagsの値 数値 説明
None 0 オプションの指定はなしです。
GenerateMipMaps 1 生成するリソースががテクスチャの場合、mipmapの生成を有効にします。これを使う場合、BindFlagsでこのリソースがレンダーターゲットであり、シェーダーリソースであるということを指定しなければいけません。
Shared 2 生成するリソースが複数のDirect3Dデバイス間でのデータ共有することを可能にします。ただし2Dのテクスチャでなくてはいけません。
TextureCube 4 生成するリソースが6枚のテクスチャから成る、テクスチャキューブになることを可能にします。
DrawIndirect 16 GPUで生成されたコンテントのインスタンシングを可能にします。たぶんインスタンシングを使って描画するので、直接描画するわけではない、ということなのでしょう。
RawBuffer 32 生成するリソースが、ひとかたまりの、バイトアドレスで操作するバッファでになることを可能にします。
StructuredBuffer 64 生成するリソースが、Structured Bufferとなることを可能にします。たくさんの構造体が並んだ配列のようなものです。
ClampedResource 128 DeviceContext.SetMinimumLod()メソッドを使ったときに、mipmapのclampを有効にします。
KeyedMutex 256 SlimDX.DXGI.KeyedMutexクラスの、KeyedMutex.Acquire()メソッドとKeydMutex.Release()メソッドを使ったときに、シンクロするようになります。
GdiCompatible 512 リソースがGDIと互換性を持てます。これを指定するとSlimDX.Direct2D.GdiInteropRenderTargetクラスのGetDC()メソッドを使ってGDIで描画できます。

今回は、上記のうちStructuredBufferだけをオンにします。

さて、実はこの設定をしただけでは、まだCPUからアクセス出来るのです。
GPUで計算結果を格納できるような設定にすると
例外をスローするようになるのですが、
その設定については次回にしたいと思います。

コード

GPUのバッファからバッファへコピーして、
それをCPUに持ってくるプログラムは以下のようになります。

Program.cs
using System.Collections.Generic;
using System.Linq;


using SlimDX;
using SlimDX.Direct3D11;


class Program
{
    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);
        Buffer onlyGpuBuffer = createStructuredBuffer(device, Enumerable.Range(10, 10).ToArray());
        writeBuffer(device, onlyGpuBuffer);
    }


    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, Enumerable.Range(0, 10).ToArray());
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

        foreach (var number in readBack)
        {
            System.Console.WriteLine(number);
        }
    }


    static Buffer createStructuredBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }


    static Buffer createCpuAccessibleBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }


    static int[] readBackFromGpu(Buffer from)
    {
        DataBox data = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        return getArrayInt32(data.Data);
    }


    static int[] getArrayInt32(DataStream stream)
    {
        int[] buffer = new int[stream.Length / sizeof(int)];
        stream.ReadRange(buffer, 0, buffer.Length);
        return buffer;
    }
}

このプログラムはまず、
GPU内でだけアクセスできるバッファ「10, 11, 12, 13, 14, 15, 16, 17, 18, 19」
を作ります。

次にGPUに、CPUからもアクセスできるバッファ「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」
を作ります。

前者から後者に値をコピーします。
すると後者の値は消えて、後者の方も「10, 11, 12, 13, 14, 15, 16, 17, 18, 19」
になります。

それをCPUに読み込みます。
その結果を出力するので、
このプログラムは実行すると以下のようになります。
10
11
12
13
14
15
16
17
18
19

ここで表示されているのは
もとはGPU内からしかアクセス出来ないバッファに
格納されていたint配列です。

それが回りまわってきちんとCPU側に引き渡され
System.Console.WriteLine()で出力できました。










拍手[0回]


C#でGPGPUチュートリアル その1 GPUからCPU側へのデータ転送

SlimDX

C#でGPGPUを使うにはいくつか方法があると思います。
DirectXかOpenCL、CUDAをラップして使うなどです。

ここではDirectXを使うということにしておきます。
DirectXにはオープンソースのSlimDXというよくできたラッパーライブラリーが存在します。

MicrosoftもWindows API Code Pack for .NET Frameworkというラッパーを公開していますが、
現時点ではSlimDXのほうが上手くラッパーできているように思えます。

たとえばWindows API Code Pack for .NET FrameworkではIntPtrでしかアクセス出来ないデータが、
SlimDXではSystem.IO.Stream(か、それを継承したクラス)になっていたりといった具合です。
MSのラッパーでは、unsafeコードやMarshalクラスの使用を強いられることになるのです。
それよりはSlimDXのほうがスマートです。
少なくとも現時点では。

というわけで、今までのWindows API Code Pack for .NET Frameworkの使用はなかったことにして
ここからはSlimDXを使うことにします

SlimDXのインストール

インストールはSlimDXのページの上にある「Download」リンクを押して、
「DeveloperSDK」という項目の
「Install Developer SDK」という所をクリックすればいいようです。

howToDownloadSlimDX.jpg


ダウンロードされたファイル(SlimDX SDK (June 2010).exe)を実行すると
どうやら自動的にアセンブリが追加され使えるようになります。

インストールが終わると
Visual C# 2010の参照の追加ダイヤログに、
以下のように出てきます。

SlimDXAssembliesInstalled.jpg


なぜかSlimDXという名前のものが2つありますね。
これはx86かx64かという違いです。
OSが64bitならx64の方を選びたいところです。


DirectX SDKのサンプル、BasicCompute11を簡単に

SlimDXをインストールできたら早速なにかやってみましょう。
なにか・・・といきなりいわれても何をすればいいのかよくわかりませんので
本家DirectXのSDKに付いているサンプルを簡単に再現してみるのがいいでしょう。
ここではBasicCompute11というのを少しずつやってみたいと思います。

BasicCompute11はDirectX11で新たに導入された演算シェーダーを使った簡単なサンプルで、
GPUを使って足し算を行います

もちろんただの足し算ではなく、2つのベクトルの足し算です。

howBasicCompute11Works.jpg

buffer0に入れた数字と、buffer1に入れた数字を足し合わせ
bufferResultに格納するのです。
上の図では図を分かりやすくするために0~7まで8つの数字を書いていますが、
実際のサンプルでは1024個もの数字!(といってもその程度の数GPUにとってはなんてこと無いのでしょう)を足しあわせています。

さてこのサンプルは一見簡単そうですが、C++のコードだけで700行ちかくもあるという大きなプログラムです。
HLSL側も70行くらいあります。

簡単なはずのサンプルが難解というのはDirectXのサンプルではよくあることです。
そこでこのサンプルがやっていることを少しずつ、チュートリアル風にしてやっていきます。
まずは計算結果のbufferResultをCPU側に読み戻す事を考えましょう。
計算結果はGPUにあるため直接C#のコードで扱うことは出来ないのです。

GPUからCPU側へデータを送る

GPGPUではGPUで計算をし、計算結果もGPU内に格納されます。
ですから、そのままでは計算結果をCPU側で動くプログラムから扱えません。
GPUにある計算結果をCPU側に送ることが必要となります。

まずは、CPUからGPUに「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータを送り、
それをまたGPUからCPUへ戻すだけのプログラムを書いてみます。
using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;

class Program
{
    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);
        Buffer buffer = createCpuAccessibleBuffer(device, Enumerable.Range(0, 10).ToArray());

        int[] readBack = readBackFromGpu(buffer);

        foreach (var number in readBack)
        {
            System.Console.WriteLine(number);
        }
    }

    private static Buffer createCpuAccessibleBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }

    static int[] readBackFromGpu(Buffer from)
    {
        DataBox data = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        return getArrayInt32(data.Data);
    }

    static int[] getArrayInt32(DataStream stream)
    {
        int[] buffer = new int[stream.Length / sizeof(int)];
        stream.ReadRange(buffer, 0, buffer.Length);
        return buffer;
    }
}

コードはこれだけです。
GPUで計算はしていないので、HLSLファイルなどは必要ありません。
 
実行結果はこうなります。
0
1
2
3
4
5
6
7
8
9
ここで表示しているのはGPUにあるバッファの内容をCPU側に写しとった配列です。
きちんと、はじめにGPU側に渡してやった
「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータが戻ってきています。


Deviceクラス

このプログラムで最初に行ったのはSlimDX.Direct3D11.Deviceクラスの初期化です。
このクラスはGPUを制御するクラスです。
GPUにバッファを作りたい、計算させたい、というときに大抵必要になります。
public class Device : ComObject
コンストラクタは8個用意されていますが、今回必要なのは
一番シンプルなこのコンストラクタです。
public Device(DriverType driverType);

deviceTypeはデバイスの種類で、つぎの6つの中から選びます。

DeviceTypeの値 説明
Unknown タイプが不明
Hardware Direct3Dの機能を持つハードウェアです。まずほとんどのばあいこれを使ってDeviceクラスを初期化することになります。ハードウェアを使うためかなり速いです。というかこれ以外では遅すぎてまず通常目的では使えません。これを使いましょう。
Reference リファレンスドライバです。リファレンスドライバはソフトウェアで動いているため遅いです。遅いです―が正確で、デバッグ時やDirectXの機能をテストするというようなときには悪くはないのかもしれません。ただ、実際のアプリケーションで使う為のものではありません。
Null Nullドライバです。これはリファレンスドライバから描画機能がなくなったようなものです。これもデバッグ用で、まず普通のアプリケーションで使うためのものではありません。
Software Softwareドライバです。これは完全にソフトウェアで実装されています。やはり遅いため普通のアプリケーションで使うためのものではありません。
Warp WARPドライバです。これもソフトウェアで動きますが、パフォーマンスは高いです。

DeviceクラスにはGPGPUを行う上で重要なプロパティがあります。
ImmediateContextプロパティです。
デバイスの状態にアクセスするときには大抵このプロパティを通します。

public DeviceContext ImmediateContext { get; }

と、いうよりDeviceクラスはほとんど抜け殻のようなもので、
重要な機能はだいたいここに入っています。
たとえばXNAのGraphicsDeviceのメソッドに相当するものは多くがこの中に入っています。

Bufferクラス

Deviceクラスを初期化した後は
CPUからのデータを使ってGPUでSlimDX.Direct3D11.Bufferクラスのインスタンスを生成しています。
public class Buffer : Resource
このバッファというものはGPUに存在するデータの塊のようなもので、
頂点バッファであったりインデックスバッファであったり、定数バッファであったりします。

今回はシェーダーを動かさないため、「これは~~バッファだ」とか言うようなバッファの種類に気を使う必要はありません。

コンストラクタは6種類用意されています。
今回使っているのは2番目に簡単な(1番簡単なものはバッファの初期データを渡せないのです)
このコンストラクタです。
public Buffer(Device device, DataStream data, BufferDescription description);
deviceはこのバッファを作るデバイスです。

dataはバッファの初期データ。今回の場合は「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータです。
DataStreamというのはSystem.IO.Streamを継承したクラスです。

descriptionはこのバッファのサイズやバッファの種類などの情報です。
有り得そうなコンストラクタ引数はだいたいここにまとめられていると考えて構いません。
実際、この中身を直接コンストラクタ引数としたコンストラクタも存在します。


DataStreamクラス

Windows API Code Pack for .NET Frameworkの欠点の一つは
APIに渡すデータをIntPtrでしか渡せないことでした。
これではユーザーにunsafeやらMarshalやらネイティブとの境目を意識させるような機能を使えと言っているようなもので、
C#のAPIとしてはあまり気持ちのいいものではありません。

それに対してSlimDXではIntPtrの代わりに
System.IO.Streamを継承したSlimDX.DataStreamクラスを使い、境界を意識する必要はありません。
純粋なC#の世界だけで完結できるのです。

public class DataStream : Stream, IDisposable
コンストラクタは3種類用意されていますが、
今回使うのは次の初期値の配列を引数に取るコンストラクタです。
public DataStream(Array userBuffer, bool canRead, bool canWrite);

userBufferはこのインスタンスが初期値に使う配列です。
canReadはこのストリームから読み込むことができるかどうか、です。(Stream.CanReadと関係があるのでしょうか)
canWriteはこのストリームに書き込むことができるかどうか、です。(Stream.CanWriteと関係があるのでしょうか)

DataStreamはマネージ側に配列を持ってくるメソッドも定義しています。

public T[] ReadRange(int count) where T : struct; public int ReadRange(T[] buffer, int offset, int count) where T : struct; 

bufferは読み込んだ結果を格納する配列。
offsetは読み込みを開始するインデックス。
countはよみこむ要素数。
戻り値は読み込んだ要素数です。

BufferDescriptionクラス

Bufferクラスにはコンストラクタに大量の引数を用意したくないためか
たくさんのオプションをメンバーに持ったSlimDX.Direct3D11.BufferDescriptionを引数に取ります。
BufferDescriptionはBufferのインスタンスを生成するのに必要ないろいろな情報を格納します。

プロパティ名 説明
BindFlags BindFlags パイプライン中でどのように使われるか、を表します。 たとえば頂点バッファとして使われるか?インデックスバッファとして使われるか?定数バッファとして使われるのか?シェーダーのリソースとして使われるのか?ストリームの出力として使われるのか?レンダーターゲットとして使われるのか?デプスステンシルバッファとして使われるのか?アンオーダードアクセス(複数のスレッドから同時に読み書き可能な)リソースとして使われるのか? ・・・・・・などです
CpuAccessFlags CpuAccessFlags このバッファにCPUがいかほどのアクセス権限を持っているかを表します。None, Write, Readの3種類があります。
OptionFlags ResourceOptionFlags その他のオプションです。 たとえば、MipMapを生成するか?複数のデバイス間で共有されるか?TextureCubeか?インスタンシングを有効にするか?RawBufferか?StructuredBufferか?深度バイアスはどうするか?KeyedMutexか?GDIと互換性があるか? ・・・・・・などです。
SizeInBytes int バッファが何バイトか、を表します。
StructureByteStride int StructuredBufferとして使用する場合(OptionFlagsでそのように指定するのですが)に使います。ひとつの構造体のサイズが何バイトかを表します。
Usage ResourceUsage バッファの使われ方を表します。GPUが読み書きのできるDefault、GPUで読み取りのみ可能なImmutable、GPUからは読み取りのみCPUからは書き込みのみのDynamic、GPUからCPUへデータを転送できるStagingの4つです。

今回はGPUからCPUへ転送が可能になるように、
CpuAccessFlagsはReadに、
UsageはStagingにしたわけです。


DeviceContextクラス

このクラスは解説が最後らへんになってしまいながらも一番大切なクラスです。
今回のサンプルではたいしたことはしていませんが、それでも重要です。
Deviceクラスはこれのために存在すると言ってもいいくらいです。
実際、このSlimDX.Direct3D11.DeviceContextクラス
描画のための命令全般を司っています。

今回このクラスがやってくれたことは、GPU上に存在するバッファを
CPUがアクセス可能なメモリに持ってきてくれたことです。
これがなければせっかくGPGPUをやっても結果をConsole.WriteLine()で確認できません。
public DataBox MapSubresource(
        Resource resource,
        MapMode mode, 
        MapFlags flags);
resourceはメモリに展開するリソース、今回の場合はバッファです。
modeはCPUの読み書き許可です。
flagsはこのメソッドを実行するに当たってGPUが忙しい状態だった場合どうするかです。

modeをもう少し詳しく言うとこうなります:
MapModeの名前 説明
Read リソースは読み取り用にマップされます。ただしリソースが読み取りアクセス可能なように生成されていなければいけません。
Write リソースは書き込み用にマップされます。ただしリソースが書き込みアクセス可能なように生成されていなければいけません。
ReadWrite リソースは読み書き用にマップされます。ただしリソースが読み書きアクセス可能なように生成されていなければいけません。
WriteDiscard リソースは書き込み用にマップされますが、以前の情報は未定義になります。ただしリソースが書き込みアクセス可能なように生成されていなければいけません。
WriteNoOverwrite リソースは書き込み用にマップされますが、リソースのすでに存在しているコンテンツは上書きできません。このフラグは頂点バッファとインデックスバッファでのみ有効です。リソースは書き込みアクセス可能なように生成されていなければいけません。


flagsをもう少し詳しく言うとこうなります:
 
MapFlagsの名前 説明
None リソースが使えるようになるまで待ちます。
DoNotWait リソースが使えるようになるまで待ちません。GPUがCPUからのアクセスをブロックするようであればWasStillRenderingを返します。

戻り値のDataBoxはCPUからアクセス可能なメモリへの参照を持っています。

 

DataBoxクラス

SlimDX.DataBoxクラスはデータを3次元で保持するクラスです。
ただし今回の場合バッファは1次元ですしあまり意味はありません。
実質的にDataStreamのラッパーのようなものです。

持っているデータへアクセスはDataBox.Dataプロパティを使います。
public DataStream Data { get; }
DataはSystem.IO.Streamを継承したSlimDX.DataStreamクラスです。
 




拍手[12回]


[Windows® API Code Pack for Microsoft® .NET Framework] C#でDirectX11をつかう その5 三角形を回転



 前回は色のついた三角形を表示しました。
ここでは、その三角形を回転させてみましょう。

三角形を回転させるためにカメラの方を回転させます。
つまり今回はカメラの情報を新たに扱うことになります。


定数バッファー

DirectX10の頃からそうだったと思いますが、
GPU側にConstantBufferという物を作り、
それにカメラの情報をセットしてやります。
GPUはその情報を利用して三角形を表示します。

ですから今回の主役はConstantBufferです。
ConstantBufferは頂点バッファやインデックスバッファのような
GPUのバッファリソースの一つで、
シェーダー内で使われる変数群です。
生成も頂点バッファと同じ、D3DDevice.CreateBuffer()メソッドを使います。

違うのはそのメソッドに使われるパラメーターで、
BindingOptionsBindingOptions.ConstantBufferに指定してやります。

そして一旦生成した後、その中身を変更するには
DeviceContext.UpdateSubresource()メソッドを使います。
このメソッドを使ってカメラ情報を毎回描画前にアップデートしてやります。

生成、データをセット、だけでは定数バッファは使えるようになりません。
定数バッファをデバイスにセットしてやる必要があります。
これを行うのはVS.SetConstantBuffers()メソッドです。


Program.cs

using Microsoft.WindowsAPICodePack.DirectX.Direct3D;
using Microsoft.WindowsAPICodePack.DirectX.Direct3D11;
using Microsoft.WindowsAPICodePack.DirectX.Graphics;

using System.IO;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        using (Game game = new MyGame())
        {
            game.Run();
        }
    }
}

class Game : System.Windows.Forms.Form
{
    protected DeviceContext DeviceContext;
    protected SwapChain SwapChain;
    protected RenderTargetView RenderTargetView;
    
    public void Run()
    {
        D3DDevice device = initDevice();
        LoadGraphicsContent(device);
        Show();

        while (Created)
        {
            Draw();
            System.Windows.Forms.Application.DoEvents();
        }
    }

    private D3DDevice initDevice()
    {
        D3DDevice device = D3DDevice.CreateDeviceAndSwapChain(this.Handle);
        this.SwapChain = device.SwapChain;
        this.DeviceContext = device.ImmediateContext;

        using (Texture2D texture2D = SwapChain.GetBuffer<Texture2D>(0))
        {
            this.RenderTargetView = device.CreateRenderTargetView(texture2D);
            DeviceContext.OM.RenderTargets = new OutputMergerRenderTargets(new[] { RenderTargetView });
        }

        DeviceContext.RS.Viewports = new[]
        {
            new Viewport
            {
                Width = ClientSize.Width,
                Height = ClientSize.Height,
                MaxDepth = 1
            }
        };
        return device;
    }

    protected virtual void LoadGraphicsContent(D3DDevice device) { }
    protected virtual void Draw() { }

    protected D3DBuffer CreateVertexBuffer<T>(D3DDevice device, T[] vertices) where T : struct
    {
        int vertexSize = Marshal.SizeOf(typeof(T));
        System.IntPtr verticesPointer = Marshal.AllocCoTaskMem(vertices.Length * vertexSize);

        for (int i = 0; i < vertices.Length; i++)
        {
            Marshal.StructureToPtr(vertices[i], verticesPointer + vertexSize * i, false);
        }

        D3DBuffer result = device.CreateBuffer(
                new BufferDescription
                {
                    ByteWidth = (uint)(vertexSize * vertices.Length),
                    BindingOptions = BindingOptions.VertexBuffer,
                },
                new SubresourceData
                {
                    SystemMemory = verticesPointer
                }
                );

        Marshal.FreeCoTaskMem(verticesPointer);

        return result;
    }

    protected void ToNative(object structure, System.Action<System.IntPtr> nativeAction)
    {
        System.IntPtr nativeConstantBuffer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(MyCamera)));
        Marshal.StructureToPtr(structure, nativeConstantBuffer, false);
        nativeAction(nativeConstantBuffer);
        Marshal.FreeHGlobal(nativeConstantBuffer);
    }

}

struct VertexPositionColor
{
    public Vector3F Position;
    public Vector3F Color;

    public static readonly InputElementDescription[] VertexElements = new[]
    {
         new InputElementDescription
         {
             SemanticName = "SV_Position",
             Format = Format.R32G32B32Float,
         },
         new InputElementDescription
         {
             SemanticName = "COLOR",
             Format = Format.R32G32B32Float,
             AlignedByteOffset = uint.MaxValue //MaxValueにするとオフセットを自動的に決定
         }
    };

}

struct MyCamera
{
    public Matrix4x4F View;
    public Matrix4x4F Projection;

    public static int SizeInBytes
    {
        get
        {
            return Marshal.SizeOf(typeof(MyCamera));
        }
    }
}

class MyGame : Game
{
    D3DBuffer cameraConstantBuffer;

    protected override void Draw()
    {
        this.DeviceContext.ClearRenderTargetView(RenderTargetView, new ColorRgba(0.39f, 0.58f, 0.93f, 1));

        updateCamera();

        this.DeviceContext.Draw(3, 0);
        this.SwapChain.Present(0, PresentOptions.None);
    }

    private void updateCamera()
    {
        double time = System.Environment.TickCount / 100d;

        Matrix4x4F view = MatrixHelper.CreateLookAt(
                new Vector3F((float)System.Math.Cos(time), 0, (float)System.Math.Sin(time)),
                new Vector3F(0, 0, 0),
                new Vector3F(0, 1, 0)
                );
        Matrix4x4F projection = MatrixHelper.CreatePerspectiveFieldOfView(
                (float)System.Math.PI / 2,
                (float)ClientSize.Width / ClientSize.Height,
                0.1f, 1000);

        ToNative(
            new MyCamera
            {
                View =  MatrixHelper.Transpose(view),
                Projection = MatrixHelper.Transpose(projection)
            }, 
            nativeCamera => 
            {
                DeviceContext.UpdateSubresource(
                    cameraConstantBuffer,
                    0,
                    nativeCamera,
                    0,
                    0
                    );
            }
            );

        DeviceContext.VS.SetConstantBuffers(0, new[] { cameraConstantBuffer });
    }

    protected override void LoadGraphicsContent(D3DDevice device)
    {
        initShadersAndInputLayout(device);
        initTriangleToDraw(device);
        initCamera(device);
    }

    private void initShadersAndInputLayout(D3DDevice device)
    {
        using (Stream vertexShaderBinary = File.Open("MyShader.vs", FileMode.Open))
        {
            this.DeviceContext.VS.Shader = device.CreateVertexShader(vertexShaderBinary);
            vertexShaderBinary.Position = 0;
            this.DeviceContext.IA.InputLayout = createInputLayout(device, vertexShaderBinary);
        }

        using (Stream pixelShaderBinary = System.IO.File.Open("MyShader.ps", FileMode.Open))
        {
            this.DeviceContext.PS.Shader = device.CreatePixelShader(pixelShaderBinary);
        }
    }

    private InputLayout createInputLayout(D3DDevice device, Stream vertexShaderBinary)
    {
        return device.CreateInputLayout(
            VertexPositionColor.VertexElements,
            vertexShaderBinary
            );
    }

    private void initTriangleToDraw(D3DDevice device)
    {
        VertexPositionColor[] vertices = new []
        {
            new VertexPositionColor{ Position = new Vector3F(0, 0.5f, 0), Color = new Vector3F(1, 1, 1) },
            new VertexPositionColor{ Position = new Vector3F(0.5f, 0, 0), Color = new Vector3F(0, 0, 1) },
            new VertexPositionColor{ Position = new Vector3F(-0.5f, 0, 0), Color = new Vector3F(1, 0, 0) },
        };

        D3DBuffer vertexBuffer = CreateVertexBuffer(device, vertices);

        this.DeviceContext.IA.SetVertexBuffers(
            0,
            new D3DBuffer[] { vertexBuffer },
            new uint[] { (uint)Marshal.SizeOf(typeof(VertexPositionColor)) },
            new uint[] { 0 }
            );

        this.DeviceContext.IA.PrimitiveTopology = PrimitiveTopology.TriangleList;
    }

    private void initCamera(D3DDevice device)
    {
        ToNative(
            new MyCamera {},
            nativeCamera => 
            {
                this.cameraConstantBuffer = device.CreateBuffer(
                    new BufferDescription
                    {
                        ByteWidth = (uint)MyCamera.SizeInBytes,
                        BindingOptions = BindingOptions.ConstantBuffer
                    }, 

                    //nativeCameraの中身はカラなので
                    //これはいらないように一見見えますが、
                    //これがないと何故か例外をスローします。
                    //今後改善なりなんなりがあるのでしょうか?
                    new SubresourceData
                    {
                        SystemMemory = nativeCamera
                    }
                    );
            }
            );
    }
}
 

カメラの情報とは何かというと、
2つのマトリクスです。
この2つのマトリクスに従って頂点情報を変形させてやると
実際にウィンドウに表示される三角形の形になるのですが、
困ったことにマトリクスを生成するためのメソッドが
Windows API Code Packにはありません

厳密に言うと無いのではなく、
付属のサンプルプログラムの中には存在するのですが、
その数はXNAより豊富ではありませんし
サンプルのものをコピーしたり参照したりすることになるので
どのみち以下のようなプログラムを書くことになると思います。

MatrixHelper.cs
using Microsoft.WindowsAPICodePack.DirectX.Direct3D;

class MatrixHelper
{
    public static Matrix4x4F CreateLookAt(
        Vector3F cameraPosition, Vector3F cameraTarget, Vector3F cameraUpVector
        )
    {
        Vector3F newZ = (cameraPosition - cameraTarget).Normalize();
        Vector3F newX = Vector3F.Cross(cameraUpVector, newZ).Normalize();
        Vector3F newY = Vector3F.Cross(newZ, newX);
        return new Matrix4x4F(
            newX.X,
            newY.X,
            newZ.X,
            0,
            newX.Y,
            newY.Y,
            newZ.Y,
            0,
            newX.Z,
            newY.Z,
            newZ.Z,
            0,
            -Vector3F.Dot(newX, cameraPosition),
            -Vector3F.Dot(newY, cameraPosition),
            -Vector3F.Dot(newZ, cameraPosition),
            1f
            );
    }

    public static Matrix4x4F CreatePerspectiveFieldOfView(
        float fieldOfView, float aspectRatio,
        float nearPlaneDistance, float farPlaneDistance
        )
    {
        return new Matrix4x4F
        {
            M11 = (float)(1 / (aspectRatio * System.Math.Tan(fieldOfView / 2))),
            M22 = (float)(1 / System.Math.Tan(fieldOfView / 2)),
            M33 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance),
            M34 = -1,
            M43 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance),
        };
    }

    public static Matrix4x4F Transpose(Matrix4x4F matrix)
    {
        return new Matrix4x4F(
            matrix.M11, matrix.M21, matrix.M31, matrix.M41,
            matrix.M12, matrix.M22, matrix.M32, matrix.M42,
            matrix.M13, matrix.M23, matrix.M33, matrix.M43,
            matrix.M14, matrix.M24, matrix.M34, matrix.M44
            );
    }
}

そしてGPU側のコードにも
以下のようにカメラの情報を考慮して三角形を表示するよう
書き加えます。

2つのマトリクスを使って位置ベクトルを変形させるのです。

cbuffer MyCamera
{
    matrix View;
    matrix Projection;
}

struct VertexPositionColor
{
    float4 Position : SV_Position;
    float4 Color : COLOR;
};

VertexPositionColor MyVertexShader(VertexPositionColor input)
{
    VertexPositionColor output = input;
    output.Position = mul(output.Position, View);
    output.Position = mul(output.Position, Projection);
    return output;
}

float4 MyPixelShader(VertexPositionColor input) : SV_Target
{
    return input.Color;
}
 

例によってこのファイルはfxc.exeを使ってコンパイルしてください。

結果

三角形がY軸を回転軸として回転します。

directX11RotatingTriangle1.jpg
directX11RotatingTriangle2.jpg
directX11RotatingTriangle3.jpg


三角形を逆方向からみると表示されないので、
回転しては
消えて
回転しては
消えて
を繰り返します。

拍手[0回]


[Windows® API Code Pack for Microsoft® .NET Framework] C#でDirectX11をつかう その4 色つき三角形を表示

 前回表示した三角形は真っ白で色が付いていなかったので
今回は各頂点に色をつけてみます。

今回の変更は前回ほど多くありません。

using Microsoft.WindowsAPICodePack.DirectX.Direct3D;
using Microsoft.WindowsAPICodePack.DirectX.Direct3D11;
using Microsoft.WindowsAPICodePack.DirectX.Graphics;

using System.IO;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        using (Game game = new MyGame())
        {
            game.Run();
        }
    }
}

class Game : System.Windows.Forms.Form
{
    protected DeviceContext DeviceContext;
    protected SwapChain SwapChain;
    protected RenderTargetView RenderTargetView;
    
    public void Run()
    {
        D3DDevice device = initDevice();
        LoadGraphicsContent(device);
        Show();

        while (Created)
        {
            Draw();
            System.Windows.Forms.Application.DoEvents();
        }
    }

    private D3DDevice initDevice()
    {
        D3DDevice device = D3DDevice.CreateDeviceAndSwapChain(this.Handle);
        this.SwapChain = device.SwapChain;
        this.DeviceContext = device.ImmediateContext;

        using (Texture2D texture2D = SwapChain.GetBuffer<Texture2D>(0))
        {
            this.RenderTargetView = device.CreateRenderTargetView(texture2D);
            DeviceContext.OM.RenderTargets = new OutputMergerRenderTargets(new[] { RenderTargetView });
        }

        DeviceContext.RS.Viewports = new[]
        {
            new Viewport
            {
                Width = ClientSize.Width,
                Height = ClientSize.Height,
                MaxDepth = 1
            }
        };
        return device;
    }

    protected virtual void LoadGraphicsContent(D3DDevice device) { }
    protected virtual void Draw() { }

    //ジェネリクスな型Tをネイティブに変換するのは骨が折れます。
    //XNAでは別の方法を使っているようですが…
    protected D3DBuffer CreateVertexBuffer<T>(D3DDevice device, T[] vertices) where T : struct
    {
        int vertexSize = Marshal.SizeOf(typeof(T));
        System.IntPtr verticesPointer = Marshal.AllocCoTaskMem(vertices.Length * vertexSize);

        for (int i = 0; i < vertices.Length; i++)
        {
            Marshal.StructureToPtr(vertices[i], verticesPointer + vertexSize * i, false);
        }

        D3DBuffer result = device.CreateBuffer(
                new BufferDescription
                {
                    ByteWidth = (uint)(vertexSize * vertices.Length),
                    BindingOptions = BindingOptions.VertexBuffer,
                },
                new SubresourceData
                {
                    SystemMemory = verticesPointer
                }
                );

        Marshal.FreeCoTaskMem(verticesPointer);

        return result;
    }
}

struct VertexPositionColor
{
    public Vector3F Position;
    public Vector3F Color;

    public static readonly InputElementDescription[] VertexElements = new[]
    {
         new InputElementDescription
         {
             SemanticName = "SV_Position",
             Format = Format.R32G32B32Float,
         },
         new InputElementDescription
         {
             SemanticName = "COLOR",
             Format = Format.R32G32B32Float,
             AlignedByteOffset = uint.MaxValue //MaxValueにするとオフセットを自動的に決定
         }
    };
}

class MyGame : Game
{
    protected override void Draw()
    {
        this.DeviceContext.ClearRenderTargetView(RenderTargetView, new ColorRgba(0.39f, 0.58f, 0.93f, 1));
        this.DeviceContext.Draw(3, 0);
        this.SwapChain.Present(0, PresentOptions.None);
    }

    protected override void LoadGraphicsContent(D3DDevice device)
    {
        initShadersAndInputLayout(device);
        initTriangleToDraw(device);
    }

    private void initShadersAndInputLayout(D3DDevice device)
    {
        using (Stream vertexShaderBinary = File.Open("MyShader.vs", FileMode.Open))
        {
            this.DeviceContext.VS.Shader = device.CreateVertexShader(vertexShaderBinary);
            vertexShaderBinary.Position = 0;
            this.DeviceContext.IA.InputLayout = createInputLayout(device, vertexShaderBinary);
        }

        using (Stream pixelShaderBinary = System.IO.File.Open("MyShader.ps", FileMode.Open))
        {
            this.DeviceContext.PS.Shader = device.CreatePixelShader(pixelShaderBinary);
        }
    }

    private InputLayout createInputLayout(D3DDevice device, Stream vertexShaderBinary)
    {
        return device.CreateInputLayout(
            VertexPositionColor.VertexElements,
            vertexShaderBinary
            );
    }

    private void initTriangleToDraw(D3DDevice device)
    {
        VertexPositionColor[] vertices = new []
        {
            new VertexPositionColor{ Position = new Vector3F(0, 0.5f, 0), Color = new Vector3F(1, 1, 1) },
            new VertexPositionColor{ Position = new Vector3F(0.5f, 0, 0), Color = new Vector3F(0, 0, 1) },
            new VertexPositionColor{ Position = new Vector3F(-0.5f, 0, 0), Color = new Vector3F(1, 0, 0) },
        };

        D3DBuffer vertexBuffer = CreateVertexBuffer(device, vertices);

        this.DeviceContext.IA.SetVertexBuffers(
            0,
            new D3DBuffer[] { vertexBuffer },
            new uint[] { (uint)Marshal.SizeOf(typeof(VertexPositionColor)) },
            new uint[] { 0 }
            );

        this.DeviceContext.IA.PrimitiveTopology = PrimitiveTopology.TriangleList;
    }
}
前回からの変更点は
頂点データに集中しています。
色のデータを追加したのでそれに関連した変更がいくつか。

まず頂点に位置だけでなく色も持たせるため、新たに頂点を表す構造体を作ります。
そしてその変更をGPU側に教えるためInputElementDescriptionを新たに一つ(色)追加します。
そしてその三角形頂点の初期化部分に色を追加。
そこから頂点バッファを作成する部分もより柔軟にジェネリクスを使ったものに変更です。

ついでに背景色もXNAと同じものに変更します。
今までのままだと表示する三角形と色がかぶって見づらいからです。

シェーダーの方にも色を追加します。

struct VertexPositionColor
{
    float4 Position : SV_Position;
    float4 Color : COLOR;
};

VertexPositionColor MyVertexShader(VertexPositionColor input)
{
    return input;
}

float4 MyPixelShader(VertexPositionColor input) : SV_Target
{
    return input.Color;
}
 
例によってこのファイルはfxc.exeを使ってコンパイルしてください。


実行結果

結果はこうです
directX11TutorialDrawColoredTriangle.jpg

真っ白一色だった三角形に
青と赤の色がつきました。




















拍手[0回]