WebGL中gl.drawArrays()与gl.drawElements()对比

来源:互联网 发布:蓝光原盘转换mp4软件 编辑:程序博客网 时间:2024/04/28 05:04

本次我们来绘制一个这样的网格(5*10),我们采用程序实现。

为什么绘制网格?!因为网格可以绘制很多不同的对象,比如球、圆环……,对于我们地理信息科学专业来说,地球就是我们关注的重点……


通过绘制这个图形,我们来学习一下:

  1. drawArrays()和gl.TRIANGLES
  2. drawArrays()和gl.TRIANGLE_STRIP
  3. drawElements()和gl.TRIANGLES
  4. drawElements()和gl.TRIANGLE_STRIP

以上组合的不同!!


首先,我们需要了解绘制的三角形数量与顶点数量或者索引数量之间的关系


1.独立的三角形


gl.TRIANGLES是最基本的三角形图元。

如上图所示,用顶点(V0,V1,V2)定义一个三角形,用顶点(V3,V4,V5)定义另一个三角形,以下公式决定所绘制三角形的个数:




                                                  

2.三角形带

使用三角形带图元相比于使用独立的三角形图元的优点是可重用三角形之间的顶点。因此在构建几何模型的三角形时,可以定义较少的顶点。较少的的顶点意味着需要处理的数据量较少,这也意味着从内存传送到GPU的数据量也较少。


由上我们得到这样的公式:


3.三角扇

绘制几个相互连接的三角形的另一个方法是用gl.TRIANGLE_FAN绘制三角扇。


由上我们得到这样的公式:

通过分析上面的三角形、三角形带、三角形扇,我们得出了三角形数量与顶点/索引数之间的关系。其中图元概念我们需要单独说明一下,还有就是细心的朋友可以发现,我们的三角形绘制顺序都是逆时针


图元

全称为图形输出原语(graphicsoutput primitive),用来描述各种图形元素的函数。

“图元”是为了区分不同数据信息而对某一类数据所取的名字。图元指的是图形数据,所对应的就是绘图界面上看得见的实体。英文中,图元的名字为“entity”,翻译为中文有“图素”、“图元”、“实体”等称呼。与图元对应的是图面看不见的信息,如图层,文字样式,标注样式等等,这些信息被称为“对象”,英文名字为“object”。

顶点绘制顺序

在WebGL中,三角形的一个重要属性是顶点绘制顺序。三角形的顶点绘制顺序是逆时针(CounterClockWise,CCW)或者顺时针(ClockWise,CW)。当三角形按顶点的逆时针顺序构建时,我们称它的绘制顺序为逆时针,当三角形按顶点的顺时针顺序构建时,我们称它的组绕顺序为顺时针。


绘制顺序之所以重要,是因为它决定了三角形的面是否朝向观察者。朝向观察者的三角形为正面三角形,否则为背面三角形。在许多情形中,WebGL不需要对背面三角形进行光栅化处理。例如,如果在某个场景中观察者看不见一些对象的背面,则可以指示WebGL剔除这些无法看到的面。很容易调用下面的3个方法来实现这一点:

gl.frontFace(gl.CCW);

gl.enable(gl.CULL_FACE);// CULL意为“剔除”

gl.cullFace(gl.BACK);

第一个方法告诉WebGL采用逆时针顺序绘制的三角形是正面三角形。即使不调用gl.frontFace()方法,WebGL默认也会采用这种处理方式。第二个方法是激活面剔除功能,默认情况下这个功能处于禁用状态,因此需要调用gl.enable(gl.CULL_FACE)方法启动它。第三个方法告诉WebGL剔除背面三角形。即使没有调用gl.cullFace()方法,背面剔除也是WebGL的默认处理方式。


如果一个场景由许多对象组成,它们的背面对用户是不可见的,则最好激活背面剔除功能。这可以改善WebGL应用程序的性能,因为GPU不需要对不可见的三角形进行光栅化处理。

接着,我们再了解一个叫做退化三角形的东西,然后我们就可以开始步入主题了。


退化三角形

         从性能的角度来看,gl.drawArrays()或gl.drawElements()函数的调用次数越少越好。例如,如果一个顶点数组包含200个三角形,则一次调用gl.drawArrays()或gl.drawElements()比100次调用绘图函数且每次绘制两个三角形的效率要高许多。

         如果使用独立三角形图元(即gl.TRIANGLES),这很容易实现。但是,如果使用gl.TRIANGLE_STRIP图元,则当三角形带之间存在不连续性时,就不那么容易组合不同的三角形带。例如我们本次需要绘制的图形。

         这种不连续性或在两个三角形带间存在跳转的解决方法是插入额外的索引(如果使用gl.drawArrays()函数,则插入额外的顶点),这样就得到退化三角形。退化三角形是指三角形至少有两个索引(或顶点)是相同的。因此,存在面积为0的三角形。这样的三角形很容易被GPU检测并删除。

         连接两个三角形带需要增加额外的索引,额外索引的数量取决于第一个三角形带所使用的索引数。这是因为,三角形的绘制顺序起决定作用。假设我们想使第一个和第二个三角形带都使用相同的绘制顺序,则需要考虑两种情况:

1. 如果第一个三角形带包含偶数个三角形,则为了连接第二个三角形带,为了保证绘制顺序不变需要增加2个额外的索引。

2. 如果第一个三角形带包含奇数个三角形,则为了连接第二个三角形带,为了保证绘制顺序不变需要增加3个额外的索引。

在此,我们只讨论第一种情况。第二种情况分析跟第一种情况类似。


         上图第一个三角形带由(v0,v1,v2)和(v2,v1,v3)这两个三角形组成,它们对应于元素数组缓冲中的元素索引(0,1,2,3)。第二个三角形带由(v4,v5,v6)和(v6,v5,v7)这两个三角形组成,它们对应于元素数组缓冲中的元素索引(4,5,6,7)。

         为了连接这两个三角形带并建立所需要的退化三角形,需要在这两个三角形带之间添加两个额外的索引。这两个索引就是上图中的索引3和4。这里使用的规则是第一个三角形带的最后一个顶点和最后一个三角形带的第一个顶点重复。

前面我们说过,索引数=三角形数+2。

这个公式针对的是一个三角形带,但是目前有两个三角形带,所以索引/顶点;由于连接两个三角形带需要2个额外的索引/顶点,因此总计的索引/顶点数为10。

推论:假设目前有m个三角带,每个三角带有n个三角形。那么为了连接所有三角带,需要个索引或者顶点数

以上推论读者可以自行验证。

        

好了,以上就是我们正式绘制网格前所需要的知识,接下来就是正式绘制!


在WebGL中,最常绘制的图元可能是三角形,绘制三角形可以使用gl.drawArrays()或gl.drawElements()方法。通常我们使用gl.TRIANGLES或gl.TRIANGLE_STRIP图元。gl.TRINAGLE_FAN图元比较少见,在绘制通用的三角形网格时很少使用它。

本次我们绘制的网格有5行,10列,由100个三角形组成。


①  gl.drawArrays()和gl.TRIANGLES图元


使用gl.drawArrays()和gl.TRIANGLES是最简单的方法。因为使用了gl.drawArrays(),就不需要使用一个元素数组缓冲。对于gl.TRIANGLES图元,每个三角形需要3个顶点。顶点总数由以下公式决定:

因此,需要定义一个能够存放300个顶点的数组缓冲。如果只考虑顶点位置,并且用Float32Array表示位置,则存储此顶点数据所需要的内存由以下公式决定:

我们按照下面的结构进行组织


这里我们编写核心代码,为了好看,我们贴上纹理。

        var rowCount = 5;        var colCount = 10;        var positions = [];//存储x,y,z坐标        var textureCoordData = [];//存储纹理坐标u,v,纹理坐标与顶点坐标一一对应        for(var i = 0; i < rowCount; i++){            for(var j = 0; j < colCount; j++){                var x = j;                var y = i;                var z = 0;                //第一个点                positions.push(x);                positions.push(y + 1);                positions.push(z);                //第二个点                positions.push(x);                positions.push(y);                positions.push(z);                //第三个点                positions.push(x + 1);                positions.push(y + 1);                positions.push(z);                //第四个点(与第三个点相同)                positions.push(x + 1);                positions.push(y + 1);                positions.push(z);                //第五个点(与第二个点相同)                positions.push(x);                positions.push(y);                positions.push(z);                //第六个点                positions.push(x + 1);                positions.push(y);                positions.push(z);                //第一个点对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push((y + 1) / rowCount);                //第二个点对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push(y / rowCount);                //第三个点对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push((y + 1) / rowCount);                //第四个点对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push((y + 1) / rowCount);                //第五个点对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push(y / rowCount);                //第六个点对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push(y / rowCount);            }        }

②  gl.drawArrays()和gl.TRIANGLE_STRIP图元


使用gl.TRIANGLE_STRIP,与独立三角形相比,这种模式中的每个三角形需要较少的顶点。意味着100个三角形所需要的顶点数由以下公式决定:

但是,这个顶点数是在只有一个三角带的基础上得到的!而我们有5个三角带,每个三角带有20个三角形。因此绘制这个三角形网格需要调用gl.drawArrays()五次。从性能角度来看,gl.drawArrays()方法的调用次数越少越好。解决的方法是使用退化三角形。在1,、2、3、4行的末尾需要跳转到下一行。则绘制这个网格只需要调用一次gl.drawArrays()方法。

由此,100个三角形所需要的顶点数由前面推导公式决定:

可以用下面的公式计算118个顶点所需要的内存大小:

这里我们编写核心代码,同样为了好看,我们贴上纹理。

var rowCount = 5;        var colCount = 10;        var positions = [];//存储x,y,z坐标        var textureCoordData = [];//存储纹理坐标u,v,纹理坐标与顶点坐标一一对应        for(var i = 0; i < rowCount; i++){            var x, y,z;            for(var j = 0; j < colCount; j++){                x = j;                y = i;                z = 0;                //第一个点                positions.push(x);                positions.push(y + 1);                positions.push(z);                //第二个点                positions.push(x);                positions.push(y);                positions.push(z);                //第三个点                positions.push(x + 1);                positions.push(y + 1);                positions.push(z);                //第四个点                positions.push(x + 1);                positions.push(y);                positions.push(z);                //第一个点对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push((y + 1) / rowCount);                //第二个点对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push(y / rowCount);                //第三个点对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push((y + 1) / rowCount);                //第四个点对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push(y / rowCount);            }            if(x != (rowCount - 1)){                //退化三角形额外结点1                positions.push(x + 1);                positions.push(y);                positions.push(z);                //额外结点1对应的纹理节点                textureCoordData.push((x + 1) / colCount);                textureCoordData.push(y / rowCount);                //退化三角形额外结点2                x = x - (rowCount - 1);                y = y + 2;                positions.push(x);                positions.push(y);                positions.push(z);                //额外结点2对应的纹理节点                textureCoordData.push(x / colCount);                textureCoordData.push(y / rowCount);            }        }

③ gl.drawElements()和gl.TRIANGLES图元


如果使用gl.drawElements()方法,则需要定义一个数组缓冲保存顶点数据和一个元素数组缓冲保存索引。对于数组缓冲,由于实际上此网格有66个不同的顶点,因此需要在数组缓冲中保存这66个顶点,所需要的内存大小可以根据下面的公式得到:


对于此示例,还需要为元素数组缓冲中的索引分配内存,由于使用gl.TRIANGLES图元,每个三角形需要3个索引,因此有以下公式:

如果索引保存在类型为Uint16Array的元素数组缓冲中,则每个元素需要2个字节,因此所需要的内存由以下公式决定:

由上可知,总内存为1392字节

这里我们编写核心代码,同样为了好看,我们贴上纹理。

var rowCount = 5;        var colCount = 10;        var positions = [];//存储x,y,z坐标        var textureCoordData = [];//存储纹理坐标u,v,纹理坐标与顶点坐标一一对应        var indices = [];//三角形列表(索引值)        for(var i = 0; i <= rowCount; i++){            for(var j = 0; j <= colCount; j++){                var x = j;                var y = i;                var z = 0;                var u = x / colCount;                var v = y / rowCount;                positions.push(x);                positions.push(y);                positions.push(z);                textureCoordData.push(u);                textureCoordData.push(v);            }        }        for(var i = 0; i < rowCount; i++){            for(var j = 0; j < colCount; j++){                var first = (i + 1) * (colCount + 1) + j;                var second = first - (colCount + 1);                indices.push(first);//第一个点                indices.push(second);//第二个点                indices.push(first + 1);//第三个点                indices.push(first + 1);//第四个点(与第三个点相同)                indices.push(second);//第五个点(与第二个点相同)                indices.push(second + 1);//第六个点            }        }

④  gl.drawElements()和gl.TRIANGLE_STRIP图元


此网格有66个不同的顶点,因此需要在数组缓冲中保存这66个顶点,所需要的内存大小可以根据下面的公式得到:

不同之处在于元素数组缓冲的索引。现在再次利用三角形数与顶点数的关系(推导公式):

如果索引保存在类型为Uint16Array的元素数组缓冲中,则元素数组缓冲所占用的内存大小为:

由上可知,总内存为1028字节

这里我们编写核心代码,同样为了好看,我们贴上纹理。

var rowCount = 5;        var colCount = 10;        var positions = [];//存储x,y,z坐标        var textureCoordData = [];//存储纹理坐标u,v,纹理坐标与顶点坐标一一对应        var indices = [];//三角形列表(索引值)        for(var i = 0; i <= rowCount; i++){            for(var j = 0; j <= colCount; j++){                var x = j;                var y = i;                var z = 0;                var u = x / colCount;                var v = y / rowCount;                positions.push(x);                positions.push(y);                positions.push(z);                textureCoordData.push(u);                textureCoordData.push(v);            }        }        for(var i = 0; i < rowCount; i++){            var first,second;            for(var j = 0; j <= colCount; j++){                first = (i + 1) * (colCount + 1) + j;                second = first - (colCount + 1);                indices.push(first);//第一个点                indices.push(second);//第二个点            }            if(i != (rowCount - 1)){                indices.push(second);//退化三角形额外结点1                indices.push(first + 1);//退化三角形额外结点2            }        }

总结比较

前面分析了全部4中情况,下表归纳了前面的分析结果。从表中可以看出,当共享顶点很多时,gl.drawElements()方法与gl.TRIANGLE_STRIP图元的组合比gl.drawArrays()方法与gl.TRIANGLES图元的组合可以节省很多的内存!


 

         通过分析指导,这是一个只包含100个三角形的小网格,而且只考虑顶点位置。由于只考虑顶点的位置,因此数组缓冲的每个元素占用12个字节的内存。如果面对的是一个很大的网格,而且还需要包括法线和纹理坐标,则使用gl.drawElements()方法和gl.TRIANGLE_STRIP图元的组合可以节省更多的内存。

         重要的是,不要毫无必要地浪费内存,但是从性能角度来看,内存并不是唯一重要的因素。从性能角度来看,三角形绘制的顺序以及顶点数据的组织形式也是很重要的因素。



0 0
原创粉丝点击