《JavaScript高级程序设计》第十五章,使用canvas绘图。
我的体会是,WebGL比我以为的要更难些。
包括具备基本绘图能力的2D上下文和命名为WebGL的3D上下文。
基本用法
首先,设置元素的宽高来指定绘图区域大小;其次,通过canvasElem.getContext("2d")或canvasElem.getContext("webgl")取得绘图上下文,这样方能绘图。
1 | <canvas id="drawing" width="200" height="200"> |
1 | const drawing = document.getElementById("drawing"); |
* 一个神奇的方法
使用canvas.toDataURL()方法可以导出在canvas上绘制的图像,它接收图像的MIME类型格式作为参数。下例把画布上的内容导出为一个PNG格式图像。
1 | const drawing = document.getElementById("drawing"); |
2D上下文
用2D上下文提供的方法,可以绘制简单的2D图形。下面内容就权当记录一下这些API吧~
填充和描边
就是给图像填充颜色/渐变/图像的fillStyle,以及给图形边缘画线的strokeStyle。这俩属性的默认值都是"#000000",可以指定任何格式的颜色(颜色名、十六进制码、rgb、rgba、hsl、hsla)。
1 | const ctx = drawing.getContext("2d"); |
画矩形
没想到矩形居然是唯一一种可以直接在2D上下文绘制的形状!有几个方法:fillRect()、strokeRect()、clearRect(),它们接收4个参数:x坐标、y坐标、宽度、高度。前面两个要分别配合fillStyle和strokeStyle来使用。clearRect()则用于将画布上某个矩形区域变透明。
1 | const ctx = drawing.getContext("2d"); |
画路径
通过路径可以创造出复杂的形状和线条。画路径的第一步是,调用beginPath()方法,表示要开始绘制新路径。接着,用下面这些方法来实际地画:
arc(x, y, radius, startAngle, endAngle, counterclockwise)
以(x, y)为圆心画一条半径为radius、起始和结束弧度为startAngle和endAngle的弧线,最后一个参数是表示是否为逆时针的一个布尔值。arcTo(x1, y1, x2, y2, radius)
从上一点开始绘制一条弧线,并以radius为半径穿过(x1, y1),到(x2, y2)为止。bezierCurveTo(c1x, c1y, c2x, c2y, x, y)
从上一点开始绘制一条曲线,并以(c1x, c1y)和(c2x, c2y)为控制点,到(x, y)为止。* bezier意为贝塞尔lineTo(x, y)
从上一点开始绘制一条直线,到(x, y)为止。moveTo(x, y)
将绘图游标移动到(x, y),不画线。quadraticCurveTo(cx, cy, x, y)
从上一点开始绘制一条二次曲线,并以(cx, cy)作为控制点,到(x, y)为止。rect(x, y, width, height)
从点(x, y)开始绘制一个宽为width高为height的矩形,这个方法绘制的是矩形路径,而非strokeRect()和fillRect()绘制的形状。
创建完路径后,有几种选择。如果想把终点连回起点,可以用closePath();如果路径已完成,可以用fillStyle和fill()去填充颜色、用strokeStyle和stroke()去对路径描边;如果想在路径上创建一个剪切区域,可以用clip()。
举一个画线的例子:
1 | const ctx = drawing.getContext("2d"); |
由于路径使用得很频繁,所以有了一个叫isPointInPath(x, y)的方法,可以在路径被关闭之前确定画布上的某一点是否位于路径上。
写文本
主要用fillText()和strokeText(),它们接收4个参数:文本字符串、x坐标、y坐标、可选的最大像素宽度(这个参数的作用是,当字符串长超过宽度时,会适当地横向压缩)。它们都以下面3个属性为基础(这3个属性都有默认值,因此不需要每次都重新设置):
font
表示文本样式、大小、字体,如"10px Arial"。textAlign
表示文本对齐方式,有5种取值:start、end、left、right、center。一般建议用start/end而非left/right,这样对LTR和RTL语言都比较友好。textBaseline
表示文本基线,有6种取值:top、hanging、middle、alphabetic、ideographic、bottom。
有一些场景下,需要将文本控制在某一区域中,2D上下文提供了一个辅助方法measureText(),它可以根据font、textAlign、textBaseline计算处指定文本的大小。它将要绘制的文本作为参数,返回一个TextMetrics对象,目前这个对象只有个width属性,但之后还会增加的。
举个例子,假设我们想在140px的矩形区域里绘制文本,可以用下面的方法枚举来找到合适的字体大小:
1 | var fontSize = 100; |
变换
有几种变换:
rotate(angle)
围绕原点旋转图像angle弧度。scale(scaleX, scaleY)
缩放图像,在x方向乘以scaleX,在y方向乘以scaleY,二者的默认值为1。translate(x, y)
将坐标原点移动到(x, y)。transform(m11, m12, m21, m22, dx, dy)
直接修改变换矩阵,将其乘以矩阵[m11 m12 dx, m21 m22 dy, 0 0 1]。setTransform(m11, m12, m21, m22, dx, dy)
将变换矩阵重置为默认状态,然后再调用transform()。
举个例子,像之前的绘制表针,如果把原点换到表盘中心,就会很方便,因为所有计算都会基于(0, 0)而非(100, 100):
1 | const ctx = drawing.getContext("2d"); |
再基于上面的例子,演示一下当原点移到中心之后rotate()的好处,结果如下图:
1 | const ctx = drawing.getContext("2d"); |
跟踪状态
无论是变换还是fillStyle、strokeStyle等属性,都会在当前上下文中一直有效,除非再对上下文进行什么修改。虽然没法把上下文重置回默认值,但是有两个方法可以跟踪上下文状态变化:save()、restore()。当知道当前的所有属性或变换,会在将来有用时,可以用save()将当前的所有设置都存进一个栈结构中;然后可以对上下文进行其他修改;当想要回到之前保存的设置时,可以用restore()让栈结构向前返回一级,恢复之前的状态。连续调用save()可以把很多设置都保存到栈中,之后再连续调用restore()则可一级一级返回。
1 | ctx.fillStyle = "red"; |
图像
1 | const image = document.images[0]; |
除了可以传<img>,还可以传另一个<canvas>元素,这样就能把另一个画布的内容绘制到当前画布上。
阴影
2D上下文会根据下面几个属性的值,自动为形状或路径绘制阴影。只要在绘制前先为这几个属性设置好值,就能在之后绘制形状或路径的时候自动产生阴影。
shadowColor:阴影颜色,默认黑色shadowOffsetX:形状或路径x轴方向的阴影偏移量,默认为0shadowOffsetY:形状或路径y轴方向的阴影偏移量,默认为0shadowBlur:模糊的像素数,默认为0,即不模糊
1 | const ctx = drawing.getContext("2d"); |
渐变
首先需要创建一个指定大小的渐变实例CanvasGradient,有两种渐变方式如下。
线性渐变
createLinearGradient()
接收4个参数:起点x坐标、起点y坐标、终点x坐标、终点y坐标。径向渐变
createRadialGradient()
接收6个参数,对应两个圆的圆心和半径:起点圆圆心x坐标、起点圆圆心y坐标、起点圆半径、终点圆圆心x坐标、终点圆圆心y坐标、终点圆半径。
创建之后,需要通过addColorStop()来指定色标,它接收2个参数:作为色标的一个从0(开始的颜色)到1(结束的颜色)之间的数字、颜色值。
1 | // 从(30, 30)到(70, 70),起点为白色,终点为黑色的线性渐变 |
创建好渐变之后,就可以把fillStyle或strokeStyle设置为这个对象,从而使用渐变来绘制形状或描边。这里要注意的是渐变坐标和形状坐标要斟酌匹配一下,尽量不要出现渐变在图形中突然消失之类的。
1 | // 绘制红色矩形 |
模式
模式就是重复,可以用来填充或描边图形。首先要用createPattern()创建模式,此方法接收2个参数:一个<img>/<vedio>/<canvas>元素、表示如何重复的字符串(和CSS的background-repeat属性值相同,包括repeat、repeat-x、repeat-y、no-repeat)。
这里要注意的是,模式与渐变一样,都是从画布的(0, 0)开始的。将fillStyle设置为模式,只表示在特定的区域内「显示」重复的图像,而不是从特定的区域起点开始「绘制」重复的图像。
1 | const image = document.images[0]; |
像素操作
可以通过getImageData()来取得原始图像数据,此方法接收4个参数:欲取数据的区域的x坐标、y坐标、宽度、高度。它会返回ImageData的实例,每个ImageData都有3个属性:width、height、data,其中data是数组,按顺序保存着图像中每个像素的rgba值(0~255)。
1 | const imageData = ctx.getImageData(10, 5, 50, 50); |
还可以通过putImageData()来把图像数据回写,绘制到画布上,此方法接收3个参数:一个ImageData实例、x坐标、y坐标。通常的使用方式是,取得data、对data进行一些操作(比如将某像素点上rgb都设置为rgb的平均值等)、再把data写回ImageData填回去。
合成
- 透明度
globalAlpha
值为0到1之间的一个数字,表示后续操作的全局透明度,默认值为0(即不透明)。下面这个例子中,蓝色矩形位于红色矩形之上,如果没有在画蓝色矩形之前设置透明度,那么重叠的地方就会被蓝色覆盖,而现在蓝色矩形呈现半透明效果,因此可以透过它看到下面的红色矩形。
1 | // 绘制红色矩形 |
- 图形结合方式
globalCompositionOperation
值为字符串,取值如下(后:后画的;先:先画的),表示后绘制的图形应该怎样与先绘制的图形结合。其实文档上好像还不止这么些,可以参考参考。下面的例子中,蓝色矩形位于红色矩形之下。source-over:后在先上,为默认值source-in:后重叠部分可见,其它(指后非重叠+前)都透明source-out:后非重叠部分可见,其它(指后重叠+前)都透明source-atop:后重叠部分可见,先非重叠部分可见destination-over:后在先下destination-in:先重叠部分可见,其它(指先非重叠+后)都透明destination-out:先非重叠部分可见,其它(指先重叠+后)都透明destination-atop:先重叠部分可见,后非重叠部分可见lighter:重叠部分值相加,使该部分变亮copy:当有重叠时,只显示后画的xor:重叠部分异或操作(?)
1 | // 绘制红色矩形 |
3D上下文
WebGL是针对Canvas的3D上下文。与其它Web技术不同,WebGL不是W3C制定的标准,而是由Khronos Group制定的。这个group还设计了其它图形处理API,如WebGL基于的OpenGL ES 2.0。
书从这里就开始讲类型化数组,令人比较一头雾水,遂去看了Learn WebGL serials和WebGL Fundamentals,稍微理解了一点点点点…感觉要学计算机图形学+项目实践才能比较透彻,这里先鸽一下,把书里的先看完。
基础知识
ArrayBuffer
以JavaScript的精度,完全支持不了WebGL涉及的复杂计算需要,因此WebGL引入了类型化数组(typed array)。它其实也是数组,只不过元素被设置为了ArrayBuffer这个类型。ArrayBuffer对象以字节为单位,表示内存中的指定字节数。
1 | // 这个buffer含20字节 |
视图
可以使用ArrayBuffer来创建视图,常见的视图是DataView类型的,它的构造函数接收参数:一个ArrayBuffer、可选的字节偏移量(表示从该字节开始选择)、可选的字节数(表示选择这么多字节)。
1 | // 用整个buffer创建一个视图 |
读取和写入DataView时,要根据实际操作的数据类型,选择相应的getter和setter方法,下面是DataView支持的数据类型及响应的读写方法。其中offset是字节偏移量;littleEndian是一个布尔值,表示读写数值时是否采用小端字节序(即数据的最低有效位保存在低内存地址),默认为false的大端字节序(即数据的最低有效位保存在高内存地址)。
| 数据类型 | getter | setter |
|---|---|---|
| 有符号8位整数 | getInt8(offset) |
setInt8(offset, value) |
| 无符号8位整数 | getUint8(offset) |
setUint8(offset, value) |
| 有符号16位整数 | getInt16(offset, littleEndian) |
setInt16(offset, value, littleEndian) |
| 无符号16位整数 | getUint16(offset, littleEndian) |
setUint16(offset, value, littleEndian) |
| 有符号32位整数 | getInt32(offset, littleEndian) |
setInt32(offset, value, littleEndian) |
| 无符号32位整数 | getUint32(offset, littleEndian) |
setUint32(offset, value, littleEndian) |
| 32位浮点数 | getFloat32(offset, littleEndian) |
setFloat32(offset, value, littleEndian) |
| 64位浮点数 | getFloat64(offset, littleEndian) |
setFloat64(offset, value, littleEndian) |
当使用这样的读写方法,我们需要明确地管理数据细节,举例如下。
1 | const buffer = new ArrayBuffer(20); |
由于这里的offset使用字节偏移量而非数组元素数,因此可以通过不同的方式来访问同一字节,举例如下。在这个例子中,数值25以16位无符号整数形式写入,字节偏移量为0,即0000 0000 0001 1001;再以8位有符号整数形式读取数据,偏移量为0时,读取到的是16位中的前8位,即0000 0000。
1 | const buffer = new ArrayBuffer(20); |
以字节级别读写需要我们明确记住每个数据的具体保存位置,这会带来很多工作量,因此类型化视图应运而生。
类型化视图
一般也被称为类型化数组,分为以下几种Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array,它们都继承了DataView。
它们有三种构造方式,第一种和DataView一样,先创造buffer,再传入相应参数表示占有buffer的特定位置;第二种是传入希望数组保存的元素数,构造函数就会自动创建一个包含足够字节数的buffer;第三种则是把常规数组转换为类型化视图,只需传入常规数组即可。
1 | /*--- 构造方式1 ---*/ |
既然可以指定使用哪部分字节段,那很显然,同一个buffer中可以保存不同类型的数值,举例如下。
1 | // 使用buffer的一部分保存8位整数,另一部分保存16位整数 |
在这里,不同的类型,占用不同的字节数,那举个例子,20B的ArrayBuffer可以保存20个Int8Array/Uint8Array,或者10个Int16Array/Uint16Array,或者5个Int32Array/Uint32Array/Float32Array,或者2个Float64Array。
虽然占的字节数非常显而易见,但是每个视图构造函数都还是有提供一个BYTES_PER_ELEMENT属性,表示类型化数组中每个元素需要多少字节。可以利用它来辅助初始化,就像sizeof那样。
1 | alert(Uint8Array.BYTES_PER_ELEMENT); // 1 |
创建说完了,那就要说说访问了。类型化数组的访问使用方括号的下标语法,就和普通数组一样。只不过这里要注意数值是否够放的问题,如果相应元素指定的字节数放不下相应的值,那么实际保存的值则为最大可能值的模,譬如无符号16位整数所能表达的最大数值为65535,如果想存65536,那实际存的是0;如果想存65537,那实际存的是1。
1 | const uint16s = new Uint16Array(10); |
除了创建、访问,类型化数组还有一个subarray()方法,用它可以基于底层buffer的子集创建一个新的视图。它接收2个参数:开始元素的索引、结束元素的索引。
1 | const uint16s = new Uint16Array(10); |
这里的sub也是Uint16Array的一个实例,且底层与uint6s都基于同一个ArrayBuffer,通过大视图创建小视图的好处是可以避免修改到无关元素。
获取上下文
即canvas.getContext("webgl"),不过相比2D上下文,这里可以给getContext()传递第二个参数,作为一些配置项。该参数是一个对象,包含下面这些属性:
alpha:默认true,表示为上下文创建Alpha通道缓冲区depth:默认true,表示可使用16位深缓冲区stencil:默认false,表示可使用8位模板缓冲区antialias:默认true,表示将使用默认机制执行抗锯齿操作premultipliedAlpha:默认true,表示绘图缓冲区有预乘Alpha值preserveDrawingBuffer:默认false,表示在绘图完成后保留绘图缓冲区
1 | const drawing = document.getElementById("drawing"); |
API特点
基本上都是OpenGL的命名特点,毕竟基于它嘛。
对于常量,在OpenGL中一般带前缀GL_,而在WebGL中则去掉了相应的前缀,它以这种方式支持大多数OpenGL常量。比如OpenGL的GL_COLOR_BUFFER_BIT常量在WebGL中是gl.COLOR_BUFFER_BIT。
对于方法,很多方法名都会传达有关数据类型的信息。比如gl.uniform4f()意味着要接收4个浮点数,gl.uniform3i()意味着要接收3个整数,gl.uniform3iv()意味着要接收一个包含3个值的整数数组(v即vector)。
错误
WebGL操作一般不会抛出错误,而是需要我们在可能出错的地方,去手动调用gl.getError()来获取表示错误类型的常量,有下面几个取值:
gl.NO_ERROR:上次操作没发生错误,值为0gl.INVALID_ENUM:应传入WebGL常量时传错了gl.INVALID_VALUE:应传入无符号数时传了负值gl.INVALID_OPERATION:在当前状态下不能完成操作gl.OUT_OF_MEMORY:没有足够的内存去完成操作gl.CONTEXT_LOST_WEBGL:由于外部事件干扰(如断电)丢失了当前WebGL上下文
如果发生多个错误,需要反复调用它直到返回gl.NO_ERROR,这时可以使用循环来调用:
1 | let errorCode = gl.getError(); |
准备绘图
在操作WebGL上下文之前,需要使用某种颜色清除<canvas>,可通过方法clearColor(r, g, b, a)来进行。
1 | gl.clearColor(0, 0, 0, 1); // 黑色 |
上例中,先把清理颜色缓冲区设置为黑色,再调用clear()(与OpenGL的glClear()等价),传入的参数gl.COLOR_BUFFER_BIT告诉WebGL使用之前定义的颜色来填充相应区域。
视口与坐标
默认情况下,视口可以使用整个<canvas>区域,也可以通过viewport()来改变视口大小,只使用部分<canvas>,它接收4个参数:视口相对于<canvas>的x坐标、y坐标、宽度、高度。
定义视口坐标与常规网页坐标不一样:视口坐标原点(0, 0)在<canvas>左下角,x轴向右,y轴向上。
视口内部的坐标系与定义视口的坐标系也不一样:在视口内部,坐标原点(0, 0)是视口的中心点,因此视口左下角为(-1, -1),右上角为(1, 1)。
1 | // 用整个画布 |
缓冲区buffer
WebGL中呈现的3D图形里,比较重点的决定因素是Vertex(开始还打成了Vortex…奇异人生里那个俱乐部的名字),即各个平面之间的交点,它们的坐标信息会存储在类型化数组中,而我WebGL如果想用这些数据,那就要将其转换过来,放到我WebGL的缓冲区里。那么我们的操作就是,先创建缓冲区,再将该缓冲区指定给WebGL使用,再把原始数据填进该缓冲区。
1 | // 创建buffer |
这里,第二步将buffer绑定到WebGL上下文之后,所有的缓冲区操作都会在buffer对象中执行,因此第三步的填充数据无需明确传入buffer对象。这里的第三步中,第一个参数表示绑定的连接点,如果想用drawElements()输出缓冲区内容,也可以传入gl.ELEMENT_ARRAY_BUFFER;第二个参数处使用Float32Array初始化了buffer,通常都会用这个数据类型来保存顶点信息;第三个参数处则表示使用缓冲区的方式,包括下面几种取值:
gl.STATIC_DRAW:数据只加载一次,在多次绘图中使用gl.STREAM_DRAW:数据只加载一次,在几次绘图中使用gl.DYNAMIC_DRAW:数据动态改变,在多次绘图中使用
在包含缓冲区的页面重载之前,缓冲区始终保留在内存中。如果不想要某个缓冲区了,可以使用gl.deleteBuffer(buffer)来释放内存。
着色器shader
搞定了各个顶点的坐标信息,那就要知道这些顶点要以什么样的方式渲染到平面页面上,那就引入了shader。shader是OpenGL中的概念,WebGL里有两种:vertex shader和fragment shader,前者用于将3D顶点转换为需要渲染的2D点,后者用于准确计算要绘制的每个像素的颜色。WebGL的shader并不是用JavaScript写的,而是用GLSL写的(OpenGL Shading Language)。
每个shader都有一个main()方法,它会在绘图期间重复执行。为shader传递数据的方式有两种:通过attribute可以向vertex shader传入顶点信息,通过uniform可以向任何shader传入常量值。下面是一个简单的vertex shader例子:
1 | attribute vec2 aVertexPosition; |
在这个shader中,我们定义了一个叫aVertexPosition的attibute,它是包含2个元素的数组(vec2),表示x坐标和y坐标。即使只接收到了2个坐标,vertex shader也必须把一个包含4个方面信息的顶点赋值给特殊变量gl_Position,这里我们就创建了一个包含4个元素的数组(vec4),填补了缺失的坐标,把2D坐标转换成了3D坐标。
fragment shader与上面类似,除了只能通过uniform传入数据,举例如下:
1 | uniform vec4 uColor; |
在这个shader中,一个包含4方面信息(vec4)的统一颜色uColor被赋给变量gl_FragColor,表示绘图时使用的颜色。值得注意的是,uColor的值在这个shader内部不能改变。
程序program
浏览器不能理解GLSL,因此需要经过一系列处理。一般会按照这么个顺序处理:
- 把GLSL程序放到
<script>里并指定一个自定义的type(这样由于无法识别type,浏览器不会解析其中的内容) - 通过
text属性提取文本,即GLSL的代码内容 - 通过
gl.createShader()创建着色器对象(传入代表着色器类型的gl.VERTEX_SHADER或gl.FRAGMENT_SHADER) - 通过
gl.shaderSource()将GLSL代码和着色器对象关联 - 通过
gl.compileShader()来编译着色器
1 | <script type="x-webgl/x-vertex-shader" id="vertexShader"> |
1 | const vertexGLSL = document.getElementById("vertexShader").text; |
「着色器」都编写好了之后,就要考虑「着色器程序」了。这里的做法是:
- 通过
gl.createProgram()创建着色器程序 - 通过
gl.attachShader()和gl.linkProgram()将两个着色器对象与程序关联(将着色器封装到程序中) - 通过
gl.useProgram()将着色器程序与WebGL关联(通知WebGL可以使用这个程序了) - 调用
gl.useProgram()后,所有后续的绘图操作都将使用这个程序
1 | const program = gl.createProgram(); |
为shader传入值
之前定义的shader都必须接收一个值才能工作,为此需要:
- 先找到接收这个值的变量位置
- 再基于变量的位置来赋值
对于uniform变量,赋值过程如下:
1 | const uColor = gl.getUniformLocation(program, "uColor"); |
这里,通过gl.getUniformLocation()可以返回一个对象,表示uniform变量uColor在内存中的位置,再通过gl.uniform4fv()给uColor赋值。
对于attribute变量,也是差不多的赋值过程。
1 | const aVertexPosition = gl.getAttribLocation(program, "aVertexPosition"); |
这里,通过gl.getAttribLocation()可以找到attribute变量aVertexPosition在内存中的位置;接着通过gl.enableVertexAttribArray()来启用它;最后创建了指针,指向gl.bindBuffer()指定的缓冲区,并将其保存在aVertexPosition中,以便vertex shader使用。其中vertexAttribPointer()参数分别为:attribute位置、一个顶点的元素数(比如二维坐标为2,三维坐标为3)、顶点坐标类型、坐标是否标准化、表示取得下一值要跳过多少数组元素的步长值、起点偏移量。
调试shader和program
和WebGL中其它操作一样,着色器操作也可能失败,而且也是静默失败。如果想知道shader和program执行中是否发生了错误,需要手动查找。
1 | if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { |
这个例子中,shader部分通过gl.getShaderParameter()获取了shader的编译状态,当编译成功时,返回true,否则为false,当编译失败时,通过gl.getShaderInfoLog()可以获取错误信息;program部分类似,先获取了program的链接状态(是最常出现错误的地方),再打印日志。
绘图
WebGL只能绘制三种形状:点、线、三角。其它所有形状都是由这三种基本形状合成之后,再绘制到三维空间的。绘制操作由两种:用于数组缓冲区的gl.drawArrays()、用于元素数组缓冲区的gl.drawElements()。它们的第一个参数都是一个常量,表示绘制的形状,如下:
gl.POINTS
将每个顶点当成点来绘制。gl.LINES
将数组当成一系列顶点,并在顶点间画线。每个顶点既是起点又是终点,因此数组长度要为偶数。gl.LINE_LOOP
将数组当成一系列顶点,并在顶点间画线。线条依次穿过第一个顶点、第二个顶点…最后一个顶点、再回到第一个顶点,形成一个轮廓。gl.LINE_STRIP
与gl.LINE_LOOP类似,除了不把最后一个和第一个连起来。gl.TRIANGLES
将数组当成一系列顶点,并在顶点间画三角形。除非明确指定,每个三角形都单独绘制,不与其它三角形共享顶点。gl.TRIANGLES_STRIP
与gl.TRIANGLES类似,除了要复用顶点(将前三个顶点后的那个顶点,与它的前两个顶点结合,构成新的三角形),如数组中包含A、B、C、D,则第一个三角形连接ABC,第二个三角形连接BCD。gl.TRIANGLES_FAN
与gl.TRIANGLES类似,除了要复用顶点(将前三个顶点后的那个顶点,与它的前一个顶点和第一个顶点结合,构成新的三角形),如数组中包含A、B、C、D,则第一个三角形连接ABC,第二个三角形连接ACD。
下面举一个用gl.drawArrays()画三角形的例子。
1 | // 假设已经使用前面定义的着色器清除了视口 |
上面绘图的结果就是在(0, 1)、(1, -1)、(-1, -1)三点之间绘制了填充了黑色的三角形。
当修改gl.drawArrays()的第一个参数时,绘制三角形的方式会不同,如下:
纹理
1 | const image = new Image(); |
要注意的是,用作纹理的图像必须与包含页面来自同一个域,或者存在启用了CORS的服务器上。
像素操作
和2D上下文类似,通过WebGL上下文也能读取像素值,使用方法为readPixels(),参数为:
xy- 宽度
- 高度
- 图像格式(几乎总是
gl.RGBA) - 数据类型(用来指定保存在类型化数组中的数据的格式)
- 类型化数组(从帧缓冲区读取的像素信息保存处)。
其中前四个参数表示读取哪个区域内的像素。要注意的是,数据类型和类型化数组的关联有以下限制:
- 如果数据类型是
gl.UNSIGNED_BYTE,那么类型化数组必须是Uint8Array - 如果数据类型是
gl.UNSIGNED_SHORT_5_6_5、gl.UNSIGNED_SHORT_4_4_4_4、gl.UNSIGNED_SHORT_5_5_5_1,那么类型化数组必须是Uint16Array
1 | const pixels = new Uint8Array(25 * 25); |
这个例子中,从帧缓冲区读取了25*25像素的区域,将读取到的像素信息保存在pixels数组中,其中每个像素的颜色都由4个数组元素表示,分别为R、G、B、A,都介于0到255之间。
还要注意的是,在WebGL图像绘制发生前调用此方法,返回的像素数据没问题;而当绘制发生后,帧缓冲区会恢复原始的干净状态,调用此方法返回的像素数据就是清除缓冲区之后的状态。如果想要在绘制后读取像素数据,需要在初始化WebGL上下文时将preserveDrawingBuffer设置为true,这样可以让帧缓冲区在下一次绘制之前保留其最后的状态,不过会造成性能损失。