什么是buffer
buffer, 这是啥东西啊 ,或者说为什么要有buffer 这个东西 对于webgl 来说。我们先看下官方定义:
「WebGLBuffer」 接口属于 WebGL API 的一部分,表示一个不透明的缓冲区对象,储存诸如顶点或着色之类的数据。
没事由我来大家解读,首先第一句话就是一句废话, 属于「webgl API 的一部分」, 这我他🐴的也知道哇,我们重点分析 一下这句话,是一个不透明的缓冲区对象,
什么是缓冲区对象
「webgl提供一种很方便的机制 ,就是缓冲区对象」,它可以一次性向着色器传入多个顶点的数据。缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。
确实是很方便机制,为什么这么说呢, 我就拿 画3个点的 这个例子 去做说明, 「下面写的都是伪代码」
没用缓冲区
我想在webgl 中画3个点, 很简单吧 ,不搞一些花里胡哨的,就简单的3个点:
// 清除颜色缓冲区 gl.clear(gl.COLOR_BUFFER_BIT); const len = g_poiont.length; for(var i = 0; i<len; i++){//循环添加点 //将点的位置传递到变量中a_position gl.vertexAttrib3f(a_Position,g_poiont[i].x,g_poiont[i].y,0.0); //绘制点 gl.drawArrays(gl.POINTS,0,1); }
这里的画 我对这段代码做一个简单的说明
- 「清楚颜色缓冲区」
- 「然后循环遍历 数组列表」
- 「位置传递到顶点着色器, 然后去绘制顶点」
看不懂的话也没关系,我在后面的webgl 系列会一步一步讲解,「本篇还是文章的重点还是在buffer 的理解」, 希望小伙伴们明白,看不明白没关系。
这里的话主要介绍下gl.drawArrays 这个「api」, 理解这个api十分重要,建议多思考。
这是一个强大的api, 用来绘制各种图形,我们看下下面这张图:
api 展示图
- 第一个参数其实呢就是webgl绘制的方式, 有点、线、三角形 .... 我们这里是采用的绘制点的方式
- 第二参数:就是从哪个顶点绘制参数
- 第三个参数:当前绘制需要用 几个顶点
因为我们是在循环中,所以呢就是每一次循环,绘制一个顶点 。也就是在程序运行的时候,顶点着色器运行一次, 因为我们也只绘制一个点对吧, 好像看着还挺好的哇,合情合理。
我们看下下面这张图:
image-20220115141239602
上面这张图是一个哆唻A梦的3维网格模型,从图像中去看,他的顶点有很多,我们难道要用循环一次一次 就去画出这个顶点嘛,那这样 gpu 渲染会快嘛, 肯定不是这么搞的, 这时候你再去读一下上面的「buffer」 定义:「它可以一次性向着色器传入多个顶点的数据」
我们使用buffer的方式再去绘制3个顶点:
使用缓冲区
我们看下伪代码:
//设置顶点位置 initVertexBuffers(gl); //将三个点绘制出来 gl.drawArrays(gl.POINTS,0,3);
- 第一个函数使用了 使用了buffer, 并且做了数据绑定
- 然后我们告诉webgl 我们当前绘制 3个顶点,并且数据啥的 我们已经在缓冲中绑定好了
这样对于各种复杂的模型,我们都可以轻易拿捏,真的很帅哇, 一次性可以处理 很多个顶点,充分发挥GPU「并行渲染」的能力。
我画了一个流程对比图,方便你理解
使用了缓冲区对象
没有使用缓冲区对象的顶点着色器 可能会根据顶点的个数执行多次
未使用顶点着色器对象
好了到下面一个问题了到底如何使用缓冲区的呢????
如何使用缓冲区
其实就是刚才上面的「initVertexBuffers」函数做的几件事
- 第一步:创建缓冲区对象(gl.createBuffer())
- 第二步:绑定缓冲区对象(gl.bindBuffer())
- 第三步:将数据写入缓冲区对象(gl.bufferData())
- 第四步:将缓冲区对象分配给一个attribute变量(gl.vertexAttribPointer())
- 第五步:开启attribute变量(gl.enableVertexAttribArray())
跟着我的脚步一步一步来分析:
创建缓冲对象
代码其实很简单 就是下面这一行:
const buffer = gl.createBuffer()
但是他实际会发什么呢, 如图所示:
展示
其实在内存中就会分配一块缓冲区对象,有创建其实就有对应的 删除缓冲区对象
const buffer = gl.createBuffer() gl.deleteBuffer(buffer)
绑定缓冲区对象
有了对象了,我们要开始绑定缓冲区对象, 细心的同学看上面图片 可能会发现 在缓冲区的上面 「gl.ARRAY_BUFFER」 还有 「gl.ELEMENT_ARRAY_BUFFER」 这两个有什么区别呢???什么时候使用呢
就是因为buffer 的类型很多变, 所以你必须手动绑定,表示当前buffer
我先介绍下 这两个buffer 的区别:
gl.ARRAY_BUFFER
: 包含顶点属性的Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。
gl.ELEMENT_ARRAY_BUFFER
: 用于元素索引的Buffer。
- 当使用 「WebGL 2 context」
时,可以使用以下值:
gl.COPY_READ_BUFFER
: 从一个Buffer对象复制到另一个Buffer对象。
gl.COPY_WRITE_BUFFER
: 从一个Buffer对象复制到另一个Buffer对象。
gl.TRANSFORM_FEEDBACK_BUFFER
: Buffer for transform feedback operations.
gl.UNIFORM_BUFFER
: 用于存储统一块的Buffer。
gl.PIXEL_PACK_BUFFER
: 用于像素传输操作的Buffer。
gl.PIXEL_UNPACK_BUFFER
: 用于像素传输操作的Buffer。
这里不理解的话没关系,后面会讲, 你只要知道,buffer有很多类型, 因为我们是顶点嘛, 所以绑定的肯定是第一个类型
代码如下:
const canvas = document.getElementById('canvas'); const gl = canvas.getContext('webgl'); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
然后看下下面的图片,
绑定缓冲区对象
注意看小箭头, 箭头就是绑定buffer的过程
写入数据到缓冲对象了
这时候我们的前置工作都已经准备好了, 那么数据呢如何写进去呢,在这之前我先简单讲一个数据类型——类型数组
类型数组:
JavaScript类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。正如你可能已经知道,
Array
存储的对象能动态增多和减少,并且可以存储任何JavaScript值。JavaScript引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着Web应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问WebSockets的原始数据等,很明显有些时候如果使「用JavaScript代码可以快速方便地通过类型化数组来操作原始的二进制数据将会非常有帮助。」
但是,不要把类型化数组与正常数组混淆,因为在类型数组上调用
Array.isArray()
会返回false
。此外,并不是所有可用于正常数组的方法都能被类型化数组所支持(如 push 和 pop)。
为了优化性能,webgl需要同时处理大量相同类型的数据, 例如顶点的坐标 和顶点的颜色。浏览器事先知道数组的类型,处理起来更高效, 有的强类型的感觉。
类型数组
所以代码如下:
// 三角形的顶点数据 const vertices = new Float32Array([0.0,0.5,-0.5,-0.5,0.5,-0.5]); //创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.log("创建缓冲区对象失败"); return -1; } //将缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer); //向缓冲区对象中写入数据 gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
最后一步就是我们的绑定数据, 我们来一起解读下这个「api」「将 第二个参数数据绑定到第一个参数的缓冲区对象中」,第三个参数表示什么呢???, 表示的向缓冲区对象写入一次数据,但是需要绘制很多次
如图:
写入数据
分配变量
现在数据也写入缓冲区中了, 然后就是分配变量。这里其实也是只有两步
- 第一步其实 从着色器找出属性 对应的位置
- 然后将缓冲区对象分配给这个变量
我们先看下代码:
//获取attribute变量的存储位置 const a_Position = gl.getAttribLocation(gl.program, "a_Position"); if(a_Position < 0){ console.log("无法获取变量的存储位置"); return; } //将缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0);
前半部分其实 这里的话会有一点前置小知识, 会在后面文章讲解, 主要是创建 webgl 程序, 然后会初始化着色器程序, 同时在顶点着色器中 其实就是一段 字符串中 ,有「a_Position」 这么一部分,然后我通过gl.getAttribLocation 去查找到这个 变量 对应的存储位置。
后半部分的话其实:就是将缓冲区的对象 分配给这个变量 ,我们看下这个api 「gl.vertexAttribPointer」
- 第一个参数的话其实 指定要修改的顶点属性的索引, 而这个索引 就是我们通过上面在顶点着色器中去查找到的
- 第二参数 的表示 指定每个顶点属性的组成数量,必须是1,2,3或4。这是为什么呢 ,因为顶点坐标 默认是(x,y,z,w)如果你选择是1 那么 程序会帮你把剩下的参数补充。2 3 设置为0 4 设置为 1 。因为我们这里 其实就是个二维坐标 , 所以只用到了 x,y 这样的坐标 所以是2
- 第三个 表示数据类型, 我们这里是浮点型
- 第四个 表示归一化 ,就是把 非浮点型的数据 归一到 【0,1】 或者 是【-1,1】 区间
- 第五个 表示相邻两个顶点之间的字节数
- 最后一个 表示 我们获取的这个顶点索引 是在缓冲区 的何处 开始存储的 , 我们缓冲区对象 存的都是顶点数据, 所以这里是0
详细文档仔细的话, 可以去MDN 查看。一个参数 6个参数,所以学习webgl 真的很有挑战哇, 「不过还是那句话, 大家觉得难,你学会了 你的市场价值就体现了」。还是一个字 冲冲!!!
棒
看下图片
此时的场景
激活
有人看到上面图片就说, 数据也写入了, 也进行变量分配了, 为啥中间有一段连线是断的呢???webgl 是真的狗, 搞得是真的复杂,我这就想画了3个点, 搞了 好几步。头疼哇,「所以说three.js 这种框架 设计 不得不说 一个字牛逼 ,但是为了学习, 我还是要学习原理, 对于three.js 你才能轻松驾驭」
我们看下怎么激活的 就一行代码
//链接a_Position变量与分配给它的缓冲区对象 gl.enableVertexAttribArray(a_Position);
其实就是这么简单的一行代码,表示开启变量 , 同时建立了 缓冲区对象 和 attribute 之间的链接了
看下图片:
链接
总结
这是今年Webgl 系列的第一篇文章主要讲解了buffer 从 what 、 how 、 why 三个方面去阐述了 ,今年的文章都是以系列的模式去展示给大家, 如果不想错过我的第一时间发布 ,请将公众号设置为星标, 能第一时间看到哦。
最后我用文字进行总结下知识点
- 「第一步:创建缓冲区对象(gl.createBuffer())」
- 「第二步:绑定缓冲区对象(gl.bindBuffer())」
- 「第三步:将数据写入缓冲区对象(gl.bufferData())」
- 「第四步:将缓冲区对象分配给一个attribute变量(gl.vertexAttribPointer())」
- 「第五步:开启attribute变量(gl.enableVertexAttribArray())」
最后再来一张图加深 同学们的记忆, 序号都标的好好的,
序号图