忍者ブログ

Memeplexes

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

[PR]

×

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


かんたんXNA4.0 その12 Game.Initializeメソッド

小さなサンプルではその有効性はさっぱりわかりませんが、
大きなプログラムではGame.Initializeメソッドが
(グラフィックスで無い)データの初期化を分離するのに役に立ちます。
(これはちょうどjavaアプレットのinitメソッドのようなものです)

例えば座標データをこの中で初期化することが出来ます。

 

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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices;
    VertexBuffer vertexBuffer;

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

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

    protected override void LoadContent()
    {
        effect = 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     //カメラからこれより遠い物体は画面に映らない
            )
        };

        vertexBuffer = new VertexBuffer(
            GraphicsDevice, 
            typeof(VertexPositionColor), 
            vertices.Length,
            BufferUsage.None
            );
        vertexBuffer.SetData<VertexPositionColor>(vertices);
    }

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

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

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

            GraphicsDevice.SetVertexBuffer(vertexBuffer);
            GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,  //startIndex
                vertices.Length / 3 //primitiveCount
                );
        }
    }
}
xna4.0SimplestInitialize.jpg
(※前回と見た目は全く変わりません。)

ここでは、今までコンストラクタ内で初期化していた三角形の頂点データを、
MyGame.Initializeメソッド内で初期化しています。

そのあとbase.InitializeでGame.Initializeメソッドを呼んでいます。

不可解です
なぜスーパークラスのInitializeメソッドを呼ばなければならないのでしょう?
その理由はもちろんWindowsFormsのControl.OnPaintメソッドの
オーバーライドでスーパークラスのOnPaintメソッドを
呼んでやらなければならない理由と同じです。
つまり、Game.Initializeメソッドが何か大切なことをしているからです。

このケースでは、Game.InitializeメソッドはLoadContentメソッドを呼んでおり、
base.Initialize
を忘れるとグラフィックス関係の変数が全て初期化されずにnullのままで、
描画すると同時に例外(NullReferenceException)が飛びます。
(同様に、DrawメソッドとUpdateメソッドも本当はスーパークラスの
メソッドを呼んでやらなければなりません。
しかしGameComponentを使わない限りは大して問題はないですし
サンプルなので割愛しました。
実際に問題が出る時に対処しても遅くは無いでしょう。
YAGNIの原則です。)


これはまぁいいでしょう。
しかしなぜ一番最後にbase.Initializeを呼んでいるのでしょうか?

コンストラクタは普通基底クラスのものから先に呼ばれます。
派生して出来たクラスのコンストラクタで基底クラスの何かを
利用するときに困らないようになっているのです。
とすると同じ初期化を表すInitializeメソッドも一番最初に
base.Initializeを呼ぶべきであるようにも思えます。
一番最後に呼ぶのではまるでデストラクタです。

一番最後にbase.Initializeとするのは
気持ち悪いように思えます。
なぜ一番最後に呼ぶのでしょう?

まず一番最初に呼ぶとどうなるかを実際に見るのがいいかもしれません。
…例外がスローされます。
verticesがまだnullだというのです。

実はこれは一つ目の疑問と関係しています。
Game.InitializeメソッドはLoadContentを呼んでいるので、
一番最初にGame.Initializeメソッドを呼ぶと
まだ三角形の頂点データが初期化されていないまま
VertexBufferが初期化されてしまうのです。

したがってVertexBufferのnew時に
例外がスローされるわけです。
 

拍手[0回]

PR

かんたんXNA4.0 その11 VertexBuffer

ここまで三角形や四角形を描くのに
GraphicsDevice.DrawUserPrimitives<T>メソッドを使ってきました。
これはシンプルではあるのですがパフォーマンスがあまりよくありません。

良いパフォーマンスを持っているのは
GraphicsDevice.DrawPrimitivesメソッドです。
ただし、これは頂点データの配列そのものは直接使えません。
配列からVertexBufferを作り、それを使わねばならないのです。
 

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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices = new[]
    { 
        new VertexPositionColor(new Vector3(1, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red),
        new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue),
    };
    VertexBuffer vertexBuffer;

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

    protected override void LoadContent()
    {
        effect = 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     //カメラからこれより遠い物体は画面に映らない
            )
        };

        vertexBuffer = new VertexBuffer(
            GraphicsDevice, 
            typeof(VertexPositionColor), 
            vertices.Length,
            BufferUsage.None
            );
        vertexBuffer.SetData<VertexPositionColor>(vertices);
    }

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

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

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

            GraphicsDevice.SetVertexBuffer(vertexBuffer);
            GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,  //startIndex
                vertices.Length / 3 //primitiveCount
                );
        }
    }
}

 

xna4.0SimplestVertexBuffer.jpg

このサンプルではGraphicsDevice.DrawUserPrimitivesのかわりに
GraphicsDevice.DrawPrimitivesメソッドを
使って三角形を描画しています。
見た目は変わりません。
ただ、ほんの少しパフォーマンスはよくなっているはずです。
(サンプルではありがたみは全く感じられませんが)

このメソッドを呼ぶ前にはSetVertexBufferでvertexBufferを
セットしてやらなければなりません。
DrawPrimitivesはVertexBufferが引数に無いのです。


さて、ここで注意しておきたいことはこのSetVertexBufferとDrawPrimitivesの
ペアは一回のDrawメソッドの中で何度呼んでもいいということです。
(そうでなければ物体を2つ以上描画できなくなってしまいます。)

物体を2つ以上描画するときはこんな感じでいいでしょう。

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

            GraphicsDevice.SetVertexBuffer(vertexBuffer1);
            GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,  //startIndex
                vertices1.Length / 3 //primitiveCount
                );
        }

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

            GraphicsDevice.SetVertexBuffer(vertexBuffer2);
            GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,  //startIndex
                vertices2.Length / 3 //primitiveCount
                );
        }










拍手[1回]


かんたんXNA4.0 その10 3D四角形

ここでは少し話を戻して普通の四角形を表示します。

四角形を表示するためには三角形を2つ組み合わせます。

2triangles.JPG
この調子でどんな複雑な図形も
三角形を組み合わせることで描画できます。
 

 


四角形の描画

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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices = new[]
    { 
        new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue),
        new VertexPositionColor(new Vector3(1, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red),

        new VertexPositionColor(new Vector3(1, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(0, -1, 0), Color.Blue),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red),
    };

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

    protected override void LoadContent()
    {
        effect = 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     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

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

    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,  //vertexOffset
                vertices.Length / 3 //primitiveCount
                );
        }
    }
}

xna4.0SimplestRectangle.jpg

このプログラムは6つの頂点を使って2つの三角形を描いています。

MyGame.DrawメソッドのDrawUserPrimitivesに注目してください。
このprimitiveCount引数が1から2に変わっています。
これは三角形の数を1から2に変えたからです。

さて、このコードには気持ち悪いところがあります。
頂点のデータの重複です。
四角形を書くのに必要な頂点は4つです。
6つも必要ありません。2つは無駄です。

重複は良いプログラムの天敵です。
何とかして取り除かなければなりません。

そうするにはGraphicsDevice.DrawUserIndexedPrimitivesメソッドを使います。
これを使うと、頂点データは4つだけで済みます。
そのかわり、四角形を構成する三角形の
頂点のインデックスを6つ指定してやります。

ここで言うインデックスというのは、4つのうち
何番目の頂点を使うのかということです。
頂点データの代わりにインデックスが2つの三角形を指定するのです。
 

 


インデックス

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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices = new[]
    { 
        new VertexPositionColor(new Vector3(1, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-1, 0, 0), Color.Red),
        new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue),
        new VertexPositionColor(new Vector3(0, -1, 0), Color.Blue),
    };
    int[] indices = new[]
    {
        0, 1, 2,
        0, 3, 1
    };

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

    protected override void LoadContent()
    {
        effect = 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     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

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

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

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

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

これも、全く同じ四角形を描画します。

違うのはインデックスを使っているという点です。
xnaTriangleWithIndices.JPG
このように、インデックスを使うと
頂点データの重複をなくすことが出来ます。

 

拍手[0回]


かんたんXNA4.0 その9 キーボード入力

ここまではアニメーションを行いましたが
これだけではもちろんゲームになりません。

ゲームはプレイヤーの入力に反応してこそゲームです。
そうでなければよく出来たデジタルアニメーションに過ぎません。

XNAの入力する方法には
1.ゲームパッド
2.キーボード
3.マウス
の3つがあります。

ここではキーボード入力をあつかいます。
(他の2つも似たようなものです。インテリセンスの助けがあれば問題なく出来ます。)
 


簡単な例

Xnaで入力を扱うには、1秒に60回、
Game.Updateメソッド内でいちいち入力機器
の状態を調べてやると言う方法を取ります。
入力機器を表すユーティリティクラスから、
入力機器の状態を表す構造体を取得します。

入力機器の状態を表す構造体は*****Stateという名前になっていて、
キーボードの場合はMicrosoft.Xna.Framework.Input.KeyboardState構造体です。

public struct KeyboardState

この構造体にはキーの状態を取得するメソッド、IsKeyDownメソッドと、IsKeyUpメソッドがあります。

public bool IsKeyDown ( Keys key )
public bool IsKeyUp( Keys key )


IsKeyDownメソッドはキーが押されていればtrue, 押されていなければfalseを返し、
IsKeyUpメソッドはキーが押されていなければtrue, 押されていればfalseを返します。
引数はキーボードの特定のキーを表す列挙型、Microsoft.Xna.Framework.Input.Keysです。

Keys列挙型のメンバは多いのでここに書くことはしませんが、
たとえばキーのAボタンが押されているかを調べるには "Keys.A" というふうに書きます。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

public class MyGame : Game
{
    protected override void Update(GameTime gameTime)
    {
        KeyboardState keyboardState = Keyboard.GetState();

        if (keyboardState.IsKeyDown(Keys.Space))
        { Window.Title = "Space key is pressed."; }
        else
        { Window.Title = ""; }
    }
}

このサンプルを実行すると、スペースキーが押されたときだけウィンドウのタイトルが変わります。
これは、1秒間に60回、スペースキーの状態を調べてウィンドウのタイトルを設定しなおしているのです。
普通のアプリケーションがイベントを使ってキーの状態を調べるのとは大きく違います。


カメラを動かす

もうすこし実際のゲームで使われるのに近いコードにしてみましょう。

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices = new[]
    { 
        new VertexPositionColor(new Vector3(1, 1, 0), Color.Blue),
        new VertexPositionColor(new Vector3(0, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-1, 1, 0), Color.Red),
    };
    float angle = 0;

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

    protected override void LoadContent()
    {
        effect = new BasicEffect(GraphicsDevice)
        {
            VertexColorEnabled = true,
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                GraphicsDevice.Viewport.AspectRatio,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

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

    protected override void Update(GameTime gameTime)
    {
        KeyboardState keyboardState = Keyboard.GetState();

        if(keyboardState.IsKeyDown(Keys.Left))
        {
            angle -= MathHelper.ToRadians(0.5f);//1/60秒に0.5°回転
        }
        if(keyboardState.IsKeyDown(Keys.Right))
        {
            angle += MathHelper.ToRadians(0.5f);//1/60秒に0.5°回転
        }
    }

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

        effect.View = Matrix.CreateLookAt
        (  
            new Vector3
            (
                3 * (float)Math.Sin(angle),
                0,
                3 * (float)Math.Cos(angle)
            ),  //カメラの位置
            new Vector3(0, 0, 0),   //カメラの見る点
            new Vector3(0, 1, 0)    //カメラの上向きベクトル
        );

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

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                vertices,
                0,//vertexOffset
                1 //primitiveCount
                );
        }
    }
}

このプログラムは「←」キーと「→」キーでカメラの位置を操作しています。
xna4.0SimplestKeyboardInput0.jpg
最初の状態

xna4.0SimplestKeyboardInputLeft.jpg
「←」キーを押したとき。カメラは左に。

xna4.0SimplestKeyboardInputRight.jpg
「→」キーを押したとき。カメラは右に。

拍手[1回]


かんたんXNA4.0 その8 3Dカメラのアニメーション

前回は2Dでしたが今回は
3Dカメラのアニメーションを行います。

これもやはりGame.Updateメソッド内で
カメラの座標と向きを変えることによって
アニメーションを行っています。
 

 


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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect effect;
    VertexPositionColor[] vertices = new[]
    { 
        new VertexPositionColor(new Vector3(1, 1, 0), Color.Blue),
        new VertexPositionColor(new Vector3(0, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-1, 1, 0), Color.Red),
    };
    float angle = 0;

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

    protected override void LoadContent()
    {
        effect = new BasicEffect(GraphicsDevice)
        {
            VertexColorEnabled = true,
            Projection = Matrix.CreatePerspectiveFieldOfView
            (
                MathHelper.ToRadians(45),   //視野の角度。ここでは45°
                GraphicsDevice.Viewport.AspectRatio,//画面のアスペクト比(=横/縦)
                1,      //カメラからこれより近い物体は画面に映らない
                100     //カメラからこれより遠い物体は画面に映らない
            )
        };
    }

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

    protected override void Update(GameTime gameTime)
    {
        angle += MathHelper.ToRadians(0.5f);//1/60秒に0.5°回転
    }

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

        effect.View = Matrix.CreateLookAt
        (  
            new Vector3
            (
                3 * (float)Math.Sin(angle),
                0,
                3 * (float)Math.Cos(angle)
            ),  //カメラの位置
            new Vector3(0, 0, 0),   //カメラの見る点
            new Vector3(0, 1, 0)    //カメラの上向きベクトル
        );

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

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                vertices,
                0,//vertexOffset
                1 //primitiveCount
                );
        }
    }
}

このサンプルは三角形の周りをカメラがぐるぐる回ることによってアニメーションを行っています。
(三角形が回っているように見えますが、実際に回っているのはカメラの方です。
つまり天動説ではなく地動説というわけです。)


xna4.0SimplestTriangle3DWithCameraRotating1.jpg
xna4.0SimplestTriangle3DWithCameraRotating2.jpg
xna4.0SimplestTriangle3DWithCameraRotating3.jpg

ただし、これ以上回転して、裏から見ると三角形は見えなくなります。
xna4.0SimplestTriangle3DWithCameraRotating4.jpg

これはXNAが、
座標が時計回りの順番になっている三角形しか描画しない
からです。
xnaCullingDescription.JPG
回転して裏から見ると反時計回りの順番になっているように見え、
描画されなくなるのです。
(もちろん、さらに回転して元の時計回りの順番に見えるようになったら
再び描画されるようになります。)
これはパフォーマンスの問題で、
これによって描画が高速化するようです。


どうしても裏から見たときも描画したいという場合は、
GraphicsDeviceにRasterizerState.CullModeCullMode.Noneに設定したオブジェクトをセットしてやると
裏側も描画されるようになります。
(下のコードをLoadContentかDrawメソッドの内部にでも追加してみてください)

        GraphicsDevice.RasterizerState = RasterizerState.CullNone;


xna4.0SimplestTriangle3DWithCameraRotatingCullModeNone.jpg 
(ちなみに、RasterizerState.CullModeのデフォルトは
CullMode.CullCounterClockwiseFaceで、反時計回りは描画しないという意味です。
CullModeは全部で3つあり、
1つがデフォルトのCullCounterClockwiseFace、
2つ目がここで使ったNone(「描画しないということは無い」という意味)、
3つ目はCullClockwiseFaceで、デフォルトの逆です。)

拍手[2回]