忍者ブログ

Memeplexes

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

[PR]

×

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


XNA爆発チュートリアル その8 フェードアウト

XNA爆発チュートリアル その7.5のつづきです。

最後に新しい機能を付け加えた「その7」では
頂点を増やしてより爆発っぽくなりました。
しかし、それぞれの爆炎は時間がたっても
消えたりはせず、不自然でした。

ここではさらに本物に近づけるため、炎をフェードアウトさせます。
頂点のアルファ・チャンネルを時間に従ってうまく制御すれば、
それらしく炎が消えてくれるはずです。
頂点シェーダ内でその頂点の色(アルファ・チャンネルを操作してある)を決め、
ピクセルシェーダでそいつを乗算するのです。

では具体的にどのようにアルファ・チャンネルを決めればいいのでしょうか?
Xna Creators Clubのサンプルでは、次のようにして決めていました。

まず、各頂点に対応する、ノーマライズされた年齢を求めて、
これをもとにαを決めます。

ノーマライズされた年齢 = 現実の年齢 / 現実の寿命

どういうことかというと、この年齢は、0~1の範囲内になっており、
たとえば現実に炎の寿命が2秒、現在の歳が1秒だとすると、0.5です。(1 / 2)
現実の寿命が4秒、現実の歳が1秒だとすると0.25です(1 / 4)。

この、ノーマライズされた年齢を元にαを決めます。
Particle 3Dサンプル(Xna Creators Club)のシェーダー、ParticleEffect.fxでは
次のような三次関数でαを求めています:

color.a *= normalizedAge * (1 - normalizedAge) * (1 - normalizedAge) * 6.7;

この式を使って獏炎のα・チャンネルを計算するとうまい具合に
爆発が消えていきます。
(一番最後にマジックナンバー6.7がかけられているのは、ある一定の時間がたつまでは炎がはっきり(α = 1で)映し出されるようにするためです。)
ただし!これはなにぶん三次関数なので、寿命が切れた後に呼び出すと、
また爆発が見えるようになります。
たとえば2秒で消えることを予定している爆発を、
2秒後も3秒後も描画し続けていると、
いったん消えたはずの爆発がまた画面に現れるのです。
このことに気をつけましょう。
(いやでも目に入ってきますが)


この変更はエフェクトファイルだけです。
float4x4 View;
float4x4 Projection;

float Time;

texture ExplosionTexture;

sampler explosionSampler = sampler_state
{
	Texture = <ExplosionTexture>;
};

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

float4 ComputeColor(float normalizedAge)
{
	float4 color = 1;
	color.a *= normalizedAge * (1 - normalizedAge) * (1 - normalizedAge) * 6.7;
	return color;
}

ExplosionVertexShaderOutput ExplosionVertexShader(float4 velocity:COLOR)
{
	ExplosionVertexShaderOutput output;
	float4 worldPosition = float4(velocity.xyz * Time, 1);
	output.Position = mul(worldPosition, mul(View, Projection));
	output.Color = ComputeColor(Time / 2);
	return output;
}

float4 ExplosionPixelShader(
	float2 textureCoordinate:TEXCOORD,
	float4 color:COLOR
	):COLOR
{
	return tex2D(explosionSampler, textureCoordinate) * color;
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}

explosionTutorialFadingOut.jpg
(爆発はほとんどフェードアウトしてしまっていて、かすかにしか見えません)

これで実行すると、2秒でフェードアウトします。
(爆発の寿命を2秒に設定しているので、ノーマライズされた寿命はTime / 2です。)

しかしそのまま実行し続けていると、すぐにまた炎があらわれます。
これはα・チャンネルを計算している関数の性質によるもので、
しかたがありません。
爆発に寿命が来たら描画しないような仕掛けが必要でしょう。





拍手[0回]

PR

XNA爆発チュートリアル その7.5 ゲームコンポーネントにする

XNA爆発チュートリアルその7のつづきです。
(今回は機能を付け加えるのではなく、
リファクタリングが目的なので、
「その7.5」です。)


どうも紛糾してきたので爆発の描画をするクラスを作り、
それをGameから利用することにします。

その爆発を描画するクラスは、ゲームコンポーネント
ということにします。
Gameからやることは、そのゲームコンポーネントを
自分に登録することだけです。
ですからExplosionTest.csはうんと短くなって、
こうなります:

ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        this.Components.Add(new ExplosionRenderer(this));
    }
}



ExplosionRendererクラスは、DrawableGameComponentを
継承しており、今までExplosionTestクラスでやっていたこと、
つまり爆発の描画を行います。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            effect.Parameters["View"].SetValue(
                Matrix.CreateLookAt(
                    new Vector3(0, 0, 5), new Vector3(), new Vector3(0, 1, 0)
                    )
                );
            effect.Parameters["Projection"].SetValue(
                Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.ToRadians(45),
                    aspectRatio,
                    0.1f, 1000
                    )
                );


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

    float aspectRatio
    {
        get
        {
            return (float)GraphicsDevice.Viewport.Width 
                / GraphicsDevice.Viewport.Height;
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}


これでプログラムを実行しても結果は変わりません。
クラスの分離はうまく行っているようです。


さて、この時点でちょっと気持ち悪いところがあります。
それは、カメラの位置とレンズを表すViewとProjectionが、
ゲームコンポーネントの内部で決められていることです。
これではゲーム中にカメラを動かしたりが出来ません。
自分が動けないゲームなんておもしろくないでしょう。

そこで、カメラを自由に動かせるように、
カメラを表すViewとProjectionをGame側から
制御できるようにします。
こんな感じでしょうか:

ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        ExplosionRenderer explosion = new ExplosionRenderer(this);
        Matrix view = Matrix.CreateLookAt(
            new Vector3(0, 0, 5),
            new Vector3(),
            new Vector3(0, 1, 0)
            );
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45),
            (float)Window.ClientBounds.Width/Window.ClientBounds.Height,
            0.1f, 1000
            );
        explosion.SetCamera(view, projection);
        this.Components.Add(explosion);
    }
}


ゲームコンポーネント側はこうです:

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();

    private Matrix view;
    private Matrix projection;


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

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


    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );
        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }

    public void SetCamera(Matrix view, Matrix projection)
    {
        this.view = view;
        this.projection = projection;
    }
}


SetCameraメソッドで直接エフェクトに値をセットできていないのは
エフェクトがまだ初期化できていない(まだnull)可能性があるからです。
変数を間接層にすることで、いつでもこのメソッドを
呼ぶことが出来るようになります。

これはなんだか意味のない間接層で、必要のない複雑性をプログラムに
組み込んでしまったような気もしますが、この程度ならまだ許容範囲ということにしておきましょう。
SetCameraメソッドを呼ぶタイミングを縛るというのもなんですしね。



最後に、ゲームコンポーネントとして再利用できるように、
別のシチュエーションで使われるときのことを考えて見ましょう。

現時点では再利用を難しくする二つの欠点があります。

1つめは、VertexDeclarationを一度だけ、LoadGraphicsContent内で
セットしていることです。
この方法はシンプルですが、再利用しようとすると問題が生まれます。
別のVertexDeclarationを使うモデルを描画した場合、上書きされてしまい
このコンポーネントの描画はうまくいかないでしょう。。

ですから、VertexDeclarationのインスタンスをDrawメソッド内で毎回セットする
必要があります。

2つ目の問題も似たようなタイプの問題で、Drawメソッドにからんでいます。
今のところDrawメソッド内でレンダーステートを色々と変更していますが、
このままだと、別のモデルを描画したときにその設定が受け継がれてしまい、
メチャクチャに描画されるでしょう。
どうしても元に戻さない設定はAlphaBlendEnableとDepthBufferWriteEnableです。

コードを書き換えて、この2つの問題を解決しましょう。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexDeclaration vertexDeclaration;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();

    private Matrix view;
    private Matrix projection;


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

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


    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        GraphicsDevice.VertexDeclaration = vertexDeclaration;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );
        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();

        GraphicsDevice.RenderState.AlphaBlendEnable = false;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
    }

    public void SetCamera(Matrix view, Matrix projection)
    {
        this.view = view;
        this.projection = projection;
    }
}



これでこのゲームコンポーネントを再利用できるようになったはずです。

拍手[0回]


XNA爆発チュートリアル その7 頂点を増やしてみる

XNA爆発チュートリアル その6のつづきです。

前回は爆炎を初めて動かしてみました。

今回はこれをもっと派手にしてみましょう。
頂点(爆炎)を増やしてみます。
前回までは爆炎は3つだけでしたが、ここではその10倍の30個にしてみます。
(なんで30個かというと、Xna Creators ClubのParticle 3Dサンプルの爆発で使われていた数(Particle3DSample.Projectile.numExplosionParticles)がちょうど30だったからです。)

そして、各頂点の速度をあらわすVelocityメンバはランダムに初期化することにします。

ひとまず、この変更はC#側だけですね。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}


explosionTutorialManyVertices1.jpg
explosionTutorialManyVertices2.jpg

にぎやかになってきましたね。
なんといっても、爆炎の数を30に増やしたのですから当然です!

かく爆炎の速度はランダムに、System.Randomクラスで決めています。
速度成分X, Y, Zそれぞれが、-1 / 10 から1 / 10 の範囲内になるようになっています。

しかしここには明らかに2つ、問題があります。

1つめは、「爆炎の周りに透明な四角形が見える」ということです。
これは、それぞれの深度テストの問題です。
すぐに問題は解決するでしょう。
各頂点を描画するときに深度バッファに書き込まないようにすればいいのです。

2つめは、「爆炎の数が明らかに、(総頂点数の)30より少ない」ということです。
せいぜい10個かそのくらいです。
あとの10~20個はどこへいったのでしょうか?

それは、描画されない領域です。
つまり、描画されるZの範囲は0~1までなので、
そこからはみ出てしまった約20個の爆炎は描画されないということです。
BasicEffectでこの問題を解決していたのはProjectionプロパティで、
各頂点のZの値を、うまく0~1の領域に押し込んでいたのです。
解決するにはエフェクトファイルでBasicEffectのProjectionに
相当するものを作ってやる必要があるでしょう。

この問題を解決することにしましょう。
まずは1つめの問題、透明な四角を消すことからはじめます。
これは透明な四角の領域の深度バッファが更新されて、
それ以後そこに新たな爆炎が描画されなくなってしまうことが原因なので、
爆発を描画する間は深度バッファが書き込まれないようにします。
RenderState.DepthBufferWriteEnableをfalseにするのです。
(ただ、これには副作用もあります。この方法で爆発を描画した後、より遠いところに何か別のものを描画した場合、上書きされてしまい、前後関係がおかしくなります。このテクニックによる描画はなるべく最後に回したほうが良いでしょう。)


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        graphics.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

explosionTutorialDepthBufferWriteDisabled.jpg
透明な四角形が消えました。

次は爆炎が消えてしまう問題を解決します。
この問題を解決するには、Projectionマトリックスを使って、
描画されるZの領域を0~1からもっと大きく、
たとえば0.1f~1000というように広げてやればいいでしょう。
(正確には最終的に描画される領域は変わりません。Projectionマトリックスは頂点データを変換するだけです。0.1f~1000を0~1に変換するというわけです)

ただ、このシチュエーションでこの方法を使うと、カメラと爆発が重なってしまうため、
カメラの位置を表すViewマトリックスも使ってカメラをもう少し手前にずらすべきでしょう。

この2つのマトリックス、ProjectionとViewを使って、頂点データを変換してやります。

ExplosionTest.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            effect.Parameters["View"].SetValue(
                Matrix.CreateLookAt(
                    new Vector3(0, 0, 5), new Vector3(), new Vector3(0, 1, 0)
                    )
                );
            effect.Parameters["Projection"].SetValue(
                Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.ToRadians(45),
                    aspectRatio,
                    0.1f, 1000
                    )
                );


            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }

    float aspectRatio
    {
        get
        {
            return (float)graphics.GraphicsDevice.Viewport.Width 
                / graphics.GraphicsDevice.Viewport.Height;
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        graphics.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

ExplosionShader.fx
float4x4 View;
float4x4 Projection;

float Time;

texture ExplosionTexture;

sampler explosionSampler = sampler_state
{
	Texture = <ExplosionTexture>;
};

float4 ExplosionVertexShader(float4 velocity:COLOR):POSITION
{
	float4 worldPosition = float4(velocity.xyz * Time, 1);
	return mul(worldPosition, mul(View, Projection));
}

float4 ExplosionPixelShader(float2 textureCoordinate:TEXCOORD):COLOR
{
	return tex2D(explosionSampler, textureCoordinate);
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}


explosionTutorialTransformedByMatrices.jpg
静止画ではわかりにくいですが、
だいぶそれらしくなってきました。

最初の数秒間はまさに「爆発」って感じです!
かなり前進したといえるでしょう。

これを今後、それぞれの爆炎をフェードアウトさせたり、
フェードアウトするにしたがって回転させたり、
その他色々なことをすることによって
より本物の爆発らしくしていくのです!
(というか、そうなる予定です)


















拍手[0回]


XNA爆発チュートリアル その6 動かす

XNA爆発チュートリアル その5 HLSLへのつづきです。

前回でBasicEffectから、HLSLで書いたカスタムエフェクトへの移行が済んだので、今回からはさらにいろんなことが出来るようになります。

まずは爆発を動かしてみましょう。
中心から、周囲に「ぶわっ」っと広がっていくような感じです。

これをやるためにエフェクトファイルにTimeというグローバル変数を用意して、
その値にしたがってそれぞれの爆炎を頂点シェーダで動かしていくことにします。

ただ、これをやるためには頂点の構造体の
メンバに速度が含まれていなければならないのですが、
これはとりあえず位置の情報を流用することにします。
(臭う!コードの臭いです!でも訂正は後回しです)

ExplosionTest.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


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

    private Effect effect;
    private VertexPositionColor[] vertices = {
        new VertexPositionColor(new Vector3(0, 0.1f, 0), Color.White),
        new VertexPositionColor(new Vector3(0.1f, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-0.1f, 0, 0), Color.White)
    };

    private ContentManager content;


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexPositionColor.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

ExplosionShader.fx
float Time;

texture ExplosionTexture;

sampler explosionSampler = sampler_state
{
	Texture = <ExplosionTexture>;
};

float4 ExplosionVertexShader(float4 position:POSITION):POSITION
{
	return float4(position.xyz * Time, 1);
}

float4 ExplosionPixelShader(float2 textureCoordinate:TEXCOORD):COLOR
{
	return tex2D(explosionSampler, textureCoordinate);
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}


("return position * Time;"ではなくて"return float4(position.xyz * Time, 1);"
なのはそうしないとうまくいかないからです。float4の最後は1でなくてはいけません。
この値をたとえば2にすると爆発の速さが1/2になってしまいます。
大きくすると速さが遅くなっていくのです。
逆に、小さくすると速くなります。
xyzを拡大し、wも拡大すると、ちょうど互いに影響を打ち消しあい、結局爆発は動かなくなります。
"return position * Time;"では動かないように見えるのです。)



explosionTutorialExpanding1.jpg
explosionTutorialExpanding2.jpg
explosionTutorialExpanding3.jpg
うまくいきました。
3つの爆炎が時間の経過とともに周りへ広がっていきます。

ここで気になるのは、変数名とその役割が一致していないことです。
どういうわけか、位置を表す変数に速度が入ってしまっています。
これはいただけません。
直すことにしましょう。

C#側で新しい頂点の構造体を用意して、そのメンバの名前をVelocity(速度)にします。
HLSL側での変更は変数名の変更だけで良いでしょう。(position → velocity)

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


struct VertexVelocity
{
    public Vector3 Velocity;

    public VertexVelocity(Vector3 velocity)
    {
        this.Velocity = velocity;
    }

    public static readonly VertexElement[] VertexElements = {
        new VertexElement(
            0,
            0,
            VertexElementFormat.Vector3,
            VertexElementMethod.Default,
            VertexElementUsage.Color,
            0)
    };
}

ExplosionTest.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private Effect effect;
    private VertexVelocity[] vertices = {
        new VertexVelocity(new Vector3(0, 0.1f, 0)),
        new VertexVelocity(new Vector3(0.1f, 0, 0)),
        new VertexVelocity(new Vector3(-0.1f, 0, 0))
    };

    private ContentManager content;


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

ExplosionShader.fx
float Time;

texture ExplosionTexture;

sampler explosionSampler = sampler_state
{
	Texture = <ExplosionTexture>;
};

float4 ExplosionVertexShader(float4 velocity:COLOR):POSITION
{
	return float4(velocity.xyz * Time, 1);
}

float4 ExplosionPixelShader(float2 textureCoordinate:TEXCOORD):COLOR
{
	return tex2D(explosionSampler, textureCoordinate);
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}


実行結果はさっきと同じです。
さっきと違うのは、その内部です。
変数の名前と目的をだいたい一致させました。

「だいたい」と言うのは、(名前ではないですが)
セマンティクスを一致させることが出来ず、(VELOCITYがないため)
しかたがなくCOLORを使ったのです。
でもまあ、「色 → 特色 → 頂点の特色 → ここでは速度」という風に、
連想ゲームでたどり着けないこともないので
ここはおおめに見ても良いかもしれませんね。


ともかく!これでうまくリファクタリング(?)できました。
いままで頂点の構造体にはVertexPositionColorを使っており、
使っていない、無駄なメンバ(Color)があったのです。
今、それが取り除かれ、本当に必要な速度だけをメンバに持つ、
VertexVelocity構造体が作られました。
これでさらなる変更を加えやすくなったはずです。

つまり、
爆発を表しているそれぞれの頂点をフェードアウトさせたり、
それぞれの爆炎を回転させたり、
その他いろいろな変更を加えやすくなり、
結果としてより簡単に本物の爆発に近づくことが出来るのです。
(というか、そうなる予定です。)

拍手[0回]


XNA爆発チュートリアル その5 HLSLへ

XNA爆発チュートリアル その4 透過のつづきです。

前回までに、爆炎っぽいものをそれらしく表示することが出来ました。
しかしまだ本物には及びません。
本物の爆発は「ぶわっ」という感じに広がっていくものだからです。
今のところ爆炎は動きません。
サイズも変わりません。

こういった、ポイントのいろんな変化を行うには、
HLSLを使うべきでしょう。
BasicEffectとはおさらばです。
HLSLでエフェクトファイルを直接書くことによって、
より詳しく描画をコントロールすることが出来ます。


ただ……、より詳しく描画をコントロールすることが出来るのはいいのですが、
そのかわり代償として、やらなければならないことが多くなり、
面倒になったり、一時的に見た目が逆戻りする(お粗末になる)ことがあるでしょう。

やる気を保つためにも、見た目をあんまりお粗末にしないことが大切です。
ですから、この回が終わるころにはBasicEffectで
やったものに追いついておくべきでしょうね。

心構えはこのくらいにして、さっそくプロジェクトにエフェクトファイルを追加しましょう。
ExplosionShader.fxという名前で、Contentフォルダの中に作ることにします。
それをContentManagerから"Content/ExplosionShader"という風に読み込みます。
こうして出来たEffectはただのEffectなのでTextureプロパティは使えません。
この関連は削除することになるでしょう。

ExplosionTest.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


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

    private Effect effect;
    private VertexPositionColor[] vertices = {
        new VertexPositionColor(new Vector3(0, 0.1f, 0), Color.White),
        new VertexPositionColor(new Vector3(0.1f, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-0.1f, 0, 0), Color.White)
    };

    private ContentManager content;


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexPositionColor.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

HLSLで書いたエフェクトファイルは考えられる限りもっともシンプルなものにします。
頂点シェーダは何もしてませんし、ピクセルシェーダは常に白を返します。
もちろんこんなのですから、結果は炎から真っ白タイルに戻ることが予想できます。

ExplosionShader.fx
float4 ExplosionVertexShader(float4 position:POSITION):POSITION
{
	return position;
}

float4 ExplosionPixelShader():COLOR
{
	return 1;   //white
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}

explosionTutorialHLSLSimplest.jpg

あいたたた……見た目がただの白タイルに戻ってしまいました。
わかっていたこととはいえ、これは精神的にきついですね。
今まで築き上げてきたものが失われる、いやーな気分です。
でもすぐに建て直しできるので、気を取り直していきましょう。

テクスチャ

ここで白タイルから爆炎に復活させるにはHLSLで
ポイントにテクスチャを貼るようにしなければなりません。

これは簡単で、ポイントスプライトを使っているときには
ピクセルシェーダに自動的にテクスチャ座標が引数として入ってくるので
それを元にピクセルシェーダの戻り値をだせばいいのです。

C#側ではまたさっきまでのようにテクスチャをロードして
エフェクトにセットするようにします。

ExplosionTest.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


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

    private Effect effect;
    private VertexPositionColor[] vertices = {
        new VertexPositionColor(new Vector3(0, 0.1f, 0), Color.White),
        new VertexPositionColor(new Vector3(0.1f, 0, 0), Color.White),
        new VertexPositionColor(new Vector3(-0.1f, 0, 0), Color.White)
    };

    private ContentManager content;


    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexPositionColor.VertexElements
                );
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

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

        graphics.GraphicsDevice.RenderState.PointSize = 50;
        graphics.GraphicsDevice.RenderState.PointSpriteEnable = true;

        graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
        graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}

ExplosionShader.fx
texture ExplosionTexture;

sampler explosionSampler = sampler_state
{
	Texture = <ExplosionTexture>;
};

float4 ExplosionVertexShader(float4 position:POSITION):POSITION
{
	return position;
}

float4 ExplosionPixelShader(float2 textureCoordinate:TEXCOORD):COLOR
{
	return tex2D(explosionSampler, textureCoordinate);
}

technique ExplosionShaderTechnique
{
	pass P0
	{
		VertexShader = compile vs_2_0 ExplosionVertexShader();
		PixelShader = compile ps_2_0 ExplosionPixelShader();
	}
}


explosionTutorialHLSLTextured.jpg

復活できました。
めでたく、HLSLでBasicEffectを使っていたときと同じことが出来ました。
きちんと、白タイルではなく爆炎が表示されています。

「このまま白いタイルのままだったらどうしよう」
という恐怖があったのですが、安心しました。
これは気分としては「冤罪でつかまったけれど無事疑いが晴れた」
というのに似ている気がしますね!

さて、今回無事にBasicEffectからHLSLに移行できたので
次回からはいろいろなことが詳しく調節できるようになるはずです。
(BasicEffectは柔軟性に欠けるため、さらなる進歩は難しいのです)

たとえばそれぞれの爆炎を時間の経過とともに周囲に「ぶわっ」と広げたり、
フェードアウトさせたり、回転させたり、
遠近法のようにカメラからの距離によって大きさを調節したり、
いろいろなことが出来るようになり、
さらに本物の爆炎に近づくことが出来るのです。
(というか、そうなる予定です)

拍手[0回]