webgl的初见(二维)
你是谁?
WebGL经常被当成3D API,人们总想“我可以使用WebGL和一些神奇的东西做出炫酷的3D作品”。 事实上WebGL仅仅是一个光栅化引擎,它可以根据你的代码绘制出点,线和三角形。 想要利用WebGL完成更复杂任务,取决于你能否提供合适的代码,组合使用点,线和三角形代替实现。
WebGL在电脑的GPU中运行。因此你需要使用能够在GPU上运行的代码。 这样的代码需要提供成对的方法。每对方法中一个叫顶点着色器, 另一个叫片断着色器,并且使用一种和C或C++类似的强类型的语言 GLSL。 (GL着色语言)。 每一对组合起来称作一个 program(着色程序)。——资料来源
前端的角度
对于前端而言,我们一般不太需要直接使用webgl的原生API,主流的框架就足以应对我们的开发了,但是如果我们能够了解webgl的原理的话,对于我们使用框架会更加容易上手。
知根知底
webgl特别的地方
- WebGL在电脑的GPU中运行。
- WebGL是一个“中央空调”,不仅可以在浏览器使用,通过一定的方法,也可以直接在OpenGL使用。
- 降低了浏览器的依赖:可以实现一些更加酷炫的效果,同时又不增加浏览器的负担。
简单的流程图
交个“朋友”吧
shader
-
顶点着色器
作用:计算顶点的位置 -
片断着色器
作用:计算出当前绘制图元中每个像素的颜色值还是不知道这2个是什么东东?
GPU的翻译:GLSL语言
GLSL全称是 Graphics Library Shader Language (图形库着色器语言),是着色器使用的语言。 它有一些不同于JavaScript的特性,主要目的是为栅格化图形提供常用的计算功能。
-
顶点着色器
- 从JS获取的数据方式
- Attributes 属性 (从缓冲中获取的数据)
- Uniforms 全局变量 (在一次绘制中对所有顶点保持一致值)
- Textures 纹理 (从像素或纹理元素中获取的数据,来自于图片的数据)
- Varyings 可变量(一种顶点着色器给片断着色器传值的方式)
-
把最终渲染的位置返回给GPU
gl_Position = Vec4(0,0,0,0);
-
片断着色器
- 从JS获取的数据方式
- Attributes 属性 (从缓冲中获取的数据)
- Uniforms 全局变量 (在一次绘制中对所有顶点保持一致值)
- Varyings 可变量(一种顶点着色器给片断着色器传值的方式)
-
把最终渲染的位置返回给GPU
gl_FragColor = Vec4(r,g,b,a);
其他语法:
更多详细请点击链接了解
JS层级的操作
渲染前的准备
- 着色器(shader)程序创建
- 缓存区创建与绑定
- 渲染配置设置与渲染
结合代码来看
着色器(shader)程序创建
1. 读取shader
在JS代码中,不管你怎么样引入,GLSL在JS代码中存在的方式是字符串。
-
HTML标签引入
<!-- vertex shader --> <script id="vertex-shader-2d" type="x-shader/x-vertex"> attribute vec2 a_position; uniform mat3 u_matrix; varying vec4 v_color; void main() { gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); v_color = gl_Position * 0.5 + 0.5; } </script> <!-- fragment shader --> <script id="fragment-shader-2d" type="x-shader/x-fragment"> precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; } </script> <script> var vertexShaderSource = document.querySelector("#vertex-shader-2d").text; var fragmentShaderSource = document.querySelector("#fragment-shader-2d").text; </script>
-
文件引入
-
内置字符串
getFragmentShaderSourece() { return `void main() { gl_FragColor = vec4(0, 0.5, 0.5, 1); }`; }
2. 编译shader
const type = gl.VERTEX_SHADER// 顶点着色器 gl.FRAGMENT_SHADER是片断着色器
let shader = gl.createShader(type); // 创建着色器对象
// 将引入的shader
const source = ``// 引入的glsl
gl.shaderSource(shader, source); // 提供数据源
shader = gl.compileShader(shader); // 编译 -> 生成着色器
可以看出来不管是片断还是顶点,流程都是一样的,只是数据源和类型不一样,所以我们封装为一个方法。
// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
createShader(gl, type, source) {
var shader = gl.createShader(type); // 创建着色器对象
gl.shaderSource(shader, source); // 提供数据源
gl.compileShader(shader); // 编译 -> 生成着色器
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
gl.deleteShader(shader);
}
3. shader的Program程序创建
-
着色器引入之后,就需要让JS识别shader,这样才能将数据与shader着色器进行绑定。所以,webgl有一个program对象,只有通过它才能联动shader着色器。
创建program
const vertrexShader=this.createShader(this.gl, this.gl.VERTEX_SHADER, vertrexShader), const fragmentShader=this.createShader(this.gl, this.gl.FRAGMENT_SHADER, fragmentShader), var program = gl.createProgram(); // 把编译的shader添加到program gl.attachShader(program, vertrexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program);// 连接program gl.useProgram(program);// 使用program
缓存区创建与数据绑定
我们来分类型讲各自的数据绑定的方法
-
attribute绑定
首先是需要在shader
上声明好变量。
以顶点着色器举例attribute vec2 b_position; void main() { gl_Position = vec4(b_position, 0, 1); }
1. 绑定program与JS层
申明了一个b_position
的二维变量。
在代码中我们只需要这样就可以绑定了。const positionAttributeLocation = gl.getAttribLocation(program, 'b_position');
program是我们上面绑定好着色器的程序。
绑定好之后,并不代表这个数据就完成了,还需启用这个变量的状态。gl.enableVertexAttribArray(positionAttributeLocation);
2. 绑定缓存区与JS层
绑定好了与着色器的通信管道,我们就要开始传数据了,数据一般都是使用缓存区来保存。
首先是创建const positionBuffer = gl.createBuffer();
然后告诉webGL我们接下来要传输的缓存区的数据与格式。
// WebGL内部的全局变量。 首先绑定一个数据源到绑定点,然后可以引用绑定点指向该数据源。 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
绑定之后,我们只需要往里面传数据就可以了。
// 三个二维点坐标 var positions = [0, 0, 0, 1, 1, 1]; // 指定缓存的绑定点, 缓存区大小, 缓存的usage类型 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
3. 通过JS层 打通program与缓存区层
这样我们与shader的连接与缓存区的数据与连接都搞定了,最后就是打通着色器程序program与缓存区之间的隔阂了。var size = 2; // 每次迭代运行提取两个单位数据 var type = gl.FLOAT; // 每个单位的数据类型是32位浮点型 var normalize = false; // 不需要 整型数据值在转换为浮点数归一化数据 var stride = 0; // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type)) // 每次迭代运行运动多少内存到下一个数据开始点 var offset = 0; // 从缓冲起始位置开始读取 // 操作GUI渲染的方式 gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
完成上面的步骤之后,就意味着我们已经把一个顶点的数组传给
program
的顶点着色器里去了。 -
uniform全局变量传递
首先是shader肯定需要一个变量来接受数据。uniform vec2 u_resolution; attribute vec2 b_position; void main() { vec2 zeroToOne = b_position / u_resolution; vec2 zeroToTwo = zeroToOne * 2.0; vec2 clipSpace = zeroToTwo - 1.0; gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); }
1. 绑定program与JS层
首先JS
层肯定要知道绑定到什么变量。var resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');
2. JS层传递数据
由于顶点着色器代码里是一个二维的数据,所以绑定的方法是uniform2f
,如果是三维的则是uniform3f
,四维的是uniform4f
gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height)
不需要缓存区?
uniform这个保存的是一个变量,所以并不需要想attribute那样是一组的顶点数据,这种才需要缓存区来绑定。当然attribute也支持单个传递数据,而不是一组数据:gl.vertexAttrib[1234]f[v]() -
textrue纹理
纹理可以认为是现实生活中的贴纸,我们将一个图片贴在我们画好的形状上。我们首先要获取到贴纸(纹理的数据),然后把它对准形状的位置贴好(纹理地址)。
纹理一般是用在图片上读取到数据,图片上的数据既有顶点的数据也有颜色的数据,webgl已经提供了API来进行自动处理,我们不需要把它们进行拆分。
我们需要先传给顶点着色器,再传给片断着色器。这是因为webgl只提供了绑定顶点着色器上attribute的方法:
我们来看看简单的着色器代码。- 顶点着色器
attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position , 0, 1); v_texCoord = a_texCoord; }
- 片断着色器
precision mediump float; uniform sampler2D u_image; varying vec2 v_texCoord; void main() { gl_FragColor = texture2D(u_image, v_texCoord).bgra; }
1.读取图片
前面我们绑定的顶点位置的数据不在赘述,我们之间看纹理如何绑定。
引入图片:laodImage(imgURL) { return new Promise((resolve) => { const image = new Image(); image.src = imgURL; // 必须在同一域名下 image.onload = function () { resolve(image); }; }).then((res) => res); } const image = await this.laodImage('/img,jpg');
2.创建纹理缓存
// 创建纹理 var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 设置参数,让我们可以绘制任何尺寸的图像 // webgl 1.0 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 纹理缩小过滤器 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
3.把贴纸(纹理数据)递给program
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);// 加载的图片
4.图片贴的位置(纹理地址)
很显然,贴的地址和画的形状一样,需要传入一组数据才能进行对应。
所以着色器是通过attribute
来接受。
所以一组数据需要缓存区来保存。const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); // 给矩形提供纹理坐标 var texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW); gl.enableVertexAttribArray(texCoordLocation); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
由于我们画的是正方形,所以一共需要画2次的三角形来进行拼接,同样,也需要一一对应顶点的位置,告诉shader如何把贴纸贴在“正确的地方”。
- 顶点着色器
渲染
- 清空画布
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 指定清空canvas的颜色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空canvas
- 画出形状
gl.drawArrays(gl.TRIANGLES, 0, n);
图片的形式来了解上述的过程
- 创建program着色程序
2.初始化shader与编译
3.编译之后
4.创建缓冲区并绑定数据
5.渲染
总结
至此,我们已经了解如何使用webgl来画二维形状的过程,看到这里相信你已经步入了webgl的大门。相信你看到这里,也知道了,其实webgl并不难懂,无非就是多了一个GLSL语言以及如何使用应用程序(JS、UNITY、cocos2d…等)把数据传给GPU进行渲染。