【WebGL教程】Lesson7,了解OpenGL的几种Array Buffer,以及映射纹理坐标(附弹力网格小demo,修正demo无法显示情况) 13

本文地址: http://blog.wysaid.org/836.html
原创,转载请注明出处。系列教程: webgl-lesson.wysaid.org





第七话, 了解OpenGL的几种Array Buffer,实现大量顶点的批量绘制, 以及映射纹理坐标

每一话都间隔很久,其实早就可以更新了,但是一直偷懒,实在不好意思。
本来想第七话讲一讲绘制茶壶之类的东西,但是刚好前几天做了个小的网格demo,还是这种原创的东西比较讨喜,就拿这个来作为本次的蓝本吧
废话就到这里,这一次的demo原型是下面这个样子的,但是下面是html5做的,现在要使用WebGL,并且做得更加好看:(请用鼠标拖拽网格)

TARGET:

如果你还记得我们前面几章的内容的话,应该知道,通常我们绘制部分会有类似于下面的代码:

前面一直没有解释过为什么要这样用,本章将对此作出一些解释。并以此为主要功能制作一个小demo.

GL_ARRAY_BUFFER 和 GL_ELEMENT_ARRAY_BUFFER

假设本章讲到的WebGL的Context对象就叫webgl,那么webgl.ARRAY_BUFFER和webgl.ELEMENT_ARRAY_BUFFER就分别指的GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER这两个东西了。
webgl.bindBuffer函数的第一个参数支持且仅支持这两个参数。 桌面OpenGL中支持更多扩展但跟这里无关,不做任何解释。

这两个参数分别代表了绑定顶点缓冲区对象和顶点索引缓冲区对象。

而缓冲区对象则是通过webgl.createBuffer() 函数来创建的。 需要注意的是,缓冲区对象(buffer object)创建时并不需要指定其类型,而是在这个缓冲区对象第一次被绑定时,根据绑定时的第一个参数确定其真正的类型。

通常,我们把这两种缓冲区对象均称作VBO (Vertex Buffer Object). 同时提一下, OpenGL ES 2.0并不支持VAO,所以这里不做讨论

有了创建和绑定,接下来当然需要上传数据了, 上传数据使用的函数叫: webgl.bufferData, 在OpenGL中函数原型大致为:

由此可见,webgl.bufferData函数包含三个参数, 第一个参数为webgl.ARRAY_BUFFER或webgl.ELEMENT_ARRAY_BUFFER,

第二个参数为buffer数据,通常为 Float*Array 或者 Uint*Array 系列数组 (此处的* 代表 64, 32, 16, 8).
通常 ARRAY_BUFFER 将使用 Float*Array 而 ELEMENT_ARRAY_BUFFER 必须使用 Uint*Array。 (请不要直接使用js里面的Array数组, 因为它可能是数据没有对齐的, Array数组中的对象可能有不同的类型)
需要特别注意的是, 就目前来看,WebGL在使用 ELEMENT_ARRAY_BUFFER 时,不得使用精度超过 Uint16Array。 本章demo在编写时,习惯性地使用了 Uint32Array,导致Chrome (版本:34.0.1847.131 m) 一直对此报错。而改为 Uint16Array 之后运行正常,希望引起您的注意。(猜测原因可能是WebGL暂时还不能支持太大量的ARRAY_BUFFER数据从而限制了索引大小)

第三个参数为buffer数据的使用方式, 通常这一步跟性能有关。根据对数据使用情况的不同, OpenGL可能对数据进行一些优化,当然,你如果不关心这些的话,看心情随便写一个也是可以的。
这里贴一点khronos.org官方解释,希望了解的请自己点击下面的链接:

usage is a hint to the GL implementation as to how a buffer object’s data store will be accessed. This enables the GL implementation to make more intelligent decisions that may significantly impact buffer object performance. It does not, however, constrain the actual usage of the data store. usage can be broken down into two parts: first, the frequency of access (modification and usage), and second, the nature of that access.
(From: http://www.khronos.org/opengles/sdk/docs/man/xhtml/glBufferData.xml)

bufferUsage有三种可能值:

webgl.DYNAMIC_DRAW: 动态绘制模式, 缓冲区对象中的数据需要常常更新和使用 (webgl.bufferData函数会频繁调用), 这种模式下,OpenGL会将数据放置在能够快速更新和使用的地方。例:绘制了一个面包,当你不去碰它时,它的每一帧绘制不会更改,不需要bufferData更新,但是一旦被咬或者挤压就会变形,如果被不断撕咬则持续更新bufferData

webgl.STATIC_DRAW: 静态绘制模式, 缓冲区对象中的数据只指定1次,但是常常使用。这种模式下,OpenGL会将数据放在能够快速渲染的地方。例: 绘制一堵无法被破坏的墙, 那么每一帧它都不会变。本章的demo中,纹理采样顶点缓冲区网格顶点索引缓冲区不曾有变化,适合这种情况。

webgl.STREAM_DRAW: 流绘制模式, 缓冲区对象中的数据只指定1次,并且也不经常使用。
一些人对STREAM_DRAW了解较不清楚,这里特别讲一下。先是苹果官方对于它的解释:
GL_STREAM_DRAW is used when your application needs to create transient geometry that is rendered and then discarded. This is most useful when your application must dynamically change vertex data every frame in a way that cannot be performed in a vertex shader. To use a stream vertex buffer, your application initially fills the buffer using glBufferData, then alternates between drawing using the buffer and modifying the buffer.(From: https://developer.apple.com/library/mac/documentation/graphicsimaging/conceptual/opengl-macprogguide/opengl_vertexdata/opengl_vertexdata.html#//apple_ref/doc/uid/TP40001987-CH406-SW9)
我来翻译一下, 这段话的意思大致为, STREAM_DRAW的应用场景为几乎每一次 webgl.bindBuffer使用后都需要使用 webgl.bufferData 更新一遍数据的情况,就比如本章demo中网格顶点缓冲区数据的情况。

前面说了,如果不关心这些,可以随便写一个,bufferUsage参数只是对于OpenGL的一种暗示,并不一定非得按照描述的那样吻合才行。你填写任何一个都能正确运行,只是性能上可能存在一定差异。 对于,介于一些强迫症患者一定非得找到属于自己的style,我的建议是,不要纠结描述,直接把三种方式全部试一遍,哪种最快,帧率最高就用哪个吧,其他的都是浮云。

buffer 对象的使用

对于 webgl.ARRAY_BUFFER, 前面的章节已经提到过了, 直接绑定一个顶点属性即可, 代码类似于:

anyVertexAttributeIndex 为待使用的 Vertex Shader里面的某个attribute变量的location,
attributeSize 顾名思义为这个attribute含有几个分量,如果为float,则是1,如果为vec2则是2,依此类推。

对于 webgl.ELEMENT_ARRAY_BUFFER, 前面的章节不曾使用过,这里特别解释一下:
ELEMENT_ARRAY_BUFFER 本身并不是可以使用的顶点数据, 它存储的是一系列的顶点索引值,所以它的值必须为整数,且为无符号整数。

用索引和直接使用buffer绘制的区别如下:

首先,我们要明白, shader里面 attribute属性对应的数据为每个顶点一个的,比如你定义了 attribute vec4 color; 和 attribute vec4 vertex; 那么在传递 color和vertex两个attribute的data时,大小必须一样,且与最终绘制时使用到的顶点个数相同。

但是如果我们绘制的图形顶点不多,但是图形比较复杂,每个顶点要用到多次怎么办?
一般来说有两种解决办法,其一就是,把要多次用到的顶点在合适的位置多写几次,相应的,其他的attribute也对应增加。 若是顶点经常更新, 几何图形经常变动做起来就特别蛋疼。

其二就是我们可爱的 ELEMENT_ARRAY_BUFFER 了, 我们只需要记录绘制时的顶点索引顺序即可完成绘制,而顶点数据不需要任何重复。

以本章的demo为例,我们的网格是按三角形的方式绘制的,在网格中间,我们可以看到每个定点周围有六条线, 也就是说这个顶点至少被用到六次,如果不使用 ELEMENT_ARRAY_BUFFER ,我们竟然要把每个顶点至少写六次! 如果shader里面还有其他的 attribute,它们也莫名其妙地跟着增加,是不是很蛋疼呢?
这还不算完, 顶点是要更新的,也就是说,每个顶点数据的更新,也需要修改六个地方……蛋疼哭了吗,骚年?不要怀疑了, GL_ELEMENT_ARRAY_BUFFER 是解救你的神器。

到这里,对于这两个buffer的解释差不多够了。那么如何绘制呢?

对于本章的demo, 需要三个VBO, 两个 webgl.ARRAY_BUFFER和一个 webgl.ELEMENT_ARRAY_BUFFER

这三个VBO分别表示网格顶点缓冲区, 纹理顶点缓冲区 以及一个顶点绘制的索引缓冲区。

解释一下用途吧,前面说过了,纹理顶点缓冲区是不需要更新的,索引缓冲区同样不需要更新。 这两个缓冲区描述了整个网格的构成以及纹理绘制方式(纹理填满整个网格)

在OpenGL中,如果不考虑越界处理的话,纹理坐标的范围应该是[0, 1], 而本网格中的纹理应该是刚好填充满整个网格,所以纹理定点缓冲区里面的所有数据范围都应该介于[0,1],
而本demo并没有引入深度的概念(可以看到存在先绘制部分被遮挡的情况),并且整张纹理都在xOy平面内,所以顶点只需要x和y值就够了。那么刚好,x和y的范围也刚好都在[0, 1]之间,并且均匀变化。

这里你可能会奇怪,因为本章的demo并没有单独创建一个纹理顶点的数据,而是直接将未变化时的网格顶点数据作为纹理顶点数据传递进去了。这里解释一下:

本章demo中的顶点数据就是按纹理顶点数据方式创建的,范围就是[0, 1],而shader内部 gl_Position 默认的全屏取值为 [-1, 1], 为了绘满全屏, demo在vertex shader里面做了一次坐标映射,代码大致为:

算是偷了个懒吧,以后如果你遇到了也可以这样偷懒。

而对于整个网格的绘制,则主要在于顶点索引的设定了,设定代码大致如下:

因为想保留绘制一个线型的网格的功能,所以不得不采用S形路线来绘制整个网格。 这里的顶点数据是一维数组, 每两个为一对,分别表示x和y, 一次填充一个三角形,最终填满整个网格。

小提示: 如果你注意观察的话,可以发现每个小三角形的顶点顺序都是逆时针方向的哦, 这样可以区分网格的正反面,当然,本次demo并没有体现出区别, 以后如果有机会讲到光照和面剔除(CullFace)的话就会知道这样做的意义了。

本章关于WebGL的东西差不多就这么多了,剩下的如果还有什么疑问请自己clone下本章的demo

本章的demo如下:(请用鼠标点击。与上方html5版本是同步的,但是为了防止某些单核电脑卡顿,这两个demo不会同时刷新)


还没完哦, 鉴于你可能对这个网格如何做到拟物的比较感兴趣,这里粗略地讲一点。

首先声明一下,这个并不是个人纯原创,借鉴了一下某网友编写的“碧波荡漾”的小程序。也看了下它的源代码……但是这个源代码呢,写得比较晦涩,基本没怎么看懂,放弃理解原版的源代码。但是凭借寡人的想象力,不知道算不算”原创地”完成了本次demo,哦呵呵~

于是说一下这个网格的原理吧。 首先,我认为将这个demo理解为“碧波荡漾”是不恰当的,虽然看起来有点像水波,实质上是在模拟一个弹力网格。

我们来想象:我们用一些长橡皮绳编织出这么一张网, 网的连接线只有弹力没有质量,只有结点有质量,相邻结点与结点之间有一根橡皮绳相连。

学过物理的孩子都知道,物体加速度的方向与受力方向和其质量有关,设每个结点的质量相同(为m), 那么根据 F = ma, 受力越大加速度越大。

我们要求的是加速度,质量只是一个系数我们并不关心, 所以只需要知道每个结点的受力情况就可以算出来了。

(以html5版本显示出的网格为视觉蓝本)不难看出,每个结点都有四条线与之相连,分别受到这四根线的拉力。

换一个角度理解,假如此时处于平衡状态(加速度为0),则四个方向的力的合力肯定是0 对吧?

那么如何计算合力呢? 根据本例的应用场景, 这里有一个简单粗暴的方法,把这个顶点上下左右的坐标全部加起来除以四, 看看是否等于中间点坐标。(原理: 连接线的弹力与它被拉长的距离成正比, 两个点之间的距离越大,它们之间的拉力也就越大, 所以如果某个点想得到平衡状态,那么它跟左右两个点之间的距离必须得相等, 这样的话它的坐标刚好就应该等于两边点的坐标除以二, 因为不考虑重力, 垂直方向上也是一样)

能够相同上面那一层,整个网格的原理差不多弄清楚了。 假如一个点受力方向向左, 肯定是左侧距离较大, 那么两侧点加起来除以二肯定小于当前点坐标,对吧 ?

而且,它们之间的坐标差值跟力的大小成正比,而力的大小跟加速度成正比,对吧? 我们只要假定一个系数乘上去, 一个简单的网格就模拟出来了。

当然,到这里还没完,我们必须模拟每个点的速度和加速度, 使用一个变量保存速度,然后累加每一次的加速度,整个程序就完成了。

好的,第七期教程到此为止,请关注lesson 8。系列教程地址:webgl-lesson.wysaid.org