忍者ブログ

Memeplexes

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

[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