忍者ブログ

Memeplexes

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

[PR]

×

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


[XNA] SpriteBatch + Effect

SpriteBatchEffectを組み合わせる方法に手こずったのでメモしておきますね。

SpriteBatchはXNAの2D機能(といってもテクスチャの描画くらいしかできませんが)をカプセル化したクラスで、それに対してEffectは頂点データをピクセルの色に変換する方法を表すクラスで、どちらかというと3Dです。
もしこの2つを組み合わせることができるのなら(実際できるのですが)、テクスチャにピクセルシェーダーを簡単に適用することができます(たとえば、水面がゆらゆらと揺れている様子がかんたんに表現できるでしょう)

しかし困ったことに、SpriteBatchにはEffectを使うメンバが見当たりませんでした。
ではこの2つは一緒に使うことができないのかというと、そうではありません。
きちんと一緒に使うことができます。
で、ぐだぐだ説明を続けてもいいですが、ここはコードを見たほうが早いでしょう。とくに難しいところはありませんからね。


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

public class MyGame : Game
{
    GraphicsDeviceManager graphics;

    SpriteBatch spriteBatch;
    Texture2D texture;
    Effect effect;

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

    protected override void LoadContent()
    {
        texture = Content.Load("henohenomoheji");
        effect = Content.Load("2DEffect");

        spriteBatch = new SpriteBatch(GraphicsDevice);
    }

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

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


        spriteBatch.Begin(
            SpriteBlendMode.None,
            SpriteSortMode.Immediate,
            SaveStateMode.None
            );

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

                spriteBatch.Draw(texture, new Vector2(), Color.White);

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

        spriteBatch.End();
    }


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


2DEffect.fx
sampler TextureSampler : register(s0);

float4 PixelShaderFunction(float2 textureCoordinate : TEXCOORD) : COLOR0
{
    float2 texOffset = {sin(textureCoordinate.x * 30) / 20 , 0};
    
    return tex2D(
        TextureSampler,
        textureCoordinate + texOffset
        );
}

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



spriteBatchWithPixelShader.jpg

なんだかよくわからないものが左上に表示されていますが、これは「へのへのもへじ」をピクセルシェーダーで変形したものです。

henohenomoheji.jpghenohenomoheji.jpg


ピクセルシェーダー内で、適当にテクスチャ座標をいじってやると、水面が揺らいでいるようにみえなくもないというわけです。









拍手[0回]

PR

[XNA] GPUを使って計算した結果をCPUに戻す

なんとなくGPUを使った計算結果をCPU側でも使ってみたくなったので、やってみました。

ただ、これはXbox360ならともかくパソコンではパフォーマンスが悪くなるそうですね。
グラフィックスメモリをCPUからは読むのはあまり速くないとかだそうです。
ですからよほど面白いことができない限り、控えるべきでしょう。

実際に何かに使うとしたら、並列的な計算でしょうね。
GPUは、CPUと違って並列的な計算が得意ですから、そこら辺で面白いことができるに違いありません。
ある研究室でGPUを使ってニューラルネットワークを動かし、人の顔を判別している研究を見たことがありますが、まあそんな感じでしょうか。
その研究では、CPUを使った時よりも、何倍も計算速度が速くなったそうです。
こういうのはCPUよりもGPUのほうが向いているんですね。

もちろんニューラルネットワークは単なる一例で、「この手があったか!」みたいな応用方法が他にもあるに違いありません。
そしてその応用方法の中には、ゲームをもっと面白くする何かがあるにちがいありません!(たぶん)

というわけでXNAを使ってやってみました。

かといって、いきなりGPUでニューラルネットワークを動かしたりというのはあまりにハードルが高すぎるので、まずは一番簡単な計算をしてみたいと思います。

つまり、1 + 1です。
たぶん人が一番簡単だと考える計算ではないでしょうか!
たぶん幼稚園児でもできます。
これをGPUに計算させて、CPU側に戻し、ウィンドウに「2」と表示するのです。
「1 + 1なんてGPU使わなくたってわかるよ!」って感じですが、話をシンプルにするにはこれが一番です。

MyGame.cs


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

public class MyGame : Game
{
    GraphicsDeviceManager graphics;

    Texture2D inputTexture;
    RenderTarget2D result;
    Effect additionCalculator;
    VertexDeclaration vertexDeclaration;

    //GPUから計算結果を受け取るバッファ
    float[] resultBuffer = new float[2];

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


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

    protected override void LoadContent()
    {
        inputTexture = new Texture2D(GraphicsDevice, 2, 1, 0, TextureUsage.None, SurfaceFormat.Single);
        inputTexture.SetData<float>(new float[] { 1, 1 });

        result = new RenderTarget2D(GraphicsDevice, 2, 1, 0, SurfaceFormat.Single);

        additionCalculator = Content.Load<Effect>("AdditionCalculator");


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

    protected override void UnloadContent()
    {
        inputTexture.Dispose();
        result.Dispose();
        vertexDeclaration.Dispose();
    }

    protected override void Update(GameTime gameTime)
    {
        Window.Title = resultBuffer[1].ToString();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.SetRenderTarget(0, result);

        additionCalculator.Parameters["InputTexture"].SetValue(inputTexture);
        additionCalculator.Parameters["InputWidth"].SetValue(2);
        additionCalculator.Begin();
        additionCalculator.CurrentTechnique.Passes[0].Begin();

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

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

        //GetDataを呼ぶには、レンダーターゲットはいったん
         //デバイスから外されなければいけません。
        GraphicsDevice.SetRenderTarget(0, null);

        result.GetTexture().GetData<float>(resultBuffer);
    }

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


AdditionCalculator.fx(Contentフォルダ内)
float InputWidth;
texture InputTexture;

sampler InputSampler = sampler_state
{
    Texture = (InputTexture);
};

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

VertexPositionTexture VertexShaderFunction(VertexPositionTexture input)
{
    return input;
}

float4 PixelShaderFunction(VertexPositionTexture input) : COLOR0
{
    return tex2D(InputSampler, input.TextureCoordinate + float2(-1/InputWidth, 0))
		+ tex2D(InputSampler, input.TextureCoordinate);
}

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

AdditionByGPU.jpg

できました。
やったー!

きちんと、ウィンドウのタイトルに"2"と表示されているのがわかると思います(もちろん1 + 1の結果ですよ)。
GPUに、{1, 1}という配列をテクスチャとして送り込んで、ピクセルシェーダーで足し算しているのです。
そして、描画結果を2x1のテクスチャとしてCPU側に戻します。
そのデータをGetDataメソッドで読み込み、ウィンドウのTitleプロパティにセットすれば完了です。



additionByGPUDiagram.jpg

これを1秒間に60回繰り返しています。
最初の{1, 1}を送り込むのは一度だけですが、1 + 1は何度も何度も、60ヘルツで計算しています。
なんせDrawメソッド内でやっていますからね。

念のため、SetDataメソッドの引数をいろいろ変えて検証してみました。

inputTexture.SetData<float>(new float[] { 1, 2 });

a6d637c6.jpg

3か・・・・・・1 + 2は3ですから、うまくいっているようです。
つぎは2 + 5くらいをやってみましょう

inputTexture.SetData<float>(new float[] { 2, 5 });

9b78bdf9.jpg

2 + 5は7ですから、大丈夫ですね。

どうやらこれでうまくいっているようです。
ここでは単なる足し算の計算をしましたが、現実にはもっとエキサイティングでファンタスティックなことをやることができるでしょう。

拍手[2回]


XNA 2.0 beta

XNA Game Studio 2.0のベータ版を少し(本当に少しだけ!)使ってみました。

まずはWindows用のゲームのプロジェクトを作ってみたのですが、これはかなりわかりやすくなっている印象がありますね。

Game.Contentプロパティ

まず、画像や音、3Dモデルなどを読み込むContentManagerのインスタンスを自前で作る必要がありません
あのわけのわからないSystem.IServiceProviderを引数にとるコンストラクタを呼ばなくていいのです。
かわりに、Gameのプロパティになっています。(Game.Contentプロパティ)
ContentManager Content { get; }

おそらくContentManagerはGameクラスが管理することになったためでしょう、Content.Unloadメソッドは呼んでいる様子がありません。
きっとGame内部で呼んでいるのでしょうね。

Game.LoadContent、Game.UnloadContentメソッド

次に気付いたのはGPUを使うオブジェクトの生成と破棄をするメソッドです。
XNA 1.0ではGPUのオブジェクトの生成はGame.LoadGraphicsContentメソッド、破棄にはGame.UnloadGraphicsContentメソッドの内部で行っていたのですが、この2つのメソッドにはクセがありました。
まず両方ともどれだけ呼ばれるかプログラムを書く段階では予測が付きません。
もちろんロードとアンロードですから、必ず1回は呼ばれるわけですが、デバイスの状態によって、これがもっと呼ばれる場合だってあるのです。
ロードとアンロードなら、1回だけ呼ばれる方がわかりやすくていいはずです。

さらに、この2つのメソッドはbool型の引数を取っており、この引数の値によってメソッド内部で自前の条件分岐をしてやらなければいけません。
そうすると、どんなゲームを作るにしても、条件分岐の部分は必ず自前でやら無ければいけないのです。
つまり、条件分岐の部分がコードの重複になります。
そういった重複をなくすためのTemplate Methodパターンを使って何でまだ重複が残っているんだ!って話です。

XNA 2.0ベータではそのようなわかりにくさはなくなっています。
前述の2つのメソッドも残っていますが、GPUリソースを生成、破棄するのは新しいGame.LoadContentGame.UnloadContentメソッドをオーバーライドしたメソッド内部で行います。
protected virtual void LoadContent();
protected virtual void UnloadContent();


この2つのメソッドはゲーム中1回だけ呼ばれます。
2.0ではデバイスの仮想化が行われたそうですが、それのおかげでしょう(たぶん)。

さらに、あのわけのわからないbool型の引数も消えています。
万々歳ですね!

Game.GraphicsDeviceプロパティ

さらに、これはプロジェクトテンプレートの中には出てきませんが、GraphicsDeviceのインスタンスを取得できるGame.GraphicsDeviceプロパティも新しく追加されてます。
public GraphicsDevice GraphicsDevice { get; }


1.0ではDrawableGameComponentにはGraphicsDeviceプロパティがあるのにGameクラスの中にはGraphicsDeviceプロパティが無いというちぐはぐな感じでした。
Gameの派生クラスからGraphicsDeviceのインスタンスにアクセスするにはgraphics.GraphicsDeviceといううふうに、GraphicsDeviceManagerのインスタンスから間接的に行うのが普通だったのです。
ポリゴンを描画する時はもちろん、アルファブレンディングをオンにしたりオフにするたびにgraphics.GraphicsDevice.~です。(もちろん変数にとってやってもいいのですが、それだとスコープ内が余計な変数で汚染されるという副作用があります)
でも2.0になって、Game.GraphicsDeviceプロパティが導入されて、これでやっとシンプルな書き方が出来るようになるのではないでしょうか!


ぱっと見たくらいではこれくらいですが、実際に何かゲームを作ってみるといろいろと改良された点がわかっておもしろいかもしれませんね。




拍手[0回]


カスタムコンテントプロセッサの設定

コンテントパイプラインでカスタム・コンテントプロセッサを使うにはどうすればいいのかわからなかったのですが、
いい記事を見つけました:

XNA Tutorials and XNA Tools - Custom Content Processor
http://www.ziggyware.com/readarticle.php?article_id=69

英語ですが、9割以上は画像とコードなのでまったく心配ご無用です。

Visual C#のプロパティグリッドを使えばよかったんですね・・・。コードばっかりに目が向いていて気付きませんでした。

拍手[0回]


モデルを膨らます

Xna Creators ClubのShatterサンプルを簡略化してやってみようとしたのですが、失敗してしまいました(笑)。
でも結果は・・・・・・これはこれで面白いので書いておきます。(バラバラになるのではなく、風船のように膨らんでしまいました)

まず、Shatterサンプルと言うのが一体どういうものかを説明しておきます。
これは、モデルをリアルタイムで粉々にするサンプルです。

shatterSample1.JPG
shatterSample2.JPG
shatterSample3.JPG

shatterSample4.JPG
(クリックで画像を拡大)

最初に戦車のモデルが表示されるのですが、キーボードの↑キーを押すとバラバラになっていきます(↓キーで元に戻す)。

これは、「Updateメソッド内で頂点データを書き換えている」とかそういうのではなく、HLSLで書いたエフェクトファイルの中でバラバラにする操作をやっています。
モデルを粉々に砕いているのはCPUではなくGPUです。

ではエフェクトファイルの中でどのようにしてモデルをバラしているのかというと、これはモデルの法線を利用しています。
基本的なアイデアは、「法線ベクトルの方向に頂点の位置を動かせばバラバラになるだろう」というものです。(ただし、Shatterサンプルではそれぞれのポリゴンを回転させてもいます。たぶんこの回転がないと、今回のように失敗するんでしょう。)

そのため次のようにエフェクトファイルを書いてみました:

ShatterEffect.fx
float4x4 World, View, Projection;
float Shatter;

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


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


VertexPositionTexture ShatterVertexShader(
	float4 position:POSITION,
	float4 normal:NORMAL,
	float2 textureCoordinate :TEXCOORD
	)
{
	VertexPositionTexture output;
	
	position.xyz += normal.xyz * Shatter;
	float4x4 transform = mul(World, mul(View, Projection));
	output.Position = mul(position, transform);
	
	output.TextureCoordinate = textureCoordinate;
	
	return output;
}


float4 ShatterPixelShader(float2 textureCoordinate : TEXCOORD) : COLOR
{
	return tex2D(TextureSampler, textureCoordinate);
}

technique ShatterEffect
{
	pass
	{
		VertexShader = compile vs_2_0 ShatterVertexShader();
		PixelShader = compile ps_2_0 ShatterPixelShader();
	}
}

最初に「失敗した」と書きましたが、これにはミスは無いはずです(たぶん)!

ミスがないというか不十分みたいですね。
これは頂点のPositionを法線の向きに移動するようなシェーダなのですが、これだとモデルは風船のように膨らむことになります。

Creators ClubのShatterサンプルでは、法線の向きへの移動だけではなくポリゴンの回転もやっているので、きちんと粉々に割れてるように見えるのだと思います。

で、結果ですが、風船のようにモデルが膨らんでしまいました:

shatterFailed.JPG
そういえばDirectXのサンプルにこんなのがあったような気がします。
たしか人間が風船のように膨らんでいたような……


参考までにC#側のコードも書いておきます。

今回の失敗の原因はあまりに単純化しすぎたということでしょうか・・・
モデルを粉々にするのはまたの機会にしましょうかね…。

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

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;

    //Graphics Device Objects
    Effect effect;
    Model model;


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


    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("ShatterEffect");
            model = loadModel("Ship", effect);
        }
    }

    Model loadModel(string modelName, Effect effect)
    {
        Model result = content.Load<Model>(modelName);

        foreach (ModelMesh mesh in result.Meshes)
        {
            foreach (ModelMeshPart part in mesh.MeshParts)
            {
                effect.Parameters["Texture"].SetValue(
                    ((BasicEffect)part.Effect).Texture
                    );
                part.Effect = effect.Clone(graphics.GraphicsDevice);
            }
        }

        return result;
    }


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


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


        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (Effect effect in mesh.Effects)
            {
                setEffectParameters(
                    effect,
                    gameTime.TotalGameTime.TotalSeconds
                    );
            }

            mesh.Draw();
        }
    }

    private void setEffectParameters(Effect effect, double totalSeconds)
    {
        Matrix world = Matrix.CreateRotationY(
            (float)totalSeconds * 2
            );

        Matrix view = Matrix.CreateLookAt(
            new Vector3(0, 0, 3000), new Vector3(), Vector3.UnitY
            );

        Matrix projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(90),
            (float)graphics.GraphicsDevice.Viewport.Width / graphics.GraphicsDevice.Viewport.Height,
            1, 10000
            );

        effect.Parameters["Shatter"].SetValue(
            (float)(1 + System.Math.Sin(totalSeconds)) * 100
            );
        effect.Parameters["World"].SetValue(world);
        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);
    }
}



ちなみに、モデルはChaseCameraサンプルShip.fbx、そのモデルのテクスチャはShipDiffuse.tgaを使っています。

拍手[0回]