忍者ブログ

Memeplexes

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

かんたんXNA4.0 HLSL編 その6 マトリックス


ここまではHLSLで面白い効果を出すと言っておきながら、
見た目は3Dではなく2Dでした。
頂点シェーダに流し込む頂点データが変換されずにそのまま返されていたからです。
この点で、Projectionプロパティでモデルを立体的に見せることのできるBasicEffectより劣っています。
この差を埋めることにしましょう。

頂点の座標データを変換するにはマトリックスを使います。
ちょうどBasicEffectのViewプロパティやProjectionプロパティのようにです。
グローバル変数としてマトリックスを宣言し、それをC#側から操作すればいいのです。
HLSL側でマトリックスを宣言するには、次のようにします:

[型][変換前のベクトルの次元数]x[変換後のベクトルの次元数] [変数名];

普通は4次元ベクトル→4次元ベクトルの変換なのでfloat4x4です。
(3DCGの話をしているのになぜ4次元が出てくるのか不思議に思えるかもしれませんが、これはマトリックスによる変換の柔軟性を大きくするためです。3次元ベクトルではなく4次元ベクトルの変換ということにしたほうが、よりいろんな種類の変換が出来るのです。そこで、位置ベクトルなんかは3次元でなくて4次元ベクトルとして扱います。その4次元ベクトルの一番最後の成分には位置の情報ではなく1が入っていますが(ジョークではありませんよ)、これは数学的な計算のつじつまを合わせるためで、特に意味はありません。かといって一番最後の成分に全く意味がないと言うわけでもなく、この値が1でないとマトリックスで変換したときの結果が変になってしまいます。)

float4x4 transform;

ただし、『プログラミングDirectX9グラフィックスパイプライン』なんかをみると、
4次元ベクトル→3次元ベクトルの変換を表すfloat4x3や
3次元ベクトル→3次元ベクトルの変換を表すfloat3x3も使われるようです。
きっとこれはそれほど柔軟性が必要ないからでしょう。(多分)

HLSL側でグローバル変数として宣言したマトリックスはたいていC#側で値がセットされるので
初期化についてはあんまり気にする必要はないでしょう。

このグローバル変数を使って頂点シェーダに引数として入ってくる位置ベクトルを変換していくわけですが、
それにはmul関数を使います。

mul ( vector, matrix )

vectorは変換前のベクトル、matrixは変換を表すマトリックスです。
戻り値は変換後のベクトルです。

mulという名前から想像できるように、これは掛け算を意味する関数です。
(※マトリックスによるベクトルデータの変換は掛け算として表されるのです。)
そのため、mul関数の引数は実はスカラーでもベクトルでも何でもかまいません

mul(2, 3);    // == 6
mul(2, float3(1, 0, 0));    // == float3(2, 0, 0)

もちろん、マトリックス同士のかけ算をすることも出来ます。
その場合は、2つのマトリックスの変換を同時に行うマトリックスを返します。

mul(view, projection);    // viewとprojectionの変換をいっぺんにやってしまうマトリックス。



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");
        Matrix view = Matrix.CreateLookAt(
            new Vector3(2, 0, 3),
            new Vector3(),
            new Vector3(0, 1, 0)
            );
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(90),
            GraphicsDevice.Viewport.AspectRatio,
            0.1f,
            100
            );

        effect.Parameters["Transform"].SetValue(view * projection);
    }

    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
            );
        }
    }
}

MyEffect.fx
float4x4 Transform;

struct VertexPositionColor
{
	float4 Position : POSITION;
	float4 Color : COLOR;
};

VertexPositionColor MyVertexShader(VertexPositionColor input)
{
	VertexPositionColor output = input;
	output.Position = mul(input.Position, Transform);
	return output;
}

float4 MyPixelShader(float4 color : COLOR) : COLOR
{
	return color;
}

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

hlslTransformed.jpg
斜めから見た図です。
きちんと三角形が遠近法の効果でゆがんでいるのがわかると思います。
(いやちょっと微妙? カメラの位置をいろいろ変えてみてください)
これでようやくBasicEffectに一歩近づきました。

BasicEffectと違ってViewやProjectionを分けていません。
この2つのマトリックスを一緒にしてTransformという名前のグローバル変数にしています。
C#側では別々に作ってはいますが、結局掛け合せてエフェクトにセットしています。
こういうのもアリと言うことです。

もちろん、ViewとProjectionを分けることも出来ます。

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");
        Matrix view = Matrix.CreateLookAt(
            new Vector3(2, 0, 3),
            new Vector3(),
            new Vector3(0, 1, 0)
            );
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(90),
            GraphicsDevice.Viewport.AspectRatio,
            0.1f,
            100
            );

        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);
    }

    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
            );
        }
    }
}

MyEffect.fx
float4x4 View;
float4x4 Projection;

struct VertexPositionColor
{
	float4 Position : POSITION;
	float4 Color : COLOR;
};

VertexPositionColor MyVertexShader(VertexPositionColor input)
{
	VertexPositionColor output = input;
	output.Position = mul(input.Position, mul(View, Projection));
	return output;
}

float4 MyPixelShader(float4 color : COLOR) : COLOR
{
	return color;
}

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

実行結果はさっきと同じです。
さっきと違うのは、マトリックスを2つに分けていることです。
このようにマトリックスはいくらでも増やすことが出来ます。

が、あんまり増やしすぎるのも現実的ではないでしょう。
デフォルトで作成されるエフェクトファイルの中には
WorldとViewとProjectionの3つのマトリックスがあります。
そのくらいが適当なのではないでしょうか

拍手[1回]

PR