忍者ブログ

Memeplexes

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

[PR]

×

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


[XNA] シャドウマップで影を作る

XNAでシャドウマップを使った影をやってみました。

シャドウマップ技法のわかりやすい説明は4Gamer.netにありますね。

ようするに、各ピクセルに光が当たっているかどうかを調べることの出来るマップ(テクスチャ)を作って、光が当たっているのなら影なし、当たっていないのなら影ありということですね。

マップの中にはライトに照らされる全ての点の情報が書き込まれています(もちろんグラフィックメモリは有限なので範囲を妥協することになります)
情報といっても難しいことはなくって、単なるその点のライトからの距離です。
このマップの中にある全ての点はライトに照らされるので明るくなり、このマップに含まれない点は照らされないので暗くなり影になります
これは当たり前のことで、実はこのシャドウマップはライトの位置にカメラを持ってきてシーンを描画することによって作られるのです。
ライトから見える点は光に照らされるのは当然ですし、ライトから見えない点に光が当たらないのもあたりまえです(いやまあ、光の回折とかありますけどね!)。
シャドウマップの作成が普通のシーンの描画と違うのは、色の代わりにライトからの距離をピクセルに書き込むことです。

実際にシーンを描画するときは、「このピクセルをライトから見たとき、その画面のどの位置に来るか」を計算して(実は難しいことは何も無くて、ライトのビュー・マトリクスとプロジェクション・マトリクスをかけるだけです)、その位置を元にマップにアクセスします。
ピクセルは、自分自身がマップに含まれるかどうかを確認するのです。含まれていたら光が当たっていますし、含まれていないのなら影になるわけです。
位置をキーにマップにアクセスして得られる情報は、ライトからの距離です。
そうして得られたライトからの距離を、一種のIDとして使います。
距離が自分自身のライトからの距離と一致するのなら、自分はマップに含まれている事がわかりますが、そうでないのならマップに含まれない、つまり影になることがわかります。
(原理的には「距離が一致するかどうか」ですが、視点変更の計算のときの誤差もあるでしょうから、大小で比較することになります。ライトから見て光が当たる点の距離より遠いのなら影になるはずです)


実際にやってみる

shadowMapDemo1.jpg

なるべく話をシンプルにするために、ポリゴン2つで影を作ってみました。
たぶんこれ以上シンプルにするのはムリだと思います。
(ここでは三角形という単純な図形だけですが、シャドウマップ技法は原理上どんな複雑な物体でも上手く働きます。そのかわりグラフィックメモリを食いまくるのですが・・・)

ここでは、上に小さい三角形、下に大きな三角形を配置し、上から平行光で照らしています。
つまりこんなかんじです:
shadowMapSimpleModel.jpg
上の小さな三角形の影が下の大きな三角形に映るというわけです。
光は上からやってくるので、シャドウマップは上からこの2つの三角形をみた感じになります:

shadowMapCreation.jpg

ここでは、白いほどライトから遠くて黒いほどライトに近いということを意味しています。

真ん中にほとんど黒の三角形が一つ、その周りにグレーの三角形が一つ見えると思います。
黒い三角形は上の模式図で言うと、上にある小さな三角形です。
灰色の三角形は下にある大きな三角形です。

この2つの三角形の色が違うのはライトからの距離が違うからです。
ライトは真上から照らしているので、ライトに(平行光に位置という概念はありませんが、便宜上の)距離が近い上の小さな三角形の方が濃くなっているのです。
それに対し大きな三角形はライトからちょっと遠くなるので、少しだけ薄くなり灰色になっているというわけです。

ではコードです。


ShadowMapCreator.fx
float4x4 World;
float4x4 View;
float4x4 Projection;


struct VertexShaderInput
{
    float4 Position : POSITION0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float DistanceFromCamera : TEXCOORD0;
};



VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.DistanceFromCamera = output.Position.z;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return input.DistanceFromCamera;
}

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

シャドウマップを作るシェーダーです。
やることはもう本当に単純で、ピクセルにカメラからの(すなわちライトからの)距離を書き込んでいるだけです。


DrawUsingShadowMap.fx
float4x4 World;
float4x4 View;
float4x4 Projection;

texture ShadowMap;
sampler ShadowMapSampler = sampler_state
{
    Texture = (ShadowMap);
};

float4x4 LightView;
float4x4 LightProjection;

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 PositionOnShadowMap : TEXCOORD0;
    float4 Color : COLOR0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    
    output.Color = input.Color;
    
    output.PositionOnShadowMap = mul(
        worldPosition, 
        mul(LightView, LightProjection)
        );

    return output;
}

bool isLighted(float4 positionOnShadowMap)
{
    float2 texCoord;
  texCoord.x = (positionOnShadowMap.x / positionOnShadowMap.w + 1) / 2;
  texCoord.y = (-positionOnShadowMap.y / positionOnShadowMap.w + 1) / 2;
//誤差があるはずなので、光が当たっているかどうかは //ほんの少しだけ甘く判定します。 return positionOnShadowMap.z <= tex2D(ShadowMapSampler, texCoord).x + 0.001f; } float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { if(isLighted(input.PositionOnShadowMap)) return input.Color; else return input.Color / 3; } technique Technique1 { pass Pass1 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_2_0 PixelShaderFunction(); } }

シャドウマップを使って影を付けながらシーンを描画するシェーダーです。
ピクセルシェーダー内でシャドウマップにアクセスして、光が当たっているかどうかを判定します。
もしそこが影になっていたら色を3で割ります。

MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

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

    VertexPositionColor[] triangleVertices = {
            new VertexPositionColor(new Vector3(0, 1, 0), Color.White),
            new VertexPositionColor(new Vector3(1, 0, 0), Color.Red),
            new VertexPositionColor(new Vector3(-1, 0, 0), Color.Blue)
        };

    Matrix[] triangles = { Matrix.Identity, Matrix.Identity };

    Effect effect;

    Effect shadowMapCreator;
    RenderTarget2D shadowMap;


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


    protected override void Initialize()
    {
        triangles[0] *= Matrix.CreateRotationX(MathHelper.ToRadians(-90));
        triangles[0] *= Matrix.CreateTranslation(0, 0, 0.5f);
        triangles[0] *= Matrix.CreateScale(3);
        triangles[0] *= Matrix.CreateTranslation(0, -4, 0);

        triangles[1] *= Matrix.CreateRotationX(MathHelper.ToRadians(-90));
        triangles[1] *= Matrix.CreateTranslation(0, 0, 0.5f);
        triangles[1] *= Matrix.CreateTranslation(0, -2, 0);

        base.Initialize();
    }


    protected override void LoadContent()
    {
        GraphicsDevice.VertexDeclaration = new VertexDeclaration(
            GraphicsDevice,
            VertexPositionColor.VertexElements
            );


        Matrix lightView = Matrix.CreateLookAt(
                new Vector3(0, 1, 0),
                new Vector3(),
                Vector3.Forward
            );
        Matrix lightProjection = Matrix.CreateOrthographic(
            8, 8,
            0.1f, 30
            );

        shadowMapCreator = Content.Load<Effect>("ShadowMapCreator");
        shadowMapCreator.Parameters["View"].SetValue(lightView);
        shadowMapCreator.Parameters["Projection"].SetValue(lightProjection);



        effect = Content.Load<Effect>("DrawUsingShadowMap");
        effect.Parameters["View"].SetValue(
            Matrix.CreateLookAt(
                new Vector3(0, 1, 8),
                new Vector3(),
                Vector3.Up
            )
        );
        effect.Parameters["Projection"].SetValue(
            Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(90),
                GraphicsDevice.Viewport.AspectRatio,
                0.1f, 100
            )
        );
        effect.Parameters["LightView"].SetValue(lightView);
        effect.Parameters["LightProjection"].SetValue(lightProjection);




        shadowMap = new RenderTarget2D(
            GraphicsDevice,
            512, 512,
            1,
            SurfaceFormat.Single
            );
    }



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


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


        effect.Parameters["ShadowMap"].SetValue(shadowMap.GetTexture());


        foreach (Matrix triangleTransform in triangles)
        {
            effect.Parameters["World"].SetValue(triangleTransform);


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

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                triangleVertices,
                0,
                1
                );

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

        base.Draw(gameTime);
    }


    private void initShadowMap()
    {
        GraphicsDevice.SetRenderTarget(0, shadowMap);
        GraphicsDevice.Clear(Color.White);



        foreach (Matrix triangleTransform in triangles)
        {
            shadowMapCreator.Parameters["World"].SetValue(triangleTransform);

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

            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.TriangleList,
                triangleVertices,
                0,
                1
                );

            shadowMapCreator.CurrentTechnique.Passes[0].End();
            shadowMapCreator.End();
        }

        GraphicsDevice.SetRenderTarget(0, null);

    }

    static void Main(string[] args)
    {
        using (MyGame game = new MyGame())
        {
            game.Run();
        }
    }
}




三角形のモデルは一つだけ用意して、それを大小2つの三角形で使いまわしています。
























拍手[0回]

PR

[XNA] 高さマップでバンプマッピング

前回は法線マップでバンプマッピングをしましたが、法線はどうもエディタで描きにくいので、今度は代わりに高さマップを使ってバンプマッピングをしてみることにしました。

今回使うのはこいつです:

HeightMap.png
HeightMap.png

しばらく起動していなかったフォトショップ(の、エレメント)を使って、まずはじめに黒く塗りつぶし、そのあと白で文字を書いています。
黒いところは高さが低く、白いところは高いつもりです。
ですから、これをバンプマッピングに使うとXNAという文字が飛び出して見えるはずです。

結果を先に言うと、四角形を動かしまくらないとライトの位置が把握しにくいからか、飛び出しているというよりもへこんでいるように見えますね・・・。

heightMappingResult1.jpg

ライトは右手前から左奥に向かって照らしているのですが、ちょっと動かしたくらいでは、人間の心理的効果からか、飛び出しているかへこんでいるかの判定はビミョーです(ネッカーキューブを思い出します)

heightMappingResult2.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["HeightMap"].SetValue(
            Content.Load<Texture2D>("HeightMap")
            );


        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 HeightMap;
sampler HeightSampler = sampler_state
{
	Texture = (HeightMap);
};

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)
{
	const float HeightMapWidth = 128;
	const float HeightMapHeight = 128;
	
	float3 current = 2 * tex2D(HeightSampler, texCoord);
	float3 left = 2 * tex2D(
		HeightSampler,
		float2(texCoord.x - 1.0 / HeightMapWidth, texCoord.y)
	 );
	float3 up = 2 * tex2D(
		HeightSampler, 
		float2(texCoord.x, texCoord.y - 1.0 / HeightMapHeight)
		);
	return normalize(float3(left.x - current.x, up.x - current.x, 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();
	}
}




拍手[0回]


[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回]


XNAでエリア総和テーブル実験その2

エリア総和テーブル(Summed Area table)を作る実験をまたやってみました。

前回は横方向にだけ足しましたが、今回は縦方向にも足します。
驚いたことに、ちょっとした修正をするだけで縦方向にも足せるようになりますね。
これでようやくエリア総和テーブルとして使えるようになるはずです。(たぶん)



MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class MyGame : Game
{
    GraphicsDeviceManager graphics;

    Texture2D texture;
    BasicEffect basicEffect;
    VertexDeclaration vertexDeclaration;

    Effect tableCreationEffect;
    RenderTarget2D renderTarget;
   

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

    protected override void LoadContent()
    {
        //指定したサイズのテクスチャを作ります。
        //サイズをいろいろ変えると、
        //いろいろなサイズのエリア総和テーブルが作れます。
        initTexture(8, 8);

        renderTarget = new RenderTarget2D(
            GraphicsDevice,
            texture.Width, texture.Height,
            1,
            SurfaceFormat.Vector4
            );

        tableCreationEffect = Content.Load<Effect>("SummedAreaTableCreator");
        
        basicEffect = new BasicEffect(GraphicsDevice, null);


        vertexDeclaration = new VertexDeclaration(
            GraphicsDevice,
            VertexPositionTexture.VertexElements
            );
        GraphicsDevice.VertexDeclaration = vertexDeclaration;
    }

    private void initTexture(int width, int height)
    {
        texture = new Texture2D(
            GraphicsDevice,
            width, height,
            1,
            TextureUsage.None,
            SurfaceFormat.Vector4
            );
        Vector4[] data = new Vector4[texture.Width * texture.Height];

        for (int i = 0; i < data.Length; i++)
            data[i] = Vector4.One / data.Length;

        texture.SetData<Vector4>(data);
    }

    protected override void UnloadContent()
    {
        texture.Dispose();
        renderTarget.Dispose();

        basicEffect.Dispose();
        vertexDeclaration.Dispose();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.SetRenderTarget(0, renderTarget);
        GraphicsDevice.Clear(Color.CornflowerBlue);

        drawTexture(texture);

        createSummedAreaTable();

        GraphicsDevice.SetRenderTarget(0, null);
        GraphicsDevice.Clear(Color.CornflowerBlue);

        drawTexture(renderTarget.GetTexture());
    }

    //GraphicsDeviceにセットされているレンダーターゲットを、
    //自分自身のエリア総和テーブルに変えます。
    private void createSummedAreaTable()
    {
        tableCreationEffect.Parameters["Width"].SetValue(renderTarget.Width);
        tableCreationEffect.Parameters["Height"].SetValue(renderTarget.Height);


        tableCreationEffect.Begin();

        //横方向に足す
        for (int i = 0; i < System.Math.Log(renderTarget.Width, 2); i++)
        {
            tableCreationEffect.CurrentTechnique.Passes["SumXPass"].Begin();

            GraphicsDevice.SetRenderTarget(0, null);
            tableCreationEffect.Parameters["PreviousProduct"].SetValue(renderTarget.GetTexture());
            GraphicsDevice.SetRenderTarget(0, renderTarget);

            tableCreationEffect.Parameters["PassIndex"].SetValue(i);

            drawRect();

            tableCreationEffect.CurrentTechnique.Passes["SumXPass"].End();
        }


        //縦方向に足す
        for (int i = 0; i < System.Math.Log(renderTarget.Height, 2); i++)
        {
            tableCreationEffect.CurrentTechnique.Passes["SumYPass"].Begin();

            GraphicsDevice.SetRenderTarget(0, null);
            tableCreationEffect.Parameters["PreviousProduct"].SetValue(renderTarget.GetTexture());
            GraphicsDevice.SetRenderTarget(0, renderTarget);

            tableCreationEffect.Parameters["PassIndex"].SetValue(i);

            drawRect();

            tableCreationEffect.CurrentTechnique.Passes["SumYPass"].End();
        }

        tableCreationEffect.End();

    }

    private void drawTexture(Texture2D texture)
    {
        basicEffect.TextureEnabled = true;
        basicEffect.Texture = texture;
        basicEffect.Begin();
        basicEffect.CurrentTechnique.Passes[0].Begin();

        drawRect();

        basicEffect.CurrentTechnique.Passes[0].End();
        basicEffect.End();
    }

    private void drawRect()
    {
        //四角形(テクスチャつき)の頂点
        VertexPositionTexture[] vertices = {
            new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2()),
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1)),
            
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(1, -1, 0), new Vector2(1, 1)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1))
        };

        GraphicsDevice.DrawUserPrimitives<VertexPositionTexture>(
            PrimitiveType.TriangleList,
            vertices,
            0,
            2
            );
    }

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




SummedAreaTableCreator.fx (Contentフォルダ内)
texture PreviousProduct;

sampler TextureSampler = sampler_state
{
	Texture = (PreviousProduct);
};

float Width;
float Height;
int PassIndex;


struct VertexPositionTexture
{
    float4 Position : POSITION;
    float2 TextureCoordinate : TEXCOORD;
};

VertexPositionTexture VertexShaderFunction(VertexPositionTexture input)
{
    return input;
}

float4 getColor(float2 texCoord)
{
    if(texCoord.x < 0 || texCoord.y < 0) return 0;
    else return tex2D(TextureSampler, texCoord + float2(0.5/Width, 0.5/Height));
}


float4 SumX(float2 textureCoordinate : TEXCOORD):COLOR
{
    return getColor(textureCoordinate + float2((-1/ Width) * exp2(PassIndex), 0))
        + getColor(textureCoordinate);
}

float4 SumY(float2 textureCoordinate : TEXCOORD):COLOR
{
    return getColor(textureCoordinate + float2(0, (-1/ Height) * exp2(PassIndex)))
        + getColor(textureCoordinate);
}

technique Technique1
{
    pass SumXPass
    {
		VertexShader = compile vs_1_1 VertexShaderFunction();
		PixelShader = compile ps_2_0 SumX();
    }
    
    pass SumYPass
    {
		VertexShader = compile vs_1_1 VertexShaderFunction();
		PixelShader = compile ps_2_0 SumY();
    }
}



initTextureの引数をいろいろ変えてやってみました。
結果は・・・良くも悪くも予想どおりといった感じです。
パッと見問題はないようにも見えますが、
横方向のときにあった問題をそのまま受け継いでいます(4x4のときが顕著ですね)。

2x2:
satFrom2x2.jpg

4x4:(こりゃないよ・・・)
satFrom4x4.jpg

8x8:
satFrom8x8.jpg

16x16:
satFrom16x16.jpg

32x32:
satFrom32x32.jpg

64x64:
satFrom64x64.jpg

128x128:
satFrom128x128.jpg

あいかわらず「完全な白にはならない」問題と、「(左上の領域が)2倍の太さになる」問題は残っていますが、いちおうエリア総和テーブルらしくなってきました。


このうち、「完全な白にはならない」問題は、パスのBeginとEndを呼ぶタイミングで解決できるようです。
つまり、パスのBeginとEndをforループの外にくくりだし、drawRectの直前でEffect.CommitChanges()を呼び出すのです。

    private void createSummedAreaTable()
    {
        tableCreationEffect.Parameters["Width"].SetValue(renderTarget.Width);
        tableCreationEffect.Parameters["Height"].SetValue(renderTarget.Height);


        tableCreationEffect.Begin();

        //横方向に足す
        tableCreationEffect.CurrentTechnique.Passes["SumXPass"].Begin();
        for (int i = 0; i < System.Math.Log(renderTarget.Width, 2); i++)
        {
            GraphicsDevice.SetRenderTarget(0, null);
            tableCreationEffect.Parameters["PreviousProduct"].SetValue(renderTarget.GetTexture());
            GraphicsDevice.SetRenderTarget(0, renderTarget);

            tableCreationEffect.Parameters["PassIndex"].SetValue(i);
            tableCreationEffect.CommitChanges();

            drawRect();
        }
        tableCreationEffect.CurrentTechnique.Passes["SumXPass"].End();


        //縦方向に足す
        tableCreationEffect.CurrentTechnique.Passes["SumYPass"].Begin();
        for (int i = 0; i < System.Math.Log(renderTarget.Height, 2); i++)
        {
            GraphicsDevice.SetRenderTarget(0, null);
            tableCreationEffect.Parameters["PreviousProduct"].SetValue(renderTarget.GetTexture());
            GraphicsDevice.SetRenderTarget(0, renderTarget);

            tableCreationEffect.Parameters["PassIndex"].SetValue(i);
            tableCreationEffect.CommitChanges();

            drawRect();
        }
        tableCreationEffect.CurrentTechnique.Passes["SumYPass"].End();

        tableCreationEffect.End();

    }


これをやると一応右下は白くなります。
4x4:
satFrom4x4WithCommitChangesInvocation.jpg

が、トレードオフもあって、「太さが2倍になる」症候群が悪化します。

8x8:
satFrom8x8WithCommitChangesInvocation.jpg

16x16:
satFrom16x16WithCommitChangesInvocation.jpg

もう踏んだり蹴ったりですね。
いちおうこれでも平均の色を求めるのにはたいして支障はないはずですが、それでも気持ち悪いです。
もう少し実験を重ねたほうがいいかもしれません。












 

拍手[0回]


XNAでエリア総和テーブルの実験

XNAでエリア総和テーブルの実験をしてみました。(まだ実験段階で、未解決の問題があります)
参考にした資料はATIの2つのpdfファイル(1, 2)です。

テクスチャのあるエリアの色の平均を求めるのに、エリア総和テーブルを使うと、かなり効率的に行えます。
(この方法のおもしろい所は、10x10の領域の平均だろうが、1000x1000の領域の平均だろうが、同じ程度の計算で済んでしまうことです。一見1000x1000の方がずっと計算が多くなりそうですが、そうはならないのがエリア総和テーブルの面白いところです。これはどういうことかというと、「100時間走った車の速度の平均を求める計算は、1時間走った車の速度の平均を求める計算の100倍時間がかかるわけではない」理由と、本質的には同じです。ようするにどちらとも、始めと終わりの車の位置を出して、引き算して、時間で割ってやるだけでいいわけですからね。エリア総和テーブルを作るということは、車の速度のデータから、車の位置のデータを作り出すことと、本質的に同じです。とりあえず足しまくるのです。)

エリア総和テーブルとは、右上にあるエリアの合計を表すテーブルです。
例えばこんなテクスチャがあったとします。
2 4 1 3
1 0 2 0
0 3 0 2
2 1 0 4

すると、そのテクスチャのエリア総和テーブルはこんなんです:
2 6 7 10
3 7 10 13
3 10 13 18
5 13 16 25

テーブルの各値は、右上のエリアのテクスチャの色の合計です。

実装に関してまだ納得できないところもあるので、詳しい説明はまたの機会に一度にすることにします。
(別に説明を思いつかないわけじゃないですよ!噛み砕いた説明はもう思いついていますが、説明する本人が自信なさげだったら説得力がないじゃないですか!)

ではコードです(ちょっと引っかかるところがいくつかあるのですが・・・・・・):
MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class MyGame : Game
{
    GraphicsDeviceManager graphics;

    Texture2D texture;
    BasicEffect basicEffect;
    VertexDeclaration vertexDeclaration;

    Effect tableCreationEffect;
    RenderTarget2D renderTarget;
   

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

    protected override void LoadContent()
    {
        //このinitTextureの引数をいろいろ変えてみることで、
        //テクスチャのサイズ(横幅)が変わり、
        //画面に表示される画像の滑らかさが変わります。
        //たとえば、initTexture(8);ならカクカクしますが、
        //initTexture(128)なら滑らかです。
        initTexture(128);

        renderTarget = new RenderTarget2D(
            GraphicsDevice,
            texture.Width, texture.Height,
            texture.LevelCount,
            SurfaceFormat.Vector4
            );

        tableCreationEffect = Content.Load<Effect>("SummedAreaTableCreator");
        basicEffect = new BasicEffect(GraphicsDevice, null);



        vertexDeclaration = new VertexDeclaration(
            GraphicsDevice,
            VertexPositionTexture.VertexElements
            );
        GraphicsDevice.VertexDeclaration = vertexDeclaration;
    }

    private void initTexture(int width)
    {
        texture = new Texture2D(
            GraphicsDevice,
            width, 1,
            1,
            TextureUsage.None,
            SurfaceFormat.Vector4
            );
        Vector4[] data = new Vector4[texture.Width * texture.Height];

        for (int i = 0; i < data.Length; i++)
            data[i] = Vector4.One / texture.Width;

        texture.SetData<Vector4>(data);
    }

    protected override void UnloadContent()
    {
        texture.Dispose();
        renderTarget.Dispose();

        basicEffect.Dispose();
        vertexDeclaration.Dispose();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.SetRenderTarget(0, renderTarget);
        GraphicsDevice.Clear(Color.CornflowerBlue);

        drawTexture(texture);

        createSummedAreaTable();

        GraphicsDevice.SetRenderTarget(0, null);
        GraphicsDevice.Clear(Color.CornflowerBlue);

        drawTexture(renderTarget.GetTexture());
    }

    private void createSummedAreaTable()
    {
        tableCreationEffect.Parameters["Width"].SetValue(renderTarget.Width);
        tableCreationEffect.Parameters["Height"].SetValue(renderTarget.Height);


        tableCreationEffect.Begin();

        for (int i = 0; i < System.Math.Log(texture.Width, 2); i++)
        {
            tableCreationEffect.CurrentTechnique.Passes[0].Begin();


            GraphicsDevice.SetRenderTarget(0, null);
            tableCreationEffect.Parameters["PreviousProduct"].SetValue(renderTarget.GetTexture());
            GraphicsDevice.SetRenderTarget(0, renderTarget);

            tableCreationEffect.Parameters["PassIndex"].SetValue(i);

            drawRect();


            tableCreationEffect.CurrentTechnique.Passes[0].End();
        }

        tableCreationEffect.End();

    }

    private void drawTexture(Texture2D texture)
    {
        basicEffect.TextureEnabled = true;
        basicEffect.Texture = texture;
        basicEffect.Begin();
        basicEffect.CurrentTechnique.Passes[0].Begin();

        drawRect();

        basicEffect.CurrentTechnique.Passes[0].End();
        basicEffect.End();
    }

    private void drawRect()
    {
        //四角形(テクスチャつき)の頂点
        VertexPositionTexture[] vertices = {
            new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2()),
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1)),
            
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(1, -1, 0), new Vector2(1, 1)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1))
        };

        GraphicsDevice.DrawUserPrimitives<VertexPositionTexture>(
            PrimitiveType.TriangleList,
            vertices,
            0,
            2
            );
    }

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


SummedAreaTableCreator.fx (Contentフォルダ内)
texture PreviousProduct;

sampler TextureSampler = sampler_state
{
    Texture = (PreviousProduct);
};

float Width;
float Height;
int PassIndex;


struct VertexPositionTexture
{
    float4 Position : POSITION;
    float2 TextureCoordinate : TEXCOORD;
};

VertexPositionTexture VertexShaderFunction(VertexPositionTexture input)
{
    return input;
}

float4 getColor(float2 texCoord)
{
    if(texCoord.x < 0 || texCoord.y < 0) return 0;
    else return tex2D(TextureSampler, texCoord + float2(0.5/Width, 0.5/Height));
}


float4 SumX(float2 textureCoordinate : TEXCOORD):COLOR
{
    return getColor(textureCoordinate + float2((-1/ Width) * exp2(PassIndex), 0))
        + getColor(textureCoordinate);
}


technique Technique1
{
    pass SumXPass
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_2_0 SumX();
    }
}


いきなり何かの画像のエリア総和テーブルを計算するのは難しいので、とりあえずは同じ色の詰まった1次元のテクスチャのエリア総和テーブルを作ってみました。(そのためY方向には足し算をしません)
もしうまくいけば、左から右に行くにしたがって色が足されていくので、徐々に明るくなっていき、きれいなグラデーションになるはずです。

なお、テクスチャに一番最初に入る色は、エリア総和テーブルで一番明るい色が白になるように決めました(もちろん実用では、複数のピクセルの色を足しまくるため、白より明るい色になることはしょっちゅうです。しかしそれではうまくいっているかどうか目で確認できません。テストファースト、テストファーストです)
テクスチャのサイズはいろいろ変更することができ、大きくなればなるほど、テクスチャの最初の色は暗くなります。
これはエリア総和テーブルを作った時に、色があふれださないようにする工夫です。

ではinitTextureの引数をいろいろ変えていき、上手くいっているかのテストです。
結論を先に言うと、ビミョーですが、まあ見ていきましょう。

まずは2x1のテクスチャのエリア総和テーブルです。
satFrom2x1.jpg

なるほどうまくいっているようです。
ここでは
0.5 0.5
のテクスチャから、
0.5 1.0
のエリア総和テーブルが作られているのでしょう(多分)。
正しく実装で来ているのなら、こんな風に、これ以後も全部右端は白くなるはずです。


ここではうまくいっているようですが、もう少し大きくしてみるとどうでしょうか?
4x1を試してみます:
satFrom4x1.jpg

いやいや、おかしいよ!
どうなっているのでしょう?
グラデーションにならなきゃいけないのに、真ん中の2つが同じ色になってしまっています。
なにより右端は白じゃなくちゃいけません。

この問題はこの記事を書いている今のところ未解決です(わかる人教えてください!)。
さらに大きくすると、それほど深刻には見えなかったからです。

8x1:
satFrom8x1.jpg
左端と右端を除いた真ん中全部が同じ色になりはしないかとヒヤヒヤしましたが、杞憂でしたね。もっとも問題は全然よくなっていませんが。

16x1:
satFrom16x1.jpg
もしかしておかしくなる条件は「左にある」こと?
でもどうしてそんな・・・・・・

32x1:
satFrom32x1.jpg
右端はほとんど白です。
これを見た瞬間問題を解決しようというモチベーションがガタ落ちしました。

64x1:
satFrom64x1.jpg
こまかくなってきました。
しかし「左側問題」は相変わらず解決しません。

128x1:
satFrom128x1.jpg
いいでしょう、もうたくさんです。
これ以上大きくしても結果は変わらないでしょう。

でもいったいどうしてこんな問題が起きたのでしょう?
たぶん何かのインデックスが1つずれているとかそんなのだと思いますが・・・





拍手[0回]