忍者ブログ

Memeplexes

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

かんたん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回]

PR