入门 WebGL,看这一篇就够了

用户头像
一颗大橄榄
关注
发布于: 2020 年 08 月 02 日
入门WebGL,看这一篇就够了

前言

大家好,今天写这片文章的目的是为了给大家分享一下WebGL的基本原理是怎么样的,使用WebGL与canvas2D绘制图形之间有什么样的不同。

有的同学可能会问这对我们开发有什么样的帮助呢?

其实,在实际开发中可能用不到这类的知识,因为WebGL是相对来说比较底层的API了。如果要进行3D相关需求的开发,可能大部分的同学会使用three.js或者是 babylon.js之类的类库。但是从长远上来讲,学习WebGL的底层原理,会让自己有恍然大悟的感觉,还也许也会有同学想要编写属于自己的类库,那么学习WebGL的底层原理是非常有必要的。



WebGL基础

CPU vs GPU

在这之前我想问问大家知道CPU与GPU之间的区别是什么吗?下面这个图展示了CPU与GPU处理数据之间的差异性



大家可以看到,数据是有秩序的进入CPU管道中,然后被依次输出。



在GPU中,数据可以被大量的读入,GPU可以一次性处理大量的数据,然后讲其一次性的吐出来。



我再做一个很形象的比喻,CPU处理数据就像我们写字一样,只能一个一个字的写,而GPU处理数据就像活字印刷术一般,先将字排版好,然后可以批量的进行印刷。



接下来我们再看一个小例子,我们现在要绘制一个矩形,如图所示

在Canvas2D中,我们会写出以下的代码:

function ctxDraw() {
ctx.fillStyle = '#f60';
ctx.fillRect((width - rectWidth) / 2, (height - rectHeight) / 2, rectWidth, rectHeight);
}



我们可以看到,这段代码是命令式的,我们先设定了要填充的颜色,再告诉浏览器我们要绘制一个矩形并填充



那么,在WebGL中的代码是这样的

const vertexShader = `
attribute vec4 a_position;
void main () {
gl_Position = a_position;
}
`
const fragmentShader = `
precision mediump float;
void main () {
highp vec4 color = vec4(1.0, 0.4, 0.0, 1.0);
gl_FragColor = color;
}
`
function glDraw() {
let program = util.initWebGL(gl, vertexShader, fragmentShader);
gl.useProgram(program);
let points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.5, 0.5,
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);
const a_position = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(a_position);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}

大家可以看到,同样是绘制一个矩形,使用WebGL比使用canvas2D多出了不少的代码。这里简要概述一下,在WebGL中,它不是命令式的,而是“连接式”或者说是通过配置WebGL中一些状态,WebGL再通过渲染管线这一整套流程完成图形的绘制。(渲染管线的流程会在下面的篇幅中提到)

WebGL的本质

这里也有一个很形象的比喻帮助大家理解这个问题,大家可以认为WebGL是一套复杂的电路。我们将其中的一些变量连接起来,然后为其加上电源,那么WebGL就可以正常的工作起来了。

渲染管线

所谓的渲染管线,实际上就是渲染过程流水线,指的不是具体某一样东西,而是一个流程。因为渲染管线的流程中总是将上一步的结果作为下一步的输入,就像水管一样接起来,管线的名字也因此得来。



下图简要的展示了渲染管线的一个流程



  1. 顶点着色器(可编程):首先通过顶点着色器,确定我们设置的顶点位置

  2. 图元装配:gl.drawArray方法会指定图元装配的方式(点、线、三角形),根据我们设定的装配方式将其组装成我们想要的基本图形

  3. 光栅化:实际上就是一个将上一步装配好的图形用像素来表示的一个过程

  4. 片元着色器(可编程):光栅化完成后,每个像素的片元都会执行片元着色器中的程序,得到最后的颜色值

  5. 测试与混合:这一阶段主要是WebGL内部进行了一些模版测试、深度测试,最后再与上一帧的数据进行混合。



基本图形

接下来我们来讲讲在WebGL中如何绘制一些基本图形

在WebGL的世界中,只有三种基本图形:点、线、三角形

那么,有同学就会问了,如果只能绘制基本的这三种图形,那我们在各种游戏中那些人人物模型又是如何产生的呢?这里就涉及到三角剖分的问题了。没错,所有的复杂的图形都是由点、线、三角形这三类基本图形所产生。

如上图所示,这样的许多三角形近似的构成了一个圆,如果我们把这些三角形划分的足够小,那么这个圆也就会越光滑,越逼真。对于3D图形,也同样如此,如下图所示。

我们是如何告诉WebGL要选择什么样的基本图形呢?我们通过gl.drawArray(mode, first, count)这个API在告诉WebGL我们要绘制什么样的基本图形,其中mode参数就是绘制的基本图形类型,具体的方式如下图所示(参考《WebGL编程指南》)





坐标系统



在这里我先就2D范围讨论坐标系统,如图所示,在HTML的坐标表示中如左下图所示。但是在webgl的坐标系统中,它的左右上下值的范围都是-1~1之间的。如果我们要把HTML中的坐标转换到webgl的坐标系统中来,需要使用到后面图形学基础中提高的裁剪矩阵来做坐标的转换工作。后面的篇幅中我们会详细讲解,关于坐标系统的知识大家就暂时了解这些就可以了。



着色器

当当当!重点来了,敲黑板。着色器可以说是WebGL编程中最重要的一环也不为过了。最终在屏幕上显示出来的效果绝大部分都是要靠着色器中的程序来实现的。现在我们就详细的介绍一下着色器吧。

先让我们看一个简单的着色器程序:

顶点着色器

attribute vec4 a_position;
void main () {
gl_Position = a_position;
}

我们逐行的解释一下它代表的意思

  1. 声明了一个全局vec4类型的全局变量,attribute是什么意思呢?attribute是一个存储限定符,被它所修饰的变量表示是接受外部传入的顶点属性的。它是WebGL外部顶点信息传入WebGL内部的桥梁变量。vec4类型是4维向量类型,用于表示顶点的坐标信息。这时有同学就会问了坐标信息不就是x, y, z吗,这明明只需要3维向量就可以了呀,哪里来的第四个参数呢?这是因为在WebGL中是采用的齐次坐标表示的坐标信息,我们用这样的形式(x, y, z, w) 来表示一个坐标的位置。大家可以将它等同于这样的一个三位坐标(x / w, y / w, z / w)

  2. void main 表示顶点着色器的main函数,类似于C语言的main函数,他是顶点着色器中的唯一入口函数。

  3. gl_Position是顶点着色器中的内置变量,它就表示了当前顶点的实际位置,所以我们需要将从外界接受信息的a_position的值赋给gl_Position这个变量。



片元着色器

precision mediump float;
void main () {
highp vec4 color = vec4(1.0, 0.4, 0.0, 1.0);
gl_FragColor = color;
}



接下来我们继续解释片元着色器中的代码含义

  1. 全局声明了片元着色器中浮点数类型的精度。

  2. main函数,与顶点着色中的含义相同

  3. 声明了一个vec4类型的局部变量color,并且指定了精度为高精度。

  4. 将color的值赋给WebGL的内置变量gl_FragColor, gl_FragColor表示的就是当前片元的颜色值



当然除了attribute这种存储限定符,还存在uniform, varying这两种存储限定符,我们再来看看其他两种限定符



  1. attribute: 只能出现在顶点着色器中,被用来从外部向WEBGL内部中传递顶点信息

  2. uniform: 可以出现在顶点着色器和片元着色器中,表示统一的值

  3. varying:(光栅化阶段)可以出现在顶点着色器和片元着色器中,表示变化的值,是顶点着色器和片元着色器之间的连接桥梁



代码如下:

// 顶点着色器
const vertexShader = `
attribute vec4 a_position;
attribute vec4 a_color;
varying vec4 v_color;
uniform mat4 u_projection;
void main () {
gl_Position = u_projection * a_position;
v_color = a_color;
}
`;
// 片元着色器
const fragmentShader = `
precision mediump float;
varying vec4 v_color;
void main () {
gl_FragColor = v_color;
}
`



这里在此解释下关于varying和uniform存储限定符的意思。我们看到在顶点着色器中有一个u_projection的uniform变量,这就是之前提到的裁剪矩阵,用于进行坐标转换的。由于每个顶点使用的裁剪矩阵都是一样的。所以我们就是一个uniform存储限定符来修饰它,我们可以通过gl.uniformMatrix4fv()来向顶点着色器中传递这个矩阵。



那么我们还观察到在顶点着色器中和片元着色器中都有varying变量。它的作用就是起到一个将顶点着色器中的变量传递到片元着色器中。我们需要在顶点着色器和片元着色器中声明两个名字一样的变量,那么就可以将他们的数据连接起来



还有注意看第10行代码,我们将attribute类型的数据赋值给了varying类型的变量,这样在光栅化的阶段,a_color的值就会被进行插值处理赋给v_color。



数据传递

那么,我们在shader中声明了这么多的全局变量,那么我们又是如何给这些变量赋值的呢?或者可以说是我们如何从WebGL外传递数据到WebGL内部的呢?



在这里我主要分为以下三类数据

  1. 顶点数据

  2. 纹理数据

  3. uniform类型的数据(浮点数、向量、矩阵)



往着色器中传递顶点数据我们需要使用WebGLBuffer这样的一个对象。

往着色器中传递纹理数据我们需要使用WebGLTexture这样的一个对象

往着色器中传递uniform类型的数据我们直接使用WebGL提供的API即可(例如:uniformMatrix4fv)而不需要额外的对象作为媒介了。



现在让我们具体介绍一下这三种方式,先从简单的开始

uniform类型的数据(浮点数、向量、矩阵)

传递浮点数、向量、矩阵这类的uniform数据直接使用WebGL提供的API。例如:

gl.uniform1f,它表示传递1个浮点数;

gl.uniform1fv, 它表示传递一个1维的浮点数向量;

gl.uniform2f,它表示传递2个浮点数;

gl.uniform2fv, 它表示传递一个2维的浮点数向量;



依次类推,具体的API可以参考https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/uniform



咳咳,现在重点又来了。如何向着色器中传递顶点信息和纹理信息

传入顶点数据(WebGLBuffer)

往着色器中传递顶点数据我们需要使用WebGLBuffer,具体步骤如下:

流程:

代码:



const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorBufferData, gl.STATIC_DRAW);



下面这个图很明确的揭示了他们之间的关系



ARRAY_BUFFER是gl上下文对象中的一个属性,我们可以看到ARRAY_BUFFER在其中起到了一个桥梁的作用,我们将ARRAY_BUFFER与我们创建的WebGLBuffer绑定在一起,然后再将真正要传入的数据传递给ARRAY_BUFFER,由于ARRAY_BUFFER与WebGLBuffer之间的绑定关系,所以最终数据是流向了WebGLBuffer的。

此时WebGLBuffer中已经有了数据了,那么我们还需要一步将着色器中的a_position与这个buffer建立联系。所以我们还需要通过gl.enableVertexAttribArray和gl.vertexAttribPointer这两个API建立着色器中变量与WebGLBuffer的联系。



gl.enableVertexAttribArray —— 是告诉WebGL,我现在所绑定的WebGLBuffer是要与着色器中的哪个变量建立联系。



gl.vertexAttribPointer —— 是告诉WebGL如何去读取我存在WebGLBuffer中的数据。



传入纹理数据(WebGLTexture)



先提出一个小问题,什么是纹理?

做一个比喻的话,大家家里都有桌子,桌子上的桌布就可以理解为是纹理。

在WebGL中,什么样的对象可以作为纹理呢? <img> <video> <canvas> 标签,或者是ImageBitmap对象,甚至是TypedArray都可以作为纹理。



那么传入纹理数据的流程其实和传入顶点数据的流程差不多。如下图所示





我们可以看到,同样是有一个中间的对象TEXTURE_2D作为桥梁来传递纹理数据。略有不同的是在创建纹理对象之后,会有一个设置纹理裁剪参数的过程。为什么要设置纹理裁剪的参数,我以下面的例子来说明:

为什么要设置纹理裁剪参数

首先,先普及一个概念,我们如何从纹理上取到相应的颜色,我们要提供相应的纹理和纹理坐标,WebGL会根据相应纹理和纹理坐标去进行采样。比如一张图片的实际大小是100 x 100大小的,我提供的纹理坐标是(0.1, 0.2)。那么,我得到的颜色就是图片实际坐标第(10, 20)个像素的颜色。那么这是刚好得到一个整数,如果不是整数呢?比如我提供的纹理坐标是(0.111, 0.222), 那么就没有对应的实际像素与其对应,所以我们需要告诉WebGL此时应该怎么做,一般的做法可以取它相邻的四个像素做插值运算,或者直接取与其相邻的最近的像素颜色也可以。



另外,如果我们设置的纹理坐标超过了1,或者小于0 又该怎么办呢?我们也需要告诉WebGL怎么做,是需要进行重复采样?还是将采样坐标强制控制在0~1之间呢?用下面的图来进行说明

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);



告诉WebGL超出的纹理坐标就重复镜像采样处理。(只适用于长宽都是2的整幂大小的图片)



gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

告诉WebGL超出的纹理坐标就重复采样处理。(只适用于长宽都是2的整幂大小的图片)



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);

这是告诉WebGL将采样坐标强制限制在0~1的范围内,超过的部分就按最大/最小值处理



上面的例子很清晰的展示了我们为什么要设置纹理裁剪参数。



以上就是关于WebGL的一些基础概念的理论了。



现在总结一下这部分的知识



小结

WebGL绘制图形的方式不是命令式的,而是“连接式”的。我们通过WebGLRenderingContext这个上下文将外部的变量与GPU内部的shader程序发生了联系。渲染管线就可以通过shader运行出我们想要的结果,大家需要掌握的重难点就在于如何向WebGL中传入各种类型的数据。



接下来会继续为大家带来关于图形学基础方面的知识,感兴趣的朋友可以收藏关注一下哦!



本文为本人原创作品,未经允许禁止转载。

发布于: 2020 年 08 月 02 日 阅读数: 160
用户头像

一颗大橄榄

关注

还未添加个人签名 2019.01.15 加入

还未添加个人简介

评论

发布
暂无评论
入门WebGL,看这一篇就够了