忍者ブログ

Memeplexes

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

[PR]

×

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


XNA爆発チュートリアル その11 ポイントサイズの調節

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

だいたい機能がまとまってきたので
あら捜しをしていこうとおもいます。

まず最初に気になるのは、カメラからの距離によって
爆発の表示がおかしくなるということです。
近づけば近づくほど、バラバラになってしまいます。


explosionTutorialDistance4.jpgカメラからの距離:4

explosionTutorialDistance3.jpgカメラからの距離:3

explosionTutorialDistance2.jpgカメラからの距離:2

explosionTutorialDistance1.jpgカメラからの距離:1

カメラからの距離が1になるともうバラバラすぎで爆発には見えません。

これはXNA爆発で取り扱った問題と似ています(あちらのほうが症状は軽いですが)。

ここでのこの現象の原因は、それぞれの炎の大きさが固定だということです。
ExplosionRenderer.Drawメソッド内で、

RenderState.PointSize = 50;

としているのがまずいのです。
ここでセットしたポイントのサイズはスクリーン座標上の大きさで、固定なのです。

問題を解決するには、XNA Creators ClubのParticle 3Dサンプルのように、
HLSLで書いたエフェクトファイルで、ポイントサイズが
カメラからの距離を考慮するように調節しなければなりません。

こうですね:

ExplosionShader.fx
float4x4 View;
float4x4 Projection;
float ViewportHeight;

float3 Center;
float Age;
float StartLife;

float PointSize;

texture ExplosionTexture;

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

struct ExplosionVertexShaderOutput
{
	float4 Position : POSITION;
	float4 Color : COLOR;
	float PointSize : PSIZE;
};

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

ExplosionVertexShaderOutput ExplosionVertexShader(float3 velocity:COLOR)
{
	ExplosionVertexShaderOutput output;
	float4 worldPosition = float4(velocity * Age + Center, 1);
	output.Position = mul(worldPosition, mul(View, Projection));
	output.Color = ComputeColor(Age / StartLife);
	output.PointSize = PointSize * Projection._m11 / output.Position.w * ViewportHeight / 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();
	}
}

頂点シェーダの戻り値の構造体ExplosionVertexShaderOutputのメンバ、
PointSizeはC#側のRenderState.PointSizeにとってかわります。

このメンバに、それなりの計算をすることでポイントのスクリーン上でのサイズを入れます。
気をつけるべきなのは、スクリーン上でのサイズが必要なので、スクリーンの大きさが必要になることです。
(頂点の座標はスクリーンの横縦をそれぞれ2とした-1~1の座標なんですけどね)
そこで描画するビューポートの高さを表すViewportHeightというグローバル変数を使います。
(というか、Particle 3Dサンプルがそうしています。)

こうして出来たエフェクトに、C#側からPointSizeとViewportHeightをセットしてやります。
そうすると、カメラから遠くなればそれぞれの爆炎は小さくなりますし、近づけば大きくなるわけです。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System.Collections.Generic;


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;

    private List<Explosion> explosions = new List<Explosion>();


    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 Update(GameTime gameTime)
    {
        foreach (Explosion explosion in explosions)
        { explosion.Update(gameTime); }

        explosions.RemoveAll(
            delegate(Explosion explosion) { return explosion.Life < 0; }
            );
    }

    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["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);
        effect.Parameters["ViewportHeight"].SetValue(
            GraphicsDevice.Viewport.Height
            );
        effect.Parameters["PointSize"].SetValue(0.3f);

        foreach (Explosion explosion in explosions)
        { drawExplosion(explosion); }

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

    private void drawExplosion(Explosion explosion)
    {
        effect.Parameters["Center"].SetValue(explosion.Center);
        effect.Parameters["Age"].SetValue(explosion.Age);
        effect.Parameters["StartLife"].SetValue(explosion.StartLife);

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

    public void AddExplosion(Explosion explosion)
    {
        this.explosions.Add(explosion);
    }
}


explosionTutorialSizeDistanceIsConsidererd4.jpgカメラからの距離:4

explosionTutorialSizeDistanceIsConsidererd2.jpgカメラからの距離:2

explosionTutorialSizeDistanceIsConsidererd1.jpgカメラからの距離:1


変更前の画像と比べてみてください。
結果は一目りょう然です。

ポイントスプライトの大きさそのものが大きくなっているので、
カメラとの距離が変わってもきちんと爆発に見えます。











拍手[0回]

PR

XNA爆発チュートリアル 番外(リファクタリング)

エフェクトファイルのコードを見返していて気付いたところがあったので
簡略化します。

リファクタリング前

ExplosionVertexShaderOutput ExplosionVertexShader(float4 velocity:COLOR)
{
        ExplosionVertexShaderOutput output;
        float4 worldPosition = float4(velocity.xyz * Age + Center, 1);

リファクタリング後

ExplosionVertexShaderOutput ExplosionVertexShader(float3 velocity:COLOR)
{
        ExplosionVertexShaderOutput output;
        float4 worldPosition = float4(velocity * Age + Center, 1);

このほうがずっと簡単ですね。

拍手[0回]


XNA爆発チュートリアル その10 複数の爆発を表示

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

なんだかグダグダになってきたので(笑)さっさと
このチュートリアルを終わらせるつもりで急ぎます。

今回は複数の爆発を表示できるようにしましょう。
普通ゲームでは複数のアイテムが同時に爆発するのは
よくあることだからです。

そこで、爆発のセットの方法を変えます

SetExplosion → AddExplosion

ExplosionRendererゲームコンポーネントの内部にExplosion構造体のリストを作り、
それにAddExplosionメソッドを使って追加するという方法にしましょう。
SetExplosionは廃止です。


ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private ExplosionRenderer explosion;
    private float life;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        explosion = new ExplosionRenderer(this);
        Matrix view = Matrix.CreateLookAt(
            new Vector3(0, 0, 4),
            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);
    }


    protected override void Update(GameTime gameTime)
    {
        life -= (float)gameTime.ElapsedGameTime.TotalSeconds;

        if (life < 0)
        {
            life = 2;
            explosion.AddExplosion(new Explosion(new Vector3(), life));
            explosion.AddExplosion(new Explosion(new Vector3(1, 0, 0), life));
        }

        base.Update(gameTime);
    }

}


クライアント側はこんな感じです。
爆発を画面に表示したかったら、AddExplosionを使って追加するという方法をとります。
内部では寿命の尽きた爆発オブジェクトは自動的に取り除かれるのだということにしておきます。
そのため、使うのはAddExplosionメソッドだけです。
爆発オブジェクトを取り除くメソッドはありません。

次に、この実装を行います。
ExplosionRendererゲームコンポーネント内には爆発オブジェクトのリストが必要です。
これはSystem.Collections.Generics.Listを使えば良いでしょう。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System.Collections.Generic;


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;

    private List<Explosion> explosions = new List<Explosion>();


    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 Update(GameTime gameTime)
    {
        for (int i = 0; i < explosions.Count; i++)
        {
            Explosion explosion = explosions[i];
            explosion.Update(gameTime);
            explosions[i] = explosion;
        }

        explosions.RemoveAll(
            delegate(Explosion explosion) { return explosion.Life < 0; }
            );
    }

    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["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        foreach (Explosion explosion in explosions)
        { drawExplosion(explosion); }

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

    private void drawExplosion(Explosion explosion)
    {
        effect.Parameters["Center"].SetValue(explosion.Center);
        effect.Parameters["Age"].SetValue(explosion.Age);
        effect.Parameters["StartLife"].SetValue(explosion.StartLife);

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

    public void AddExplosion(Explosion explosion)
    {
        this.explosions.Add(explosion);
    }
}


explosionTutorialDistance4.jpg
これで十分ですね。
ただ、このコードには気持ち悪いところがあります。
それは、爆発をUpdateするのに、次のように出来なかったことです:

foreach(Explosion explosion in explosions)
{ explosion.Update(gameTime); }


こっちのほうがはるかにスマートです。
にもかかわらずこう書くことが出来なかったのは、
Explosionがクラスではなく構造体だったからです。(参照型ではなく値型)
つまり、上のように書いた場合、アップデートされるのは爆発オブジェクトのコピーであって、
リストの中に入っている爆発オブジェクトではないのです。
つまり、爆発オブジェクトが全くアップデートされなくなってしまいます。

さて困りました。
パフォーマンスの観点からExplosionを構造体にするとこのような問題が生まれてしまいました。
Explosionを構造体からクラスに戻すべきでしょうか?
それとも後々のことを考えて構造体のままにしておくべきでしょうか?

ここは構造体からクラスに変えようと思います。
なぜなら、後のことを考えて現在のコードに複雑さを持ち込むのは
まずい場合が多いからです。
複雑さを持ち込むのは後でそこがパフォーマンスの
ボトルネックになってから考えても良いでしょう。

ExplosionRenderer.Updateメソッド
    public override void Update(GameTime gameTime)
    {
        foreach (Explosion explosion in explosions)
        { explosion.Update(gameTime); }

        explosions.RemoveAll(
            delegate(Explosion explosion) { return explosion.Life < 0; }
            );
    }

Explosion.cs
using Microsoft.Xna.Framework;

public class Explosion
{
    private float life;
    private float startLife;

    public float Life { get { return life; } }
    public float StartLife { get { return startLife; } }
    public float Age { get { return startLife - life; } }
    public Vector3 Center;

    public Explosion(Vector3 center, float life)
    {
        this.Center = center;
        this.life = life;
        this.startLife = life;
    }

    public void Update(GameTime gameTime)
    {
        life -= (float)gameTime.ElapsedGameTime.TotalSeconds;
    }
}





拍手[0回]


XNA爆発チュートリアル その9 爆発の位置

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

前回は爆発を表すオブジェクトを作りました。
爆発の描画される寿命を管理するオブジェクトです。
しかしこれには足りないものがあるように思えます。

たとえば、爆発の位置です。
現在の作っているゲームコンポーネントの機能は、爆発を描画することですが、
これは位置を制御できません。
常に爆発は原点O(0, 0, 0)のまわりに描画されます。
これでは使い物になりません

現実には、ゲーム中のアイテムが壊れたときに爆発を表示しなければならないため、
爆発の位置をコントロールすることが出来なくてはいけないでしょう。
つまり、爆発を表す構造体Explosionには爆発の中心を表すフィールドが必要です。

そしてExplosionオブジェクトはクライアントコードであるGame側から
セットするようにすべきでしょう。
ExplosionRendererゲームコンポーネントクラスの責任ではありません。

使い方はこんな感じでしょう:

ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    private ExplosionRenderer explosion;
    private float life;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        explosion = new ExplosionRenderer(this);
        Matrix view = Matrix.CreateLookAt(
            new Vector3(0, 0, 4),
            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);
    }


    protected override void Update(GameTime gameTime)
    {
        life -= (float)gameTime.ElapsedGameTime.TotalSeconds;

        if (life < 0)
        {
            life = 2;
            explosion.SetExplosion(new Explosion(new Vector3(), life));
        }

        base.Update(gameTime);
    }

}


爆発のセットはゲームクラスで行っています。
爆発を描画するゲームコンポーネントのExplosionRendererに
SetExplosionというメソッドを追加し、それを使って爆発オブジェクトをセットします。

爆発を表す構造体Explosionのコンストラクタも変わりました。
一番最初の引数に爆発の中心が追加されています。
これで爆発する場所を自由に決められるようになりました。(実装はまだですけどね)

とりあえずExplosion構造体のほうをこの変更に合わせましょう

Explosion.cs
using Microsoft.Xna.Framework;

public struct Explosion
{
    private float life;
    private float startLife;

    public float Life { get { return life; } }
    public float StartLife { get { return startLife; } }
    public float Age { get { return startLife - life; } }
    public Vector3 Center;

    public Explosion(Vector3 center, float life)
    {
        this.Center = center;
        this.life = life;
        this.startLife = life;
    }

    public void Update(GameTime gameTime)
    {
        life -= (float)gameTime.ElapsedGameTime.TotalSeconds;
    }
}


つまり、爆発中心を表すCenterフィールドを追加しただけです。
(プロパティにしたほうがよかったかな?いやでもむやみに複雑化しても困ります)

次はこれを使うゲームコンポーネント、ExplosionRendererの変更です。
まずSetExplosionメソッドを追加して、さらにセットされた爆発の中心を読み込んでエフェクトにセットしなきゃいけません。
(ややこしいですね。もしかするともうちょっと作業を細分化したほうがよかったかもしれないと反省しつつ、続行です)

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;

    private Explosion explosion;


    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 Update(GameTime gameTime)
    {
        explosion.Update(gameTime);
    }

    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["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        drawExplosion(explosion);

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

    private void drawExplosion(Explosion explosion)
    {
        effect.Parameters["Center"].SetValue(explosion.Center);
        effect.Parameters["Age"].SetValue(explosion.Age);
        effect.Parameters["StartLife"].SetValue(explosion.StartLife);

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

    public void SetExplosion(Explosion explosion)
    {
        this.explosion = explosion;
    }
}


最後に、HLSLで書くエフェクトファイルにグローバル変数Centerを
追加しなくてはいけません。

ExplosionShader.fx
float4x4 View;
float4x4 Projection;

float3 Center;
float Age;
float StartLife;

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 * Age + Center, 1);
	output.Position = mul(worldPosition, mul(View, Projection));
	output.Color = ComputeColor(Age / StartLife);
	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();
	}
}



これでひとまずOKです。
ウィンドウに描画される爆発は以前と変わりませんが、
ソースコードに柔軟性が生まれました。
爆発の位置を色々と変えることが出来ます。
ためしに、右に1ずらしてみましょう。

explosion.SetExplosion(new Explosion(new Vector3(1, 0, 0), life));

explosionTutorialExplosionAtO.jpg(ずらす前。爆発の中心は(0, 0, 0))

explosionTutorialExplosionMoved.jpg(ずらした後。爆発の中心は(1, 0, 0))

うまくずれています。
これで好きなところに爆発を描画することができるようになりました。

拍手[0回]


XNA爆発チュートリアル その8.5 爆発オブジェクトの導入

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

前回では3Dの爆発をフェードアウトさせたものの、
時間がたつとまた爆炎が描画されるようになってしまいます。
これではいつまでたっても爆発が画面から消えません。

この問題を解決するために、C#のコードのほうを
少々いじります。
爆発を表すクラス(まあ正確には構造体ですが)を作り、それに寿命を管理させるのです。
寿命が尽きたら新しい、別のインスタンスを使えばいいわけです。
そうすれば寿命が尽きた爆発は決して描画されません。

この構造体を利用すれば、ついでにHLSL側の全体の寿命を表す
マジックナンバー”2”も消してしまえるでしょう。
一石二鳥というわけです。

さらにせっかくですからHLSL側とC#側の変数名のミスマッチも解消します。

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;

    private Explosion explosion;


    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 Update(GameTime gameTime)
    {
        explosion.Update(gameTime);

        if (explosion.Life < 0)
        {
            explosion = new Explosion(2);
        }
    }

    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["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        drawExplosion(explosion);

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

    private void drawExplosion(Explosion explosion)
    {
        effect.Parameters["Age"].SetValue(explosion.Age);
        effect.Parameters["StartLife"].SetValue(explosion.StartLife);

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

爆発の寿命が尽きたら直ちにライフ2秒にして初期化するようにしました。
実行したら2秒ごとに爆発していく様子が見られるでしょう。

次は爆発を表す構造体です。
クラスではなくて構造体なのはこの方がたぶん
パフォーマンス上有利なのではないかという理由です。

構造体で作った変数はヒープではなくスタック上におかれるため、
ガベージコレクションが発動しないので、
パフォーマンスのボトルネックにはならないはずです。(たぶん)

ふつうパフォーマンスを考慮したコードは、
可読性が低下するため避けるべきなのですが、
classをstructにするくらいは問題ないでしょう。

Explosion.cs
using Microsoft.Xna.Framework;

struct Explosion
{
    private float life;
    private float startLife;

    public float Life { get { return life; } }
    public float StartLife { get { return startLife; } }
    public float Age { get { return startLife - life; } }

    public Explosion(float life)
    {
        this.life = life;
        this.startLife = life;
    }

    public void Update(GameTime gameTime)
    {
        life -= (float)gameTime.ElapsedGameTime.TotalSeconds;
    }
}


お次はHLSLで書いたエフェクトファイルです。
前回のマジックナンバー”2”をグローバル変数StartLifeで置き換え、
C#側から設定するようにしました。
また、変数名をC#側と同じになるようにそろえました。(Time → Age)

ExplosionShader.fx
float4x4 View;
float4x4 Projection;

float Age;
float StartLife;

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 * Age, 1);
	output.Position = mul(worldPosition, mul(View, Projection));
	output.Color = ComputeColor(Age / StartLife);
	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();
	}
}



変更は以上です。
これでプログラムを実行すると2秒ごとに爆発が
「ぶわっ」っと広がっていくのがわかると思います。

フェードアウトした爆発は、もはや決して再び描画されることはありません。
さらに一歩前進しましたね。


拍手[0回]