【发布时间】:2013-06-14 19:01:50
【问题描述】:
我在尝试进行实时 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);
}
}
EDIT2:
好的,我决定采纳您的第二个建议,但遇到了一些问题。我像这样修改了我的方法:
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 无法将表达式映射到顶点着色器指令集”。
然后,我觉得你可以在VertexShader 中使用SampleLevel 来采样颜色数据并认为我找到了解决方案,但是我收到了一个奇怪的错误,该错误仅记录在一个俄罗斯博客中,但我不会说话或阅读俄语。错误是:“纹理声明上的 X4814 意外别名”
有没有办法对PixelShader 中的颜色进行采样,然后将它们传递给VertexShader?
这可能会起作用,因为我设法将float4 Height 设置为各种值,并且它改变了顶点高度。问题是,我不知道如何读取VertexShader 中的纹理颜色,或者如何将红色纹理颜色数据从PixelShader 传递到VertexShader。
编辑3:
我想我找到了解决方案。正在网上搜索,发现tex2Dlod 函数用作VertexShader 纹理采样器。但是显示的语法不同,我无法使它们起作用。
任何人都可以指出好的 HLSL 文献以了解一些有关 HLSL 编码的知识。这项任务似乎很容易,但不知何故,我无法让它工作。
【问题讨论】:
-
最后一段代码好像少了“
.SetData”,但总体思路是对的(如果设备丢失,需要刷新GPU数据)。您需要提供更多有用的代码。您发布的代码(用于修改heightData)似乎很合理。你能告诉我们SetUpTerrainVertices和CalculateNormals。可能还值得向我们展示您如何创建和绘制您的terrainVertexBuffer。 -
也许我有一个解决方案。我红色我需要将我的地图分成瓷砖,然后在只显示我地图的一部分的同时改变一个瓷砖。明天我会试试,但是这有点愚蠢,或者说得更好,改变 16 个 128x128 分辨率的图像来获得一张 512x512 的地图是不切实际的。
-
另外,在某个地方,我红色的是你只能绘制面向相机的顶点。这对我的项目有帮助吗?至少对于 512x512 分辨率,这是否可以控制帧率下降?
-
GPU 会自动剔除背面的多边形,速度比你快(使用
RasterizerState.CullCounterClockwise)。