PS:自己翻译的,转载请著明出处
2-10.使用一个四叉树隐藏部分网格是看不到的
问题
地形绘制是创建一个游戏一个最基本的部分。然而,这个方法在5-8也有描述,你建立一个巨大的地形可能不会注意到祯速率下降。
解决方案
用四叉树您可以减轻大地形渲染工作量。这是一个类似八叉树,因为你将你的地形划分成较小的方格,直到所有这些方格不超过指定的大小。
就象在图表2-13中左边看到的进程。A16*16方格被分裂成4个方格,它们然后又被分成4个方格。
这个方式的好处是等你准备绘制地形时,只需要绘制在摄象机视阈中的方格就可以了。你要做到这一点,就靠方格与摄象机的可视范围碰撞测出。正如图表2-13右半部所示,这些方格在应该灰色被绘制出来。
四叉树是一个简化版本的八叉树。四叉树需要划分为四个只规模较小的方格,而八叉树节点需要分为八个子立方体。一个八叉树也需要跟踪在它里面所有对象的位置。但是四叉数不需要做这些。
如何工作的
建立一个新类,它是一个方格,四叉树的一个节点。
2 {
3 class OTNode
4 {
5 private BoundingBox nodeBoundingBox;
6 private bool isEndNode;
7 private QTNode nodeUL;
8 private QTNode nodeUR;
9 private QTNode nodeLL;
10 private QTNode nodeLR;
11 private int width;
12 private int height;
13 private GraphicsDevice device;
14 private BasicEffect basicEffect;
15 private VertexBuffer nodeVertexBuffer;
16 private IndexBuffer nodeIndexBuffer;
17 private Texture2D grassTexture;
18 public static int NodesRendered;
19 }
20 }
让我们来了解需要被级住的每一个节点的变量。第一个,boundingbox它十分重要。它是最小的合子,所有当前节点的顶点都在这个盒子里。每一祯,你将要检查的当前盒子是否在摄象机视阈范围内。如果在里面,Draw调用当前节点的子节点应该向前推进。
当创建一个四叉树,你应该指定的最大规模的节点。isEndNode变量会存储当前的节点是否比最大值大的信息。如果大于最大值这个节点将产生四个子节点,它们被存储在nodeUL到nodeLR里,UL代表UpperLeft,而LR代表LowerRight.
如果这个节点没有子节点,当isEndNode为真时节点实际上已经得出一些三角形。你只需知道它的宽和高。还需要结合图形设备上的正在工作的BasicEffect,VertexBuffer和IndexBuffer与一个纹理一起来绘制这个三角形到屏幕上(5-8节)
一个节点是靠调用它的结果方法来创建的,如下:
2 {
3 this.device=device;
4 this.grassTexture=grassTexture;
5 basicEffect=new BasiceEffect(device,null);
6 width=VertexArray.GetLength(0);
7 width=VertexArray.GetLength(1);
8 nodeBoundingBox=CreateBoundingBox(vertexArray);
9 isEndNode=Width<=maxSize;
10 isEndNode&=height<=maxSize;
11 if(isEndNode)
12 {
13 VertexPositionNormalTexture[] vertices=Reshape2Dto1D<VertextPositionNormalTexture>(vertexArray);
14 int[] indices=TerrainUtils.CreateTerrainIndices(width,height);
15 TerrainUtils.CreateBuffers(vertices,indices,out nodeVertexBuffer,out nodeIndexBuffer,device);
16 }
17 else
18 {
19 createChildNodes(vertexArray,maxSize);
20 }
21 }
一个节点需要一个二维数组包含其所有的顶点,同时链接到该设备和纹理,以及之后的大小节点应停止分裂成子节点。
被连接的设备和文理很快的储存到节点的里面。当前节点的宽和高来自包涵了顶点的2D数组。鉴于这个节点的所有顶点你用CreateBoundingBox方法来计算boundingbox,你会用一分钟来定义它。
最后,你检查当前节点的宽和高是否比最大尺寸小。如果小,你从当前的节点的顶点里建立了一个可用的VertexBuffer和IndexBuffer如果这个节点比当前的最大尺寸大,把当前的节点分成4个子节点。
创建BooundingBox
鉴于二维数组包含顶点,您可以轻松地存储他们所有的位置到一个表中。BoundingBox类的CreateFromPoints方法能从位置表中生成一个BoundingBox.
2 {
3 List<Vector3> pointList=new List<Vector3>();
4 foreach(VertextPositionNormalTexture vertex in vertexArray)
5 pointList.Add(vertex.Position);
6 BoundingBox nodeBoundingBox=BoundingBox.CreateFromPoints(pointList);
7 return nodeBoundingBox;
8 }
生成VertexBuff和IndexBuffer
使用CreateVertices和CreateIndices方法详见5-8节,它们创建了一个VertexBuffer和IndexBuffer传到1D顶点数组。意思就是说你首先将会把2D数组改造成1D数组,下面的方法可以作到:
2 {
3 int width=array2D.GetLength(0);
4 int height=array2D.GetLength(1);
5 T[] array1D=new T[width*height];
6 int i=0;
7 for(int z=0;z<height;z++)
8 for(int x=0;x<width;x++)
9 array1D[i++]=array2D[x,z];
10 return array1D;
11 }
这种通用的方法接受一个二维数组的类型,发现它的大小,并复制其内容到一维数组。
注意:一般功能,介绍了在NET 2.0版中。当调用一个通用方法,您在方括号内指定哪种应取代T ,就像这样:
VertexPositionNormalTexture[] vertices=Reshape2Dto1D<VertexPositionNormalTexture>(vertexArray);
分一个节点成4个节点
如果这个节点太大,就应该分成4个子节点。这里的挑战是4个子节点不会是相等的大小。在图2-13的左边,可以看到16*16的网格不能被分成4个8*8的网格。如果每个子节点保存了8*8个顶点,不可能使三角形之间的第八和第九列或行,留下差距。在图2-13中,这个问题被这一问题已经得到解决,使左上角节点行和列大于其他的。
不对称的方格,这种情况是不存在的:9*9的方格被分成4个5*5顶点的方格,如上面左边的图2-13所示。它的问题仍然是,你如何计算顶点的数量存储在每个方格?如此列,你是如何知道16能被9和8分,而且9又可以分成5和5呢?第一个值是可以的,如,找到除以以2为母公分,取最小的整数(这是做免费除以整数!),结果加1。另外的一个值可以靠公分母大小为2得到,取最小的整数,在用被除数减去这个整数。
如,16除2得8,然后加1,你会得到9作为第一个值。接下来,16除2得8,用16减去8得8。
这里是第2的例子:9除2得4.5,取整得4.加1后你得到5作为第一个值。接下来,9除2得4.5,取整为4;9减去4得5。
一旦你知道子方格的大小被创建,你可以用下面的代码去创建它们。4方格中的每一个,你首先复制顶点,需要创建一个QTNode对象:
2 {
3 VertexPositionNormalTextture[,] ulArray=new VertexPositionNormalTexture[width/2+1,height/2+1];
4 for(int w=0;w<width/2+1;w++)
5 for(int h=0;h<height/2+1;h++)
6 ulArray[w,h]=vertexArray[w,h];
7 nodeUL=new OTNode(ulArray,device,grassTexture,maxSize);
8 VertexPositionNormalTexture[,] urArray=new VertexPositionNormalTexture[width-(width/2),height/2+1];
9 for(int w=0;w<width-(width/2);w++)
10 for(int h=0;h<height/2+1;h++)
11 urArray[w,h]=vertexArray[width/2+w,h];
12 VertexPositionNormalTexture[,] llArray=new VertexPositionNormalTexture[width/2+1,height-(height<2)];
13 for(int w=0;w<width/2+1;w++)
14 for(int h=0;h<height-(height/2);h++)
15 llArray[w,h]=vertexArray[w,height/2+h];
16 nodeLL=new QTNode(llArray,device,grassTexture,maxSize);
17 VertexPositionNormalTexture[,] lrArray=new VertexPositionNormalTexture[width-(width/2),height-(height<2)];
18 for(int w=0;w<width-(width/2);w++)
19 for(int h=0;h<height-(height/2);h++)
20 lrArray[w,h]=vertexArray[width/2+w,height/2+h];
21 nodeLR=new QTNode(lrArray,device,grassTexture,maxSize);
22 }
绘制四叉树
您准备好,使您的四叉树所有的创建和分割功能被实现。你需要确定在摄象机视阈里呈现的是方格。在主程序中,你想调用你的四叉树的根节点唯一的Drwa方法去呈现所有在摄象机视阈内的所有节点。
根节点应该被检查是否在摄象机视阈内。如果不是,你什么都不要做。如果是,4个子节点中的每个都会传递Draw方法的调用。
每个子节点做的同样的事:检测自己是否在摄象机视阈内,如果是,传递调用到自己的子节点一直到它们收到为止。如果都在摄象机视阈内,为它们的顶点绘制网格:
2 {
3 BoundingBox transformedBBox=XNAUtils.TransformBoundingBox(nodeBoundingBox,worldMatrix);
4 ContainmentType cameraNodeContainment=cameraFrustum.Contains(transformedBBox);
5 if(cameraNodeContainment!=ContainmentType.Disjoint)
6 {
7 if(isEndNode)
8 {
9 DrawCUrrentNode(worldMatrix.viewMatrix,projectionMatrix);
10 }
11 else
12 {
13 nodeUL.Draw(worldMatrix,viewMatrix,projectionMatrix,cameraFrustum);
14 nodeUR.Draw(worldMatrix,viewMatrix,projectionMatrix,cameraFrustum);
15 nodeLL.Draw(worldMatrix,viewMatrix,projectionMatrix,cameraFrustum);
16 nodeLR.Draw(worldMatrix,viewMatrix,projectionMatrix,cameraFrustum);
17 }
18 }
19 }
注意这个方法希望摄象机的截面被调用这个方法传递,因此,它被用来检测当前的方格的boundingbox是否与其相撞,这说明这些方格是否在摄象机的视阈内。
如果这一调用到达结束节点是相机的视阈内,DrawCurrentNode方法被调用,这个方法用来绘制指定的方格。代码参看5-8节
2 {
3 basicEffect.World=worldMatrix;
4 basicEffect.View=viewMatrix;
5 basicEffect.Projection=projectionMatrix;
6 basicEffect.Texture=grassTexture;
7 basicEffect.VertexColorEnabled=false;
8 basicEffect.TextureEnabled=true;
9 basicEffect.EnableDefaultLighting();
10 basicEffect.DirectionalLightO.Direction=new Vector3(1,-1,1);
11 basicEffect.DirectionalLight0.Enable=true;
12 basicEffect.AmbientLightColor=new Vector3(0.3f,0.3f,0.3f);
13 basicEffect.DirectionalLight1.Enabled=false;
14 basicEffect.DirectionalLight2.Enabled=false;
15 basicEffect.SpecularColor=new Vector3(0,0,0);
16 basicEffect.Begin();
17 foreach(EffectPass pass in basicEffect.CurrentTechnique.Passes)
18 {
19 pass.Begin();
20 device.Vertices[0].SetSource(nodeVertexBuffer,0,VertexPositionNormalTexture.SizeInBytes);
21 device.Indices=nodeIndexBuffer;
22 device.VertexDeclaration=new VertexDeclaration(device,VertexPositionNormalTexture.VertexElements);
23 device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip,0,0,width*height,0,(width*2*(height-1)-2));
24 pass.End();
25 }
26 basicEffect.End();
27 NodesRendered++;
28 }
每次此代码执行,NodeRendered变量是递增的。因为它是一个静态变量,它是所有节点之间共享的四叉树,以便在绘制进程最后, 它将包含实际绘制的节点的总数。
您可以取消最后一行,这将绘制BoundingBox的轮廓,使您可以验证是否是您的四叉树正常工作。
初始化四叉树
随着你的QTNode类完成,你差不多创建好了你的四插树。更多的代码见5-8节,从一个包涵大量数据的2D材质开始。然后你创建了VertexPositionNormal材质元素的1D树组。如果你想使用GenerateNormalsFromTriangleStrip方法,详细解释在5-7节,添加正确的值给他们,你首先需要创建名单indices.add此代码到您的方法
2 Texture2D heightMap=Content.Load<Texture2D>("heightmap");
3 int width=heightMap.Width;
4 int height=heightMap.Height;
5 float[,] heightData=TerrainUtils.LoadHeightData(heightMap);
6 VertexPositionNormalTexture[] vertices=TerrainUtils.CreateTerrainVertices(heightData);
7 int[] indices=TerrainUtils.CreateTerrianIndices(width,height);
8 vertices=TerrainUtils.GenerateNormalsForTraingleStrip(vertices,indices);
9 VertexPositionNormalTexture[,] vertexArray=Reshape1Dto2D<VertexPositionNormalTexture>(vertices,width,height);
10 rootNode=new QTNode(vertexArray,device,grassTexture,64); 最后,你结束了一个一维数组的顶点。QTNode的构造,尽管需要2D的树组,所以最后一行调用Reshape1Dto2D方法: 1 private T[,] Reshape1Dto1D<T>(T[] vertices,int width,int height)
2 {
3 T[,] vertexArray=new T[width,height];
4 int i=0;
5 for(int h=0;h<height;h++)
6 for(int w=0;w<width;w++)
7 vertexArray[w,h]=vertices[i++];
8 return vertexArray;
9 } 再次,这是一个通用的方法,所以它可以让您把任何一个一维数组转换成二维数组。
与此二维阵列提供的顶点参数,这最后一行添加到您的LoadContent方法: 1 rootNode=new QTNode(vertexArray,device,grassTexture,64); 这单行生成整个四叉树。您通过在二维数组的顶点和最大尺寸为64 。只要方格的大小大于64 ,他们将继续被分裂子网方格。
使用你的四叉树
在你的XNA工程里,把下面代码放入到Draw方法中: 1 QTNode.NodesRendered=0;
2 BoundingFrustum cameraFrustum=new BoundingFrustum(fpsCam.ViewMatrix*fpsCam.ProjectionMartix);
3 rootNode.Draw(Matrix.CreateTranslation(-250,-20,250),fpsCam.ViewMatrix,fpsCam,ProjectionMatrix,cameraFrustrum);
4 Window.Title=string.Format("{0}nodes rendered",QTNode.NodesRendered);
第一和最后一行调试之用,因为它们将导致方格总数实际上正在窗口的标题栏打印出来。
第二行创建相机的截面,需要你四叉树的每一个节点检测它们是否在摄象机视阈中。第三行一开始就调用Draw方法,这将检索通过所有节点的四叉树,使只看见终端节点。
代码
1 //在前面的章节里你可以找到所有的QTNode类的代码。在XNA的主要工程里,这里是你的LoadContent方法里的代码:2 protected override void LoadContent()
3 {
4 device=graphics.GraphicsDevice;
5 cCross=new CoordCross(device);
6 Texture2D grassTexture=content.Load<Texture2D>("grass"):
7 Texture2D heightMap=content.Load<Texture2D>("heightmap");
8 int width=heightMap.Width;
9 int height=heightMap.Height;
10 float[,] heightData=TerrainUtils.LoadHeightData(heightMap);
11 VertexPositionNormalTexture[] vertices=TerrainUtils.CreateTerrainVertices(heightData);
12 int[] indices=TerrainUtils.CreateTerrainIndices(width,height);
13 vertices=TerrainUtils.GenerateNormalsForTriangleStrip(vertices,indices);
14 VertexPositionNormalTexture[,] vertexArray=Reshape1Dto2D<VertexPositionNormalTexture>(vertices,width,height);
15 rootNode=new QTNode(vertextArray,device,grassTexture,64);
16 }//这里是绘制你地行上所有可以看见的四叉树
17 protected override void Draw(GameTime gameTime)
18 {
19 graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
20 cCross.Draw(fpsCam.ViewMatrix,fpsCam.ProjectionMatrix);
21 QTNode.NodesRendered=0;
22 BoundingFrustum cameraFrustrum=new BoundingFrustum(fpsCam.ViewMatrix*fpsCam.ProjcetionMatrix);
23 rootNode.Draw(Matrix.CreateTranslation(-250,-20,250),fpsCam.ViewMatrix,fpsCam.ProjectionMatrix,cameraFrustrum);
24 Window.Title=string.Format("{0}nodes rendered",QTNode.NodesRendered);
25 base.Draw(gameTime);
26 }
课外阅读
这节介绍你的四叉树的基本知识。四树这里有一些 缺点:
1。加载时间是十分重要的
2。DrawPrimitives调用会变的很大,程序性能慢慢下降
3。当摄象机的离开地形一些方格还是会被重新绘制
这些问题都可以一起解决。我在这里不能详细的把它们列出,是因为他们可以很容易地填满整本书的地形绘制。尽管,我会分析造成这3个问题的主要原因,但是你可以在你的解决方案里任意使用。
装卸时间所造成的复制顶点在较小的阵列,每个发生时四叉树分为4个规模较小的方格。重塑也造成巨大的开销。这个问题通常是一个可以解决这样的四代的自定义内容处理器,以便在运行时只有顶点和索引缓冲区需要流从二进制文件引入。见5-13节序列的地形。
第二个问题造成了问题,因为显卡要执行长而不能被阻断。这是越远越好,使一百万三角形一气呵成超过1000个三角形1000次。可以解决这个问题靠调用一定数量的DrawPrimitives方法。有一个方法可以检测父节点的4个子节点是否可见。如果所有4个子节点都是可见的,用父节点代替4个子节点绘制。这将提供相同的三角形,使用一个DrawPrimitives调用而不是四个。
最后一个问题的原因,大量的三角形被绘制,因为方格的非常远离相机会提供相同数量的三角形作为方格接近摄像头。您可以解决这个问题,使遥远的方格使用较低的细节是被称为一级的详细level-of=detail(LOD) 。这可能是相当困难。
另外,完全不同的方式处理大地形,读下一节重要的是要记住,你可以混合技术从章节,因为几乎所有的地形引擎使用四叉树,一个漫游引擎,或结合兼而有之。你可以, 例如,将您的地形补丁,这些补丁控制使用四叉树,但随后使其使用漫游算法中描述的在下一章节。这种结合了控制一个四叉树有力的漫游.
来源:https://www.cnblogs.com/315358525/archive/2009/07/18/1526213.html