XNA Framework 第 03 回
赤坂玲音 著
3 次元グラフィックス
前回までは 2 次元グラフィックスを基本とした XNA Framework ゲームの開発方法について解説させていただきました。XNA Framework は、Xbox 360 で動作するということからも、高品質な 3 次元グラフィックスゲームを作る環境という印象が強いかもしれません。本稿から、いよいよ 3
次元グラフィックスの描画について解説します。
2 次元グラフィックスが X 座標と Y 座標の 2 つの座標を持つように、3 次元グラフィックスでは X 座標と Y 座標に加えて、Z 座標を持ちます。これによって、立体的に点を表すことができるようになります。
3 次元グラフィックスの座標系には、左手座標系と右手座標系の 2 種類があります。違いは Z 軸の方向で、左手座標系の場合は Z 座標の値が大きいほど奥に向かい、右手座標系では Z 座標の値が大きいほど手前に向かいます。XNA Framework では、右手座標系が採用されています。
■ 図 01

2 次元グラフィックスとは異なり、3 次元グラフィックスの描画は直観的ではありません。2
次元グラフィックスは、描画するピクセルとデバイスに描画される点の関係が 1 対 1 なので明確です。
ところが、3 次元グラフィックスの場合、データは 3 次元かもしれませんが、ディスプレイなどの描画先のデバイスは平面です。SF
の世界で登場するような立体映像投影デバイスが普及すれば状況は変わるかもしれませんが、本稿執筆時点では、3
次元グラフィックスを投影するのは平面のディスプレイです。
そこで、コンピュータは立体のモデル情報を 2 次元のピクセルに変換し、最終的に一枚の絵が作られてディスプレイに描画されます。
頂点
3 次元グラフィックスの場合も、2 次元グラフィックスにおけるピクセルのような点をもちます。これを頂点と呼び、X 座標、Y 座標、Z 座標の 3
つの値の組み合わせで 1 つの点を表します。どのような複雑なグラフィックスも、複数の頂点を組み合わせた三角形で構成されています。
3 次元グラフィックスは、複数の頂点を繋ぎ合わせて 1
つの物体を描画します。これをプリミティブと呼び、頂点を表すオブジェクトの配列からプリミティブを描画することができます。プリミティブを描画するには
GraphicsDevice クラスの DrawUserPrimitives() メソッドを使います。
■ GraphicsDevice クラス DrawUserPrimitives() メソッド
public void DrawUserPrimitives<T> (
PrimitiveType primitiveType,
T[] vertexData,
int vertexOffset,
int primitiveCount
) where T : ValueType
型パラメータには、頂点情報を表すデータの型を指定します。頂点を表すオブジェクトの型については後述します。
primitiveType には、頂点をどのように描画するかを表す Microsoft.Xna.Framework.Graphics.PrimitiveType
列挙型のメンバのいずれかを指定します。vertexData には、描画する頂点の配列を、vertexOffset には vertexData
に指定した配列のどの要素から使用するかを表すインデックスを、primitiveCount には、描画するプリミティブの数を指定します。
プリミティブの数は、頂点の数ではありません。プリミティブが線である場合は始点と終点の 2 つの頂点が必要になります。よって、1 本の線は 2 つの頂点で
1 つのプリミティブとなります。
プリミティブの種類
描画するプリミティブの種類は primitiveType パラメータに指定する PrimitiveType 列挙型のメンバが表しています。
■ Microsoft.Xna.Framework.Graphics.PrimitiveType
public enum PrimitiveType
この列挙型には、表 01 のようなメンバが定義されています。
■ 表 01
| メンバ名 |
効果 |
| LineList |
頂点を個別の線として描画する。 |
| LineStrip |
頂点を連続した線として描画する。 |
| PointList |
頂点を点として描画する。インデックス付きプリミティブではサポートされない。 |
| TriangleFan |
頂点を扇型の三角形として描画する。 |
| TriangleList |
頂点を個別の三角形として描画する。 |
| TriangleStrip |
頂点を連続した三角形として描画する。 |
たとえば、頂点を点として描画する場合は PointList
メンバを指定します。この場合、プリミティブの数と頂点の数は一致します。独立した線としてプリミティブを描画する場合、LineList
を指定してください。この場合、2 つの頂点で 1 つのプリミティブが構成されます。同じように、三角形の場合は 3 つの頂点で 1 つのプリミティブとなります。
頂点を連続した線として結ぶ場合は LineStrip メンバを指定します。この場合、最初の線の描画には 2
つの頂点が必要ですが、それ以降は次々と頂点を線で結びます。よって、必要な頂点の数はプリミティブの数 + 1 となります。
当然、DrawUserPrimitives() メソッドに指定する vertexData
パラメータの配列の要素数は、プリミティブの種類とプリミティブの数で決定される頂点の数を満たしていなければなりません。LineList のプリミティブを 3
つ描画する場合、頂点は 6 つ必要になります。
プリミティブの種類についての詳細は後述します。まずは、基本的な線の描画を行いましょう。
頂点の型
DrawUserPrimitives() メソッドに渡す頂点の型は、ジェネリックによってパラメータ化されていました。XNA Framework
では、ユーザー定義の頂点を作成して利用することも可能になっています。しかし、構造体なら何でも指定できるわけではありません。
頂点の型は、GraphicsDevice クラスの VertexDeclaration
プロパティの設定で決定されます。このプロパティに、対象のデバイスで描画する頂点の構造を設定します。
■ GraphicsDevice クラス VertexDeclaration プロパティ
public VertexDeclaration VertexDeclaration { get; set; }
このプロパティは、頂点となる構造体の情報を提供する Microsoft.Xna.Framework.Graphics.VertexDeclaration クラスを使います。
■ Microsoft.Xna.Framework.Graphics.VertexDeclaration クラス
public class VertexDeclaration : IDisposable
このクラスのコンストラクタは、次のようになっています。
■ Vertexeclaration クラスのコンストラクタ
public VertexDeclaration (
GraphicsDevice graphicsDevice,
VertexElement[] elements
)
graphicsDevice には、このオブジェクトに関連付ける GraphicsDevice オブジェクトを指定します。elements
には、頂点要素と呼ばれる頂点のデータ構造を表す Microsoft.Xna.Framework.Graphics.VertexElement
構造体の配列を指定します。
■ Microsoft.Xna.Framework.Graphics.VertexElement 構造体
[SerializableAttribute]
public struct VertexElement
VertexElement は、1 つの頂点となるオブジェクトのデータを構成する 1
要素の構造を表す値です。このオブジェクトは、頂点要素のオフセットやデータ型を指定します。
ユーザー定義の頂点を作成する場合は、頂点のデータ構造に従って正しく VertexElement
を用意しなければなりません。しかし、事前に用意されている頂点を使う場合は、構造体が公開している VertexElements プロパティから
VertexElement の配列を取得することができます。
この場では、頂点の座標と色を表す Microsoft.Xna.Framework.Graphics.VertexPositionColor 構造体を使ってプリミティブを描画しましょう。
■ Microsoft.Xna.Framework.Graphics.VertexPositionColor 構造体
[SerializableAttribute]
public struct VertexPositionColor
この構造体を利用する GraphicsDevice は、事前に VertexDeclaration プロパティに、適切な VertexDeclaration
オブジェクトを設定しなければなりません。VertexDeclaration クラスのコンストラクタに指定する VertexElement の配列には、VertexElements
プロパティを使います。
■ VertexPositionColor 構造体 VertexElements プロパティ
public static readonly VertexElement[] VertexElements
このプロパティは、VertexPositionColor 構造体の頂点要素を表す配列を返します。このプロパティが返した値から生成した VertexDeclaration
を GraphicsDevice に設定することで、DrawUserPrimitives() メソッドの頂点に VertexPositionColor
構造体を利用することができます。
VertexPositionColor 構造体のコンストラクタには、頂点の座標と色を表す値を指定します。
■ VertexPositionColor 構造体のコンストラクタ
public VertexPositionColor (
Vector3 position,
Color color
)
position には、頂点の座標を表す Microsoft.Xna.Framework.Vector3 構造体の値を、Color には頂点の色を指定します。
3 次元空間の座標を表すには、Vector2 構造体で表していた X 座標、Y 座標に加えて、Z 座標が必要になります。そのため、Vector2 構造体に
Z 座標を加えた Vector3 構造体が使われます。
■ Microsoft.Xna.Framework.Vector3 構造体
[TypeConverterAttribute("typeof(Microsoft.Xna.Framework.Design.Vector3Converter)")]
[SerializableAttribute]
public struct Vector3 : IEquatable<Vector3>
この構造体のコンストラクタには、次のようなものがあります。
■ Vector3 構造体のコンストラクタ
public Vector3 (
float x,
float y,
float z
)
x には X 座標の値、y には Y 座標の値、z には Z 座標の値をそれぞれ指定します。ここで指定した値は、それぞれ X フィールド、Y
フィールド、Z フィールドから取得することができます。
■ Vector3 構造体 X フィールド
public float X
■ Vector3 構造体 Y フィールド
public float Y
■ Vector3 構造体 Z フィールド
public float Z
これで、描画するプリミティブの頂点を用意し、GraphicsDevice
に頂点の情報を設定して描画する準備を行うことができます。ただし、これだけではプリミティブを描画できません。
エフェクト
複数の頂点から構成されたプリミティブを用意することができれば、次に、プリミティブを描画するための処理方法を設定しなければなりません。3 次元グラフィックスでは、プリミティブをそのままディスプレイに投影することができないため、様々な変換処理を行ってディスプレイが表示する平面のスクリーン座標に変換します。
プリミティブを画面上に表示するための作業は、エフェクトによって行われます。エフェクトは、頂点の位置や光源などを処理し、最終的に画面に描画するピクセルの色を生成します。エフェクトは Microsoft.Xna.Framework.Graphics.Effect クラスによって表されます。
■ Microsoft.Xna.Framework.Graphics.Effect クラス
public class Effect : IDisposable
DrawUserPrimitives() などでプリミティブを描画するには、適用するエフェクトの Begin() メソッドを先に呼び出さなければなりません。
■ Effect クラス Begin() メソッド
public void Begin ()
プリミティブの描画は、Effect オブジェクトの Begin() メソッドを呼び出した後に行い、すべてのプリミティブの描画が終了した時点で End()
メソッドを呼び出します。
■ Effect クラス End() メソッド
public void End ()
これらの機能を利用するには Effect クラスのインスタンスを生成しなければなりませんが、独自の Effect を作成するのは簡単ではありません。
本稿では、ハードウェアや頂点の変換処理の詳細については割愛させていただきますが、XNA Framework ゲームが対象とする GPU は API
を介してアセンブリコードを実行することができます。このアセンブリコードはシェーダと呼ばれています。
開発者は、シェーダを記述することによってハードウェアに直接アクセスし、頂点データを変換してディスプレイに描画可能なピクセルを生成することができます。この変換作業には、頂点を変換する頂点シェーダと呼ばれる作業と、ピクセルを生成するピクセルシェーダの
2 つが存在します。
エフェクトの役割は、アプリケーションが持つデータを GPU に渡し、頂点シェーダとピクセルシェーダを提供することです。独自の Effect
を生成するということは、シェーダをプログラムするということになります。
かつては、一般的なアプリケーション開発の古い時代と同じように専用のアセンブリ言語を使ってシェーダを書いていました。しかし、アセンブリ言語は複雑で生産性が低いため、現在ではシェーダ専用の高水準言語が使われます。
XNA Framework では、Microsoft が開発した HLSL (High Level Shader Language)
と呼ばれるシェーダ言語が使われます。仕組みは一般的なプログラミング言語と同じで、テキストで書かれたソースコードがコンパイルされ、GPU
用のコードが生成されます。
しかし、最初からシェーダプログラムを書くことを要求しては敷居が高すぎます。そこで、XNA Framework では、基本的なシェーダ機能を提供する Microsoft.Xna.Framework.Graphics.BasicEffect クラスを用意しています。
■ Microsoft.Xna.Framework.Graphics.BasicEffect クラス
public class BasicEffect : Effect
このクラスは Effect を継承し、頂点の描画に必要な基本的な機能を提供します。このクラスのコンストラクタには、次のようなものがあります。
■ BasicEffect クラスのコンストラクタ
public BasicEffect (
GraphicsDevice device,
EffectPool effectPool
)
device には、このエフェクトを利用する GraphicsDevice を指定します。effectPool には、エフェクトのリソースを共有するための Microsoft.Xna.Framework.Graphics.EffectPool
クラスのオブジェクトを指定します。このパラメータは null でもかまいません。
■ Microsoft.Xna.Framework.Graphics.EffectPool クラス
public class EffectPool : IDisposable
BasicEffect のオブジェクトを生成することができれば、プリミティブを描画する前に Begin()
メソッドを呼び出して準備を行ってください。また、VertexPositionColor 構造体を頂点データとする場合は VertexColorEnabled
プロパティを true に設定します。
■ BasicEffect クラス VertexColorEnabled プロパティ
public bool VertexColorEnabled { get; set; }
このプロパティは、頂点の色を有効にするかどうかを表します。このプロパティを true に設定することで、プリミティブの描画に頂点の色が使われます。
テクニック
次に、取得したエフェクトから使用する頂点シェーダとピクセルシェーダを設定しなければなりません。そのためには、まずエフェクト内のいずれかのテクニックを選択します。
テクニックとは、使用する頂点シェーダとピクセルシェーダを定義するパスと呼ばれるオブジェクトのコンテナ(入れ物)となるオブジェクトです。エフェクトは任意の数のテクニックを保有することができ、テクニックは任意の数のパスを保有できます。
エフェクトが持つテクニックは Techniques プロパティから取得することができます。
■ Effect クラス Techniques プロパティ
public EffectTechniqueCollection Techniques { get; }
このプロパティは、Microsoft.Xna.Framework.Graphics.EffectTechniqueCollection クラスのオブジェクトを返します。このクラスは、任意の数のテクニックを管理するコレクションです。
■ Microsoft.Xna.Framework.Graphics.EffectTechniqueCollection
クラス
public sealed class EffectTechniqueCollection :
IEnumerable<EffectTechnique>
コレクションが保有しているテクニックの個数は Count プロパティから取得できます。
■ EffectTechniqueCollection クラス Count プロパティ
public int Count { get; }
テクニックは、インデクサを使って取得することができます。インデクサはオーバーロードされていて、インデックスに加えてテクニックの名前から取得することも可能になっています。
■ EffectTechniqueCollection クラスのインデクサ
public EffectTechnique this [
int index
] { get; }
public EffectTechnique this [
string name
] { get; }
index には、取得するテクニックのインデックスを整数で指定します。name は、取得するテクニックの名前を指定します。
テクニックは Microsoft.Xna.Framework.Graphics.EffectTechnique クラスで表されます。Techniques
プロパティから取得するコレクションは、EffectTechnique オブジェクトを提供します。
■ Microsoft.Xna.Framework.Graphics.EffectTechnique クラス
public sealed class EffectTechnique
または、CurrentTechnique プロパティからも EffectTechnique
を取得することができます。独自のエフェクトを使って、テクニックの検索や切り替えといった処理を行う場合を除いて、BasicEffect を使う場合は CurrentTechnique
プロパティからテクニックを取得する方法が一般的でしょう。
■ Effect クラス CurrentTechnique プロパティ
public EffectTechnique CurrentTechnique { get; set; }
テクニックの名前を取得するには Name プロパティを使います。複数のテクニックを識別する場合に利用できます。
■ EffectTechnique クラス Name プロパティ
public string Name { get; }
テクニックは、任意の数のパスを含むコンテナとして機能します。テクニックが保有するパスは Passes プロパティから取得できます。
■ EffectTechnique クラス Passes プロパティ
public EffectPassCollection Passes { get; }
このプロパティは、任意の数のパスを管理する Microsoft.Xna.Framework.Graphics.EffectPassCollection
クラスのオブジェクトを返します。
■ Microsoft.Xna.Framework.Graphics.EffectPassCollection クラス
public sealed class EffectPassCollection : IEnumerable<EffectPass>
EffectPassCollection クラスは、パスのコレクションです。このコレクションが返したパスを使ってプリミティブを描画します。このクラスの機能は、EffectTechniqueCollection
クラスと同じです。Count プロパティから保有するパスの個数を取得することができ、インデクサを使ってインデックスまたは名前からパスを取得できます。
パス
パスは Microsoft.Xna.Framework.Graphics.EffectPass クラスで表されます。EffectTechnique
クラスの Passes プロパティが返すコレクションから、EffectPass オブジェクトを取得できます。
■ Microsoft.Xna.Framework.Graphics.EffectPass クラス
public sealed class EffectPass
テクニックと同様に、パスも名前で識別することができます。パスの名前は Name プロパティから取得することができます。
■ EffectPass クラス Name プロパティ
public string Name { get; }
テクニックから取得したパスのシェーダを利用するには Begin() メソッドを呼び出します。
■ EffectPass クラス Begin() プロパティ
public void Begin ()
これで、このパスに関連付けられている頂点シェーダとピクセルシェーダが使われるようになります。この後に、GraphicsDevice の DrawUserPrimitives()
メソッドでプリミティブを描画すると、対象のパスが表すシェーダが用いられます。
プリミティブの描画が終了すれば End() メソッドを呼び出してパスを終了してください。
■ EffectPass クラス End() プロパティ
public void End ()
これで、ようやくプリミティブを描画するための一連の流れが完成しました。BasicEffect オブジェクトを使っている場合は、1 つのテクニックの中に 1
つのパスがあるだけです。そのため、次のように記述しても問題はありません。
effect.Begin();
EffectPass pass = effect.CurrentTechnique.Passes[0];
pass.Begin();
//プリミティブの描画
pass.End();
effect.End();
含まれているすべてのパスを使ってプリミティブを描画する場合は、次のようなループを作ります。
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
//プリミティブの描画
pass.End();
}
effect.End();
これまでの内容の手順を踏むことで、ようやくプリミティブを描画することができます。頂点の座標について詳しくは後述します。最初は X、Y 座標を
-1.0 〜 1.0 の範囲で、Z 座標を 0.0 〜 1.0 の範囲で設定してください。ただし、Z 座標の値は、この時点では効果を与えません。
それでは、さっそく BasicEffect を使ってプリミティブを描画してみましょう。
■ Sample01
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(0.0F, 0.0F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(1.0F, 1.0F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(0.0F, 0.0F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(-1.0F, -1.0F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(-1.0F, 1.0F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(1.0F, -1.0F, 0.0F), Color.Blue),
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.LineList, points, 0, 3);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample01 は、黒、赤、青の 3
つの線を表示します。これらの線は、あらかじめ用意した各頂点を線のプリミティブとして描画したものです。頂点の各軸の座標は 0 を中心としています。X
座標は値が増えるほど右に、Y 座標は値が増えるほど上に向かいます。
3 次元グラフィックスの座標値は、2
次元グラフィックスのようなピクセル単位ではありません。頂点の座標は相対的なもので、座標変換によって見え方が異なってきます。この場では、座標変換を一切行っていないため、画面中心を
0 として X 座標と Y 座標は -1.0 から 1.0 までの範囲が表示され、Z 座標は 0.0 から 1.0 までの範囲が表示されます。
■ 図 02

Z 座標の値を範囲外に設定すると、範囲外に到達した線の一部が消失します。ここから、Z 座標の値も機能していることが確認できます。
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(0.0F, 0.0F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(1.0F, 1.0F, 1.0F), Color.Black),
new VertexPositionColor(new Vector3(0.0F, 0.0F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(-1.0F, -1.0F, 2.0F), Color.Red),
new VertexPositionColor(new Vector3(-1.0F, 1.0F, -0.5F), Color.Blue),
new VertexPositionColor(new Vector3(1.0F, -1.0F, 1.5F), Color.Blue),
};
■ 図 03

DrawUserPrimitives() メソッドに渡す PrimitiveType
列挙型の値を変更することで、プリミティブの種類を変更することができます。Sample01 では頂点を結ぶ線を描画しましたが、一般的なポリゴンは
TriangleList を使った三角形を組み合わせたものを使います。
■ Sample02
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, 0.0F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(0.1F, 0.8F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(0.5F, 0.0F, 0.0F), Color.Black)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample02 は、用意した頂点を PrimitiveType.TriangleList
で描画した例です。このように、描画するプリミティブの種類を変更することで、同じ頂点を使って異なるプリミティブを描画することができます。複雑な物体も、基本は三角形のプリミティブを組み合わせたものです。多くの場合は三角形を使いますが、頂点の位置を確認したり、立方体の辺のみを描画したい場合などに
PointList や LineList を使います。
■ Sample03
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
private PrimitiveType type;
private int count;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, 0.1F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(0.1F, 0.8F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(0.5F, 0.1F, 0.0F), Color.Black),
new VertexPositionColor(new Vector3(-0.5F, -0.1F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(0.5F, -0.1F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.1F, -0.8F, 0.0F), Color.Green)
};
type = PrimitiveType.TriangleList;
count = points.Length / 3;
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
GamePadState prevState;
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
if (state.Buttons.A == ButtonState.Released && prevState.Buttons.A == ButtonState.Pressed)
{
if (type == PrimitiveType.PointList)
{
type = PrimitiveType.LineList;
count = points.Length / 2;
}
else if (type == PrimitiveType.LineList)
{
type = PrimitiveType.LineStrip;
count = points.Length - 1;
}
else if (type == PrimitiveType.LineStrip)
{
type = PrimitiveType.TriangleList;
count = points.Length / 3;
}
else if (type == PrimitiveType.TriangleList)
{
type = PrimitiveType.PointList;
count = points.Length;
}
}
prevState = state;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(type, points, 0, count);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample03 は、コントローラの A
ボタンを押すことで描画するプリミティブの種類を切り替えることができるプログラムです。同じ頂点を使って、異なるプリミティブの種類で描画を行います。
ただし、ここで描画する三角形の頂点は時計回りに並べなければなりません。三角形には表と裏があり、デフォルトの設定では時計回りの頂点の面を表として描画し、裏面は描画しない設定になっています。
座標変換
これまで描画したプリミティブは、すべて頂点の座標が対応するスクリーン座標に変換されていました。物体を構築する個々のプリミティブの頂点の座標は、ローカル座標、またはモデル座標と呼ばれます。
プリミティブを構築する時、頂点の座標は (0, 0, 0) を中心としたローカル座標で構築されます。このとき、他のプリミティブや 3
次元空間全体を把握する必要はありません。
作成したプリミティブを、他のプリミティブも存在する複雑な 3 次元空間上に配置するときに、目的の位置に頂点を変換します。多くのオブジェクトが配置されている
3 次元の世界全体をワールド座標と呼びます。作成した物体を空間に配置するときにワールド座標に変換し、位置やサイズ、向きを調整することができます。
こうした座標の変換には 4 × 4 の行列が用いられます。本稿では、行列による座標変換の数学的な原理については割愛させていただきます。行列は Microsoft.Xna.Framework.Matrix
構造体で表されます。
■ Microsoft.Xna.Framework.Matrix 構造体
[TypeConverterAttribute(
"typeof(Microsoft.Xna.Framework.Design.MatrixConverter)")]
[SerializableAttribute]
public struct Matrix : IEquatable<Matrix>
この構造体のコンストラクタは、次のようになります。
■ Matrix 構造体のコンストラクタ
public Matrix (
float m11, float m12, float m13, float m14,
float m21, float m22, float m23, float m24,
float m31, float m32, float m33, float m34,
float m41, float m42, float m43, float m44
)
m11 から m44 までの各パラメータには、行列の各要素となる値を指定します。ここに設定した値は、各要素の名前に対応する M11 から M44
プロパティで取得することができます。
行列による座標変換の原理を理解していれば、各要素に目的の値を設定して変換用の行列を作成することができます。しかし、XNA
では変換を行う行列を生成する静的なメソッドが用意されているので、通常はそちらを使って行列を作ります。
BasicEffect のデフォルトでは、座標変換を行わない単位行列が設定されています。単位行列とは、M11、M22、M33、M44 の各要素が 1
で、それ以外の要素は全て 0 の状態の行列です。
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1 |
単位行列は、他の行列に乗算しても常にその数なります。整数の乗算における 1 と同じ効果を持ちます。単位行列は Matrix 構造体の static な Identity プロパティから取得することができます。
■ Matrix 構造体 Identity プロパティ
public static Matrix Identity { get; }
ワールド変換には、BasicEffect クラスの World プロパティに設定されている行列が使われます。デフォルトでは、単位行列が設定されています。
■ BasicEffect クラス World プロパティ
public Matrix World { get; set; }
ワールド変換行列は、Matrix
構造体のコンストラクタに各要素の値を指定して作成することもできますが、目的の行列を生成するメソッドが用意されているので、そちらを使ったほうが便利です。
平行移動
物体を移動させるには、平行移動行列を作成してワールド変換を行います。平行移動行列を生成するには、Matrix 構造体の 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
座標の値を指定します。または、それぞれの座標の値を格納する Vector3 オブジェクトを position パラメータに渡します。
■ Sample04
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, -0.5F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(0.0F, 0.5F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.5F, -0.5F, 0.0F), Color.Green)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
effect.World = Matrix.CreateTranslation(
new Vector3(state.ThumbSticks.Left.X, state.ThumbSticks.Left.Y, 0.0F));
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample04
は、コントローラの左スティックに合わせて三角形が移動するというプログラムです。このとき、三角形の頂点の座標を直接変更するのではなく、ワールド変換行列を使った座標変換によって移動させていることに注目してください。ワールド変換によって、元の頂点データを変更することなく物体を自由に配置することができます。
回転
回転行列を作成することで、指定した軸を中心に物体を回転させることもできます。回転行列は、各軸ごとに作成します。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 には、ラジアン単位で回転させる角度を指定します。ラジアンとは角度の単位の一種で、360 度が 2π
ラジアンに相当します。度数法の単位からラジアンに変換するには、Microsoft.Xna.Framework.MathHelper クラスを使うと簡単です。
■ Microsoft.Xna.Framework.MathHelper クラス
public static class MathHelper
このクラスは、計算を行う静的なメソッドや重要な定数などを提供しています。度からラジアンの変換は ToRadians() メソッドを使います。
■ MathHelper クラス ToRadians() メソッド
public static float ToRadians (
float degrees
)
degrees
に度数法の単位の角度を指定します。メソッドは、指定された角度をラジアン単位に変換した値を返します。同様に、ラジアン単位から度数法の単位に変換するには
ToDegrees() メソッドを使います。
■ MathHelper クラス ToDegrees() メソッド
public static float ToDegrees (
float radians
)
radians にラジアン単位の角度を指定します。メソッドは、指定された角度を度数法単位に変換した値を返します。
これで、プリミティブを任意の方向に回転させることができます。ただし、現時点では物体を立体的に見せるための変換を行わないため Z
座標が機能していません。また、三角形の裏面が描画されないため X 軸や Y 軸を回転させても思うような結果が得られないでしょう。この場では Z
軸を中心に三角形を回転させてみます。
■ Sample05
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, -0.5F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(0.0F, 0.5F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.5F, -0.5F, 0.0F), Color.Green)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
effect.World = Matrix.CreateRotationZ(
MathHelper.ToRadians(180 * (state.Triggers.Left - state.Triggers.Right)));
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample05
は、コントローラの左右のトリガーを引くと三角形が回転するというプログラムです。左トリガーを引くと左方向に、右トリガーを引くと右方向に回転します。
伸縮
伸縮行列を使うことで、プリミティブを拡大したり縮小することができます。伸縮行列の作成には Matrix 構造体の 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.0 を中心に、プリミティブを 2 倍に拡大するには
2.0 を、半分に縮小する場合は 0.5 を指定するという形になります。
3 つの float 型の値を渡すメソッドでは xScale に X 軸の伸縮率を、yScale に Y 軸の伸縮を、zScale に Z
軸の伸縮率をそれぞれ指定します。Vector3 型の値を渡す scales パラメータの場合も、同様に各軸の伸縮率を格納した Vector3 型の値を渡します。
■ Sample06
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, -0.5F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(0.0F, 0.5F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.5F, -0.5F, 0.0F), Color.Green)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
effect.World = Matrix.CreateScale(1.0F + state.ThumbSticks.Right.Y);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample06 は、コントローラの右スティックを上下させることで、描画されている三角形のサイズを伸縮させることができるというプログラムです。
このように、ワールド変換を使うことで、プリミティブの頂点の座標を直接操作することなく、プリミティブを平行移動、回転、伸縮させることができます。そのため、位置や向き、サイズごとにモデルを何度も作る必要はありません。ローカル座標で作成した
1 つのモデルを、ワールド変換で 3 次元空間の任意の場所に配置することができます。
変換の組み合わせ
これまで、平行移動、回転、伸縮の行列を個別に作成してワールド変換を行いましたが、これらの変換を組み合わせたい場合はどうするのでしょうか。たとえば、プリミティブを平行移動させ、かつ回転させるような変換です。このような、複数の行列の変換を組み合わせるには、行列の積を求めます。
Matrix 構造体は乗算演算子をオーバーロードしているため、Matrix
オブジェクトどうしを通常の算術演算で書けることで簡単に行列の積を求めることができます。
■ Matrix クラス op_Multiply() メソッド
public static Matrix op_Multiply (
Matrix matrix1,
Matrix matrix2
)
たとえば、平行移動と回転を同時に行う場合、まずは個別に平行移動行列と回転行列を生成します。生成した Matrix オブジェクトを *
演算子で乗算すると、平行移動と回転を組み合わせた Matrix オブジェクトを結果として得られます。
■ Sample07
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, -0.5F, 0.0F), Color.Red),
new VertexPositionColor(new Vector3(0.0F, 0.5F, 0.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.5F, -0.5F, 0.0F), Color.Green)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
Matrix translation = Matrix.CreateTranslation(
new Vector3(state.ThumbSticks.Left.X, state.ThumbSticks.Left.Y, 0.0F));
Matrix rotation = Matrix.CreateRotationZ(
MathHelper.ToRadians(180 * (state.Triggers.Left - state.Triggers.Right)));
Matrix scale = Matrix.CreateScale(1.0F + state.ThumbSticks.Right.Y);
effect.World = translation * rotation * scale;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample07
は、これまでの平行移動、回転、伸縮の変換を組み合わせてコントロールすることができるプログラムです。コントローラの左スティックで移動、トリガーで回転、右スティックで伸縮を、同時に行えることを確認してください。
カメラの操作
これまでのように、ワールド変換によってプリミティブを移動、回転、伸縮させることで、元の頂点情報を変更することなくプリミティブを動かすことができました。ワールド変換は、単一のプリミティブを
3 次元空間に配置するときに利用することができます。
しかし、これまでのプリミティブは 3
次元グラフィックス特有の立体感がありませんでした。通常、遠くにある物体は小さく見え、近くにある物体は大きく見えるものです。Z
座標の値によって、物体の遠近感を表現するには、どこから 3
次元空間を見るかという視点が必要になります。物体を立体的に描画するには、ワールド変換に続いて特定の位置にある視点からプリミティブを捉えたように描画するためのビュー変換が必要になります。
プリミティブを移動させなくても、視点を変更することで描画する部分を変更することができます。たとえば、視点を左右に移動させることで、描画されている空間を左右にスクロールできます。3D
ゲームの視点移動や視点変更は、描画するプリミティブを変換しているのではなく、世界を見つめる視点(カメラ)を移動させているのです。
ビュー変換を設定することで、特定の位置から空間を見ているような形に頂点を変換できます。ビュー変換には BasicEffect クラスの
View プロパティを使います。
■ BasicEffect クラス View プロパティ
public Matrix View { get; set; }
View プロパティには、ビュー変換を行うために Matrix 構造体の値を設定します。ビュー変換用の行列は Matrix 構造体の
CreateLootAt() メソッドから取得することができます。
■ Matrix 構造体 CreateLookAt() メソッド
public static Matrix CreateLookAt (
Vector3 cameraPosition,
Vector3 cameraTarget,
Vector3 cameraUpVector
)
cameraPosition には視点の座標を指定します。cameraTarget に視点の焦点となる座標を、cameraUpVector
に視点の上部となる座標を指定します。
このメソッドが返す行列を View プロパティに設定することで、cameraPosition に指定された座標から、cameraTarget
に指定された座標を見つめるように変換されます。cameraUpVector は視点を傾けるときに使うもので、通常は (0.0, 1.0, 0.0)
を指定することになります。
遠近法
物体を立体的に描画するには、ビュー変換の設定に加えて視野角や物体を見ることができる距離などを設定しなければなりません。頂点には深さを表す Z 座標がありましたが、これまで描画したプリミティブは Z
座標の値を変更しても見え方に変化はありませんでした。通常、遠くのものはより小さく見え、近くのものはより大きく見えるものです。こうした遠近法をプリミティブの描画に適用するには、射影変換を行います。
射影変換を行うには BasicEffect クラスの 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 には視野角をラジアン単位で指定します。aspectRation にはアスペクト比を指定します。nearPlaneDistance
と farPlaneDistance は、投影する最も近くの面と最も遠くの面までの距離を指定します。この間にある物体は描画されますが、nearPlaneDistance
よりも手前、farPlaneDistance よりも遠くにある物体は描画されません。
視野角は、視点から垂直方向の角度を表します。視野角が狭ければ空間が絞り込まれるため物体が大きく見えるでしょう。逆に、視野角を広く設定すれば小さく見えるようになります。
アスペクト比は視野角に対する水平方向の角度を決定します。アスペクト比が 1
であれば視野角と同じ幅となります。通常は、スクリーンの幅÷高さをアスペクト比として指定しますが、アスペクト比を固定したい場合は、特定の値を指定することもあるでしょう。
nearPlaneDistance と farPlaneDistance に指定する距離は、視点の座標から見た距離となります。nearPlaneDistance
の値は、0.0 よりも大きな値で、かつ farPlaneDistance よりも小さな値を指定しなければなりません。
■ Sample08
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionColor[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
points = new VertexPositionColor[] {
new VertexPositionColor(new Vector3(-0.5F, -0.5F, 1.0F), Color.Red),
new VertexPositionColor(new Vector3(0.0F, 0.5F, 1.0F), Color.Blue),
new VertexPositionColor(new Vector3(0.5F, -0.5F, 1.0F), Color.Green)
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionColor.VertexElements
);
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.VertexColorEnabled = true;
graphics.GraphicsDevice.VertexDeclaration = declaration;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Update(GameTime gameTime)
{
GamePadState state = GamePad.GetState(PlayerIndex.One);
Vector3 pos = new Vector3(
5 * state.ThumbSticks.Left.X,
5 * state.ThumbSticks.Left.Y,
5 - (2 * state.ThumbSticks.Right.Y)
);
effect.View = Matrix.CreateLookAt(pos, new Vector3(0, 0, 0), new Vector3(0, 1, 0));
effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 4 / 3, 0.1F, 10F);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample08 は、コントローラの左スティックで視点の X 座標と Y 座標、右スティックで Z
座標の値を変更することができるプログラムです。このプログラムでは、プリミティブの座標は変更していませんが、視点を移動させることでプリミティブを動かすことができます。実際に動いているのは、プリミティブではなく視点です。
テクスチャ
これまで描画したプリミティブの表面には、頂点の VertexPositionColor
構造体に指定した色が使われていましたが、煉瓦やコンクリートのような質感のプリミティブを描画するには、通常はテクスチャが使われます。
テクスチャは、 2 次元グラフィックスでも扱った Texture2D
型のオブジェクトです。何らかの画像ファイルを使って、プリミティブの表面を描画することができます。この方法で、複雑な模様や特殊な質感の壁や地面などを簡単に作ることができます。
テクスチャを描画するには、まず BasicEffect クラスの TextureEnabled プロパティを true にしなければなりません。
■ BasicEffect クラス TextureEnabled プロパティ
public bool TextureEnabled { get; set; }
このプロパティが true である場合、BasicEffect はプリミティブの描画に設定されているテクスチャを使います。テクスチャの設定は
Texture プロパティから行います。
■ BasicEffect クラス Texture プロパティ
public Texture2D Texture { get; set; }
これで、プリミティブの表面には、Texture
プロパティで設定したテクスチャが用いられるようになります。そのためには、描画するプリミティブの頂点をテクスチャに対応させなければなりません。テクスチャを描画するには、頂点に Microsoft.Xna.Framework.Graphics.VertexPositionTexture 構造体の値を使います。
■ Microsoft.Xna.Framework.Graphics.VertexPositionTexture 構造体
[SerializableAttribute]
public struct VertexPositionTexture
この構造体のコンストラクタには、頂点の座標と頂点が対応するテクスチャの位置を指定します。
■ VertexPositionTexture 構造体のコンストラクタ
public VertexPositionTexture (
Vector3 position,
Vector2 textureCoordinate
)
position に頂点の座標を指定し、textureCoordinate に頂点が対応するテクスチャの位置を指定します。テクスチャの位置は、左上隅を原点
(0, 0) とし、右下隅を (1, 1) とした座標系を使います。長方形のテクスチャ全体を描画するには、2 つの三角形を描画する必要があります。
■ Sample09
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private ContentManager manager;
private Texture2D texture;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionTexture[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
manager = new ContentManager(this.Services);
points = new VertexPositionTexture[] {
new VertexPositionTexture(new Vector3(-0.5F, -0.5F, 0), new Vector2(0, 1)),
new VertexPositionTexture(new Vector3(-0.5F, 0.5F, 0), new Vector2(0, 0)),
new VertexPositionTexture(new Vector3(0.5F, -0.5F, 0), new Vector2(1, 1)),
new VertexPositionTexture(new Vector3(-0.5F, 0.5F, 0), new Vector2(0, 0)),
new VertexPositionTexture(new Vector3(0.5F, 0.5F, 0), new Vector2(1, 0)),
new VertexPositionTexture(new Vector3(0.5F, -0.5F, 0), new Vector2(1, 1))
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
texture = manager.Load<Texture2D>("Test");
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionTexture.VertexElements
);
graphics.GraphicsDevice.VertexDeclaration = declaration;
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.TextureEnabled = true;
effect.Texture = texture;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
if (unloadAllContent)
manager.Unload();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 2);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample09 は、2 つの三角形を組み合わせて長方形の物体を描画しています。長方形の表面には、BasicEffect
に設定されているテクスチャが用いられます。三角形の各頂点は、テクスチャの四辺に対応しているため、描画する長方形の表面にテクスチャ全体が貼られることになります。
複数のテクスチャを貼り付けたい場合、プリミティブを描画する直前に BasicEffect
オブジェクトのテクスチャを更新するだけでは効果がありません。Begin() メソッドと End() メソッドの間でエフェクトの設定を変更した場合
CommitChanges() メソッドを呼び出して設定の変更を通知しなければなりません。
■ Effect クラス CommitChanges() メソッド
public void CommitChanges ()
いくつかのプリミティブの描画で、異なる座標変換やテクスチャを使いたい場合に利用できます。エフェクトに対してプロパティを設定した後、DrawUserPrimitives()
メソッドでプリミティブを描画してください。
■ Sample10
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
public class Test : Game
{
public static void Main(string[] args)
{
using (Game game = new Test()) game.Run();
}
private GraphicsDeviceManager graphics;
private ContentManager manager;
private Texture2D texture1, texture2;
private VertexDeclaration declaration;
private BasicEffect effect;
private VertexPositionTexture[] points;
public Test()
{
graphics = new GraphicsDeviceManager(this);
manager = new ContentManager(this.Services);
points = new VertexPositionTexture[] {
new VertexPositionTexture(new Vector3(-0.5F, -0.5F, 0), new Vector2(0, 1)),
new VertexPositionTexture(new Vector3(-0.5F, 0.5F, 0), new Vector2(0, 0)),
new VertexPositionTexture(new Vector3(0.5F, -0.5F, 0), new Vector2(1, 1)),
new VertexPositionTexture(new Vector3(-0.5F, 0.5F, 0.5F), new Vector2(0, 0)),
new VertexPositionTexture(new Vector3(0.5F, 0.5F, 0.5F), new Vector2(1, 0)),
new VertexPositionTexture(new Vector3(0.5F, -0.5F, 0.5F), new Vector2(1, 1))
};
}
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
texture1 = manager.Load<Texture2D>("test1");
texture2 = manager.Load<Texture2D>("test2");
}
declaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionTexture.VertexElements
);
graphics.GraphicsDevice.VertexDeclaration = declaration;
effect = new BasicEffect(graphics.GraphicsDevice, null);
effect.TextureEnabled = true;
base.LoadGraphicsContent(loadAllContent);
}
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
declaration.Dispose();
effect.Dispose();
if (unloadAllContent)
manager.Unload();
base.UnloadGraphicsContent(unloadAllContent);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.White);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
effect.Texture = texture1;
effect.CommitChanges();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 0, 1);
effect.Texture = texture2;
effect.CommitChanges();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, points, 3, 1);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
■ 実行結果

Sample10 は、1 つの BasicEffect オブジェクトを使って 2 つの三角形上に異なるテクスチャを描画させています。テクスチャの設定は
Begin() 〜 End() メソッドの間で行っているため、プロパティを設定したあと CommitChanges() メソッドで変更を通知しています。
マインスイーパーの 3 次元化
それでは、本稿の内容を基礎に、前回までに作成した 2 次元ベースのマインスイーパーを 3
次元に対応させましょう。簡単に作るために、前回までに描画するマインスイーパーとして使っていた画像ファイルをプリミティブのテクスチャとして貼り付けます。基本的には、SpriteBatch
で行っていたことをプリミティブに変更するだけです。
特に描画関連のコードが複雑になりがちな 3 次元グラフィックスでは、パフォーマンスに十分注意しなければなりません。Draw()
メソッドの処理に時間がかかると、極端にゲームが遅くなってしまいます。
通常、ゲームのデータや描画に必要なインスタンスは、コンストラクタや Initialize() メソッド、LoadGraphicsContent() メソッドなどで生成しておきます。何度も繰り返し呼び出される
Update() メソッドや Draw()
メソッドは、可能な限り最小限の処理だけにします。この中で、ローカル変数として複雑なオブジェクトのインスタンス化を繰り返すと、著しくパフォーマンスが減少します。
そこで、コンストラクタで事前に描画するプリミティブの頂点を初期化します。そして、設定された地雷原の情報を元に各プリミティブ用の BasicEffect
を用意し、設定できる項目をこの時点で設定します。
■ Minesweeper クラス Draw() メソッド
protected override void Draw(GameTime gameTime)
{
GraphicsDevice g = graphics.GraphicsDevice;
g.Clear(Color.White);
effect.Begin();
effect.CurrentTechnique.Passes[0].Begin();
for (int r = 0; r < mineField.Row; r++)
{
for (int c = 0; c < mineField.Column; c++)
{
int mines = mineField.GetAdjacentMines(c, r);
ImageName imageName = (ImageName)Enum.Parse(typeof(ImageName), mines.ToString(), true);
effect.World = Matrix.CreateTranslation(c * 2, r * -2, 0);
effect.View = view;
effect.Projection = projection;
if ((mineField[c, r] & FieldState.Mine) == FieldState.Mine)
effect.Texture = images[ImageName.MineField];
else effect.Texture = images[imageName];
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
if ((mineField[c, r] & FieldState.Opened) != FieldState.Opened) //開かれていない
{
if (c == mineField.SelectedColumn && r == mineField.SelectedRow)
effect.Texture = images[ImageName.Selector];
else effect.Texture = images[ImageName.HideField];
effect.World = Matrix.CreateTranslation(c * 2, r * -2, 2);
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
effect.World = Matrix.CreateRotationY(
MathHelper.ToRadians(-90)) * Matrix.CreateTranslation(c * 2 - 1, r * -2, 1F);
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
effect.World = Matrix.CreateRotationY(
MathHelper.ToRadians(90)) * Matrix.CreateTranslation(c * 2 + 1, r * -2, 1F);
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
effect.World = Matrix.CreateRotationX(
MathHelper.ToRadians(-90)) * Matrix.CreateTranslation(c * 2, r * -2 + 1, 1F);
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
effect.World = Matrix.CreateRotationX(
MathHelper.ToRadians(90)) * Matrix.CreateTranslation(c * 2, r * -2 - 1, 1F);
effect.CommitChanges();
g.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, basePrim, 0, 2);
}
}
}
effect.CurrentTechnique.Passes[0].End();
effect.End();
base.Draw(gameTime);
}
■ 実行結果

今回のマインスイーパーの大部分は、第 2 回で開発したマインスイーパと同じです。異なるのは Minesweeper クラスの描画関連のコードで、SpriteBatch
でテクスチャを描画していた部分を BasicEffect と DrawUserPrimitives() メソッドで置き換えています。
あからじめ、コンストラクタで長方形となる 2 つの三角形を表す頂点を用意し basePrim フィールドに保存しています。Draw()
メソッドでは、ワールド変換でプリミティブを移動、回転させることで、basePrim から任意の行と列からなる地雷原のブロックを描画しています。
Update()
メソッドでは、データの更新に加えてコントローラの右スティックと左右のトリガーで視点の移動を行えるように書き加えています。コントローラの操作によってビュー変換用の行列
view フィールドを更新しています。
■ MineSweeper クラス Update() メソッド抜粋
//視点の座標移動
cameraPos.X = cameraPos.X + state.ThumbSticks.Right.X;
cameraPos.Y = cameraPos.Y + state.ThumbSticks.Right.Y;
cameraPos.Z = cameraPos.Z + (state.Triggers.Left - state.Triggers.Right);
//視点の移動範囲を超えている場合は固定する
if (cameraPos.X < -50) cameraPos.X = -50;
else if (cameraPos.X > 50) cameraPos.X = 50;
if (cameraPos.Y < -50) cameraPos.Y = -50;
else if (cameraPos.Y > 50) cameraPos.Y = 50;
if (cameraPos.Z < 10) cameraPos.Z = 10;
else if (cameraPos.Z > 50) cameraPos.Z = 50;
//選択されているブロックを注目点に設定する
cameraTarget.X = mineField.SelectedColumn * 2;
cameraTarget.Y = mineField.SelectedRow * -2;
view = Matrix.CreateLookAt(cameraPos, cameraTarget, cameraUpVector);
視点の移動を可能にしている場合や、プリミティブを移動、回転させるような場合、プリミティブの裏面は描画されないことに注意してください。
|