WebGL经常被当成3D API,人们总想“我可以使用WebGL和一些神奇的东西做出炫酷的3D作品”。 事实上WebGL仅仅是一个光栅化引擎,它可以根据你的代码绘制出点,线和三角形。 想要利用WebGL完成更复杂任务,取决于你能否提供合适的代码,组合使用点,线和三角形代替实现。
WebGL在电脑的GPU中运行。因此你需要使用能够在GPU上运行的代码。 这样的代码需要提供成对的方法。每对方法中一个叫顶点着色器, 另一个叫片断着色器,并且使用一种和C或C++类似的强类型的语言 GLSL。 (GL着色语言)。 每一对组合起来称作一个 program(着色程序)。——资料来源
对于前端而言,我们一般不太需要直接使用webgl的原生API,主流的框架就足以应对我们的开发了,但是如果我们能够了解webgl的原理的话,对于我们使用框架会更加容易上手。
GLSL全称是 Graphics Library Shader Language (图形库着色器语言),是着色器使用的语言。 它有一些不同于JavaScript的特性,主要目的是为栅格化图形提供常用的计算功能。
顶点着色器
把最终渲染的位置返回给GPU
gl_Position = Vec4(0,0,0,0);
片断着色器
把最终渲染的位置返回给GPU
gl_FragColor = Vec4(r,g,b,a);
在JS代码中,不管你怎么样引入,GLSL在JS代码中存在的方式是字符串。
HTML标签引入
<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>
<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);
}`;
}
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);
}
着色器引入之后,就需要让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);
至此,我们已经了解如何使用webgl来画二维形状的过程,看到这里相信你已经步入了webgl的大门。相信你看到这里,也知道了,其实webgl并不难懂,无非就是多了一个GLSL语言以及如何使用应用程序(JS、UNITY、cocos2d…等)把数据传给GPU进行渲染。