1

在尝试进行实时 3D 地形变化时,我遇到了关于帧率下降的问题。我使用 C#、XNA 4.0 和 VS 2010。这是我的老学校项目,现在是时候完成它了。

我已经从图像文件中生成了地形,包括所有效果和东西,无论图像文件是什么分辨率,它都运行顺利。问题出在我的地形编辑器上。我希望它能够手动改变地形。我也做了那部分,但它只有在地形大小等于或小于 128x128 像素时才有效。如果地形尺寸更大,我开始获得大约 150x150 像素的帧速率下降,如果地形尺寸大于 512x512 像素,则完全无法控制。

我已经尝试了几种方法:

  • 尝试使用线程,但后来我收到奇怪的错误,说“一次可以在一个线程中调用绘制方法”或类似的东西,我无法解决。

  • 接下来我尝试使用 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);

            }
        }

编辑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()方法中仅一次: vertices仅分配一次

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方法和之前的一样,但是多了一行:

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 无法将表达式映射到顶点着色器指令集”。

然后,我红色,您可以使用SampleLevelVertexShader来采样颜色数据并认为我找到了解决方案,但我得到了一个奇怪的错误,该错误仅记录在一个俄语博客中,但我不会说或读俄语。错误是:“纹理声明上的 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调用都会带来一些开销,所以不要太细化。最好每个顶点缓冲区只调用一次,即使这意味着必须发送缓冲区的某些未修改部分。

这可能是唯一一个对地形“分块”会产生重大影响的地方,因为它允许您为每个SetData调用指定更紧凑的区域 - 允许您发送更少的未修改数据。(我将弄清楚为什么会这样作为练习。)

(您已经在使用DynamicVertexBuffer,这很好,因为这意味着 GPU 将自动处理动态更改其缓冲区的管道问题。)


最后,如果性能仍然是一个问题,您可以考虑完全不同的方法。

一个例子可能是将几何计算卸载到 GPU 上。您将您的转换heightData为纹理,并使用顶点着色器(以顶点的平面网格作为输入)对该纹理进行采样并输出适当的位置和法线。

这种方法的一大优势是它heightData可以您的顶点缓冲区小得多(512x512 时为 0.25MB)——CPU 需要处理并发送到 GPU 的数据要少得多。

于 2013-06-16T09:31:16.273 回答