• 处理顶点——基于一个顶点缓冲和一个索引缓冲创建一个地形


    问题

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

    解决方案

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

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

    image

    图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)的三角形!这个三角形会横跨第一行的整个长度,这不是你想要的结果。

    image

    图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)的索引,所以实际上是一条线。如果你从右边开始定义第二行,正常情况下你会从两个顶点开始,记住实际上你绘制了两个看不见的三角形。

    image

    图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的范围中。

    image

  • 相关阅读:
    match、match_phrase、term示例
    AVL树及java实现
    eclipse集成lombok注解不起作用
    红黑树原理详解
    为什么HashMap中链表长度超过8会转换成红黑树
    用deepin堆砌工作环境
    为什么黑客都不用鼠标?你听说过Linux吗?
    为什么二流程序员都喜欢黑php?
    看Linux 之父是如何定义 Linux?
    Linux 系统故障排查和修复技巧
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120113.html
Copyright © 2020-2023  润新知