忍者ブログ

Memeplexes

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

[PR]

×

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


カスタムエフェクトでモデルを表示しようとするとテクスチャがうまく貼られない問題とその解決法

ここ数日

「カスタムエフェクトでモデルを表示しようとすると明らかにテクスチャが上手く貼られていない」

問題に悩んだので、同じ問題に苦しむ人がこれ以上出ないようにメモしておきます。


テクスチャが上手く貼られない!

まず、事の発端はXna Creators ClubのChaseCameraSampleのモデル(Ship.fbxと、テクスチャShipDiffuse.tga)を、HLSLで新しく書いたカスタムエフェクトで描画してみようとしてみたことでした。
すると、明らかにテクスチャの貼られ方がおかしいのです。

modelRenderedWithBasicEffect.JPGBasicEffectで普通に描画した場合、モデルはこのように表示されます。(ライトなし)

modelRenderedWithCustomEffect.JPGこちらがカスタムエフェクトを適用して描画したモデルです。明らかにコクピットの窓がおかしいですね。窓のテクスチャの代わりにボディのテクスチャが貼られているようです。(でもまあ上のエンジン(?)っぽいところはこっちの方が自然かもしれません)

参考までに、エフェクトファイルとゲームのコードを書いておきます:

SimpleEffect.fx
//グローバル変数(C#側からセット)
float4x4 World;
float4x4 View;
float4x4 Projection;

texture Texture;


//頂点シェーダ
struct VertexPositionTexture
{
	float4 Position : POSITION;
	float4 TextureCoordinate : TEXCOORD;
};

VertexPositionTexture VertexShader(VertexPositionTexture input)
{
	VertexPositionTexture output;

	output.Position = mul(input.Position, mul(World, mul(View, Projection)));
	output.TextureCoordinate = input.TextureCoordinate;
	
	return output;
}



//ピクセルシェーダ
sampler DiffuseSampler = sampler_state{
	Texture = (Texture);
};

float4 PixelShader(float2 textureCoordinate : TEXCOORD) : COLOR
{
	return tex2D(DiffuseSampler, textureCoordinate);
}


technique SimpleEffect
{
	pass
	{
		VertexShader = compile vs_1_1 VertexShader();
		PixelShader = compile ps_2_0 PixelShader();
	}
}


C#側はこうです:

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

public class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;

    Model model;


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


    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            model = content.Load<Model>("Ship");
            ChangeEffectUsedByModel(
                content.Load<Effect>("SimpleEffect")
                );
        }
    }

    void ChangeEffectUsedByModel(Effect replacementEffect)
    {
        Texture2D texture = content.Load<Texture2D>("ShipDiffuse");

        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (ModelMeshPart part in mesh.MeshParts)
            {
                Effect newEffect = replacementEffect.Clone(
                    graphics.GraphicsDevice
                    );

                newEffect.Parameters["Texture"].SetValue(
                    texture
                    );
                part.Effect = newEffect;
            }
        }
    }

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




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

        Matrix world = Matrix.CreateRotationY(
            MathHelper.ToRadians(-40)
            );

        Matrix view = Matrix.CreateLookAt(
            new Vector3(3000, 1500, 0),
            Vector3.Zero,
            Vector3.Up
            );

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

        DrawModel(world, view, projection);

    }

    void DrawModel(Matrix world, Matrix view, Matrix projection)
    {
        Matrix[] transforms = new Matrix[model.Bones.Count];

        model.CopyAbsoluteBoneTransformsTo(transforms);

        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (Effect effect in mesh.Effects)
            {
                Matrix localWorld = transforms[mesh.ParentBone.Index] * world;

                effect.Parameters["World"].SetValue(localWorld);
                effect.Parameters["View"].SetValue(view);
                effect.Parameters["Projection"].SetValue(projection);
            }

            mesh.Draw();
        }
    }
}


解決法
さて、どこがまずいのでしょうか?
結論から言うと、テクスチャをコンテントパイプラインから新たにロードしたのがまずかったようです。
「Modelに最初から付いているBasicEffectに付いているテクスチャ」をカスタムエフェクトにセットすると、問題は解決したように見えます(「見えます」というのは、BasicEffectとやや違う(窓が黒)ところがあるからです。でも画像としてみて明らかにおかしいということはありません。じっさい、この方法はCreators ClubのサンプルNonPhotoRealisticで使われているものです)

ChangeEffectUsedByModelメソッド
    void ChangeEffectUsedByModel(Effect replacementEffect)
    {
        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (ModelMeshPart part in mesh.MeshParts)
            {
                Effect newEffect = replacementEffect.Clone(
                    graphics.GraphicsDevice
                    );

                newEffect.Parameters["Texture"].SetValue(
                    ((BasicEffect)part.Effect).Texture
                    );
                part.Effect = newEffect;
            }
        }
    }



modelRenderedWithCustomEffectAndTextureFromBasicEffect.JPG

これで窓のテクスチャはまともに表示されます。(でもエンジンが真っ黒って言うのは審美的観点から見てどうなんでしょうね……)

さて、このことから、どうやらこのブログの記事の題名、「カスタムエフェクトで・・・」というのは実はおかしいことがわかりますね。
この問題が発生した原因はカスタムエフェクトを使ったことにあるのではなく、実はテクスチャにあるのです。
したがって「カスタムエフェクト」うんぬんは題名にあってはいけないのかもしれませんね。
でもこの問題が発生するのはたいていカスタムエフェクトを使った時でしょうから、「まぁいいんじゃないのかなぁ」と思ったり思わなかったりです。


余談(?)

この問題を解決しようとして行った実験を参考までに書いておきます。
解決の過程というのはおもしろいですからね!


まず、この問題にぶち当たってかなり早い段階でしたことは、シェーダーをBasicEffectにして描画してみることです。
つまり、コンテントパイプラインからロードするのではなく、新たにBasicEffectを生成して、モデルに適用してみたのです。
この結果によって、この問題の原因がカスタムエフェクトのHLSLのコードにあるのかC#のコードにあるのかがはっきりします。

すると、テクスチャが上手く貼られません!
同じように、コクピットの
このことからこの問題の原因がHLSLではなく、C#のコードにあることは明白です(たぶん)。

おそらくカスタムエフェクトをモデルに適用する際になにかまずいことをしてしまったのでしょうね。

NonPhotoRealisticサンプルのC#のコードを持ってきて、モデル描画に最低限必要なところ以外を消していけば、問題点が浮き彫りになるはずです!



そんな感じでテクスチャのセットに問題があることがわかったのですが、その後HLSLでカスタムエフェクトを書き直しているときに、別の問題がでてきたのです。

それは、窓の色です。

modelRenderedWithPS_1_1.JPG

窓の色が黒ではなく白になっています。
さて、サンプルと照らし合わせてみると、こんどは問題はHLSL側にあることがわかりました。
どうやらピクセルシェーダのバージョンが(2.0ではなく)1.1だと、このように窓が白になってしまうらしいのです。

教訓として、テクスチャの色がおかしい時にはピクセルシェーダのバージョンをチェックすべきだということなのでしょうね




さらに、テクスチャについても調べてみました。
窓にうまくテクスチャが貼られるかどうかはテクスチャのインスタンスが関係しているようです。(もしかしたら違うかもしれませんが、話が進みませんからね)
具体的にどのようなテクスチャなら上手くいくのでしょうか?

これを調べるために、上手くいくテクスチャ(BasicEffectから取り出したテクスチャ)と上手くいかないテクスチャ(コンテントパイプラインから直接ロードしたテクスチャ)のプロパティを比較してみました:
  上手く表示されるテクスチャ 上手く表示されないテクスチャ
Width 256 256
Height 256 256
ResourceManagementMode Automatic Automatic
ResourceUsage None None
Format Dxt5 Color
LevelCount 9 1
LevelOfDetail 0 0
IsDisposed False False
Tag
Name
ResourceType Texture2D Texture2D
Priority 0 0

あやしいのはFormatプロパティとLevelCountプロパティですね……。
上手く表示されるテクスチャと上手くいかないテクスチャでこの2つのプロパティの値が違うということは、この2つのプロパティのうちどちらか、または両方が窓の表示がおかしくなる原因であると考えたくなります。(残念ながらこの2つのプロパティには両方ともGetしかないので、検証しようがありませんが…)

でももしかしたらプロパティとは全く関係ないところに原因があるかもしれないので、油断は禁物です。
(FormatとLevelCountが何でテクスチャ座標に関係あるのかはわかりませんしね)

正直に言うと、この問題の原因は厳密にはまだよくわからないので、今のところはとりあえずBasicEffectからテクスチャを持ってこようといったところです。
(でもSpaceWarの宇宙船はテクスチャを自由に変えることができましたよね……あれはテクスチャをコンテントパイプラインからロードしているはず……あれ?)

拍手[0回]

PR

XNA 複数のメッシュを一度のDrawで描画(シェーダー・インスタンシング)

前回はハードウェア・インスタンシングをやりましたが、これはGPUシェーダー・モデル3.0以上でなければ動きません。
どうやらまだ2.0までのものがよく使われているそうなので、これでは困る場合もあるでしょう。

実は、シェーダーモデルが2.0でも上手くいく方法があります。
グラフィックスカードのメモリを少々食うのですが、モデルそのものはたくさん複製して1つのVertexBufferに入れておいて、インスタンスの情報は配列にしてHLSLのグローバル変数としてセットしてしまうというものです。
これをシェーダー・インスタンシングといいます。

ハードウェア・インスタンシングではVertexBufferにインスタンスの情報を格納しましたが、こちらはHLSLのグローバル変数に格納します。
そして、本来のVertexBufferの中にあるそれぞれのモデルに、インデックスを振ります。
そのインデックスから対応するインスタンスの情報を特定して、頂点に適用するのです。(どうもわかりにくいですね・・・)

こうすることによって、1つのVertexBufferから、自由に動かせる複数のモデルを一度に描画することが出来ます。
モデルのデータを複製して1つのVertexBufferに入れるため、その分無駄なグラフィックス・カードのメモリを食うのですが、それでも一つ一つモデルを描画するよりもパフォーマンスが良くなる(CPUの負荷が減るので)そうです。

HLSLのコードは次のようになります:

ShaderInstancing.fx

float4x4 InstanceTransforms[10];

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

VertexPositionColor VertexShader(
	VertexPositionColor input,
	float instanceIndex : TEXCOORD1
	)
{
	VertexPositionColor output;
	output.Position = mul(input.Position, InstanceTransforms[instanceIndex]);
	output.Color = input.Color;
	return output;
}

float4 PixelShader(float4 color : COLOR):COLOR
{
	return color;
}

technique ShaderInstancing
{
	pass ShaderInstancingPass
	{
		VertexShader = compile vs_2_0 VertexShader();
		PixelShader = compile ps_2_0 PixelShader();
	} 
}


これは、10個のインスタンスを同時に描画するためのエフェクトファイルです。

グローバル変数InstanceTransformsの中にはそれぞれのインスタンスの情報(マトリックス)が格納されています。
頂点シェーダの入力として入ってくるインデックスから、対応するマトリックスを割り出し、それをかけてモデルを変形しています。

C#側はこんなかんじです:
(HLSLのグローバル変数に値をセットし、インデックス付のVertexBufferを作るだけです)

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

struct VertexPositionColorIndex
{
    public Vector3 Position;
    public Color Color;
    public float Index;

    public static readonly int SizeInBytes 
        = System.Runtime.InteropServices.Marshal.SizeOf(
            typeof(VertexPositionColorIndex)
        );

    public static readonly VertexElement[] VertexElements
        = new VertexElement[]{
            new VertexElement(
                0,
                0,
                VertexElementFormat.Vector3,
                VertexElementMethod.Default,
                VertexElementUsage.Position,
                0
            ),
            new VertexElement(
                0,
                sizeof(float)*3,
                VertexElementFormat.Color, 
                VertexElementMethod.Default, 
                VertexElementUsage.Color, 
                0
            ),
            new VertexElement(
                0, 
                sizeof(float)*3 + 4, 
                VertexElementFormat.Single, 
                VertexElementMethod.Default, 
                VertexElementUsage.TextureCoordinate, 
                1
            )
        };
}

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;

    const int instanceCount = 10;
    VertexPositionColorIndex[] vertices 
        = new VertexPositionColorIndex[3 * instanceCount];
    Matrix[] instanceTransforms = new Matrix[instanceCount];

    Effect effect;
    VertexBuffer vertexBuffer;
   

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


        for (int i = 0; i < instanceCount; i++)
        {
            vertices[3 * i].Color = Color.Blue;
            vertices[3 * i].Position = new Vector3(-0.1f, 0.1f, 0);
            vertices[3 * i].Index = i;
            
            vertices[3 * i + 1].Color = Color.White;
            vertices[3 * i + 1].Position = new Vector3(0.1f, 0.1f, 0);
            vertices[3 * i + 1].Index = i;

            vertices[3 * i + 2].Color = Color.Red;
            vertices[3 * i + 2].Position = new Vector3(0.1f, -0.1f, 0);
            vertices[3 * i + 2].Index = i;
        }

        for (int i = 0; i < instanceTransforms.Length; i++)
        {
            instanceTransforms[i] = Matrix.CreateTranslation(0.1f * i, 0, 0);
        }
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("ShaderInstancing");

            vertexBuffer = new VertexBuffer(
                graphics.GraphicsDevice,
                vertices.Length * VertexPositionColorIndex.SizeInBytes,
                ResourceUsage.None
                );
            vertexBuffer.SetData<VertexPositionColorIndex>(vertices);

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


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

        graphics.GraphicsDevice.Vertices[0].SetSource(
            vertexBuffer,
            0,
            VertexPositionColorIndex.SizeInBytes
            );

        effect.Parameters["InstanceTransforms"].SetValue(instanceTransforms);

        effect.Begin();

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

            graphics.GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,
                vertices.Length / 3
                );

            pass.End();
        }

        effect.End();
        
    }
}


shaderInstancing.JPG

ここでは、三角形を10個描画しています。
複数のモデルを描画しているにもかかわらず、Drawコールは1回だけです。

VertexBufferにモデルのコピーが10個入っていて、それを一度に描画しているからです。

普通ならその方法ではそれぞれのモデルのインスタンスを別々に動かすことは難しいはずですが(少なくともVertexBufferを毎回アップデートしなければいけません)、シェーダー・インスタンシングは、動く部分のデータをVertexBufferから切り離すことによって、それを可能にしています。
動くデータはVertexBufferではなくて、HLSLのグローバル変数に入っているのです。
VertexBufferに入っているのは、そのグローバル変数から、対応する動くデータを取り出すためのキーです。
キーならば固定されていても問題ありません。

こういう方法によって、それぞれのモデルのインスタンスを自由に動かしつつ、全てを一度に描画することが出来るわけですね。

拍手[1回]


XNA 複数のメッシュを一度のDrawで描画(ハードウェア・インスタンシング)

この記事は古いです。
こちらと合わせてお読みください。



メッシュ・インスタンシング
ここのところXNA Creators Clubのサンプル、Mesh Instancingをいじっていたのですが、あまりに難解だったのでわかったことをメモしておきます。

このMesh Instancingというのが何をやっているかと言うと、複数のモデルを一度だけのDrawIndexedPrimitivesメソッドの呼び出しで描画してしまうと言うものです。(モデルのデータをC#で言う「クラス」にして、GPUの中でインスタンスをいっぱい作って増やすイメージです。普通はそれぞれのインスタンスを別々にGPUに送りますが(データ自体はGPUにあるので大したことありませんが)、Mesh Instancingではまず1つのモデルをGPUに送って、それを増やしているような感じです。)

何でそんなことをするのかと言うと、Draw****をたくさん呼ぶのははCPUに負荷がかかるからです。(GPUにパフォーマンスのボトルネックがあるのなら、これは意味がありません)
サンプルの解説によると、モデルの数が数千くらいになる(Draw****を数千回呼ぶ)とヤバいそうです。
Draw****のCPUへの負荷はモデルの複雑さにかかわらず一定なので、単純なモデルをたくさん個別に描画するのはCPUに負担がかかるわけです。(逆に、複雑なモデルを1回描画するだけならほとんどCPUに負担はないでしょう。GPUだけの問題になります)

そこで使われるのがこのMesh Instancingなるものです。
モデルをたくさん描画したい時、それぞれのモデルにつきDrawを呼ぶのではなく、全部まとめて1回のDrawにしてしまえば、CPUにはほとんど負荷がかからないというわけです。

これは、モデルが単純で、大量に描画したい時に使うCPUパフォーマンス改善テクニックです。複雑なモデルを少しだけ描画したい時にはほとんど意味が無いでしょう。

このMesh Instancingサンプルでは3つの方法をとっています。

Hardware Instancing Windows
(shader 3.0)
Shader Instancing Windows
(shader 2.0)
VFetch Xbox360

この3つ(に付け加え、Mesh Instancingテクニックを使わない方法2つ)が1つのソリューションにごっちゃになっているため、コードがスパゲッティの様相を呈しています。
別々にしてくれればまだわかりやすいんですけどね……。

Hardware Instancing
ここでは、一番上のHardware Instancingとかいうのをやってみたいと思います。

Hardware Instancingはどのようにモデルを増やしているかと言うと、VertexBufferを使います。

モデルのメッシュを表すVertexBufferとは別に、各モデルのインスタンス固有の情報(モデルのインスタンスの位置とか回転とかです・・・・・・C#で言うならちょうどメンバ変数のようなものでしょうか)を格納したVertexBufferを作るのです。
それを、2つともGraphicsDevice.Verticesにセットしてやります。

   解説
 GraphicsDevice.Vertices[0]  普通のVertexBufferと同じ。モデルを構成する頂点を持っています。ここでは、C#で言うなら「クラス」の役割を果たします。
 GraphicsDevice.Vertices[1]  頂点の代わりに、モデルの各インスタンス固有の情報をもっています。C#で言うなら「メンバ変数」がいっぱい詰まっている感じ。構造体の配列みたい。

さて、こうすると疑問が2つ出てきます。
まず、VertexDeclarationはどうなるんだと思われるかもしれません。
VertexBufferが2つあるのなら、どちらの要素からVertexDeclarationを作ればいいのでしょう?

答えは、両方です。
VertexDeclarationコンストラクタはVertexElement構造体の配列を引数に取りますが、その配列に「モデルの各インスタンス固有の情報」を表すVertexElementも入れてやればいいのです。(そのVertexElementのStreamプロパティの値は0ではなく1となります)

2つ目の疑問は、「ただGraphicsDevice.Vertices[1]に各インスタンス固有の情報とやらをセットするだけで上手くいくのかな?(上手くいくんだったら、残念だけど柔軟性に問題があるんじゃ・・・)」というものでしょう。

幸か不幸か、上手くいきません。(笑)
Vertices[0]にもVertices[1]にも周波数をセットして、頂点シェーダでうまく調和するようにしなければなりません。
Vertices[0]Vertices[1]の各要素それぞれに使われるわけですから、頂点シェーダで使われるリズム(?)が違うわけです。
2重ループの外と中みたいな感じでしょうか。
例を見ましょう:

HardwareInstancing.fx
struct VertexPositionColor
{
	float4 Position : POSITION0;
	float4 Color : COLOR;
};

VertexPositionColor VertexShader(
	VertexPositionColor input, float3 position:POSITION1
	)
{
	input.Position.xyz += position;
	return input;
}

float4 PixelShader(float4 color : COLOR) : COLOR0
{
	return color;
}

technique HardwareInstancing
{
	pass Pass1
	{
		VertexShader = compile vs_3_0 VertexShader();
		PixelShader = compile ps_3_0 PixelShader();
	}
}

まず、頂点シェーダの引数positionVertices[1]から来た)が固定されていて、Vertices[0]から来たinputは目まぐるしく変わっていきます。
その次に、positionが次のものに代わって、inputは同じように目まぐるしく変わっていき・・・・・をくりかえしていきます。
この2つは、要素が使われるタイミングが違うわけです。

この2つの使われる周波数を設定するには、VertexStream.SetFrequencyOfIndexDataVertices[0]用)とVertexStream.SetFrequencyOfInstanceDataVertices[1]用)メソッドを使います。

public void SetFrequencyOfIndexData ( int frequency )

このfrequencyはモデルのインデックスの周波数です。実際に使う時には、インスタンスの数をセットします。どうやらIndexBufferと一緒に使わないといけないみたいです(IndexBufferを使わずにやってドつぼに嵌りました・・・)

public void SetFrequencyOfInstanceData ( int frequency)

こっちのfrequencyはインスタンス・データの周波数です。とりあえず1をセットします。


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



class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;


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

    Vector3[] trianglePositions = new Vector3[] {
        new Vector3(),
        new Vector3(0.1f, 0.1f, 0)
    };


    //Graphics Device Objects
    Effect effect;
    VertexBuffer triangleVertexBuffer;
    IndexBuffer indexBuffer;
    VertexBuffer positionVertexBuffer;
   

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

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("HardwareInstancing");

            graphics.GraphicsDevice.VertexDeclaration 
                = createVertexDeclaration();


            triangleVertexBuffer = new VertexBuffer(
                graphics.GraphicsDevice,
                VertexPositionColor.SizeInBytes * vertices.Length,
                ResourceUsage.None
                );
            triangleVertexBuffer.SetData<VertexPositionColor>(vertices);


            indexBuffer = new IndexBuffer(
                graphics.GraphicsDevice,
                sizeof(int) * 3,
                ResourceUsage.None,
                IndexElementSize.ThirtyTwoBits
                );
            indexBuffer.SetData<int>(new int[] { 0, 1, 2 });


            positionVertexBuffer = new VertexBuffer(
                graphics.GraphicsDevice,
                sizeof(float) * 3 * trianglePositions.Length,
                ResourceUsage.None
                );
            positionVertexBuffer.SetData<Vector3>(trianglePositions);
        }
    }

    VertexDeclaration createVertexDeclaration()
    {
        System.Collections.Generic.List<VertexElement> elements 
            = new System.Collections.Generic.List<VertexElement>();

        elements.AddRange(VertexPositionColor.VertexElements);
        elements.Add(
            new VertexElement(
                1,
                0,
                VertexElementFormat.Vector3,
                VertexElementMethod.Default,
                VertexElementUsage.Position,
                1)
            );

        return new VertexDeclaration(
            graphics.GraphicsDevice,
            elements.ToArray()
            );
    }

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


        graphics.GraphicsDevice.Vertices[0].SetSource(
            triangleVertexBuffer,
            0,
            VertexPositionColor.SizeInBytes
            );
        graphics.GraphicsDevice.Vertices[0].SetFrequencyOfIndexData(
            trianglePositions.Length
            );

        graphics.GraphicsDevice.Vertices[1].SetSource(
            positionVertexBuffer,
            0,
            sizeof(float) * 3
            );
        graphics.GraphicsDevice.Vertices[1].SetFrequencyOfInstanceData(1);

        graphics.GraphicsDevice.Indices = indexBuffer;


        effect.Begin();

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

            graphics.GraphicsDevice.DrawIndexedPrimitives(
                PrimitiveType.TriangleList,
                0,  //baseVertex
                0,  //minVertexIndex
                vertices.Length,  //numVertices
                0,  //startIndex
                vertices.Length/3   //primitiveCount
                );

            pass.End();
        }

        effect.End();
        
    }
}


hardwareInstancing.JPG
ハードウェアインスタンシングで2つの三角形を描画しています。(Drawは一回だけ)

モデルは1つだけですが、その1つだけのモデルを使って位置{0, 0, 0}と{0.1f, 0.1f, 0}にずらして描画しています。

このようにモデルの数が少ない場合では効果は全く実感できません(シンプルにするのが目的だったんです。勘弁してください)が、これが数千個にもなるとかなり効いてくるのでしょうね。

また、ここでは単にインスタンス固有のデータとして位置(Vector3)を使っていますが、もっと柔軟にしたいのならいろんなデータの変換(平行移動も回転も拡大も)が出来るマトリックス(Matrix構造体)を使うべきでしょうね。じっさい、Xna Creators ClubのInstanced Model Sampleではそのようにマトリックスを使っています。(そして難読化しているのですが)
















拍手[2回]


ゲームのバージョンアップ

修作で作ったゲームをほんの少しですがバージョンアップしました。
http://memeplex.sakura.ne.jp/Agents0.11.zip

変更点

弾が少し本体から離れて発射されるようになりました。
銃(?)の照準ができました。
レイアウトが少し変わりました。
衝突の計算を改善しました。(これまでは球状の物体同士でぶつかるとガクガク振動していました。今回はそれがありません)

たいした変更ではないかもしれませんが、こういうのを繰り返していけばそれなりのゲームになるのではないでしょうか。

拍手[0回]


Xnaでゲームを作るときには

ゲームを作ってみて、いくつか思い知らされたことがあるのでメモしておきます。

1.シェーダはなるべく低いバージョンでコンパイルしよう!

これは当たり前のことだと思えるかもしれません。
あんまり高いバージョンにしてしまうと、そのバージョンに未対応の
GPUでは動かないからです。
でもぼくは実は今まで全部(1.1ではなく)2.0でコンパイルしていたのです。

VertexShader = compile vs_2_0 MyVertexShader();
PixelShader = compile ps_2_0 MyPixelShader();

理由は「いちいち調べるのめんどくさいからもう全部2.0でいいや」というもので、
昨日まではこれでも差し支えなかったのですが、
ついに昨日、「時には1.1にすることも考えなきゃいけないかな」という気分になりました。

古いDellのパソコン(にGPU、ATI Radeon9250を付けたもの)でゲーム
を試しに動かしていたのですが、
なんと弾と爆発が表示されません
これではゲームになりません。
嫌な汗が出ました。

これはGPUがシェーダーモデル2.0に対応していないからだろうと思い、
大急ぎで弾と爆発のシェーダーのバージョンを二つとも1.1に変えました。
(バージョンを下げたことで、使えない機能が出てきて、新しいパソコンでも上手く動かなくなる危険性もありましたが、大丈夫だったようです。)

すると幸い古いパソコンでも上手く弾や爆発が表示されました。
めでたしめでたし

この話の教訓は、「ときどきでいいですから、バージョン関係にも気を使いましょうね」ということです。
ちょっぴり反省です。



2.描画をするときにはパフォーマンスも考慮しよう!

もちろんパフォーマンスを考えすぎてコードが可読性の低い、ぐちゃぐちゃなものになってしまってもいけません。
ものの本によるとパフォーマンスを考えるべきなのはプログラムの2割で、残りの8割はパフォーマンスにさして影響がありません。
そこでぼくは描画するときはパフォーマンスをあんまり考えていませんでした。

しかし昨日、古いDellのパソコンでゲームを動かしたところ、弾を撃ったときに信じられないくらいノロくなってしまうということを発見したのです。
原因はどうやらピクセルシェーダに負荷がかかりすぎているようで、たとえば弾を撃ったばっかりで弾が画面のピクセルの多くを占めるような場合にはものすごく遅くなります。(しかし弾が遠くに行ってしまい小さくなればスムーズに動きます。)
本当に原因がピクセルシェーダなのかはわかりませんが、えらくリッチな描画をやって問題が起きたというのも事実です。
以前は弾はビルボードで描画していたのですが、そのときには快適に動いていました。

つまり描画はパフォーマンスに影響のある2割だったということです。
しかしながら新しいパソコンでは遅くなったりはしていないようなので、弾の描画を以前のようなしょぼいものに戻すのもしのびないですね。
ですから教訓としては、「処理の遅いパソコン(グラフィックスカード)のために複数の描画方法を用意しよう!」といったところでしょうか・・・
でもめんどくさいですね。うーん・・・



(もちろん、この2つの問題は、古いグラフィックスカードを切り捨てることによって無視できるんですけどね)

拍手[0回]