忍者ブログ

Memeplexes

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

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回]

PR

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クラスです。
 




拍手[11回]


[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回]


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

前回は背景を青一色に塗りつぶしました
今回は三角形を表示します。


三角形を一つ表示するだけだというのに今回は
やけにやることが増えます。
こういう所がDirectXよりもXNAのほうが
優れているところなのでしょうね

まずはコードから。

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 unsafe D3DBuffer CreateVertexBuffer(D3DDevice device, Vector3F[] vertices)
    {
        fixed (Vector3F* fixedVertices = vertices)
        {
            return device.CreateBuffer(
                new BufferDescription
                {
                    ByteWidth = (uint)(Marshal.SizeOf(typeof(Vector3F)) * vertices.Length),
                    BindingOptions = BindingOptions.VertexBuffer,
                },
                new SubresourceData
                {
                    SystemMemory = new System.IntPtr(fixedVertices)
                }
                );
        }
    }
}

class MyGame : Game
{
    protected override void Draw()
    {
        this.DeviceContext.ClearRenderTargetView(RenderTargetView, new ColorRgba(0, 0, 1, 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(
            new[] {
                    new InputElementDescription
                    {
                        SemanticName = "SV_Position",
                        Format = Format.R32G32B32Float,
                    }
                },
            vertexShaderBinary
            );
    }

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

        D3DBuffer vertexBuffer = CreateVertexBuffer(device, vertices);

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

        this.DeviceContext.IA.PrimitiveTopology = PrimitiveTopology.TriangleList;
    }
}
あまりにも長いのでクラスを2分割しました。
ある程度一般的な機能(Game)と、今回のデモに特有な部分(MyGame)とにです。
使うメソッド名などもなるべくXNAに似せます。

前回から付け加わったのはおおまかにいって
1.シェーダーの作成
2.インプットレイアウトの作成
3.頂点バッファの作成
4.ビューポートの作成

です。
もちろん生成したものはセットしてから描画します。

  説明 XNAで言うと
シェーダー(頂点シェーダとピクセルシェーダ) 頂点データがどのような処理をされて描画されるかを制御します。このコードではMyShader.vsとMyShader.psというファイルから読み込んでいます。 Effect
インプットレイアウト 頂点データの意味を表します。(何バイト目が色のデータだ、など) VertexDeclaration
頂点バッファ 描画する図形の頂点データを保持します。 VertexBuffer
ビューポート 描画先の領域です。 Viewport


このうち4.ビューポートの作成以外は描画するものに影響し
あまり抽象化するとかえってまずい気がするので、
MyGameクラスの方に持ってきます。
(ビューポートは大抵のゲームでウィンドウ全体に設定することを選ぶでしょう。)

シェーダーファイル

さらに、DirectXにはXNAのBasicEffectのような気の利いたものは無いので
シェーダーファイルも必要です。

MyShader.fx

float4 MyVertexShader(float4 position : SV_Position) : SV_Position
{
    return position;
}

float4 MyPixelShader() : SV_Target
{
    return float4(1, 1, 1, 1);
}
この意味は、ポリゴンの「頂点はそのままなにも動かさず、色は白にせよ」
です

fxc.exeを使ってシェーダーをコンパイル

さて、このエフェクト(.fx)ファイルはどうやらそのままではプログラムからは使えません。
残念ながら、このファイルをコンパイルするメソッドが
まだWindows API Code Packには実装されていないようなのです。
(DirectX10のほうはまだマシなようですが)

よってDirectX SDKのツール
fxc.exe
を使って手作業でコンパイルすることにします。
最新のDirectX SDKをダウンロードしましょう。

上のMyShader.fxを適当なフォルダに置いてください。
そしてそれと同じフォルダ内に以下のような.batファイルを作ります。
"C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Utilities\bin\x86\fxc.exe" /T vs_4_0 /E MyVertexShader /Fo MyShader.vs MyShader.fx
@pause
"C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Utilities\bin\x86\fxc.exe" /T ps_4_0 /E MyPixelShader /Fo MyShader.ps MyShader.fx
@pause
意味は
「MyShader.fxをコンパイルして、頂点シェーダMyShader.vsとピクセルシェーダMyShader.psを作成せよ」
です。

   
/T 対象とするプロファイル。例えばvs_4_0やps_4_0です。これを入れ違えたり間違ってもfx_4_0などにしないでください。C#側からArgumentExceptionがスローされます。
/E エントリーポイント。頂点シェーダなら頂点シェーダのメソッド名、ピクセルシェーダならピクセルシェーダのメソッド名です。
/Fo 出力ファイル名

.batを実行すると
MyShader.vsとMyShader.psが出来ます。

後はそれをVisual C#のプロジェクトに追加して、
それぞれのプロパティを[Copy if newer]にします。

実行

実行するとこうなります。

directX11TutorialDrawWhiteTriangle.jpg


























拍手[0回]