在基础绘制系列的文章中,你将了解如何在WebGL中绘制基本的元素,本文将向你讲解如下内容:
- WebGL如何定义和描述几何体
- 顶点、缓冲区的概念和作用
- 如何绘制一个点
- 熟悉完整的WebGL绘制流程
图元(Primitives)
WebGL中的图元(primitives,图元这个计算机图形学的术语让人感觉不那么好懂,我更喜欢说成是基本绘图元素)包含以下三种:
- 点
- 线
- 三角形(面)
可以看出图元就是基本的点、线、面,不论多么复杂的模型,都是由这三种图元构成(大多数情况下的3D模型只由三角形构成,线和点基本不用)。
绘制一个点
我们从最简单的例子学起,学习如何绘制一个点。完整的代码示例请见这里。
在代码中首先出现的是顶点着色器代码和片段着色器代码:
attribute vec3 aPos; void main(void){ gl_Position = vec4(aPos, 1); }
void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }
通过script标签,我们在页面中插入两段shader代码,注意它们的type分别是x-shader/x-vertex和x-shader/x-fragment,即顶点着色器和片段着色器。
顶点着色器第一行:
attribute vec3 aPos;
这里定义了名叫 aPos
的属性,类型是 vec3
,即三元组浮点数向量(3-component floating point vector)。
接下来的代码定义了 main
函数,如同C语言,shader的入口函数也是 main
:
void main(void){ gl_Position = vec4(aPos, 1); }
语法并不难理解,main
函数返回为 void
,参数也是 void
,函数体内只有一行,将一个四元组向量传递给 gl_Position
变量,这个四元组中前三个元素来自三元组 aPos
,第四个元素的值是1。你会奇怪代码里并没有 gl_Position
的定义,其实这是顶点着色器中内置的变量,你直接用就行了,作用是告诉着色器顶点的位置。
下面是片段着色器代码:
void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }
同样,入口函数还是 main
,这里给变量 gl_FragColor
赋值为一个四元组,表示几何体的颜色。四元素的含义就是R、G、B、A。gl_FragColor
是片段着色器中的内置变量,也是可以直接使用的。
这里我们看着代码简单介绍了一下shader的概念,更多的细节我还会单独写文章来介绍。
接下来的HTML代码中,我们放置一个 canvas
元素用来显示 WebGL 绘制的内容:
<canvas id="canvas" width="400" height="400" ></canvas>
最后看JavaScript部分代码,先来看最后的部分:
window.onload = function(){ getGLContext(); initShaders(); setupBuffer(); draw(); }
这是整个JS的入口函数,在onload事件触发后执行。其中调用了四个函数,从名字来看,作用分别是:
- 获取WebGL上下文环境
- 初始化着色器
- 设置缓冲区
- 绘制
这是WebGL绘制的一个基本的框架结构,通常在大型的3D应用中也会做类似的划分,其中前两个步骤通畅仅需要执行一次,而后面的步骤会随着场景属性变化而执行多次。下面我们分别来了解每一个步骤的具体做了什么。
获取WebGL上下文环境
获取上下文(context)是第一步,如果你有canvas的2D绘图经验就不会对此陌生。函数代码如下:
var gl = null; var canvas = document.getElementById('canvas'); var glProgram = null; function getGLContext() { var glContextNames = ['webgl', 'experimental-webgl']; for (var i = 0; i < glContextNames.length; i++) { try { gl = canvas.getContext(glContextNames[i]); } catch (e) {} if (gl) { gl.clearColor(74 / 255, 115 / 255, 94 / 255, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, canvas.width, canvas.height); break; } } }
由于有些用户的浏览器还不支持正式版本的 WebGL,因此需要通过 experimental-webgl
来获取。还有一些教程列出了更多包含浏览器前缀的名字,这些是 WebGL 刚刚推出时浏览器厂商所用的名字,现在早已经不在使用,即使有量也非常低,可以忽略不计。在编写本文时 IE11 以及 Edge 浏览器仍然不支持使用 webgl
这个名字来获取上下文。
获取成功后,gl
不为 null
,可以进而执行一些操作,这里指定了一个清除颜色(clearColor
这个方法名命名不太好啊,方法应该用动宾短语的啊,比如叫 setClearColor
)并执行了一次清除操作(clear
),接下来调用 viewport
(这个方法名也是……就不说了)来设置视口的大小,通常设置的尺寸与 canvas
元素尺寸相同。
初始化着色器
前面编写的着色器代码其实还只是文本,我们需要对其进行编译,并创建一个包含两个着色器代码的程序(program)对象。概念有点多,我们边看代码边说吧:
function initShaders() { // get shader source var vs_source = document.getElementById('shader-vs').innerHTML, fs_source = document.getElementById('shader-fs').innerHTML; // compile shaders vertexShader = makeShader(vs_source, gl.VERTEX_SHADER); fragmentShader = makeShader(fs_source, gl.FRAGMENT_SHADER); // create program glProgram = gl.createProgram(); // attach and link shaders to the program gl.attachShader(glProgram, vertexShader); gl.attachShader(glProgram, fragmentShader); gl.linkProgram(glProgram); if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) { alert("Unable to initialize the shader program."); } // use program gl.useProgram(glProgram); } function makeShader(src, type) { // compile the vertex shader var shader = gl.createShader(type); gl.shaderSource(shader, src); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("Error compiling shader: " + gl.getShaderInfoLog(shader)); } return shader; }
在 initShader
函数中我们首先获取代码的文本,接着对代码进行编译,由于编译顶点着色器和片断着色器的过程是一样的,我们把这部分代码单独放在 makeShader
函数里。makeShader
会创建 shader 对象,指定 shader 的源代码(shaderSource
方法)并进行编译(compileShader
方法),后面的 if
语句用来判断是否编译成功。成功后将 shader 对象返回。在得到两个 shader 对象后,我们创建一个 program 对象,通过 attachShader
将着色器附加到 program 对象,再调用 linkProgram
将两个着色器关联在一起,这样 GPU 才能够运行这个着色器程序。link 之后同样检查一下是否成功,成功后使用 useProgram
告知 WebGL 上下文当前所用的程序。
这个过程第一次看很繁琐,好在它仅在最开始的时候运行一次,你不需要频繁关注这里面的内容。
设置缓冲区
设置缓冲区代码如下:
function setupBuffer(){ var vertex = [ 0, 0, 0 ]; var vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW); var aVertexPosition = gl.getAttribLocation(glProgram, 'aPos'); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aVertexPosition); }
在WebGL中,缓冲区的作用至关重要,需要绘制的几何体的所有信息都是通过缓冲区来传递给着色器程序的,比如几何体的坐标、颜色等等。在上面的代码中,我们使用了顶点缓冲区对象(Vertex Buffer Object,VBO),该缓冲区就是用来存放几何体的顶点信息。对于一个点来说,它仅包含一个顶点,一个顶点包含x、y、z三个坐标值,因此我们定义的vertex数组包含三个元素:0、0、0。下面的代码创建了缓冲区对象,并进行绑定,再调用bufferData将vertex的内容放置在缓冲区当中。
在WebGL中,使用一个缓冲区前需要对其进行绑定操作,一旦绑定后,后续的缓冲区操作都会在当前绑定的缓冲区上进行,直到我们取消绑定或者绑定了其他的缓冲区对象。
bufferData方法传递数据时使用了Float32Array这个typed array,WebGL不支持直接使用JavaScript的原始数组类型。使用typed array可以以二进制方式来操作数据,同时也指定了数据的类型。
目前为止我们创建了缓冲区对象,并把点坐标数据放在了缓冲区中,下面的问题是:怎么把缓冲区中的数据传给着色器呢?我们继续看代码。
下面一行的getAttribLocation方法用来从shader中获取aPos属性的索引(你可以理解成地址或者引用,总之有了它就能操作shader对应的属性了),最关键的一句调用为:
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);
这个方法将aPos属性指向当前绑定的顶点缓冲区对象,这样aPos和vertexBuffer对象就关联在一起了。
vertexAttribPointer的原型如下:
void vertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLintptr offset);
我们依次看一下参数的含义:
- index:属性的索引,通过 getAttribLocation 可以获得。你可以理解成属性的地址。
- size:每个顶点包含多少个数值,最简单的情况是三个,即x、y、z三维坐标。
- type:指定缓冲区的数据类型。
- nomalized:和数据转换有关系,一般用false。
- stride:说明数据存储的方式,单位是字节。0表示一个属性的数据是连续存放的,非连续存放的情况我们后面会讲到。
- offset:该属性在每个顶点数据中偏移值,单位是字节并且是type的整数倍。这个值是配合stride一同使用的,0表示从头开始读取。
注意:调用vertexAttribPointer时如果当前没有绑定的顶点缓冲区对象则会报错。
最后还需要启用aPos属性。
绘制
在绘制的代码中,我们调用drawArrays将点绘制到屏幕上。
function draw(){ gl.drawArrays(gl.POINTS, 0, 1); }
drawArrays的原型:
void drawArrays(GLenum mode, GLint first, GLsizei count)
- mode:绘制模式,
gl.POINTS
常量表示点。 - first:第一个元素所在位置,即偏移量,32bit 整型。
- count:绘制的元素个数,即顶点的个数,32bit 整型。
drawArrays方法将会使用处于启用状态的包含坐标信息的属性,即本例中的aPos,我们看到它在之前已经被启用并且指向包含顶点信息的VBO。
我们最终在屏幕上会看到一个白色的点:
图解WebGL绘制流程
看了上面的示例,你可能觉得还是没有搞清楚全部过程,没有关系,WebGL本来就是一种很底层的绘图方式,学习曲线也很陡。下面我们来用流程图来描述这个绘制过程,相信你看了图之后会理解的更好。
WebGL渲染管线
渲染管线(Rendering Pipeline)是一个图形学中的术语,其实就是整个的绘制流程,流程都包含哪些步骤。wiki上的解释如下:
In 3D computer graphics, the graphics pipeline or rendering pipeline refers to the sequence of steps used to create a 2D raster representation of a 3D scene.
本例中涉及的过程可用下图来描述:
这个图近仅仅描述了整个步骤中的部分内容,后面我们会逐渐补充这张图的内容。
顶点缓冲区对象 Vertex Buffer Objects (VBO)
前面说过顶点缓冲区(后文统一用VBO来表示)用来存放几何体坐标,此外和顶点相关的其它信息也会存放在VBO中,例如法向量、颜色、纹理坐标。总之和顶点相关的信息都会放在这里。
顶点着色器 Vertex Shader
顶点着色器会针对每个顶点执行。其中的代码是针对一个顶点的数据操作,比如一个顶点的坐标、颜色等等。这些信息是通过属性来表示,属性指向相应的VBO,这样就可以将数据读取进来。
片断着色器 Fragment Shader
片断着色器负责计算输出到屏幕的像素颜色,在本文的例子中,就是负责给顶点上颜色。后面我们会看到更加负责的颜色计算过程,颜色会受到光照、纹理的影响而变化。
帧缓冲区 Framebuffer
这个概念在前面没有介绍到,单从代码当中我们看不到帧缓冲区。帧缓冲区用来存放最终需要显示的位图信息,它包含了一系列的像素点,这些像素点最终会被输出到屏幕上。
对上述程序做些修改
修改点大小
WebGL中可以指定点的尺寸,默认一个点的大小为1px,顶点着色器中内置的gl_PointSize变量可以用来修改点尺寸,单位为像素,我们在顶点着色器main函数里增加下面一行:
gl_PointSize = 20.0;
可以看到点被绘制成一个20px宽高的正方形。
gl_PointSize为float类型,因此要写成20.0而不能写成20,否则shader会在编译时报错。
修改点颜色
修改颜色也非常简单,我们把片断着色器的代码修改如下:
gl_FragColor = vec4(178.0/255.0, 105.0/255.0, 9.0/255.0, 1.0);
修改点位置
由于点坐标是在vertex变量中定义的,我们只需要修改其中的内容即可:
var vertex = [ 0.5, 0.5, 0 ];
我们修改x和y坐标为0.5,z坐标不变,你也可以自己修改z坐标,但会发现它并不影响显示效果。
做了以上三点修改,我们看到点的尺寸变大了,颜色和位置也都发生了变化。
写的好!看了好多关于 WebGL 文章,感觉这篇写的最适合入门。
国外也有个写的很详细的,这里是中文版:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
多谢分享。