忍者ブログ

Memeplexes

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

[PR]

×

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


かんたんXNA4.0 HLSL編 その1 エフェクトファイル

HLSLとは?

High Level Shader Language (HLSL)とは頂点データを
どのように処理して描画するかを表現するための特別な言語です。
この言語で書いたテキストファイルをコンパイルして、
C#でEffectクラスからコントロールするのです。
(BasicEffectのかわりです。というか、BasicEffect自体Effectから派生しています)

MSDNの記事には、これは上位レベルの言語で、
さらにC言語スタイルがベースになっててプログラマにとってなじみやすい
というような趣旨のことが書いてありますが、
しょうじきなところ真実はまったくの逆であるような気がします。
C言語ライクなのはたしかですが、C#やjavaになれているプログラマからすれば
かなり低レベルでしかもとっつきにくいように思えます。
(まぁでもHLSL以前では、アセンブリ言語をつかわなくてはならなかったため、
これでもまだマシになったほうなのです。)


ではなぜそんな言語を使わなければならないのでしょうか?
それはこの言語を使うと、C#だけを使うときより、
描画のやりかたをより細かくコントロールできるからです。
C#でBasicEffectを使うときよりより高度な描画ができるのです。
(そう、HLSLでやるのはBasicEffectとおなじです。
ただし、もう少し、細かい設定ができるのです。)


ここで少し復習です。
これまでかんたんXNAでは、頂点データをどのように描画するかを
BasicEffectクラスでコントロールしていました。
BasicEffectに頂点データをつっこんで、いろいろ処理させて、
ピクセルごとの色を計算し、ディスプレイに表示していたのです。
ところてんのかたまりをあの四角い器具に押しこんで
反対側から完成品の細長いところてんをニューとひねりだすようなかんじです。
データを突っ込み変形させていたのです。

つまり、DrawUserPrimitivesの引数に入れられた頂点データの
絶対座標をカメラからの相対座標に変換し、
それをさらに遠近法を考慮してデータ全体をゆがめ、
ライトの方向を考えて一部を暗くしたりし、
また質感をあらわすデータから色を微妙に調整し、
クスチャのデータなんかをまぜてピクセルの色を決めていたのです。
これはようするに、頂点データをピクセルごとの色に変換する
コンバータの一種と考えてもいいでしょう。

これでも十分に面白いゲームは作れるでしょう。
ただ、もう少し複雑なエフェクトを使いたいというときもあるのではないでしょうか。
たとえば画像をぼかしたりトゥーンレンダリングしたりと
ピクセルごとの色を綿密に制御したいような場合です。

そこでBasicEffectではなく、より細かいことのできるHLSLを使います。
HLSLで頂点データからピクセルの色への変換の仕方を記述し、
そのテキストファイルをContentManagerで読み込み、
Effectクラスのオブジェクトを作るのです。
その後はBasicEffectと同じようなノリで描画を行います。

HLSLの骨組み

HLSLでエフェクトファイルを書くにあたって必ず必要なのが、
テクニックパスの設定です。
このテクニックとパスというのは、BasicEffectで出てきた
CurrentTechniqueプロパティやEffectPassクラスが意味するものと同じです。
テクニックはパスを集めたもので、パスは描画の方法の設定を表すものです。
(テクニックもパスも複数書くことができますが、
ここでは話を簡単にするため1つだけの場合を見ます)


technique テクニック名
{
        pass パス名
        {
        }
}


このパスの中に書くのは、ちょうどコンストラクタや、
クラス内での変数の初期化のようなもので、
つまり設定の初期化です。
このHLSLでどのような描画をするかの設定を行うのです。

では具体的にどのような設定が必要なのでしょうか?
何とか動くようにするためには頂点シェーダピクセルシェーダの2つを
セットする必要があります。

HLSLで記述するのは頂点データから各ピクセルの色を決定する方法です。
じつはこれは一度に行われるのではなく、二つのステップにわけられます。

ひとつめはこのエフェクトに流し込む頂点データをべつの頂点データに変換します。
ここでは、たとえば絶対座標をカメラからの相対座標にしたり、
それぞれの頂点に遠近法の効果を適用して位置のデータを変更したりというふうに、
頂点を頂点に変換します。
入ってくるデータは頂点データで、出て行くデータも頂点データです。
これを頂点シェーダ(VertexShader)といいます。関数の形で表されます。

もうひとつは、各ピクセルの情報からそのピクセルの
色を決めるステップです。
この変換では引数はそのピクセルのテクスチャ座標だったり、乱反射の色だったり、
あるいは別の何かだったりします。
戻り値はそのピクセルの描画される色です。
これをピクセルシェーダ(PixelShader)といいます。関数の形で表されます。

この二つのセットの仕方はこんな感じです。

technique テクニック名
{
        pass パス名
        {
                VertexShader = compile vs_2_0 頂点シェーダ名();
                PixelShader = compile ps_2_0 ピクセルシェーダ名();
        }
}


気分的にはC#のデリゲートのセットと似た感じです。
vs_2_0とかps_2_0とか言うのがありますが、これは機能のバージョンをあらわします。
たとえば頂点シェーダのバージョンが3.0ならvs_3_0です。
(vs : VertexShader, ps : PixelShader)

MyEffect.fx
float4 MyVertexShader(float4 position : POSITION) : POSITION
{
	return position;
}

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

technique MyTechnique
{
	pass MyPass
	{
		VertexShader = compile vs_2_0 MyVertexShader();
		PixelShader = compile ps_2_0 MyPixelShader();
	}
}

MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

class MyGame : Game
{
    GraphicsDeviceManager graphics;

    Effect effect;
    VertexPositionColor[] vertices = {
            new VertexPositionColor(new Vector3(0, 1, 0), Color.White),
            new VertexPositionColor(new Vector3(1, 0, 0), Color.Blue),
            new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red)
        };


    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void LoadContent()
    {
        effect = Content.Load<Effect>("Content/MyEffect");
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        foreach (var pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>
            (
                PrimitiveType.TriangleList,
                vertices,
                0,
                vertices.Length / 3
            );
        }
    }
}

customEffectWithoutColor.jpg
これがたぶんHLSLを使った最も簡単なプログラムのひとつなのではないでしょうか。
ここでは三角形を表示していますが、頂点データに指定した情報と違って、
色は真っ白になっています。(ほんとうは白、青、赤になるはず)
HLSLで色は無視して全部白にしてしまうようにプログラムしたからです。

MyEffect.fxのピクセルシェーダ、MyPixelShader関数を見てください。
これはすべてのピクセルの色を白にするということを意味しています。

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

"return float4(1, 1, 1, 1)"というのがそのピクセルの色です。
もしこれを"return float4(1, 0, 0, 1)"にすればすべてのピクセルは赤くなり
赤い三角形が表示されるでしょう。

ピクセルシェーダというのは普通引数にそのピクセルの情報を持っていてそれを元にピクセルの色をはじき出すのですが、
ここでは話を簡単にするために引数は省いています。
ほんとうなら引数としてテクスチャ座標やディフューズ色のデータが入ってきます。

ところで、関数名の横についている" : COLOR"というのが気になるかもしれません。
これは何を意味するかというと、この関数の戻り値が何なのかということです。
戻り値のデータの使われる目的、データ型のようなものです。
(しかしややこしいことに、データ型はちゃんと別にあるのです。float4がそれです。)
あるいは、ここにくるのはVertexElementUsage列挙型の
やつと同じようなものといえばわかりやすいかもしれません。
これは「セマンティクス」というもので、実は戻り値だけではなく、
頂点シェーダやピクセルシェーダの引数にもくっつきます。
これはグラフィックスデバイスがデータを識別するために使っているらしく、
これをつけておけば、たとえば引数の順番を入れ替えても平気です。
つけ方はこんな感じ:

データ型 変数名 : セマンティクス

つぎに頂点シェーダ、MyVertexShader関数を見てください。

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

これは頂点から頂点への変換を表す関数で、
ふつうなら位置の情報に遠近法を適用して結果を返したりするのですが、
ここでは話を簡単にするため位置データをそのまま返しています。

そのため描画した結果は3Dとは言いがたいものになっています。

セマンティクスは引数も戻り値も"POSITION"です。
つまり、位置のデータを扱っているということを意味しています。
ほんとうなら引数には色のデータも来るはずでした。
VertexPositionColorを頂点として使っているわけですからね。
しかしこのサンプルでは話を簡単にするために省きました。
(省くこともできるのです!)

そして戻り値についても同じです。
ほんとうなら戻り値は構造体になっていて、色のデータも含んでいるはずでした。
しかし構造体の話をするとややこしくなるため、シンプルに位置のデータだけにしたのです。

エフェクトファイルについての説明はこのくらいでいいでしょう。
このようにしてHLSLで作ったエフェクトファイルをC#からContentManagerで読み込み、
Effectクラスを使って操作するのです。
やりかたはBasicEffectとおなじです。

拍手[4回]

PR

かんたんXNA4.0 その29 カスタム頂点

さて、ここまで、頂点データに使う構造体は
あらかじめXNAに用意されたものを使っていました。

XNAに初めから用意されている頂点の構造体には
以下の4つがあります。
それぞれの名前はそのメンバを反映しています。
(いずれもMicrosoft.Xna.Framework.Graphics名前空間にあります)

名前 座標 テクスチャ座標 法線
VertexPositionColor × ×
VertexPositionColorTexture ×
VertexPositionNormalTexture ×
VertexPositionTexture × ×

しかし、ManagedDirectXでは14個用意されていたことを考えると、
「以下の4つがあります」というより
「以下の4つしかありません」という方が適切でしょう。
だいたいVertexPositionNormalColorが無いってどういうことですか!
初学者を殺す気ですか!?

しかしながら用意されていないからといって泣き寝入りする必要はありません。
頂点の構造体は自分で新たに作ることが出来るからです。
VertexPositionNormalColorはもちろん、
さまざまなタイプの頂点を作ることが出来るのです。

といっても頂点のメンバの型は何でもいいわけではなく、
Microsoft.Xna.Framework.Graphics.VertexElementFormat列挙体
で定義された型のものだけに限られます。
(型というか正確には「メモリ上にどうデータが入るか」です。
メモリの配置が同じなら型は同じでなくてもかまいません。)

まぁstringやDateTimeをメンバに持った頂点を作ってもあまり意味無いでしょうしね。

public enum VertexElementFormat

メンバ
メンバ名 説明
Single floatです。(32bit)
Vector2 Vector2構造体です。
Vector3 Vector3構造体です。
Vector4 Vector4構造体です。
HalfVector2 16bitの浮動少数点数が2つです。(VertexShaderのバージョンが2.0以上でなければなりません)
HalfVector4 16bitの浮動小数点数が4つです。(VertexShaderのバージョンが2.0以上でなければなりません)
Color Color構造体です。
NormalizedShort2 符号付き16bit整数が2つです。それぞれ小数に展開されて0~1の範囲内の大きさになります。
NormalizedShort4 符号付き16bit整数が4つです。それぞれ小数に展開されて0~1の範囲内の大きさになります。
Short2 符号付き16bit整数が2つです。
Short4 符号付き16bit整数が4つです。
Byte4 unsinged byteが4つです。

以上のデータを頂点の構造体のメンバにすることができます。
構造体のメンバを定義したら、今度は
Microsoft.Xna.Framework.Graphics.VertexElement構造体の
配列を作る必要があります。

[Serializable]
public struct VertexElement


これは頂点の構造体がどんなメンバを持っているかを意味していて、
ハードウェアが頂点データを扱うのに役立ちます。
つまり、VertexPositionColor.VertexElements静的フィールドのようなものを
自分で作る必要があるのです。

コンストラクタはかなりフクザツです。

public VertexElement (
        short offset,
        VertexElementFormat elementFormat,
        VertexElementUsage elementUsage,
        byte usageIndex
)


offsetはメンバの構造体内での位置です。(バイト単位)
最初のメンバはこれは0で、次のメンバのは最初のメンバのサイズで、
その次のは・・・という具合にどんどん増えていきます。

elementFormatはデータのサイズを意味する列挙体、
先ほど説明したVertexElementFormatです。

elementUsageは最重要で、これはデータの利用目的です。PositionとかColorとかNormalとかそういうやつです。これは単なる印、C#でいう属性のようなもので、ここで決めたとおりにメンバを使う必要は必ずしもありませんが、特に理由がない限り正直に入力すべきでしょう。保守が難しくなるかもしれませんし。

VertexElementUsage
メンバ名 説明
Position  頂点の位置です。
Normal  法線です。
Color  色です。UsageIndexが0の時にはディフューズの色を表し、UsageIndexが1の時にはスペキュラの色を表します。
TextureCoordinate  テクスチャの座標です。
PointSize  ポイントスプライトの大きさです。
Depth  深度です。
Sample  サンプラーデータです。
Fog  フォグに使うデータです。
Binormal  従法線ベクトル(接ベクトル×法線ベクトル、接線と法線の両方に対して垂直です)データです。
BlendIndices  ブレンディングのインデックスのデータです。
BlendWeight  ブレンディングに使われる重みのデータです。
Tangent  接線ベクトルです。
TessellateFactor  テセラレーションに使われる浮動小数点数です。


usageIndexは1つの構造体の中で、別々のメンバが同じVertexElementUsageを指定した場合に、お互いを識別するために使います。例えばポリゴンに複数のテクスチャを使いたくなったとしましょう。そういった場合、両方ともelementUsageがTextureCoordinateになってしまうのですが、これが困るのです。ハードウェアとしては、別々のメンバが同じ利用目的を持っていてはそれぞれの区別が出来ないので困るのです。そこで、問題を解決するためにインデックスを振ります。片方のテクスチャ座標をTexCoord0、もう片方をTexCoord1とでもすれば区別が出来るようになり、問題解決です。この引数に指定するのは、そのインデックスです。普通は0でいいでしょう。

こうして作ったVertexElementの配列をVertexDeclarationのコンストラクタに突っ込みます。
出来たVertexDeclarationのインスタンスはIVertexType.VertexDeclarationプロパティで返してあげましょう。
この「頂点の宣言」はGraphicsDeviceがポリゴンを描画するのに必要なのです。



で、VertexPositionNormalColorを作るとしたらこんな感じになります。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


public struct VertexPositionNormalColor:IVertexType
{
    public Vector3 Position;
    public Vector3 Normal;
    public Color Color;

    public VertexPositionNormalColor(Vector3 position, Vector3 normal, Color color)
    {
        this.Position = position;
        this.Normal = normal;
        this.Color = color;
    }

    public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration
        (
            new VertexElement(
                0,
                VertexElementFormat.Vector3,
                VertexElementUsage.Position,
                0
            ),
            new VertexElement(
                sizeof(float) * 3,
                VertexElementFormat.Vector3,
                VertexElementUsage.Normal,
                0
            ),
            new VertexElement(
                sizeof(float) * (3 + 3),
                VertexElementFormat.Color,
                VertexElementUsage.Color,
                0
            )
        );

    VertexDeclaration IVertexType.VertexDeclaration
    {
        get { return VertexDeclaration; }
    }
}

これを実際に使うのはあらかじめ用意された頂点のやり方と変わりません。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect basicEffect;

    VertexPositionNormalColor[] vertices =
    {
        new VertexPositionNormalColor(new Vector3(0, 1, 0),new Vector3(0, 0, 1), Color.Red),
        new VertexPositionNormalColor(new Vector3(1, 0, 0),new Vector3(0, 0, 1), Color.Red),
        new VertexPositionNormalColor(new Vector3(-1, 0, 0),new Vector3(0, 0, 1), Color.Red)
    };

    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void LoadContent()
    {
        basicEffect = new BasicEffect(GraphicsDevice)
        {
            VertexColorEnabled = true,
            View = Matrix.CreateLookAt
            (
                new Vector3(0, 0, 3),  //カメラの位置
                new Vector3(0, 0, 0),   //カメラの見る点
                new Vector3(0, 1, 0)    //カメラの上向きベクトル
            ),
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                GraphicsDevice.Viewport.AspectRatio,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };

        basicEffect.EnableDefaultLighting();
    }

    protected override void UnloadContent()
    {
        basicEffect.Dispose();
    }

    protected override void Update(GameTime gameTime)
    {
        basicEffect.World *= Matrix.CreateRotationY(MathHelper.ToRadians(1));
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        foreach (var pass in basicEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            GraphicsDevice.DrawUserPrimitives<VertexPositionNormalColor>
            (
                PrimitiveType.TriangleList,
                vertices,
                0,
                vertices.Length / 3
            );
        }
    }
}

vertexPositionNormalColor1.JPGvertexPositionNormalColor2.JPG
このサンプルでは法線付きの赤い三角形を回転させています。
角度によって明るさが変わるのがわかると思います。
これは法線があるからです。
法線のないVertexPositionColorではライティングに意味はありません。
法線つきのVertexPositionNormalColorを作ることによって、
ライティングの出来る色つきポリゴンを作ったのです。

拍手[0回]


かんたんXNA4.0 その28 テクスチャへの描画 RenderTarget

今まではウィンドウの画面にだけポリゴンを描画していましたが、
XNAでは実はテクスチャに対して描画することも出来ます。

これによりテクスチャを動的に作り出すことができ、
例えばゲームの中のアイテムとしてパソコンを作って、その画面を動かしたり出来るでしょう。
テクスチャに対していろいろ描画して、
それをゲームの中のパソコン画面に貼り付ければいいのです。



この、描画の対象となるテクスチャを
レンダーターゲットといいます。
クラスはMicrosoft.Xna.Framework.RenderTarget2Dです。

public class RenderTarget2D : Texture2D

このクラスのインスタンスを作成し、GraphicsDeviceにセットすると、
これまでと違って、画面ではなくこのレンダーターゲットにたいして
描画されるようになります。

コンストラクタを見てみましょう。

public RenderTarget2D (
        GraphicsDevice graphicsDevice,
        int width,
        int height
)


graphicsDeviceはグラフィックスデバイスです。
widthheightは作成するレンダーターゲットのテクスチャのサイズを表します。

このコンストラクタによって作成したレンダーターゲットを
GraphicsDeviceにセットするには、
GraphicsDevice.SetRenderTargetメソッドを使います。

public void SetRenderTarget (
        RenderTarget2D renderTarget
)


renderTargetはセットするレンダーターゲットです。



using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect basicEffect;

    VertexPositionColor[] vertices =
    {
        new VertexPositionColor(new Vector3(0, 1, 0), Color.White),
        new VertexPositionColor(new Vector3(1, 0, 0), Color.Blue),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red)
    };

    RenderTarget2D renderTarget;
    SpriteBatch spriteBatch;

    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void LoadContent()
    {
        basicEffect = new BasicEffect(GraphicsDevice)
        {
            VertexColorEnabled = true,
            View = Matrix.CreateLookAt
            (
                new Vector3(0, 0, 5),  //カメラの位置
                new Vector3(0, 0, 0),   //カメラの見る点
                new Vector3(0, 1, 0)    //カメラの上向きベクトル
            ),
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                400/200,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };

        spriteBatch = new SpriteBatch(GraphicsDevice);
        renderTarget = new RenderTarget2D(GraphicsDevice, 400, 200);
    }

    protected override void UnloadContent()
    {
        basicEffect.Dispose();
    }

    protected override void Draw(GameTime gameTime)
    {
        renderToRenderTarget();

        GraphicsDevice.Clear(Color.CornflowerBlue);
        spriteBatch.Begin();
        spriteBatch.Draw(renderTarget, new Rectangle(0, 0, 400, 200), Color.White);
        spriteBatch.End();
    }

    private void renderToRenderTarget()
    {
        GraphicsDevice.SetRenderTarget(renderTarget);
        GraphicsDevice.Clear(Color.Gray);

        foreach (var pass in basicEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>
            (
                PrimitiveType.TriangleList,
                vertices, 
                0,
                vertices.Length / 3
            );
        }

        GraphicsDevice.SetRenderTarget(null);
        
    }
}
renderTarget.JPG
このサンプルでは、まず400x200のテクスチャに対して三角形を描画した後、
そのテクスチャをさらにSpriteBatchで画面に描画しています。

レンダーターゲットをGraphicsDeviceにセットして、そのテクスチャをクリア、三角形をを描画します。
その後、その描画を解決、GraphicsDeviceからレンダーターゲットを取り除きます。
(これをやらないとウィンドウに何も映らなくなります。
その後の描画が全て画面ではなくレンダーターゲットに行われるのです)


これだけではビューポートを使った描画と区別がつかないかもしれません。
しかし、こちらはビューポートを使う場合よりもずっと柔軟で、用途が広いのです。

まず、ここではSpriteBatchで2Dとして描画していますが、
実際には3Dのモデルのテクスチャとして使ってもかまいません。
例えば斜めに傾いたモデルに貼り付ければ、そのモデルが実はディスプレイで、
そのテクスチャを表示しているように見えるでしょう。
これはビューポートには出来ない芸当です。

また、レンダーターゲットは光加減やぼやけ具合みたいな描画の効果を表現するのに利用されたりもします。
まずレンダーターゲットに対して何か描画して、
その後そのテクスチャを参考にして光やぼやけ具合などを計算するといった具合です。
XNA Creators Clubのサンプル、BloomPostprocessSampleもそのようなことをやっています。

このように、レンダーターゲットはかなりいろいろなことに使えるのです!

拍手[2回]


かんたんXNA4.0 その27 ビューポートで分割スクリーン

今回はゲーム画面を2つに分割する方法です。

2人のプレイヤーでゲーム対戦する場合を考えましょう。
その場合、2Dならともかく、3Dなら
プレイヤーによって画面が2つに分かれているべきでしょう。
画面が左と右の二つに分かれていて、
プレイヤーはそれぞれ自分の画面を見て相手を倒すのです!

そういったことをする場合、ゲームのモデルを全て
それぞれの画面に1回ずつ、計2回描画することになります。

その際、単に描画したのでは二人の画面が重なってしまいます。
別々の場所にモデルを描画しなければなりません。

そのように、別々の場所に描画するのに使うのが、ビューポートです。
これを使うと、描画する領域の位置や大きさを変更することが出来ます。




ビューポートはGraphicsDevice.Viewportプロパティで設定します。

public Viewport Viewport { get; set; }

このプロパティが扱っているのは
Microsoft.Xna.Framework.Graphics.Viewport構造体です。

[Serializable]
public struct Viewport


この構造体はモデルを描画する画面の位置やサイズを
表すプロパティを持っています。

public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
...


この構造体があらわす画面は普通は
ウィンドウのクライアントエリアと同じです。
しかし、今回のように半分に分割したい場合には
プロパティをいろいろと変更しないといけないでしょう。

  左スクリーン 右スクリーン
X 0 左スクリーンの横幅
Width 画面の横幅の半分 画面の横幅の半分

また、分割スクリーンをやるさいには、ビューポート以外にも
変更しなければいけないところがあります。
それはBasicEffect.Projectionプロパティです。
幅を半分にしたので、このプロパティにセットするマトリックスも
変えなければならないのです。
描画する画面の四角形の形をあらわすアスペクト比(横/縦)
を半分にしなければなりません。
そうしなければ、表示するモデルが横に2倍に引き伸ばされてしまいます。


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;



class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect basicEffect;

    VertexPositionColor[] vertices =
    {
        new VertexPositionColor(new Vector3(0, 1, 0), Color.White),
        new VertexPositionColor(new Vector3(1, 0, 0), Color.Blue),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red)
    };

    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void LoadContent()
    {
        basicEffect = new BasicEffect(GraphicsDevice)
        {
            VertexColorEnabled = true,
            View = Matrix.CreateLookAt
            (
                new Vector3(0, 0, 2),  //カメラの位置
                new Vector3(0, 0, 0),   //カメラの見る点
                new Vector3(0, 1, 0)    //カメラの上向きベクトル
            ),
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                GraphicsDevice.Viewport.AspectRatio / 2,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

    protected override void UnloadContent()
    {
        basicEffect.Dispose();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        Viewport fullViewport = graphics.GraphicsDevice.Viewport;
        Viewport leftViewport = fullViewport;
        leftViewport.Width /= 2;
        graphics.GraphicsDevice.Viewport = leftViewport;

        drawTriangle();

        Viewport rightViewport = leftViewport;
        rightViewport.X = leftViewport.Width;
        graphics.GraphicsDevice.Viewport = rightViewport;

        drawTriangle();

        graphics.GraphicsDevice.Viewport = fullViewport;

    }

    private void drawTriangle()
    {
        foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                vertices,
                0,
                vertices.Length / 3
                );
        }
    }
}




xna4.0SimplestViewport.jpg
このサンプルプログラムでは、1つの同じ三角形を
左右2つのビューポートに描画しています。
そのため、画面が真ん中で切れていて、
分割スクリーンになっているのです。

ここで強調したいのは、三角形の座標やカメラの位置は
左右で全く変わっていないということです。
違うのは、描画している領域、ビューポートだけです。
ビューポートの違いにより2つの三角形がずれているのです。

これを応用するといろいろと面白いことが出来ます。
カメラを2つの画面で全く違ったものにして二人対戦に使うことも出来るでしょうし、
あるいはカメラを左右で少しだけずらして
交差法や平行法で立体視をしてみるのも面白いかもしれません。

拍手[0回]


かんたんXNA4.0 その26 ステンシル・バッファ

ステンシル・バッファ

3Dの映像を描画する時には、
描画する部分を限定したい時があります。
例えば水や鏡にモデルが映りこんでいるときです。

そういうときにはモデルを鏡に対して反転してもう一回
描画するというやり方がありますが、
それでは鏡をはみ出してしまうかもしれません。

出来るなら鏡の領域の中でだけ
描画できるようにしたいものです。

あるいは物体の影を描く時も、そのように描画する
領域を制限したくなる場合があります。
影で暗くなる所だけ描画するようにして
半透明の黒で描画するのです。
(ここで言っているのは、
面が斜めになっていることによって出来る「陰」ではなくて、
他の物体によって光がさえぎられることによって出来る「影」です。)


こういったときに描画する領域を限定するために使うのが、
ステンシル・バッファです。
(ステンシルというのは、何か模様の形をした穴の開いた型があって、
そこにスプレーをプシューとやるとその模様が写るあれです。)


使い方は深度バッファやアルファ・テストに似ています。
とくに深度バッファとはかなり似ているので
ひとくくりにされることもあるくらいです。
深度バッファと同じように、各ピクセルにステンシルの値を表すデータがあって、
そのデータに基づいて描画するかしないかを制御することが出来るのです。
(ちなみに最初の値は深度バッファと違って0です。
深度バッファの最初の値は1でした。)


例えばあるモデルを四角形の中でだけ描画されるようにしたいとしましょう。
まず「型」を作るようにステンシル関連の設定をして、とりあえず四角形を描画します。
そうするとその四角形の形の「型」ができます。
四角形のそれぞれのピクセルのステンシル値が書き換えられたのです。

その後ステンシルの設定を「スプレーのプシュー」が出来るようにします。
そしてモデルを描画すると、きれいに「型」の中でだけ描画されるのです。
四角形の外には描画されません。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

class Triangle
{
    VertexPositionColor[] vertices = new VertexPositionColor[3];

    public Triangle(Vector3 p1, Vector3 p2, Vector3 p3, Color color)
    {
        vertices[0] = new VertexPositionColor(p1, color);
        vertices[1] = new VertexPositionColor(p2, color);
        vertices[2] = new VertexPositionColor(p3, color);
    }

    public void Draw(GraphicsDevice graphicsDevice, BasicEffect effect)
    {
        effect.VertexColorEnabled = true;

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();

            graphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                vertices,
                0,
                1
                );
        }
    }
}

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;

    Triangle whiteTriangle = new Triangle(
        new Vector3(0, 1, 1),
        new Vector3(1, 0, 1),
        new Vector3(-1, 0, 1),
        Color.White
        );
    Triangle blueTriangle = new Triangle(
        new Vector3(1, 1, 0),
        new Vector3(0, 0, 0),
        new Vector3(-1, 1, 0),
        Color.Blue
        );

    DepthStencilState stencilTemplateCreator = new DepthStencilState
    {
        StencilEnable = true,
        StencilFunction = CompareFunction.Always,
        StencilPass = StencilOperation.Replace,

        ReferenceStencil = 1,
        DepthBufferWriteEnable = false,
    };
    BlendState noColorWriter = new BlendState
    {
        ColorWriteChannels = ColorWriteChannels.None
    };
    DepthStencilState sprayer = new DepthStencilState
    {
        StencilEnable = true,
        ReferenceStencil = 0,
        StencilFunction = CompareFunction.Less
    };


    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
        graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
    }

    protected override void LoadContent()
    {
        effect = new BasicEffect(GraphicsDevice)
        {
            View = Matrix.CreateLookAt
            (
                new Vector3(0, 0, 3),  //カメラの位置
                new Vector3(0, 0, 0),   //カメラの見る点
                new Vector3(0, 1, 0)    //カメラの上向きベクトル
            ),
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                GraphicsDevice.Viewport.AspectRatio,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

    protected override void UnloadContent()
    {
        effect.Dispose();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        GraphicsDevice.DepthStencilState = stencilTemplateCreator;
        GraphicsDevice.BlendState = noColorWriter;
        whiteTriangle.Draw(GraphicsDevice, effect);

        GraphicsDevice.DepthStencilState = sprayer;
        GraphicsDevice.BlendState = BlendState.Opaque;
        blueTriangle.Draw(GraphicsDevice, effect);
    }
}

stencilBuffer.JPG
このサンプルは大きな三角形でステンシルの型を作って、
そのあとその型に対して青い三角形を描画しています。
そのため、ちょっと領域外の部分が描画されず、
ちょっといびつな形になっています。

stencil.JPG「型」を作るのに使った三角形
triangleInStencil.JPGその「型」の中に描画した三角形

初期化の前に

さて、実はステンシル・バッファはそのままでは使えません。
Drawメソッド内でどんなにあがいても使えないのです。
まずは一番最初の、グラフィックスデバイスの設定をいじって、
ステンシルバッファを使えるようにしなければなりません。



それにはGraphicsDeviceManager.PreferredDepthStencilFormatプロパティを使います。

public DepthFormat PreferredDepthStencilFormat { get; set; }

このプロパティを使って、深度バッファやステンシルバッファのフォーマットを設定します。
なお、これのプロパティを使うのはグラフィックスデバイスが実際に
作られる前でなければなりません。
(その後、例えばLoadContentメソッド内で呼んでも効果はありません)
なので、コンストラクタ中やInitialize()中でセットするといいでしょう。

深度バッファとステンシルバッファが一緒になっているのは、
この二つはセットになって確保されるからです。

このプロパティの型であるMicrosoft.Xna.Framework.Graphics.DepthFormat列挙体は、
どんなフォーマットの深度バッファを使うのかを表します。
このメンバによってはステンシルバッファを扱うようにもできるのです。

メンバ名 説明
Depth16 深度バッファは16bitです。
Depth24 深度バッファは24bitです。ただし、実際には32bit確保されます。そのうち使える分が24bitということです。
Depth24Stencil8 深度バッファは24bitで、ステンシルバッファは8bitです。
None 深度バッファを作りません。




Drawメソッド内ではステンシル・バッファを使うにはまず
DepthStencilState.StencilEnableプロパティを使います。

public bool StencilEnable { get; set; }

これをtrueにするとステンシル・バッファが有効になります。

ステンシル・バッファに書き込むには
DepthStencilState.StencilFunctionプロパティと
DepthStencilState.StencilPassプロパティを設定する必要があります。

public CompareFunction StencilFunction { get; set; }

このプロパティはステンシルバッファに書き込むかどうかの条件を表します。
ちょうど深度バッファ関数のようなものです(使ってる列挙体も同じですしね)
サンプルではポリゴンを描いたら必ずそこに
ステンシル・バッファが書き込まれるようにAlwaysをセットしてあります。

さて、ここで「書き込む」という表現をしましたが、
これはあまり正確ではなくて、実はStencilFunctionプロパティの条件に合格したからといって
ステンシルの値が新しく書き込まれるれるとは限りません。
デフォルトの設定では書き込まれません。
きちんと条件に合格した時に「書き換える」と明示してやらなければならないのです。

それをやるのがStencilPassプロパティです。

public StencilOperation StencilPass { get; set; }

このプロパティはStencilFunctionの条件に合格した時に(Passした時に)
どのような操作を行うかを表します。

この型はMicrosoft.Xna.Framework.Graphics.StencilOperation列挙体で、
いくつかのメンバがあります。

メンバ名 説明
Decrement ステンシル・バッファの値をデクリメントします。0より小さくなったらMaxの値になります。
DecrementSaturation ステンシル・バッファの値をデクリメントします。ただし、0になるまでです。
Increment ステンシル・バッファの値をインクリメントします。Maxの値を超えたら0に戻ります。
IncrementSaturation ステンシル・バッファの値をインクリメントします。ただし、Maxの値になるまでです。
Invert ステンシル・バッファのビットを反転します。
Keep ステンシル・バッファの値を書き換えません。デフォルトの値です。
Replace ステンシル・バッファをRenderState.ReferenceStencilプロパティの値に書き換えます。
Zero ステンシル・バッファの値を0にセットします。

サンプルではReplaceを使って書き込んでいます。
これは、DepthStencilState.ReferenceStencilプロパティの値を
ステンシル・バッファに書き込むものです。

DepthStencilState.ReferenceStencilはこのように書き込みに使われたり、
あるいは後で、「スプレーのプシュー」をする時の条件に使われたりします。

public int ReferenceStencil { get; set; }

これはアルファ・テストのReferenceAlphaにも似ていますし、
あるいは深度バッファで言うならそれぞれの頂点の変換後のz座標に相当します。
ようするにこの値を使って描画するかしないかを決めているということです。

拍手[0回]