《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
轴方向的阴影偏移量,默认为0
shadowOffsetY
:形状或路径y
轴方向的阴影偏移量,默认为0
shadowBlur
:模糊的像素数,默认为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
:上次操作没发生错误,值为0
gl.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()
,参数为:
x
y
- 宽度
- 高度
- 图像格式(几乎总是
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
,这样可以让帧缓冲区在下一次绘制之前保留其最后的状态,不过会造成性能损失。