忍者ブログ

Memeplexes

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

C#でDirectX11 SlimDXチュートリアルその04 三角形の表示(色なし) クラス紹介編

前回使用したクラスを解説します。
三角形を描きたいだけなのにこのクラスの多さといったら異常です。
XNAがいかにありがたいかわかりますね。


Bufferクラス

頂点バッファを使うにはSlimDX.Direct3D11.Bufferクラスを使います。
public class Buffer : Resource

頂点バッファ(VertexBuffer)という名前のクラスはありません。
XNAにはそういう名前のクラスが存在していて、用途別に一つずつクラスが用意されて簡単です。
しかしDirect3D11にはバッファはバッファでクラスはひとつだけ。
Bufferは頂点バッファにも、並列計算するときに使う長いベクトルにも使うことができるのです。
簡単さを取るか柔軟さを取るか。
Direct3D11では後者を取ったようです。

柔軟さと引換にコンストラクタは少し厄介です。

public Buffer(Device device, DataStream data, BufferDescription description);

deviceはバッファーをつくるデバイス。
dataはバッファの初期データ。
descriptionはどういうバッファを作るかを表します。


DataStreamクラス

Bufferのコンストラクタでは初期データとして配列を渡せてもよさそうなものですが(XNAは渡せる設計ですね)、残念ながら渡せません。
かわりにSlimDX.DataStreamクラスを使います。
これはSystem.IO.Streamを継承しています。
配列をストリームとして扱うわけですね。

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

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


BufferDescriptionクラス

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

プロパティ名 説明
BindFlags BindFlags バッファがどういう役割を持っているかを表します。 たとえば頂点バッファとして使われるか(VertexBuffer)?インデックスバッファとして使われるか(IndexBuffer)?定数バッファとして使われるのか(ConstantBuffer)?シェーダーのリソースとして使われるのか(ShaderResource)?ストリームの出力として使われるのか(StreamOutput)?レンダーターゲットとして使われるのか(RenderTarget)?デプスステンシルバッファとして使われるのか(DepthStencil)?アンオーダードアクセス(複数のスレッドから同時に読み書き可能な)リソースとして使われるのか(UnorderedAccess)? ・・・・・・などです
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つです。

今回使うのはこのうちBindFlagsとSizeInBytesです。



Effectの生成

SlimDX.Direct3D11.Effectクラスは頂点バッファのデータの変換され方を司ります。
public class Effect : ComObject

生成にはコンストラクタを使います。
エフェクトの実際の振る舞いはHLSLで記述されています。
そのためHLSLファイルをコンパイルしたものを引数に取るのです。

public Effect(Device device, ShaderBytecode data);

deviceはエフェクトをつくるデバイス。
dataはHLSLで記述されたファイルをコンパイルしたものです。

SlimDX.D3DCompiler.ShaderBytecodeクラスは、ShaderBytecode.CompileFromFile()メソッドを使って生成します。

public static ShaderBytecode CompileFromFile(
    string fileName, 
    string profile, 
    ShaderFlags shaderFlags, 
    EffectFlags effectFlags
    );
fileNameはエフェクトを記述したHLSLファイルの名前。 profileはシェーダーのバージョン(シェーダープロファイル)です。この値によってシェーダー中で使える機能が変わってきます。Direct3D11では5.0です。
shaderFlagsはシェーダーのコンパイルオプションです。今回はNoneです。
effectFlagsはエフェクトのコンパイルオプションです。今回はNoneです。

Effectの構成要素 : EffectTechniqueクラス、EffectPassクラス

さてHLSLでデータの描かれ方を記述したファイルは、
次のような構成になっています。

CompositionOfEffect.jpg

ひとつのEffectにはひとつ以上のTechniqueがあります。
ひとつのTechniqueにはひとつ以上のPassがあります。

ではTechniqueやPassとはなんだ?
とうことですが、Techniqueは一回なにかを描くときに使うモードのようなもの(違うモードで描きたいならテクニックを切り替える)。
Passは描画の一部ということだと思われます。

しかし実際のところTechniqueやPassは1つだけしか無いことがそこそこあったりするので、
Pass=描画方法と考えて問題ないと思います。
Passが頂点バッファを元に描画する方法を規定します。

図形を描く前に、まずPassをオンにする必要があります。
そのためにはPassをTechniqueから、TechniqueをEffectから得る必要があります。
Techniqueを得るにはEffect.GetTechniqueByIndex()メソッドを使います。

public EffectTechnique GetTechniqueByIndex(int index);

indexは取得するテクニックのインデックスです。このインデックスはHLSLのファイルに書いた順番で決まります。最初に描いたテクニックは0です。
戻り値がテクニックです。

テクニックからパスを得るにはEffectTechnique.GetPassByIndex()メソッドを使います。

public EffectPass GetPassByIndex(int index);

indexは取得するパスのインデックスです。このインデックスはHLSLのファイルに描いた順番で決まります。最初に書いたパスは0です。 戻り値は取得するパスです。

こうして得たパスを、描画メソッドを呼ぶ前に使うことをデバイスに教えてあげる必要があります。
「これからこのパスを使って三角形を描きますよ」ということをはっきりさせる必要があります。
それがEffectPass.Apply()メソッドです。

public Result Apply(DeviceContext context);

contextはセットするデバイスのImmediateContextプロパティの値です。

他にも描画を行う上でEffectPassクラスには使うメンバがあります。
インプットレイアウトを初期化するときにパスのシグネイチャが必要なのです。
それにはEffectPass.Descriptionプロパティと、EffectPassDescription.Signatureプロパティを使います。
public EffectPassDescription Description { get; }

public ShaderSignature Signature { get; }
おそらくインプットレイアウトは、シグネイチャのなかの、頂点情報が必要なのだと思われます。


InputLayoutクラスとその構成要素

Bufferは基本的に型情報のないメモリの塊のようなものでした。
Bufferの構造をデバイスに教えるにはSlimDX.Direct3D11.InputLayoutクラスを使います。
public class InputLayout : DeviceChild

コンストラクタは3つの引数を取ります。
頂点バッファのひとつの頂点を構成する要素を引数に取ります。

public InputLayout(Device device, ShaderSignature shaderSignature, InputElement[] elements);

deviceはインプットレイアウトを作るデバイス。 shaderSignatureは使うパスのシグネイチャ。
elementsはひとつの頂点を構成する要素を示す構造体の配列です。

elementsはSlimDX.Direct3D11.InputElement構造体の配列です。
一つ一つが頂点を構成する要素(位置、色など)を表します。

public struct InputElement : IEquatable<InputElement>


この構造体にはプロパティが7つあります。 が、今回使うのはそのうち2つです。

InputElement.SemanticNameプロパティは、頂点を構成する要素の役割を表す文字列です。

public string SemanticName { get; set; }
この文字列はなんでもいいというわけではありません。
すでに決まっているいくつかの中から選びます。
たとえば位置なら"SV_Position"、色なら"COLOR"か"SV_Target"です。

InputElement.Formatプロパティは、頂点を構成する要素のフォーマットを表します。

public Format Format { get; set; }
SlimDX.DXGI.Format列挙型はフォーマットを表します。
これほどの値が用意されています。
今回は32bitのfloatが3つ連なったものを位置として使うので、R32G32B32_Floatを使います。

InputAssembler

描画を行うメソッドの引数は少なく、描くのに使うパラメーターは引数として渡すのではなく、予めセットしておく必要があります。
作ったBufferやInputLayoutはデバイスにセットしなければ描画されません。
その設定を行うのが、SlimDX.Direct3D11.InputAssemblerWrapperクラスです。

public class InputAssemblerWrapper

このクラスのインスタンスは、DeviceContext.InputAssemblerプロパティを通じて入手します。

public InputAssemblerWrapper InputAssembler { get; }

インプットレイアウトをセットするのはInputAssemblerWrapper.InputLayoutプロパティです。

public InputLayout InputLayout { get; set; }
BufferをセットするのはInputAssemberWrapper.SetVertexBuffers()メソッドです。

public void SetVertexBuffers(int slot, VertexBufferBinding vertexBufferBinding);

SetVertexBuffersというふうに複数形の名前ですが、セットするバッファはひとつだけです。
実はSetVertexBuffersという名前のメソッドはもうひとつあり、そちらには複数のバッファをセット出来るのです。

slotはバッファをセットするインデックス。(特殊な用途のため、デバイスには複数のバッファをセット出来ます。今回は1つしかセットしませんが)
vertexBufferBindingはセットするバッファの情報です。

SlimDX.Direct3D11.VertexBufferBindingクラスには3つのプロパティがあります。

プロパティ名 説明
Buffer Buffer セットするバッファです
Offset int バッファの中の最初の頂点のインデックスです。普通0でいいでしょう。
Stride int ひとつの頂点のバイトサイズです

最後に、描画するバッファが三角形の頂点のリストなのか、線分の点のリストなのかということも設定する必要があります。
それにはInputAssemblerWrapper.PrimitiveTopologyプロパティを使います。

public PrimitiveTopology PrimitiveTopology { get; set; }

今回は三角形を描くのでPrimitiveTopology.TriangleListをセットします。
(殆どの場合でそうでしょう)


Drawメソッド

デバイスに、これからどんな図形を描くかという情報を全てセットしたら、
あとはImmediateContext.Drawメソッドを呼びます。
このメソッド自体にはほとんど引数は存在しません。
どんな図形を描くかを示す情報のほとんどはInputAsseblerでセットするためです。

public void Draw(int vertexCount, int startVertexLocation);

vertexCountは描くのに使う頂点の数です。今回は三角形を描くので3です。
startVertexLocationはバッファ中のインデックスで、描く図形の頂点として使い始める位置を表します。これはバッファに三角形一つの座標しか入っていない場合は0です。

四角を描きたい時には四角を2つの三角が合体したものとみなします。
vertexCountは6です。
もちろん同時にバッファに6つの頂点を書きこまなければいけません。
 
もっと複雑な図形を書こうとしたら、vertexCountはもっと大きくなります。


HLSL :設定編

HLSLはC言語ライクな言語です。
といってもなんとなく似ているだけでC言語と互換性はありません。
C#がC言語ライクというのと同程度の妥当性においてHLSLはC言語ライクです。
HLSLで書いたコードは基本、デバイスでしか動きません。
CPUで動くC#とはまた違った思想で動きます。

HLSLにもMainメソッドのようなものはあります。
それがpassです。
passはどのような関数を使って図形を描くかを指定します。
C#側でPass.Apply()を呼ぶとHLSLでpass内に書いた設定で初期化されます。
Mainメソッドと違うところはそのなかで計算をするのではなく、初期化設定だけを行うということです。
ですから本当はMainメソッドというよりコンストラクタに近いと言えるかもしれません。

複数のpassはひとつのtechnique10によってまとめられます。
technique10の「10」はなんでしょうか?
これはDirect3D10の10だと思われます。
Direct3D9世代ではtechniqueだけで10は付いていませんでした。
10世代以降には10が付いているようです

passとtechnique10は次のような構文で書きます。
technique10 TechniqueName < Annotations >
{
    pass PassName 
    { 
       [ SetStateGroup; ]
       [ SetStateGroup; ]
       ...
       [ SetStateGroup; ]
    } 
}

TechniqueNameは省略可能です。
Annotationsは省略可能です。
PassNameは省略可能です。
SetStateGroupでは初期化設定を行います。

passの中では頂点シェーダーピクセルシェーダーを指定しなければいけません。
頂点シェーダーって?ピクセルシェーダーって?
お絵かきに例えると、頂点シェーダーは輪郭線やアタリ、ピクセルシェーダーは色塗りに相当します。

頂点シェーダーは遠近法を司る関数です。
近くにある三角形は大きく、遠くにある三角形は小さくなるよう頂点の位置座標を操作します。
(これはデバイスが自動でやってくれるのではありません。頂点シェーダーの関数の中でプログラムしなくてはいけないのです)
ただ、今回はこれはめんどくさいのでやりません。
三角形はそのままの大きさで表示します。

ピクセルシェーダーは影や質感を司る関数です。
ピクセル一つ一つがどんな色になるかを決定するのです。
ピクセルシェーダーの関数はピクセル一つにつき一回実行されます。
ただ、面倒なので今回は三角形を単に真っ白に塗るだけです。

プログラマは頂点シェーダー関数と、ピクセルシェーダー関数を書きます。
それをpassのなかで「これを使いますよ」とセットするのです。
次のような構文を使います。

SetXXXShader( CompileShader( shader_profile, ShaderFunction( args ) ) );

XXXは、シェーダーの種類です。頂点シェーダーならVertex、ピクセルシェーダーならPixelです。
shader_profileはシェーダーのバージョンです。この値によってシェーダーの中で使える機能が決まります。Direct3D11では5.0がマックスです。
ShaderFunctionはシェーダーの関数名。
argsはそのシェーダー全部で使うパラメーター。省略可能です。


HLSL:シェーダー関数編

シェーダーは関数です。
関数の形は、目的にそったものなら自由です。 最低限の複雑さのシェーダー関数(サンプルがそれです)を考えてみましょう。

頂点シェーダーは頂点を受け取り、頂点を返します。
引数1つ、戻り値一つです。

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

本来は頂点シェーダーの中で頂点情報をいじくりまわします。
いじくりまわして遠近感を出すのです。
が、話を簡単にするためここではpositionをそのまま返しています。
この場合頂点バッファにある三角形がそのままの大きさで描かれます。

一方、ピクセルシェーダーはとりあえず色を返します。
引数はなくて構いません。

float4 MyPixelShader() : SV_Target
{
    return float4(1, 1, 1, 1);
}

本当はもっと引数をとったりして複雑な色の計算をするのですが、
一番シンプルなのは上のような関数です。

「なるほどこの2つはたしかに関数のようだけど見慣れないものが付いているな
SV_PositionとかSV_Targetとはなんだろう。
あとfloat4も」
と思われる方もいるでしょう。

SV_PositionSV_Targetというのは変数や関数の返す値の使い道を表します。
これをセマンティクスといいます。
たとえば

float4 position : SV_Position;

とあるとき、positionは位置情報として扱われることをデバイスに教えます。

float4 MyPixelShader() : SV_Target {..}

は、この関数の戻り値が"SV_Target"(=色)として扱われるということです。

このSV_PositionはC#のコードInputElement.SemanticNameプロパティで指定したものと同じです。
セマンティクスはC#とHLSLを結びつける役割もあるのです。

float4というのはfloatの4次元ベクトルです。
float3にすると3次元ベクトルになります。
int4だとintのベクトルになります。






拍手[0回]

PR