忍者ブログ

Memeplexes

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

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


 前回はウィンドウを青色一色で塗りつぶしました。
Tutorial02ClearRenderTarget.jpg


今回はこれに白い三角形を一つ追加します。


Tutorial03WhiteTriangle.jpg

なぜ三角形なのか?
というと3DCGで表示する複雑な図形は実は三角形の集合体として表せるからです。
ポリゴンというやつです。
三角形がすべての基本なのですね。

追加するもの

今回は白い三角形をひとつ描くだけとはいえ、
たくさんのことをしなければいけません。

次の4つをプログラムに追加しなければいけないのです。

1.Buffer
2.InputLayout
3.Effect
4.Viewport

それぞれ単語が抽象的すぎてよくわかりませんね
一体何を意味しているのでしょうか?

ざっと説明すると「Bufferは三角形の頂点、InputLayoutは頂点情報の解釈方法、Effectは頂点情報の変換のされ方、Viewportはウィンドウのどこに絵を描くか」となります。
が、もう少していねいに説明しましょう。


頂点バッファ

Bufferは描く三角形の頂点情報を格納します。

これは今回やることを整理してみると分かりやすくなります。
今回やること、それは三角形の表示です。

Tutorial03WhiteTriangle.jpg

三角形を表示するには頂点の座標が必要です。

Tutorial03WhiteTriangleWithCoordinate2D.jpg

ところが実際には3DCGをやるので、頂点座標はZ軸方向も含めて3次元が必要です。
(※Z座標は0とします。)

ec2b0c9c.jpg

さて、この3つの頂点の情報はデバイス内に格納しておきます。 これがBufferです。
デバイスはバッファの中の情報を解釈し三角形を描くのです。

Tutorial03VertexBuffer.jpg
Bufferはデバイス内に確保されたメモリです。
(これはC#のオブジェクトのような型情報を持つ高級なものよりもC言語のmallocで確保されるような低レベルなメモリの塊に似ています。)
頂点の情報を格納しているので、これを特に「頂点バッファ」といいます。
頂点以外のバッファもあり、それは違った呼び方をしますが、ここでは扱いません。

「なぜBufferはデバイスの中にあるのか?デバイスの中にあるということにどんな意味があるのか?」
デバイスが使う情報は(なるべく)デバイスの中になければいけません。
CPUからデバイスへはあまりデータを大量に送れないからです。
ここでは三角形一つだけなのでそう負担はないのですが、
3DCGでは大量のポリゴンでできたモデルを描く場合が多いのです。
ポリゴンの情報はCPUの管理するシステムメモリ上ではなく、デバイスの中になければいけないのです。

なお、複雑な図形、例えば四角形を描こうと思ったら、
四角形を2つに分割して2つの三角形にします。
三角形の座標を続けて2つバッファに格納するのです。
つまり合計6つの頂点をバッファに書き込むことになります。

InputLayout

(InputLayoutは一つ一つの頂点にどのような情報が含まれているかを表します。)

さて、今回はこれで話は終わるのですが、場合によっては三角形に色をつけたい場合があります。


Tutorial04ColoredTriangle.jpg

このケースでは頂点バッファの中に位置座標だけではなく、色情報も格納しなければいけません。


Tutorial03VertexBufferColored.jpg


この図では「位置」とか「色」とか描いてありますが現実はそう甘くありません。
頂点バッファには、そのデータをどう解釈すればいいのかは含まれないのです。
単に{0, 0.5, 0, 1, 1, 1, 0.5, 0, 0, 0, 0, 1, -0.5, 0, 0, 1, 0, 0}というデータの列のみが存在します。
型情報のないメモリの塊のようなものです。

これではデバイスはどんな図形を描けばいいのかわかりません。
データはあるけれど、その解釈の方法がわからないのです。
解釈の方法はプログラマが明示的に指定してやる必要があります。


それがInputLayoutです。
InputLayoutには頂点情報のレイアウトが書いてあり、それにしたがってデバイスは頂点バッファを解釈し、図形を描くのです。

Tutorial03VertexBufferColoredWithInputLayout.jpg



Effect

Effectは、これは頂点バッファ内のデータの変換され方を表します。
変換とは何かというと、たとえば遠近法のようなものです。
3DCGでは近くにあるものは大きく、遠くにあるものは小さくしなければいけません。
そういった座標の変換を施して初めて立体的な3DCGになります。
絵を描く時に必要な頂点データの変換方法を指定するのです。
(ただ、今回はそれはちょっと大変なので遠近法なしのそのままのサイズで描きますが)

また、Effectは他のこともやります。
遠近法がデッサンでいうアタリなら、色塗り、質感の表現に相当することもEffectの責任です。
上では三角形に色がついたりしていますが、この色の指定もEffectがやるのです。
Effectのやることを一言で言うと「頂点バッファを使って絵を描く方法を指定している」といったところでしょうか?

このようにEffectは複雑なことをやってくれますが、これをC#のオブジェクトの設定などでやろうとするとやや難しいです。
Direct3D11では、これはC++とかC#ではなく、ひとつの独立した言語で行います。
HLSL(Hight Level Shader Language)という言語です。
プログラマはHLSLでエフェクトを記述し、そのソースコードをコンパイルしてC#でEffectクラスのオブジェクトを初期化します。

つまり、3DCGを表示しようとしたら、C#とHLSLの2つの言語を使わなければいけないということです。
めんどうくさいですね。

Viewport

最後に残ったViewportは三角形をウィンドウのどこに描くかを表しています。
もうちょっと正確に(でも分かりにくく)言うと描画先の範囲といったところでしょうか。
図で表すと次のような情報を持ちます。

viewport.jpg  つまり3次元の箱を表す情報を持っています。
合計6つのプロパティです。
どこから始まって(3つ)どこで終わるか(3つ)という情報です。

この箱が一体何を表しているのかということですが、
ウィンドウ先の、3DCGが描かれる領域です。
普通はWidth, Heightをウィンドウのクライアント領域のサイズと同じにしておきます。
つまりこのように表示されるのです。

Tutorial03WhiteTriangle.jpg

たとえばここでWidthを半分にすると、
こうなります。

Tutorial03WhiteTriangleViewportWidthHalfed.jpg

ウィンドウ上の描かれる領域の横幅が半分になったのです。

Tutorial03WhiteTriangleViewportWidthHalfedWithVisibleViewport.jpg

さてここでXをWidthと同じにしてみましょう。
するとこうなります:

b96b25a7.jpg

Xを大きくすると、三角形は右にずれていきます。
ここではX = Widthなので、ちょうど右半分に表示されているのですね。

では「MaxZとMinZはなんなのか。ウィンドウ内にZ座標なんて無いじゃないか」ということですが、
これは深度バッファという、「複数の三角形を重ねて表示するとき、どちらが前でどちらか後ろか判定して正しく表示するバッファ」を使ったときに意味を持ちます。
今回は深度バッファを使わないので両方共指定しなくてかまいません。
普通はMinZ = 0, MaxZ = 1でしょう。

withoutDepthBuffer.jpg

今回は問題ありませんが、深度バッファがないとおかしなことが起きることもあります。
上の2つは同じ大きさの三角形です。
片方の三角形は近くにあり、もう片方は遠くにあります。
深度バッファを使っていないので、遠くにある三角形が手前の三角形より前に表示されてしまっています。
しかし深度バッファを使えば、きちんと遠くにある三角形が手前の三角形に隠れます。

コード

今回、コードはC#とHLSLの2種類必要です。 (Program.csとmyEffect.fx)


Program.cs

using SlimDX.Direct3D11;
using SlimDX.DXGI;
using SlimDX.D3DCompiler;
 
class Program
{
    static void Main()
    {
        using (Game game = new MyGame())
        {
            game.Run();
        }
    }
}
 
class MyGame : Game
{
    Effect effect;
    InputLayout vertexLayout;
    Buffer vertexBuffer;
 
    protected override void Draw()
    {
        GraphicsDevice.ImmediateContext.ClearRenderTargetView(
            RenderTarget,
            new SlimDX.Color4(1, 0, 0, 1)
            );
 
        initTriangleInputAssembler();
        drawTriangle();
        
        SwapChain.Present(0, PresentFlags.None);
    }
 
    private void drawTriangle()
    {
        effect.GetTechniqueByIndex(0).GetPassByIndex(0).Apply(GraphicsDevice.ImmediateContext);
        GraphicsDevice.ImmediateContext.Draw(3, 0);
    }
 
    private void initTriangleInputAssembler()
    {
        GraphicsDevice.ImmediateContext.InputAssembler.InputLayout = vertexLayout;
        GraphicsDevice.ImmediateContext.InputAssembler.SetVertexBuffers(
            0,
            new VertexBufferBinding(vertexBuffer, sizeof(float) * 3, 0)
            );
        GraphicsDevice.ImmediateContext.InputAssembler.PrimitiveTopology
            = PrimitiveTopology.TriangleList;
    }
 
    protected override void LoadContent()
    {
        initEffect();
        initVertexLayout();
        initVertexBuffer();
    }
 
    private void initEffect()
    {
        using (ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "myEffect.fx", "fx_5_0",
            ShaderFlags.None,
            EffectFlags.None
            ))
        {
            effect = new Effect(GraphicsDevice, shaderBytecode);
        }
    }
 
    private void initVertexLayout()
    {
        vertexLayout = new InputLayout(
            GraphicsDevice,
            effect.GetTechniqueByIndex(0).GetPassByIndex(0).Description.Signature,
            new[] { 
                    new InputElement
                    {
                        SemanticName = "SV_Position",
                        Format = Format.R32G32B32_Float
                    }
                }
            );
    }
 
    private void initVertexBuffer()
    {
        vertexBuffer = MyDirectXHelper.CreateVertexBuffer(
            GraphicsDevice,
            new[] {
                new SlimDX.Vector3(0, 0.5f, 0),
                new SlimDX.Vector3(0.5f, 0, 0),
                new SlimDX.Vector3(-0.5f, 0, 0),
            });
    }
 
    protected override void UnloadContent()
    {
        effect.Dispose();
        vertexLayout.Dispose();
        vertexBuffer.Dispose();
    }
}
 
class Game : System.Windows.Forms.Form
{
    public SlimDX.Direct3D11.Device GraphicsDevice;
    public SwapChain SwapChain;
    public RenderTargetView RenderTarget;
 
 
    public void Run()
    {
        initDevice();
        SlimDX.Windows.MessagePump.Run(this, Draw);
        disposeDevice();
    }
 
    private void initDevice()
    {
        MyDirectXHelper.CreateDeviceAndSwapChain(
            this, out GraphicsDevice, out SwapChain
            );
 
        initRenderTarget();
        initViewport();
 
        LoadContent();
    }
 
    private void initRenderTarget()
    {
        using (Texture2D backBuffer
            = SlimDX.Direct3D11.Resource.FromSwapChain<Texture2D>(SwapChain, 0)
            )
        {
            RenderTarget = new RenderTargetView(GraphicsDevice, backBuffer);
            GraphicsDevice.ImmediateContext.OutputMerger.SetTargets(RenderTarget);
        }
    }
 
    private void initViewport()
    {
        GraphicsDevice.ImmediateContext.Rasterizer.SetViewports(
            new Viewport
            {
                Width = ClientSize.Width,
                Height = ClientSize.Height,
            }
            );
    }
 
    private void disposeDevice()
    {
        UnloadContent();
        RenderTarget.Dispose();
        GraphicsDevice.Dispose();
        SwapChain.Dispose();
    }
 
    protected virtual void Draw() { }
    protected virtual void LoadContent() { }
    protected virtual void UnloadContent() { }
}
 
class MyDirectXHelper
{
    public static void CreateDeviceAndSwapChain(
        System.Windows.Forms.Form form,
        out SlimDX.Direct3D11.Device device,
        out SlimDX.DXGI.SwapChain swapChain
        )
    {
        SlimDX.Direct3D11.Device.CreateWithSwapChain(
            DriverType.Hardware,
            DeviceCreationFlags.None,
            new SwapChainDescription
            {
                BufferCount = 1,
                OutputHandle = form.Handle,
                IsWindowed = true,
                SampleDescription = new SampleDescription
                {
                    Count = 1,
                    Quality = 0
                },
                ModeDescription = new ModeDescription
                {
                    Width = form.ClientSize.Width,
                    Height = form.ClientSize.Height,
                    RefreshRate = new SlimDX.Rational(60, 1),
                    Format = Format.R8G8B8A8_UNorm
                },
                Usage = Usage.RenderTargetOutput
            },
            out device,
            out swapChain
            );
    }
 
    public static Buffer CreateVertexBuffer(
        SlimDX.Direct3D11.Device graphicsDevice,
        System.Array vertices
        )
    {
        using (SlimDX.DataStream vertexStream 
            = new SlimDX.DataStream(vertices, true, true))
        {
            return new Buffer(
                graphicsDevice,
                vertexStream,
                new BufferDescription
                {
                    SizeInBytes= (int)vertexStream.Length,
                    BindFlags = BindFlags.VertexBuffer,
                }
                );
        }
    }
}
 


myEffect.fx (HLSLのコード)
float4 MyVertexShader(float4 position : SV_Position) : SV_Position
{
    return position;
}
 
float4 MyPixelShader() : SV_Target
{
    return float4(1, 1, 1, 1);
}
 
technique10 MyTechnique
{
pass MyPass
{
SetVertexShader( CompileShader( vs_5_0, MyVertexShader() ) );
SetPixelShader( CompileShader( ps_5_0, MyPixelShader() ) );
}
}


このプログラムを実行すると、こうなります。

Tutorial03WhiteTriangle.jpg

白い三角形が表示されました!

このプログラムは、まず頂点バッファに三角形の頂点である{{0, 0.5, 0}, {0.5, 0, 0}, {-0.5, 0, 0}}を格納します。
そしてインプットレイアウトに頂点バッファの解釈方法をセットします。
「頂点はFloatが3つ集まって、位置情報を表している」と教えてやるのです。
エフェクトは、myEffect.fxを読み込んでコンパイルし、頂点バッファの情報を加工してウィンドウに出力します。
myEffect.fxにはなんと書いてあるかというと、「頂点バッファからの位置データは何もいじらず、そのまま白い三角形を描け」です。
最後には無事白い三角形が表示されました。


APIの紹介は次回に続きます。

拍手[1回]

PR