前言
书接上回,在之前的篇章中我们一起探讨了 WegGL 的着色器以及着色器中的变量使用,那么在我们绘制复杂图形的时候,通过循环数据的形式动态修改着色器变量的点位信息,是不是不太友好,而且数据量过多和复杂的情况下是不是会出现嵌套循环和多个循环体的情况?那么有什么办法解决这个问题呢?对这就是今天要探讨的内容 缓冲区
!
缓冲区对象
缓冲区对象是 WebGL 的核心机制之一,通过缓冲区对象 WebGL 可以把大量的数据传递给 GPU 从而实现复杂的图形渲染。
缓冲区对象是 WebGL 系统中分配的的一块 GPU 内存区域 ,是一种存储 顶点数据、索引数据、其他相关数据 的容器,用于高效的进行图形渲染。
前情回顾
这里其实和之前有一点断层,所以重新提及一下环境代码,并且内容稍微有一丢丢多容易影响主要代码示例的观看体验,所以在这里先提及一下,后面的例子中就不再重复了:
ini
// 顶点着色器源码
const vertex = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 10.0;
}
`
// 片源着色器源码
const fragment = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
/**
* 初始化着色器程序
* @param {WebGLRenderingContext} gl - WebGL 上下文对象
* @param {string} vertexShaderSource - 顶点着色器源码
* @param {string} fragmentShaderSource - 片元着色器源码
* @returns {WebGLProgram} - 创建的着色器程序对象
*/
function initShader(gl, vertexShaderSource, fragmentShaderSource) {
// 创建着色器
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 设置着色器源码
gl.shaderSource(vertexShader, vertexShaderSource);
gl.shaderSource(fragmentShader, fragmentShaderSource);
// 编译着色器
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
// 创建着色器程序,并绑定着色器
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 链接程序,并使用该程序
gl.linkProgram(program);
gl.useProgram(program);
// 返回程序对象
return program;
}
创建缓冲区
在 WebGL 中,使用 gl.createBuffer() 来创建一个缓冲区对象。在创建缓冲区对象的时候,WebGL 会在 GPU 上分配一块内存用来存储数据。
ini
const canvas = document.getElementById('webgl')
const gl = canvas.getContext('webgl')
// 初始化着色器程序
const program = initShader(gl, vertex, fragment)
// 创建缓冲区对象
const buffer = gl.createBuffer()
绑定缓冲区
在 WebGL 中,使用 gl.bindBuffer(target, buffer) 将缓冲区对象绑定到指定的 目标(target) 上,绑定后后续对该目标的操作都作用于绑定的缓冲区对象。
这里的 目标(target) 是一个枚举值,常用的有:
gl.ARRAY_BUFFE
用于存储顶点属性数据的,例如:顶点坐标、纹理坐标、法线、颜色等;gl.ELEMENT_ARRAY_BUFFER
用于存储顶点索引的数据,旨在于用指定的顶点的绘制顺序,可以减少重复顶点的存储,提高绘制效率;gl.COPY_READ_BUFFER
和gl.COPY_WRITE_BUFFER
是用于缓冲区复制操作的绑定目标,其中 READ 是复制(读取)当前缓冲区数据的,WRITE 是粘贴(写入)数据到当前缓冲区的;- 还有一些其他的就不一一列举了,可以去点击上面的链接跳转去 MDN 看。
arduino
...
// 创建缓冲区对象
const buffer = gl.createBuffer()
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
这里说一下 目标(target) 和 缓冲区对象(buffer) 的关系,首先每一个 target 一次只能绑定一个 buffer,但是一个 buffer 可以反复绑定到多个 target;绑定的生效范围为 bindBuffer 后对改 target 关联的操作,都将作用于当前的绑定的 buffer。
写入数据到指定缓冲区
创建缓冲区对象以及绑定了目标后现在轮到给缓冲区写入数据了,哈哈有没有觉得 WebGL 的东西都很繁琐,创建、绑定、写入 or 编译 or 链接,最后才是使用,这些操作后续都可以封装起来简化操作。
在 WebGL 中,使用 gl.bufferData(target, data, usage) 来将数据从 CPU 传输到 GPU 的缓冲区中,这一步完成后,数据将会驻留在 GPU 的内存中,以供后续的渲染使用。
其中的 目标(target) 同楼上的 绑定缓冲区 的一样的,不再赘述了。
data 就是要写入到 buffer 数据存储区中的数据,这里要提一嘴的是,data 是强类型的数据,所以必须是一个ArrayBuffer,SharedArrayBuffer 或者 ArrayBufferView 类型的数组对象。
usage 是指定数据存储区的使用方法或者说类型,不同的类型会有不同的优化处理,也是一个枚举值,常见的有:
gl.STARIC_DRAW
缓冲区的内容可能经常使用,而不会经常更改;gl.STREAM_DRAW
缓冲区的内容可能不会经常使用;gl.DYNAMIC_DRAW
缓冲区的内容可能经常被使用,并且经常更改;- 还有一些其他的就不一一列举了,可以去点击上面的链接跳转去 MDN 看。
arduino
...
// 顶点坐标数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.5, 0.5
])
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
// 写入数据到指定目标的数据缓冲区
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
使用缓冲区对象
准备工作都搞定了,现在就来看看如何使用缓冲区对象把,那么在使用缓冲区对象之前,我们需要获取到着色器变量的存储地址,然后激活该变量,最后为其配置读取缓冲区对象数据的规则即可。
获取变量的引用地址就不多说了;
激活变量直接使用 gl.enableVertexAttribArray(location) 方法即可;
比较重要的是如何设置缓冲区对象数据的读取规则,这里需要用到 gl.vertexAttribPointer(location, size, type, normalize, stride, offset) 方法:
- 其中
location
是要修改的顶点属性的索引地址; sizi
是指定每个顶点属性的组成数量,值必须是整数例如 1 2 3 4,type 指的是数据类型;normalized
指的是是否要进行归一化处理(简单理解就是把数据处理到 【-1.0,1.0】之间);stride
指的是一个以字节为单位,连续顶点属性开始之间的偏移量(不能大于 255),为 0 时,表示该属性是紧密无间的;offset
是指一组顶点数据中的字节偏移量
这里的 stride
和 offset
可能有点难以理解,画一个图就清楚了:
arduino
...
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.5, 0.5
])
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
// 获取 attribute 变量的存储位置
const aPosition = gl.getAttribLocation(program, 'aPosition')
// 激活变量
gl.enableVertexAttribArray(aPosition)
// 将缓冲区对象分配给 aPosition 变量,每次从缓冲区对象中读取两个数据,数据类型为浮点型,不进行归一化处理,数据无间隔,不偏移
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
多缓冲区对象
一般情况下缓冲区对象可能存在多个,要分别使用,那怎么样才能使用多个缓冲区对象呢?
在之前绑定缓冲区对象的时候有提到过,一个 target 一次只能绑定一个缓冲区对象,并且在作用范围是在 bindBuffer 后对该 target 的操作都生效于该缓冲区对象,那么我们只需要在绑定了一个缓冲区对象并对其进行操作后,在用 bindBuffer 绑定一个新的缓冲区对象并设置新的操作即可,因为 target 是一对一的,所以重新进行绑定的时候旧的缓冲区对象就会自动被解绑,但是解绑后不会影响之前对该缓冲区对象的操作。
scss
// 顶点着色器新增了一个 aPointSize 的变量
const vertex = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`
... // 省略的重复代码
// 初始化着色器程序
const program = initShader(gl, vertex, fragment)
// 获取 attribute 变量的存储位置
const aPosition = gl.getAttribLocation(program, 'aPosition')
const aPointSize = gl.getAttribLocation(program, 'aPointSize')
//
// 从缓冲区对象中读取 坐标信息 并使用
// ----------------------------------------
// 顶点坐标数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.5, 0.5
])
// 创建缓冲区对象并进行绑定以及写入数据
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
// 将缓冲区对象分配给 a_Position 变量,并激活 a_Position 变量
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(aPosition)
//
// 从缓冲区对象中读取 大小信息 并使用
// ----------------------------------------
// 顶点的大小数据
const sizes = new Float32Array([10.0, 20.0, 30.0])
// 创建缓冲区对象并进行绑定以及写入数据
const sizeBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW)
// 将缓冲区对象分配给 aPointSize 变量,并激活 aPointSize 变量
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(aPointSize)
// 绘制
gl.drawArrays(gl.POINTS, 0, 3)
总结
缓冲区在 WebGL 里面相当重要,是其核心机制之一,提供了高效的数据传输、支持并行处理、减少 CPU 和 GPU 之间的通信开销,优化了内存管理,是 WebGL 构建高性能图形系统的关键机制。
然后查看完整的代码用例 -> 地址 !