WisdomSoft - for your serial experiences.

行列と変換

3次元空間のモデルを立体的に描画するには、行列を用いて頂点を変換します。

ワールド変換

これまで表示したプリミティブは、頂点の座標がスクリーンの座標に直接対応しているため、3 次元空間特有の奥行きのある立体感がありません。一般的な 3D ゲームのように、立体的に世界を見せるには、プリミティブの頂点に対して様々な変換を行わなければなりません。また、個々のプリミティブを移動させたり、伸縮させたりするにも座標変換が必要になります。こうした座標変換を組み合わせることで、個々のプリミティブを重ね合わせ、複雑な 3D の世界を表現します。

頂点の変換には行列を使います。本書では、数学における行列の詳細は割愛します。XNA における行列とは 4 × 4 の 2 次元配列のようなものだと考えてください。行列は Matrix 構造体で表されます。

プリミティブの頂点が持つ頂点の座標を、ローカル座標と呼びます。モデリングツールなどによって作られたプリミティブの元の座標がローカル座標となります。ゲームの世界は、個別に作られたプリミティブを組み合わせて作られます。プリミティブをゲーム世界の特定の場所に配置するには、頂点の座標をローカル座標からゲーム世界全体の座標に変換しなければなりません。このゲーム世界の座標をワールド座標と呼びます。ローカル座標からワールド座標への変換を、ワールド変換と呼びます。

ローカル座標からワールド座標への変換は、プリミティブの平行移動や回転、伸縮によって行われます。元のプリミティブの頂点の座標を移動させることで、ゲーム世界の目的の場所に配置できます。よって元のプリミティブの位置やサイズは、ワールド座標に変換するときに自由に変更できます。

BasicEffect クラスを用いている場合、ワールド変換を行うには World プロパティを用います。

BasicEffect クラス World プロパティ
public Matrix World { get; set; }

このプロパティに、ワールド行列を設定します。ワールド行列には、移動、回転、伸縮の 3 つの作用があります。

平行移動のための行列は CreateTranslation() メソッドから取得します。

Matrix 構造体 CreateTranslation() メソッド
public static Matrix CreateTranslation (
         float xPosition, float yPosition, float zPosition
)
public static Matrix CreateTranslation (Vector3 position)

xPosition パラメータには X 軸の移動距離、yPosition パラメータには Y 軸の移動距離、zPosition パラメータには Z 軸の移動距離を指定します。position パラメータには、移動距離を表す値を各要素に保存している Vector3 構造体の値を指定します。

図1 平行移動
図1 平行移動

以下のように、平行移動を行う行列を取得できます。

Matrix transration = Matrix.CreateTranslation(5, 3, 1);

回転のための行列は、各軸ごとの回転で取得する方法が異なります。X 軸に対する回転行列は CreateRotationX() メソッドから、Y 軸に対する回転行列は CreateRotationY() メソッドから、そして Z 軸に対する回転行列は CreateRotationZ() メソッドから取得します。

Matrix 構造体 CreateRotationX() メソッド
public static Matrix CreateRotationX (float radians)
Matrix 構造体 CreateRotationY() メソッド
public static Matrix CreateRotationY (float radians)
Matrix 構造体 CreateRotationZ() メソッド
public static Matrix CreateRotationZ (float radians)

これらのメソッドの radians パラメータには、軸に対する回転角度をラジアン単位で設定します。通常は、次のように MathHelper クラスの ToRadians() メソッドを使って、度をラジアンに変換して行列を取得することになります。

図2 回転
図2 回転

例えば、Z 軸に対して回転させる行列を生成するには次のように記述します。

Matrix rotation = Matrix.CreateRotationZ(MathHelper.ToRadians(90));

上のコードは、Z 軸を中心に指定した角度でプリミティブを 90 度回転させる行列を生成しています。

伸縮のための行列は CreateScale() メソッドから取得します。

Matrix 構造体 CreateScale() メソッド
public static Matrix CreateScale (float scale)
public static Matrix CreateScale (float xScale, float yScale, float zScale)
public static Matrix CreateScale (Vector3 scales)

float 型の scale パラメータを 1 つだけ受けるメソッドは、scale パラメータに指定された比率に従ってプリミティブ全体を伸縮します。float 型のパラメータを 3 つ受け取るメソッドは、それぞれの軸の方向にプリミティブを伸縮します。xScale パラメータには X 軸、yScale パラメータは Y 軸、zScale パラメータは Z 軸に対する伸縮率を指定します。同様に Vector3 型の scales パラメータも、各要素に格納されている値からプリミティブを伸縮します。

図3 伸縮
図3 伸縮

描画するプリミティブ全体を 2 倍に拡大させる行列は、次のように生成できます。

Matrix scale = Matrix.CreateScale(2);

上のコードは、CreateScale() メソッドで 2 倍に伸縮することを表す行列を作成しています。逆に半分のサイズに伸縮するには、パラメータに 0.5F を指定します。

行列の組み合わせ

移動、回転、伸縮といった変換を組み合わせるには、個々の行列を乗算します。例えば、移動行列と回転行列を乗算した行列を設定することで、移動と回転を同時に行うワールド行列となります。行列の演算は Matrix 構造体が演算子をオーバーロードしているため、通常の算術演算子で行うことができます。

コード1
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private BasicEffect effect;
    private VertexPositionColor[] vertices;
    private short[] indices;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        vertices = new VertexPositionColor[] {
	        new VertexPositionColor(new Vector3(0, 0.5F, 0), Color.Red),
            new VertexPositionColor(new Vector3(0.5F, -0.5F, 0), Color.Blue),
	        new VertexPositionColor(new Vector3(-0.5F, -0.5F, 0), Color.Lime),
        };
        indices = new short[] { 0, 1, 2 };
        base.Initialize();
    }

    protected override void LoadContent()
    {
        effect = new BasicEffect(GraphicsDevice);
        effect.VertexColorEnabled = true;
        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);

        Matrix pos = Matrix.CreateTranslation(state.ThumbSticks.Left.X, state.ThumbSticks.Left.Y, 0);
        Matrix rot = Matrix.CreateRotationZ(MathHelper.ToRadians(
            (180 * state.Triggers.Left) - (180 * state.Triggers.Right))
        );
        Matrix scale = Matrix.CreateScale(state.ThumbSticks.Right.X + 1, state.ThumbSticks.Right.Y + 1, 1);

        effect.World = pos * rot * scale;
        base.Update(gameTime);
    }

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

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            GraphicsDevice.DrawUserIndexedPrimitives(
                PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, 1);
        }

        base.Draw(gameTime);
    }
}
実行結果
コード1 実行結果

コード1は、コントローラからの入力に従って描画されている三角形のプリミティブを移動、回転、伸縮させるプログラムです。左スティックでプリミティブを移動、右スティックで伸縮できます。回転は、左右のトリガーを入力すると、最大 180 度まで回転します。左トリガーは反時計回りに、右トリガーは時計回りに回転させます。

ビュー変換

これまで描画したプリミティブの頂点の座標は、スクリーン座標に直接対応しているため物体が立体的に見えることはありませんでした。Z 座標の値を変更しても、スクリーン上での頂点の座標は変わりません。Z 座標の値に従って、すなわち頂点が手前にあるのか奥にあるのかによって物体を立体的に見せるには、視点と視野に関する情報が必要です。ゲームの世界を映すカメラのようなものだと考えてください。視点は、3D 空間のどこからプリミティブを見るかという情報です。どこから見るかによって、物体の見え方が異なります。

図4 ビュー変換
図4 ビュー変換

実際には、カメラが存在するわけではなく、ワールド変換と同じように視点を表す行列に従って頂点を変換することによって立体的に見せます。この視点を表す行列のことをビュー行列と呼び、ビュー行列による頂点の変換をビュー変換と呼びます。ビュー変換を行うには View プロパティを使います。

BasicEffect クラス View プロパティ
public Matrix View { get; set; }

このプロパティにビュー行列を設定します。

視点の位置や向きを表す行列は、Matrix 構造体の CreateLookAt() メソッドを使うと簡単です。

Matrix 構造体 CreateLookAt() メソッド
public static Matrix CreateLookAt (
         Vector3 cameraPosition,
         Vector3 cameraTarget,
         Vector3 cameraUpVector
)

cameraPosition パラメータには、視点の座標を指定します。cameraTarget パラメータには、視点の方向を表す座標を指定します。これらのパラメータから、どの位置から、どの場所を見るかが決定されます。最後の cameraUpVector パラメータには、視点の上を表す座標を指定します。首を傾げたときのように、視点を回転させることができます。

射影変換

イラストや漫画の世界で物体を立体的に表現する遠近法のように、頂点の座標情報と視点との距離に基づいて 2D のスクリーンに立体的にプリミティブを描画するには射影変換を行わなければなりません。射影変換は、視点に対する視野を表すもので、視野角やアスペクト比、物体が見える範囲などを設定します。射影変換もまた、ワールド変換やビュー変換と同じように行列を用います。射影変換を行うために行列を、射影行列と呼びます。

図5 射影変換
図5 射影変換

射影変換を行うには Projection プロパティを使います。

BasicEffect クラス Projection プロパティ
public Matrix Projection { get; set; }

このプロパティに、射影行列を設定します。

射影行列は、Matrix 構造体の CreatePerspectiveFieldOfView() メソッドから取得できます。

Matrix 構造体 CreatePerspectiveFieldOfView() メソッド
public static Matrix CreatePerspectiveFieldOfView (
         float fieldOfView,
         float aspectRatio,
         float nearPlaneDistance,
         float farPlaneDistance
)

fieldOfView パラメータには、ラジアン単位の視野角を設定します。視点から見える視野は、ラジアン単位の角度として設定します。この角度が大きければ、広く世界を描画できます。逆に視野角が狭ければ、視点の向きに対してフォーカスすることになります。視点の位置が同じでも、視野角の違いによって見え方が異なります。カメラで同じ位置から同じ被写体を広角レンズと望遠レンズで撮影した違いのようなものです。

aspectRatio パラメータには、アスペクト比を指定します。多くの場合、従来型のテレビで標準的な 4 : 3、ハイビジョンテレビなどで標準的な 16 : 9 のいずれかを設定することになります。固定しなければならない場合を除いて、描画領域のサイズの幅と高さを除算した結果を設定します。

nearPlaneDistance パラメータには、物体が見える最も近い面までの距離を、farPlaneDistance パラメータには最も遠い面までの距離をそれぞれ指定します。視点から見える世界は、これらのパラメータに指定されている範囲となります。nearPlaneDistance パラメータに指定された距離よりも視点に近い物体や、farPlaneDistance パラメータに指定された距離よりも遠い物体は描画されません。このとき、nearPlaneDistance パラメータに指定する値を近クリップ面、farPlaneDistance パラメータに指定する値を遠クリップ面とも呼びます。

コード2
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private BasicEffect effect;
    private VertexPositionColor[] vertices;
    private short[] indices;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        vertices = new VertexPositionColor[] {
	        new VertexPositionColor(new Vector3(-0.5F, 0.5F, 1), Color.Red),
            new VertexPositionColor(new Vector3(0.5F, -0.5F, 1), Color.Red),
	        new VertexPositionColor(new Vector3(-1.5F, -0.5F, 1), Color.Red),
            
	        new VertexPositionColor(new Vector3(0.5F, 0.5F, 2), Color.Blue),
            new VertexPositionColor(new Vector3(1.5F, -0.5F, 2), Color.Blue),
	        new VertexPositionColor(new Vector3(-0.5F, -0.5F, 2), Color.Blue),
        };
        indices = new short[] { 0, 1, 2, 3, 4, 5 };
        base.Initialize();
    }

    protected override void LoadContent()
    {
        effect = new BasicEffect(GraphicsDevice);
        effect.VertexColorEnabled = true;
        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);

        Vector3 position = new Vector3(
            5 * state.ThumbSticks.Left.X,
            5 * state.ThumbSticks.Left.Y,
            5
        );
        Vector3 target = new Vector3(state.ThumbSticks.Right.X, state.ThumbSticks.Right.Y, 0);
        Vector3 upVector = new Vector3(0, 1, 0);

        effect.View = Matrix.CreateLookAt(position, target, upVector);
        effect.Projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45), 16F / 9F, 1, 10
        );

        base.Update(gameTime);
    }

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

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            GraphicsDevice.DrawUserIndexedPrimitives(
                PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, 2);
        }

        base.Draw(gameTime);
    }
}
実行結果
コード2 実行結果

コード2は、同じサイズの三角形を 2 つ描画しています。これら三角形のプリミティブは、同じサイズですが Z 座標の位置が異なります。Z 座標の値は大きいほど手前にあると判断されます。実行結果を見れば、より手前にある青い三角形が大きく見え、後ろにある赤い三角形は小さく見えます。

このプログラムでは、ビュー行列を CreateLookAt() メソッドから取得しています。このとき、視点の座標を position 変数に、対象の座標を target 変数に、視点の上を表す座標を upVector 変数に、それぞれ代入しています。これらの変数の値を変更することで、カメラの座標や向きを制御できます。

コントローラの左スティックの傾きに 5 を乗算することで、その傾きに従って 0 ~ 5 までの値を取得できます。 視点は、この値に従ってスティックの X 軸と Y 軸の傾きに合わせて移動させています。Z 座標の値は常に 5 を設定しているため、デフォルトの視点の位置は (0, 0, 5) となります。

視点の対象は、右スティックの X 軸と Y 軸の傾きに従って移動させています。デフォルトで、視点は (0, 0, 0) に向かっています。右スティックが傾けられると、X 座標と Y 座標の位置が 0.0 ~ 1.0 の範囲で移動します。視点の位置はそのままで、視点の向きが変わることを確認してください。

CreatePerspectiveFieldOfView() メソッドで生成している値は、常に固定してあります。視野角は 45 度、アスペクト比は 16:9 を指定しています。ただし、アスペクト比はゲーム画面の幅と高さから設定されるべきであり、一般には後述するビューポートの範囲からアスペクト比が設定されるでしょう。