忍者ブログ

Memeplexes

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

[XNA]バンプマッピングで1つのポリゴンをでこぼこさせる

どうもGPUでのエリア総和テーブルは上手くいかないので、問題に対処するもっとも有効で一般的な方法、すなわち現実逃避に走ることにします。

そう、そもそもなぜエリア総和テーブルをやろうとしていたのかというとAIに画像処理をやらせようとしていたからなんです!
べつにGPUでなくてもCPUで計算すればいいだけのことなので、いつまでもGPUでエリア総和テーブルをやることにこだわっているのは建設的ではありません!


バンプマッピング

そういうわけで、現実逃・・・もとい気分転換のために、バンプマッピングをやってみました。

バンプマッピングとは、ポリゴンを、あたかも平面ではないかのように見せる面白いテクニックです。
ポリゴン自体は単なる三角形なのですが、ピクセルシェーダーによって、各ピクセルがあたかもでこぼこしているかのように見せるのです。
(まあ、CGそのものが「あたかも」そこに物体があるかのように見せるものなので、あんまり「あたかも」を連発するとバンプマッピングがかわいそうです)

具体例は、たとえばMSDNにがありますね。
MSDNのこの例では地球のモデルをでこぼこさせていて、とってもクールです。
地球のモデル自体は球(っぽいポリゴンの集合体)なのですが、バンプマッピングによって、地球表面の凹凸がはっきりわかります。
バンプマッピングを使っていない左側の風船ボールみたいな地球とは一線を画します。
ここでは高さのデータを保持したテクスチャを用意して、そのデータをもとに法線ベクトルを導き、ピクセルシェーダー内でその法線ベクトルを使ってライティングしているんでしょう。

また、最近Xbox360の(欧米では)大人気なゲームHalo3をやったのですが、いい感じにバンプマッピングがされていて、フラッドのあの肉の鍾乳洞が蠢く様子は本当にリアルに動いていて気持ち悪かったです。
あれは輪郭がカクカクしていたのでおそらくモデル自体はかなり単純なのでしょうが、バンプマッピングによって表面の凹凸がリアルに表現されているということなのでしょう。

要するに、ポリゴンで作ったモデル自体は単純でも、バンプマッピングによってあたかも複雑なモデルであるかのように見せることができるというわけですね。


バンプマッピングに必要なもの

バンプマッピングをやるには次の2つが必要です。

1.3Dモデル(ただし、普通のモデルとは違って頂点のメンバに、「テクスチャ座標のX軸方向を表す3Dベクトル」を持っていなければいけません。なぜYとZではなくXだけかというと、まず法線は「テクスチャ座標のZ軸方向を表す3Dベクトル」だからです。Yはこの2つの外積をとればいいだけです。この3つのベクトルを使って、ポリゴンをに当たる光のベクトルを計算するのです。そしてそのベクトルと次の法線マップからゲットした法線の内積を計算すれば・・・ポリゴンの明るさが導けて、ライティングできるのです。)

2.法線マップ(各ピクセル(テクセル)に法線の情報を格納したテクスチャです。色の情報のR, G, BをそれぞれX, Y, Zに対応させるわけです。)

3Dモデルには法線の仲間みたいなものをさらにもう1つ付け加える必要がありますが、そんなに難しくないでしょう(たぶん)。
法線は「ポリゴンのZ軸方向がどっちを向いているか」を表していましたが、ここで付け加えるのは「ポリゴンのX軸方向がどっちを向いているか」です(ここで言ってるXとかYとかは、テクスチャ座標で使っているX、Yの方向のことです)
この情報がなぜ必要かというと、法線マップの法線の情報を上手く解釈するのに必要だからです。
法線マップの情報はポリゴン上の座標系でのみ意味をもつもので、そのままではワールド座標系では使えないのです。
この2つの座標系をつなぐのに、この「ポリゴンのX軸方向がどっちを向いているか」が必要です。

もう一つはポリゴンを複雑に見せるための、法線マップです。
ここに描いた色によって、ポリゴンの法線が決まります。
これを複雑にすれば、たとえ3Dモデルが単純でも、複雑に見えることでしょう。


実際にやってみる

ここでは、シンプルにするため、3Dモデルは単純な四角形にすることにしました。
(最初は三角形の方がシンプルかとも思いましたが、見た目はこっちの方がわかりやすい気がします)

法線マップは、ペイントで2つ描きました。(たぶん実際やる時には高さマップを描いて、それをツールで法線マップに変換したりするんでしょうね)

NormalMap.png
NormalMap.png
っぽいの(0, 128, 255)は(-1, 0, 1)、ピンクっぽいの(255, 128, 255)は(1, 0, 1)を意味しています。
ようするに、宇宙船の太陽電池パネルが開きかけているようなイメージです(屏風といった方がわかりやすいでしょうか・・・)。

NormalMap2.jpg(しまった!jpgで保存しちゃった・・・)
NormalMap2.jpg
内側がへこんでいるイメージです。

コードはこんな感じです。
MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

struct VertexPositionNormalTextureTangent
{
    public Vector3 Position;
    public Vector3 Normal;
    public Vector2 TextureCoordinate;
    public Vector3 NormalMapXDirection;

    public VertexPositionNormalTextureTangent(
        Vector3 position,
        Vector3 normal,
        Vector2 textureCoordinate,
        Vector3 normalMapXDirection)
    {
        this.Position = position;
        this.Normal = normal;
        this.TextureCoordinate = textureCoordinate;
        this.NormalMapXDirection = normalMapXDirection;
    }

    public static readonly VertexElement[] VertexElements = {
        new VertexElement(
            0,
            0,
            VertexElementFormat.Vector3,
            VertexElementMethod.Default, 
            VertexElementUsage.Position,
            0),
        new VertexElement(
            0,
            sizeof(float) * 3, 
            VertexElementFormat.Vector3, 
            VertexElementMethod.Default,
            VertexElementUsage.Normal,
            0),
        new VertexElement(
            0, 
            sizeof(float) * (3 + 3), 
            VertexElementFormat.Vector2, 
            VertexElementMethod.Default, 
            VertexElementUsage.TextureCoordinate, 
            0),
        new VertexElement(
            0,
            sizeof(float) * (3 + 3 + 2),
            VertexElementFormat.Vector3, 
            VertexElementMethod.Default, 
            VertexElementUsage.TextureCoordinate,
            1)
    };
}

public class MyGame : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;

    Effect effect;

    //四角形の頂点です。
    VertexPositionNormalTextureTangent[] rectangleVertices =
    {
        new VertexPositionNormalTextureTangent(
            new Vector3(-1, 1, 0), Vector3.UnitZ, new Vector2(), Vector3.UnitX
        ),
        new VertexPositionNormalTextureTangent(
            new Vector3(1,1,0), Vector3.UnitZ, new Vector2(1,0), Vector3.UnitX
        ),
        new VertexPositionNormalTextureTangent(
            new Vector3(-1,-1,0), Vector3.UnitZ, new Vector2(0,1), Vector3.UnitX
        ),

        new VertexPositionNormalTextureTangent(
            new Vector3(1,1,0), Vector3.UnitZ, new Vector2(1,0), Vector3.UnitX
        ),
        new VertexPositionNormalTextureTangent(
            new Vector3(1,-1,0), Vector3.UnitZ, new Vector2(1,1), Vector3.UnitX
        ),
        new VertexPositionNormalTextureTangent(
            new Vector3(-1,-1,0), Vector3.UnitZ, new Vector2(0, 1), Vector3.UnitX
        )
    };

    Matrix worldTransform = Matrix.Identity;


    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        effect = Content.Load<Effect>("BumpMapping");
        effect.Parameters["View"].SetValue(
            Matrix.CreateLookAt(new Vector3(0, 0, 3), new Vector3(), Vector3.Up)
            );
        effect.Parameters["Projection"].SetValue(
            Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(90),
            (float)GraphicsDevice.Viewport.Width / GraphicsDevice.Viewport.Height,
            0.1f, 1000
            ));
        effect.Parameters["Light0Direction"].SetValue(
            new Vector3(-1, 0, -1)
            );
        effect.Parameters["NormalMap"].SetValue(
            Content.Load<Texture2D>("NormalMap2")
            );


        GraphicsDevice.VertexDeclaration = new VertexDeclaration(
            GraphicsDevice,
            VertexPositionNormalTextureTangent.VertexElements
            );
    }

    //↑↓←→キーで
    //四角形を回転します。
    protected override void Update(GameTime gameTime)
    {
        KeyboardState keyboardState = Keyboard.GetState();

        if (keyboardState.IsKeyDown(Keys.Left))
        {
            worldTransform *= Matrix.CreateRotationY(-0.03f);
        }
        if(keyboardState.IsKeyDown(Keys.Right))
        {
            worldTransform *= Matrix.CreateRotationY(0.03f);
        }
        if (keyboardState.IsKeyDown(Keys.Up))
        {
            worldTransform *= Matrix.CreateRotationX(-0.03f);
        }
        if (keyboardState.IsKeyDown(Keys.Down))
        {
            worldTransform *= Matrix.CreateRotationX(0.03f);
        }
    }


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

        effect.Parameters["World"].SetValue(worldTransform);


        effect.Begin();
        effect.CurrentTechnique.Passes[0].Begin();

        GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTextureTangent>(
            PrimitiveType.TriangleList,
            rectangleVertices,
            0,
            2
            );

        effect.CurrentTechnique.Passes[0].End();
        effect.End();

        base.Draw(gameTime);
    }


    static void Main()
    {
        using (MyGame game = new MyGame())
        {
            game.Run();
        }
    }
}

BumpMapping.fx
float4x4 World;
float4x4 View;
float4x4 Projection;

float3 Light0Direction;

texture NormalMap;
sampler NormalSampler = sampler_state
{
	Texture = (NormalMap);
};

struct VertexShaderInput
{
	float4 Position : POSITION;
	float4 Normal : NORMAL;
	float2 TextureCoordinate : TEXCOORD0;
	float4 NormalMapXDirection : TEXCOORD1;
};

struct VertexShaderOutput
{
	float4 Position : POSITION0;
	float2 TextureCoordinate : TEXCOORD0;
	float3 LightDirectionToPolygon : TEXCOORD1;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
	//Position
	VertexShaderOutput output;
	float4 worldPosition = mul(input.Position, World);
	float4 viewPosition = mul(worldPosition, View);
	output.Position = mul(viewPosition, Projection);
    
	//TextureCoordinate
	output.TextureCoordinate = input.TextureCoordinate;
    
	//LightDirectionToPolygon
	float3 zDirection = normalize(mul(input.Normal, (float3x3)World));
	float3 xDirection = normalize(mul(input.NormalMapXDirection, (float3x3)World));
	float3 normalizedLight0Direction = normalize(Light0Direction);
	output.LightDirectionToPolygon = float3(
		dot(normalizedLight0Direction, xDirection),
		dot(normalizedLight0Direction, cross(zDirection, xDirection)),
		dot(normalizedLight0Direction, zDirection)
	);
	
	
    return output;
}

float3 getNormal(float2 texCoord)
{
	return normalize(2 * tex2D(NormalSampler, texCoord) - 1);
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float4 diffuse = dot(
		getNormal(input.TextureCoordinate),
		-input.LightDirectionToPolygon
		);
	
	return diffuse;
}

technique Technique1
{
	pass Pass1
	{
		VertexShader = compile vs_1_1 VertexShaderFunction();
		PixelShader = compile ps_2_0 PixelShaderFunction();
	}
}


normalMapTest1.jpg
normalMapTest2.jpg
ビミョーですね。
これじゃ単なる縞模様にしか見えません。
これはきっと法線マップが単純すぎるからでしょう。

もっと複雑な法線マップではどうでしょうか。
normalMap2Test1.jpg
おお!?

normalMap2Test2.jpg
おおお!?

normalMap2Test3.jpg
すばらしい。
まあうまくいったと言っていいんではないでしょうか。

法線マップはある程度複雑な方が味が出るということでしょうね。






 

拍手[0回]

PR