1

リアルタイムで 3D 地形を変更しようとすると、フレーム レートが低下するという問題があります。私は C#、XNA 4.0、および VS 2010 を使用しています。これは私の古い学校のプロジェクトであり、完成させる時が来ました。

私はすでに画像ファイルからすべての効果やものを使って地形生成を行っており、画像ファイルの解像度に関係なくスムーズに実行されています。問題は私の地形エディタにあります。地形を手動で変更できるようにしてほしい。私もその部分を行いましたが、地形サイズが 128x128 ピクセル以下の場合にのみ機能します。地形のサイズが大きくなると、フレーム レートが 150x150 ピクセルあたりで低下し始めます。地形のサイズが 512x512 ピクセルを超えると、完全に管理できなくなります。

私はすでにいくつかのアプローチを試しました:

  • スレッドを使用しようとしましたが、「一度に 1 つのスレッドで Draw メソッドを呼び出すことができます」などの奇妙なエラーが発生し、解決できません。

  • 次に DynamicVertexBuffer と DynamicIndexBuffer を使ってみました。これは大いに役立ち、今では私のコードは、最大 256x256 ピクセルの地形サイズに対して許容可能なフレーム レートで動作しています。

私のコードを見てください:

    public void ChangeTerrain(float[,] heightData)
    {
        int x, y;
        int v = 1;
        if (currentMouseState.LeftButton == ButtonState.Pressed && currentMouseState.X < 512)
        {
            x = (int)currentMouseState.X / 2;
            y = (int)currentMouseState.Y / 2;
            if (x < 5)
                x = 5;
            if (x >= 251)
                x = 251;
            if (y < 5)
                y = 5;
            if (y >= 251)
                y = 251;

            for (int i = x - 4; i < x + 4; i++)
            {
                for (int j = y - 4; j < y + 4; j++)
                {
                    if (i == x - 4 || i == x + 3 || j == y - 4 || j == y + 3)
                        v = 3;
                    else
                        v = 5;
                    if (heightData[i, j] < 210)
                    {
                        heightData[i, j] += v;
                    }
                }
            }
        }
        if (currentMouseState.RightButton == ButtonState.Pressed && currentMouseState.X < 512)
        {
            x = (int)currentMouseState.X / 2;
            y = (int)currentMouseState.Y / 2;
            if (x < 5)
                x = 5;
            if (x >= 251)
                x = 251;
            if (y < 5)
                y = 5;
            if (y >= 251)
                y = 251;

            for (int i = x - 4; i < x + 4; i++)
            {
                for (int j = y - 4; j < y + 4; j++)
                {
                    if (heightData[i, j] > 0)
                    {
                        heightData[i, j] -= 1;
                    }
                }
            }
        }
        if (keyState.IsKeyDown(Keys.R))
        {
            for (int i = 0; i < 256; i++)
                for (int j = 0; j < 256; j++)
                    heightData[i, j] = 0f;
        }
        SetUpTerrainVertices();
        CalculateNormals();
        terrainVertexBuffer.SetData(vertices, 0, vertices.Length);
    }

私は 1024x512 ピクセルの解像度で作業しているので、マウスの位置を 1/2 でスケーリングして地形の位置を取得します。マウスの左右ボタンを使用して地形を変更します。つまり、3D 地形の生成元である heightData を変更します。最後の 3 行では、新しい heightData から頂点を作成し、法線を計算して陰影を適用できるようにし、最後の行では頂点データを頂点バッファーにスローしています。

その前に、LoadContent メソッドで動的な頂点とインデックス バッファーをセットアップし、最初の頂点とインデックスのセットアップを呼び出します。このメソッド (ChangeTerrain) は Update メソッドから呼び出されます。

デバッグを行ったところ、最も極端な場合の頂点の最大サイズは約 260000 +- 数千になることがわかりました。.SetData に時間がかかりすぎてフレーム レートが低下する可能性はありますか? それとも別のものですか?どうすればそれを修正し、どの地形サイズでもエディタを正常に機能させることができますか?

また、DynamicVertexBuffer でこのコードを使用する必要があることを確認しましたが、XNA 4.0 で動作させることはできません。

terrainVertexBuffer.ContentLost += new EventHandler(TerrainVertexBufferContentLost);

public void TerrainVertexBufferContentLost()
{
terrainVertexBuffer(vertices, 0, vertices.Length, SetDataOptions.NoOverwrite);
}

ご協力いただきありがとうございます!

編集:

これは私の SetUpTerrainVertices コードです:

private void SetUpTerrainVertices()
        {
            vertices = new VertexPositionNormalColored[terrainWidth * terrainLength];

            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainLength; y++)
                {
                    vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y);
vertices[x + y * terrainWidth].Color =  Color.Gray;

                }
            }
        }

そして私のCalculateNormals

private void CalculateNormals()
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].Position - vertices[index3].Position;
                Vector3 side2 = vertices[index1].Position - vertices[index2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }

次の行を使用して、XNA LoadContent メソッドで頂点バッファーとインデックス バッファーを設定します。

terrainVertexBuffer = new DynamicVertexBuffer(device, VertexPositionNormalColored.VertexDeclaration, vertices.Length,
                BufferUsage.None);

terrainIndexBuffer = new DynamicIndexBuffer(device, typeof(int), indices.Length, BufferUsage.None);

Update から ChangeTerrain メソッドを呼び出します。これが描画方法です。

private void DrawTerrain(Matrix currentViewMatrix)
        {
            device.DepthStencilState = DepthStencilState.Default;
            device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
            effect.CurrentTechnique = effect.Techniques["Colored"];
            Matrix worldMatrix = Matrix.Identity;
            effect.Parameters["xWorld"].SetValue(worldMatrix);
            effect.Parameters["xView"].SetValue(currentViewMatrix);
            effect.Parameters["xProjection"].SetValue(projectionMatrix);
            effect.Parameters["xEnableLighting"].SetValue(true);

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

                device.Indices = terrainIndexBuffer;
                device.SetVertexBuffer(terrainVertexBuffer);

                device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Length, 0, indices.Length / 3);

            }
        }

EDIT2:

わかりました、私はあなたの2番目の提案に行くことにしましたが、いくつかの問題に遭遇しました. メソッドを次のように変更しました。

public void ChangeTerrain(Texture2D heightmap)
        {
            Color[] mapColors = new Color[256 * 256];
            Color[] originalColors = new Color[256 * 256];
            for (int i = 0; i < 256 * 256; i++)
                originalColors[i] = new Color(0, 0, 0);

            heightMap2.GetData(mapColors);
            device.Textures[0] = null;
            device.Textures[1] = null;
            int x, y;
            int v = 1;
            if (currentMouseState.LeftButton == ButtonState.Pressed && currentMouseState.X < 512)
            {
                x = (int)currentMouseState.X / 2;
                y = (int)currentMouseState.Y / 2;
                if (x < 4)
                    x = 4;
                if (x >= 251)
                    x = 251;
                if (y < 4)
                    y = 4;
                if (y >= 251)
                    y = 251;                

                for (int i = x-4; i < x+4; i++)
                {
                    for (int j = y-4; j < y+4; j++)
                    {
                        if (i == x - 4 || i == x + 3 || j == y - 4 || j == y + 3)
                            v = 3;
                        else
                            v = 5;
                        if (mapColors[i + j * 256].R < 210)
                        {
                            mapColors[i + j * 256].R += (byte)(v);
                            mapColors[i + j * 256].G += (byte)(v);
                            mapColors[i + j * 256].B += (byte)(v);
                        }
                        heightMap2.SetData(mapColors);
                    }
                }
            }
            if (currentMouseState.RightButton == ButtonState.Pressed && currentMouseState.X < 512)
            {
                x = (int)currentMouseState.X / 2;
                y = (int)currentMouseState.Y / 2;
                if (x < 4)
                    x = 4;
                if (x >= 251)
                    x = 251;
                if (y < 4)
                    y = 4;
                if (y >= 251)
                    y = 251; 

                for (int i = x - 4; i < x + 4; i++)
                {
                    for (int j = y - 4; j < y + 4; j++)
                    {
                        if (mapColors[i + j * 256].R > 0)
                        {
                            mapColors[i + j * 256].R -= 1;
                            mapColors[i + j * 256].G -= 1;
                            mapColors[i + j * 256].B -= 1;
                        }
                        heightMap2.SetData(mapColors);
                    }
                }
            }
            if (keyState.IsKeyDown(Keys.R))
                heightMap2.SetData(originalColors);
        }

平面サーフェスの生成 -LoadContent()メソッドで 1 回のみ: verticesは 1 回だけ割り当てられます

private void SetUpTerrainVertices()
        {
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainLength; y++)
                {
                    vertices[x + y * terrainWidth].Position = new Vector3(x, 0, -y);
                    vertices[x + y * terrainLength].Color = Color.Gray;
                }
            }
        }

Drawメソッドは前と同じですが、1 行追加されています。

effect.Parameters["xTexture0"].SetValue(heightMap2);

また、私は と呼ばれる新しいテクニックを作成しましEditorた。これは次のようになります。

//------- Technique: Editor --------
struct EditorVertexToPixel
{
    float4 Position       : POSITION;    
    float4 Color        : COLOR0;
    float LightingFactor: TEXCOORD0;
    float2 TextureColor : TEXCOORD1;
};

struct EditorPixelToFrame
{
    float4 Color : COLOR0;
};

EditorVertexToPixel EditorVS( float4 inPos : POSITION, float4 inColor: COLOR, float3 inNormal: NORMAL, float2 inTextureColor: TEXCOORD1)
{    
    EditorVertexToPixel Output = (EditorVertexToPixel)0;
    float4x4 preViewProjection = mul (xView, xProjection);
    float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);

    float4 Height;
    float4 position2 = inPos;
    position2.y += Height;

    Output.Color = inColor;
    Output.Position = mul(position2, preWorldViewProjection);
    Output.TextureColor = inTextureColor;

    float3 Normal = normalize(mul(normalize(inNormal), xWorld));    
    Output.LightingFactor = 1;
    if (xEnableLighting)
        Output.LightingFactor = saturate(dot(Normal, -xLightDirection));

    return Output;    
}

EditorPixelToFrame EditorPS(EditorVertexToPixel PSIn)
{
    EditorPixelToFrame Output = (EditorPixelToFrame)0;   

    //float4 height2 = tex2D(HeightSAmpler, PSIn.TextureColor); 
    float4 colorNEW = float4(0.1f, 0.1f, 0.6f, 1);
    Output.Color = PSIn.Color * colorNEW;
    Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient;    

    return Output;
}

technique Editor
{
    pass Pass0
    {  
        VertexShader = compile vs_3_0 EditorVS();
        PixelShader  = compile ps_3_0 EditorPS();
    }
}

が設定されていないため、このコードは機能しませんfloat4 Height。私がやりたかったのは、float4 Height( を使用してSample) にテクスチャの色をサンプリングすることですが、 ではサンプラーを使用できませんVertexShader。「X4532 は式を頂点シェーダー命令セットにマップできません」というエラー メッセージが表示されます。

SampleLevel次に、カラーデータのサンプリングに使用できることを確認しVertexShader、解決策を見つけたと思いましたが、ロシア語のブログにのみ記載されている奇妙なエラーが発生しましたが、ロシア語を話すことも読むこともできません。エラー: 「X4814 テクスチャ宣言の予期しないエイリアス」

色をサンプリングしPixelShaderてに渡す方法はありVertexShaderますか?

これは、私がfloat4 Heightさまざまな値に設定でき、頂点の高さが変更されたのでうまくいく可能性があります。問題は、 でテクスチャ カラーを読み取る方法、または からにVertexShader赤いテクスチャ カラー データを渡す方法がわからないことです。PixelShaderVertexShader

EDIT3:解決策を見つけたと思います。ネットを検索していて、テクスチャサンプラーtex2Dlodとして使用する機能を見つけました。VertexShaderしかし、異なる構文が表示され、それらを機能させることができません。

HLSLコーディングについて少し学ぶために、HLSLの優れた文献を指摘できる人はいますか。このタスクはかなり簡単に思えますが、どういうわけか、うまくいきません。

4

1 に答える 1

2

わかりましたので、「実際の」パフォーマンスに関するアドバイスを提供することはできません。コードを測定していないためです。パフォーマンスの最適化において、測定はおそらく最も重要な部分です。「パフォーマンス目標よりも遅いですか?」という質問に答えることができる必要があります。「目標よりも遅いのはなぜですか?」

そうは言っても、経験豊富な開発者として私が際立っていることは次のとおりです。


このメソッド (ChangeTerrain) は Update メソッドから呼び出されます。

フレームごとにデータを再作成するのではなく、地形が実際に変更されたときにのみ機能するように、そのメソッドを分割することを検討する必要があります。


vertices = new VertexPositionNormalColored[terrainWidth * terrainLength];

verticesフレームごとに新しいバッファを割り当てると、膨大なメモリが割り当てられます ( 512x512 で6MB )。これはガベージ コレクターに大きな負担をかけることになります。これがパフォーマンスの問題の主な原因であると思われます。

とにかくその配列のすべてのデータを設定しようとしていることを考えると、その行を削除するだけで、配列の古いデータが上書きされます。


さらに良いことに、変更されないデータをそのままにして、実際に変更された頂点のみを変更することもできます。に対して行っているのとほぼ同じ方法でheightData

これの一部としてCalculateNormals、インデックス バッファに依存してすべての三角形を通過するのではなく、特定の頂点の周囲の頂点 (三角形を形成する) のインデックスを計算できるように変更することをお勧めします。vertices命令されているからできること。繰り返しますが、あなたがやっていることのようなものですheightData


terrainVertexBuffer.SetData(vertices, 0, vertices.Length);

これにより、6MB のバッファー全体が GPU に送信されます。SetDataフル バッファのサブセットのみを GPU に送信する のバージョンがあります。おそらくこれらを試して使用する必要があります。

SetData呼び出しにはいくらかのオーバーヘッドが伴うため、細かくなりすぎないようにしてください。頂点バッファーごとに呼び出しを 1 つだけにするのがおそらく最善です。これは、バッファーの変更されていない部分を送信する必要があることを意味する場合でもです。

これはおそらく、テレインを「チャンク化」することが大きな影響を与える唯一の場所です。これにより、各SetData呼び出しに対してより狭い領域を指定できるようになり、変更されていないデータを送信することが少なくなります。(なぜそうなるのかは、演習として説明を省略します。)

(DynamicVertexBufferこれは、GPU がその場でバッファーを変更するというパイプラインの問題を自動的に処理することを意味するため、これは良いことです。)


最後に、パフォーマンスがまだ問題である場合は、まったく別のアプローチを検討できます。

一例として、ジオメトリの計算を GPU にオフロードすることが考えられます。をテクスチャに変換heightDataし、頂点シェーダー (入力として頂点のフラット グリッドを使用) を使用して、そのテクスチャをサンプリングし、適切な位置と法線を出力します。

このアプローチの大きな利点の 1 つは、頂点バッファーよりもはるかに小さい (512x512 で 0.25MB) ことができることです。これは、CPU が処理して GPU に送信する必要があるデータがはるかに少ないことですheightData

于 2013-06-16T09:31:16.273 に答える