您的位置:首页 > 其它

基于一个顶点缓冲和一个索引缓冲创建一个地形

2012-06-01 10:42 330 查看


问题

基于一张2D高度图,你想创建一个地形并以一个有效率的方法绘制它。


解决方案

首先需要一张高度图,包含所有用来定义地形的高度数据。这张高度图有确定数量的二维数据点,我们称之为地形的宽和高。

显然,如果你想基于这些数据创建一个地形,这个地形将从这些width*height顶点中绘制,如图5-14右上角所示(注意数字是从0开始的)。



图5-14 地形网格

要使用三角形完全覆盖网格,你需要在每个网格的四个点中绘制两个三角形,如图5-14所示。一行需要(width-1)*2个三角形,整个地形需要(height-1)*(width-1)*2个三角形。

如果你想判断是否需要使用索引,可参见教程5-3的规则。本教程的情况中,(三角形数量) 除以(顶点数)小于1,所以应该使用索引。所有不在边界上的顶点会被不少于六个的三角形共享。

此外,因为所有三角形至少共享一条边,你应该使用TriangleStrip而不是TriangleList。


工作原理


定义顶点

首先定义顶点。下面的方法首先访问heightData变量,这个变量是一个包含地形所有顶点高度的2维数组。如果你还没有这样一个数组,本教程最后的LoadHeightData方法会基于一张2D图像创建一个。
private VertexPositionNormalTexture[] CreateTerrainVertices()
{
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);
VertexPositionNormalTexture[] terrainVertices = new VertexPositionNormalTexture[width * height];

int i = 0;
for (int z = 0; z < height; z++)
{
for (int x = 0; x < width; x++)
{
Vector3 position = new Vector3(x, heightData[x, z], -z);
Vector3 normal = new Vector3(0, 0, 1);
Vector2 texCoord = new Vector2((float)x / 30.0f, (float)z / 30.0f);
terrainVertices[i++] = new VertexPositionNormalTexture(position, normal, texCoord);
}
}

return terrainVertices;
}


首先基于heightData数组的大小获取地形的高和宽。然后创建一个数组保存所有顶点。如前所述,地形需要width*height个顶点。

接着在两个循环中国创建所有顶点。里面的一个循环创建一行上的顶点,当一行完成后,第一个for循环切换到下一行,直到定义完所有行的顶点。

你使用X和Z坐标作为循环的计数器,z值是负的,因此地形是建立在向前(-Z)方向的。而高度信息取自heightData数组。

现在你给与所有顶点一个默认的法线方向,这个方向马上就会使用前一个教程的方法替换成正确的方向。因为你可能要在地形上加上纹理,所以需要指定正确的纹理坐标。根据纹理,你想控制它在地形上的大小。这个例子中将除以30,表示纹理每经过30个顶点重复一次。如果你想增大纹理匹配更大的地形,可以除以一个更大的数值。

有了这些数据,就做好了创建这些新顶点并存储到数组中的准备。


定义索引

定义了顶点后,你就做好了通过定义索引数组构建三角形的准备(见教程5-3)。你将以TriangleStrip定义三角形,基于一个索引及它前两个索引表示一个三角形。

图5-16显示了如何使用TriangleStrip绘制三角形。数组中的第一个索引指向顶点0和W。然后对行中的每个顶点,添加这个顶点和下一行对应的顶点,直到到达行一行的最后。。这时,你要定义2*width个索引,对应(2*width-2)个三角形,足够覆盖整个行。

但是你只定义了第一行,你没法使用这个方法绘制第二行,这是因为你是基于前三个索引定义的三角形添加的每个索引的。基于这点,你定义的最后一个索引指向顶点(2*W-1)。如果你再次从第二行开始,会从添加一个到顶点W的索引开始,如图5-15左图所示。但是,这回定义一个基于顶点W, (2*W-1)和(W-1)的三角形!这个三角形会横跨第一行的整个长度,这不是你想要的结果。



图5-15 使用TriangleStrip定义三角形的错误方式

你可以通过从右边开始定义第二行解决这个问题。但是,简单地从最后一个索引开始不是一个好主意,因为两行的三角形的长边有不同的方向,如教程5-9中的解释,你想让三角形有相同的朝向。

图5-16显示了如何解决这个问题。在指向顶点(2*W-1)的索引后,你将立即添加一个指向相同顶点的索引!这会添加一个基于顶点(W-1)和两个顶点(2*W-1)的三角形,只会形成一条位于顶点(W-1)和(2*W-1)之间的一条线,所以这个三角形不可见,叫做ghost三角形。接下来,添加一个指向顶点(3*W-1)的索引,因为这个三角形基于两个指向相同顶点(2*W-1)的索引,所以实际上是一条线。如果你从右边开始定义第二行,正常情况下你会从两个顶点开始,记住实际上你绘制了两个看不见的三角形。



图5-16 使用TriangleStrip定义三角形的正确方式

注意:你可能认为无需添加第二个指向(2*W-1)的索引,可以立即将一个索引添加到(3*W-1)中。但是,基于两个理由需要额外的指向顶点(2*W-1)的索引。首先,如果你没有添加这个索引,那么只有一个三角形被添加,你会被TriangleStrip方式所需的绕行方向的反转所干扰。第二,这会添加一个基于(3*W-1),
(2*W-1)和 (W-1)的三角形,如果三个顶点高度不相同那么这个三角形会被显示。

下面是生成索引的方法:
private int[] CreateTerrainIndices()
{
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);

int[] terrainIndices = new int[(width)*2*(height-1)];
int i = 0;
int z = 0;

while (z < height-1)
{
for (int x = 0; x < width; x++)
{
terrainIndices[i++] = x + z * width;
terrainIndices[i++] = x + (z + 1) * width;
}
z++;

if (z < height-1)
{
for (int x = width - 1; x >= 0; x--)
{
terrainIndices[i++] = x + (z + 1) * width;
terrainIndices[i++] = x + z * width;
}
}
z++;
}

return terrainIndices;
}


首先创建一个数组,存储地形所需的所有索引。如教程5-16所示,每行需要定义width*2个三角形。在本例中,你有三行顶点,但只绘制两行三角形,所以需要width*2*(height-1) 索引。

前面代码中的z值表示当前行。你从左向右创建第一行,然后,增加z,表示切换到下一行。第二行从右向左创建,如图5-16所示,z值仍然增加。这个程序放在while循环中,直到所有偶数行从左向右建立,奇数行从右向左建立。

当z变为height-1时while循环结束,返回结果数组。


法线,顶点缓冲和索引缓冲

你要创建法线数据,通过创建一个顶点缓冲和一个索引缓冲将这些数据发送到显卡,然后绘制三角形。

在LoadContents方法中添加以下代码:
myVertexDeclaration=new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements);
VertexPositionNormalTexture[] terrainVertices = CreateTerrainVertices();

int[] terrainIndices = CreateTerrainIndices();
terrainVertices = GenerateNormalsForTriangleStrip(terrainVertices, terrainIndices);
CreateBuffers(terrainVertices, terrainIndices);


第一行代码用来告知显卡每个顶点包含位置,法线和纹理坐标的数据。我已近讨论过下面两个方法:它们生成所有顶点和索引。GenerateNormalsForTriangleStrip方法在教程5-7,它将法线数据添加到顶点中使地形光照正确。最后的方法将数据发送到显卡:
private void CreateBuffers(VertexPositionNormalTexture[] vertices, int[] indices)
{
terrainVertexBuffer = new VertexBuffer(device, VertexPositionNormalTexture.SizeInBytes * vertices.Length,BufferUsage.WriteOnly);
terrainVertexBuffer.SetData(vertices);

terrainIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly);
terrainIndexBuffer.SetData(indices);
}


你可以在教程5-3中找到所有方法和使用参数的解释。

把数据发送到显卡后,现在可以绘制地形了。代码的第一部分设置BasicEffect (包含光照,见教程6-1)的变量,所以在Draw方法中添加以下代码:
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);

basicEffect.World = Matrix.Identity;
basicEffect.View = fpsCam.ViewMatrix;
basicEffect.Projection = fpsCam.ProjectionMatrix;
basicEffect.Texture = grassTexture;
basicEffect.TextureEnabled = true;

basicEffect.EnableDefaultLighting();
basicEffect.DirectionalLight0.Direction =new Vector3(1, -1, 1);
basicEffect.DirectionalLight0.Enabled = true;
basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
basicEffect.DirectionalLight1.Enabled = false;
basicEffect.DirectionalLight2.Enabled = false;
basicEffect.SpecularColor = new Vector3(0, 0, 0);


要将一个3D场景绘制到2D屏幕上,需要设置World,View和Projection矩阵(见教程2-1和4-2)。然后指定纹理。第二个代码块设置一个定向光(如教程6-1所示)。关闭镜面高光(见教程6-4),这是因为草地地形没有闪亮的材质。

设置了effect后就可以绘制三角形了。这个代码从一个索引TriangleStrip绘制三角形,解释请见教程5-3:
basicEffect.Begin();
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
{
pass.Begin();
device.Vertices[0].SetSource(terrainVertexBuffer, 0,VertexPositionNormalTexture.SizeInBytes);
device.Indices = terrainIndexBuffer;
device.VertexDeclaration = myVertexDeclaration;
device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, width * height, 0, width * 2 * (height - 1) - 2);
pass.End();
}
basicEffect.End();


首先将VertexBuffer和IndexBuffer作为显卡的当前缓冲。VertexDeclaration表明GPU需要何种数据,在数据流中的哪儿获取必要信息。DrawIndexedPrimitives绘制TriangleStrip,这需要处理所有width*height个顶点,绘制总共width*2*(height-1)-2个三角形。要获取最后一个值,需要查询索引数组中的索引总数。因为你是从一个TriangleStrip进行绘制的,所以顶点的总数为这个值减2。


代码

LoadContent方法中的最后四行代码生成所有索引和对应的顶点。法线数据被添加到顶点,最终的数据存储在顶点缓冲和索引缓冲中。注意LoadHeightMap方法会在后面讨论:
protected override void LoadContent()
{
device = graphics.GraphicsDevice;

basicEffect = new BasicEffect(device, null);
cCross = new CoordCross(device);

Texture2D heightMap = Content.Load<Texture2D>("heightmap");
heightData = LoadHeightData(heightMap);

grassTexture = Content.Load<Texture2D>("grass");
myVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements);
VertexPositionNormalTexture[] terrainVertices = CreateTerrainVertices();

int[] terrainIndices = CreateTerrainIndices();
terrainVertices = GenerateNormalsForTriangleStrip(terrainVertices, terrainIndices);
CreateBuffers(terrainVertices, terrainIndices);
}


在下列方法中创建顶点:
private VertexPositionNormalTexture[] CreateTerrainVertices()
{
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);

VertexPositionNormalTexture[] terrainVertices = new VertexPositionNormalTexture[width * height];

int i = 0;
for (int z = 0; z < height; z++)
{
for (int x = 0; x < width; x++)
{
Vector3 position = new Vector3(x, heightData[x, z], -z);
Vector3 normal = new Vector3(0, 0, 1);
Vector2 texCoord = new Vector2((float)x / 30.0f, (float)z / 30.0f);
terrainVertices[i++] = new VertexPositionNormalTexture(position, normal, texCoord);
}
}

return terrainVertices;
}


在下列方法中创建索引:
private int[] CreateTerrainIndices()
{
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);

int[] terrainIndices = new int[(width) * 2 * (height - 1)];

int i = 0;
int z = 0;
while (z < height - 1)
{
for (int x = 0; x < width; x++)
{
terrainIndices[i++] = x + z * width;
terrainIndices[i++] = x + (z + 1) * width;
}

if (z < height - 1)
{
for (int x = width - 1; x >= 0; x--)
{
terrainIndices[i++] = x + (z + 1) * width;
terrainIndices[i++] = x + z * width;
}
}
z++;
}
return terrainIndices;
}


GenerateNormalsForTriangleStrip方法将法线数据添加到顶点中,而CreateBuffers方法 将数据储存到显卡中:
private void CreateBuffers(VertexPositionNormalTexture[] vertices, int[]indices)
{
terrainVertexBuffer = new VertexBuffer(device, VertexPositionNormalTexture.SizeInBytes * vertices.Length, BufferUsage.WriteOnly);
terrainVertexBuffer.SetData(vertices);

terrainIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly);
terrainIndexBuffer.SetData(indices);
}


最后,在Draw方法中地形以TriangleStrip方式绘制:
protected override void Draw(GameTime gameTime)
{
device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0);
cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix);

//draw terrain
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);

basicEffect.World = Matrix.Identity;
basicEffect.View = fpsCam.ViewMatrix;
basicEffect.Projection = fpsCam.ProjectionMatrix;
basicEffect.Texture = grassTexture;
basicEffect.TextureEnabled = true;

basicEffect.EnableDefaultLighting();
basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 1);
basicEffect.DirectionalLight0.Enabled = true;
basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
basicEffect.DirectionalLight1.Enabled = false;
basicEffect.DirectionalLight2.Enabled = false;
basicEffect.SpecularColor = new Vector3(0, 0, 0);

basicEffect.Begin();
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
{
pass.Begin();
device.Vertices[0].SetSource(terrainVertexBuffer,0, VertexPositionNormalTexture.SizeInBytes);
device.Indices = terrainIndexBuffer;
device.VertexDeclaration = myVertexDeclaration;
device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, width * height,0, width * 2 * (height - 1) - 2);
pass.End();
}
basicEffect.End();

base.Draw(gameTime);
}


从一张图像读取heightData数组

大多数情况中,你并不想手动地指定heightData数组,而是从一张图像加载。这个方法加载一张图像,将每个像素的颜色映射为高度值:
private void LoadHeightData(Texture2D heightMap)
{
float minimumHeight = 255;
float maximumHeight = 0;

int width = heightMap.Width;
int height = heightMap.Height;
Color[] heightMapColors = new Color[width * height];
heightMap.GetData<Color>(heightMapColors);
heightData = new float[width, height];

for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
heightData[x, y] = heightMapColors[x + y * width].R;
if (heightData[x, y] < minimumHeight)
minimumHeight = heightData[x, y];
if (heightData[x, y] > maximumHeight)
maximumHeight = heightData[x, y];
}

for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * 30.0f;
}


第一部分将每个像素的红色通道的强度存储在heightData数组中。后面的代码重新缩放每个值,这样可以让数组中的值介于0到30的范围中。



原文链接http://shiba.hpe.sh.cn/jiaoyanzu/wuli/showArticle.aspx?articleId=471&classId=4
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: