Canvas 是什么?
在 HTML5 之前,想在网页上显示图像,只能使用 <img>
。这个标签显示图像虽然简单,但只能显示静态的图片,不能进行实时绘制和渲染。因此,后来出现了一些第三方解决方案,如 FlashPlayer 等
HTML5 引入了 <canvas>
,允许 JavaScript 动态地绘制图形
- Canvas 2D context:基于像素的绘图系统,通过 JavaScript 脚本控制渲染过程
- 提供了简单的 2D 图形绘制功能,包括绘制基本形状、路径、文本和图像等。适用于绘制简单的图形和动画
- WebGL:Web Graphics Library,基于 OpenGL ES 2.0 标准的 JavaScript API
- 可以利用 GPU 进行硬件加速的 3D 图形渲染。WebGL 使用着色器(shaders)编程(GLSL 语言),允许更复杂和高性能的图形渲染
- 提供了强大的 3D 图形渲染功能,包括高级的着色器编程、纹理映射、深度缓冲、光照效果等。它适用于创建复杂的 3D 图形、游戏和交互式可视化
清空(用指定颜色填充)绘图区
<canvas id="canvas" width="500" height="500">当前浏览器不支持 Canvas</canvas>
// 获取 canvas 元素
const canvas = document.getElementById('canvas')
// 获取 WebGL 上下文
const gl = canvas.getContext('webgl') // 'webgl'、'webgl2'
if (!gl) {
console.error('当前浏览器不支持 WebGL')
return
}
// 设置清空的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空 canvas
gl.clear(gl.COLOR_BUFFER_BIT)
gl.clearColor(red, green, blue, alpha)
:设置清空的颜色- 红色,绿色,蓝色,不透明度
- 范围
[0.0, 1.0]
,任何值小于 0.0 或大于 1.0 的值会分别截断为 0.0 或 1.0 - 一旦指定了清除背景色,背景色就会驻存在 WebGL 系统中,在下一次调用
gl.clearColor()
前不会改变。即:将来还想用同一个颜色再清空一次绘图区,没必要再指定一次背景色
gl.clear(buffer)
:清除缓存区 ^clearbuffer
:指定待清空的缓冲区,位操作符可用来指定多个缓冲区gl.COLOR_BUFFER_BIT
:颜色缓存区gl.DEPTH_BUFFER_BIT
:深度缓存区gl.STENCIL_BUFFER_BIT
:模板缓存区
- 如果没有指定背景(没有调用
gl.clearColor
),将使用默认值:- 颜色缓存区:
(0.0, 0.0, 0.0, 0.0)
- 深度缓存区:
1.0
- 模板缓存区:
0
- 颜色缓存区:
- 错误:
INVALID_VALUE
,缓冲区不是以上三种类型
绘制一个点(版本 1)
着色器绘图机制
以 Canvas 2D context 的思维:
ctx.fillStyle = 'rgba(255, 0, 0, 1)' // 设置颜色
cxt.filRect(0, 0, 10, 10) // 画
可能会认为,WebGL 也差不多,比如:
gl.drawColor(1.0, 0.0, 0.0, 1.0)
gl.drawPoint(0, 0, 0, 10) // 点的位置和大小
不幸的是,事情没这么简单。WebGL 依赖于一种称为着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法
- 顶点着色器(Vertex shader):用来描述顶点特性(如位置、尺寸、颜色等),顶点是指二维或三维空间中的一个点(如二维或三维图形的端点或交点)
- 片元着色器(Fragment shader):进行逐片元处理(如颜色、光照等),片元是一个 WebGL 术语,可以将其理解为像素
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // 顶点坐标
gl_PointSize = 10.0; // 顶点尺寸
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 片元坐标
}
`
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
if (!initShaders(gl, vertexShaderSource, fragmentShaderSource)) {
console.error('初始化着色器失败')
return
}
// 设置清除颜色并清空 canvas
// 绘制图形
gl.drawArrays(gl.POINTS, 0, 1)
- 着色器程序:类似 C 语言,必须包含一个
main()
函数,main()
无参数(()
)、无返回值(void
) - 顶点着色器的内置变量:
vec4 gl_Position
:顶点位置,必须被赋值,否则着色器就无法正常工作vec4
:四维矢量 ^2- 齐次坐标
(x, y, z, w)
等价于三维坐标(x/w, y/w, z/w)
,所以如果齐次坐标的第四个分量是1
,就可以将它当做三维坐标来使用 w
的值必须是大于等于 0 的,如果w
趋近于 0 则它所表示的点将趋近无穷远。所以在齐次坐标系中可以有无穷的概念- 齐次坐标的存在,使得用矩阵乘法来描述顶点变换成为可能,三维图形系统在计算过程中,通常使用齐次坐标来表示顶点的三维坐标
- 齐次坐标
float gl_PointSize
:顶点尺寸(像素数),默认值为1.0
- 片元着色器的内置变量:
vec4 gl_FragColor
:片元颜色(RGBA 格式)
initShaders
:在 WebGL 系统内部建立和初始化着色器- 抽象的函数,真实环境并不存在,可以看成一个黑盒,现阶段无需知道其内部实现细节
- 行为:
- 着色器运行在 WebGL 系统中,而不是 JavaScript 程序中。即:WebGL 程序包括运行在浏览器中的 JavaScript 和运行在 WebGL 系统的着色器程序这两个部分
gl.drawArrays(mode, first, count)
:执行顶点着色器,按照mode
指定的方式绘制图形 ^1mode
:指定绘制的方式,可接收这些 常量符号first
:int
,指定从哪个顶点开始绘制count
:int
,指定绘制需要用到多少个顶点- 错误:
INVALID_ENUM
:传入的mode
参数不是前述参数之一INVALID_VALUE
:参数first
或count
是负数
- 当
gl.drawArrays
调用时,顶点着色器将被执行count
次,每次处理一个顶点,每个顶点将调用并逐行执行顶点着色器程序的main()
函数。顶点着色器执行完后,片元着色器就会开始执行,调用对应的main()
函数
WebGL 不需要交换颜色缓冲区
如果你拥有使用 OpenGL 的经验,你也许会觉得漏掉了什么东西:没有交换颜色缓冲区的代码。WebGL 的一个显著特征就是不需要交换颜色缓冲区
WebGL 坐标系统
WebGL 使用三维坐标系统(笛卡尔坐标系):
- X 轴:水平,正方向为右
- Y 轴:垂直,正方向为上
- Z 轴:垂直于屏幕,正方向为外
- 坐标原点:
<canvas>
中心
<canvas>
绘图区的坐标(二维坐标系统):
- X 轴:水平,正方向为右
- Y 轴:垂直,正方向为下
- 坐标原点:
<canvas>
左上角
WebGL 坐标与 <canvas>
坐标的对应关系:
绘制一个点(版本 2)
本节将讨论如何在 JS 和着色器之间传输数据。 版本 1 总是将点绘制在固定的位置,因为是直接编写(“硬编码”)在顶点着色器中的,虽然易于理解,但缺乏可扩展性
使用 attribute
变量
储存限定符变量必须声明成全局变量,数据将从着色器外部传给该变量
attribute
变量传输的是与顶点相关的数据uniform
变量传输的是对于所有顶点都相同(或与顶点无关)的数据
使用储存限定符变量的步骤(以 attribute
为例):
- 在顶点着色器中,声明
attribute
变量 - 将
attribute
变量赋值给对应属性(如gl_Position
) - 向
attribute
变量传输数据
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position; // 声明全局 attribute 变量,数据将从着色器外部传给该变量
void main() {
gl_Position = a_Position; // 将 attribute 变量赋值 gl_Position
gl_PointSize = 10.0;
}
`
// 片元着色器程序(GLSL ES)
// 与版本 1 相同,略
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
if (a_Position < 0) {
console.error(`attribute 变量 'a_Position' 位置获取失败`)
return
}
// 将数据从外部传输给该 attribute 变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// 设置清除颜色并清空 canvas
// 绘制图形
获取 attribute
变量的存储位置
gl.getAttribLocation(program, name)
:获取 name
指定的 attribute
变量的存储地址
program
:指定包含顶点着色器和片元着色器的着色器程序对象- 必须在调用
initShaders()
之后再访问gl.program
,因为是initShaders()
函数创建了这个程序对象
- 必须在调用
name
:指定想要获取其存储地址的attribute
变量的名称- 返回:
- 大于等于 0:
attribute
变量的存储地址 - -1:指定的
attribute
变量不存在,或其命名具有gl_
或webgl_
前缀
- 大于等于 0:
- 错误:
INVALID_OPERATION
:程序对象未能成功连接INVALID_VALUE
:name
参数的长度大于attribute
变量名的最大长度(默认 256 宇节)
向 attribute
变量赋值
gl.vertexAttrib3f(location, v0, v1, v2)
:将数据 (v0, v1, v2)
传给由 location
参数指定的 attribute
变量
location
:指定将要修改的attribute
变量的存储位置v0
、v1
、v2
:填充的值,单精度浮点型(float32
)- 错误:
INVALID_OPERATION
:没有当前的 program 对象INVALID_VALUE
:location
大于等于attribute
变量的最大数目(默认为 8)
- 三个浮点型如何填充 vec4?
- 实际上,如果省略了第 4 个参数,这个方法就会默认地将第 4 个分量设置为
1.0
。颜色值的第 4 个分量为1.0
表示该颜色完全不透明,而齐次坐标的第 4 个分量为1.0
使齐次坐标与三维坐标对应起来,所以1.0
是一个“安全”的第 4 分量
- 实际上,如果省略了第 4 个参数,这个方法就会默认地将第 4 个分量设置为
同族函数:
gl.vertexAttrib1f(location, v0)
gl.vertexAttrib2f(location, v0, v1)
gl.vertexAttrib3f(location, v0, v1, v2)
gl.vertexAttrib4f(location, v0, v1, v2, v3)
将数据传输给 location
参数指定的 attribute
变 量。gl.vertexAttrib1f()
仅传输一个值,这个值将被填充到 attribute
变量的第 1 个分量中,第 2、3 个分量将被设为 0.0
,第 4 个分量将被设为
1.0
。其他函数同理
也可以使用这些方法的矢量版本,它们的名字以“v”(vector)结尾,并接受类型化数组作为参数,函数名中的数字表示数组中的元素个数,如:
gl.vertexAttrib4fv(a_Position, new Float32Array([1.0, 2.0, 3.0, 1.0]))
通过鼠标点击绘点
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position; // 声明全局 attribute 变量,数据将从着色器外部传给该变量
void main() {
gl_Position = a_Position; // 将 attribute 变量赋值 gl_Position
gl_PointSize = 10.0;
}
`
// 片元着色器程序(GLSL ES)
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 获取 attribute 变量的存储位置
// 设置清除颜色并清空 canvas
// 注册鼠标点击事件响应函数
canvas.onmousedown = event => click(event, gl, a_Position)
响应鼠标点击事件
click
函数做什么:
- 通过鼠标点击位置,计算 WebGL 坐标
- 将位置存储在一个数组中
- 清空
<canvas>
- 根据数组的每个元素,在相应的位置绘制点
const g_points = [] // 鼠标点击位置数组
function click(event, gl, a_Position) {
// 通过鼠标点击位置,计算 WebGL 坐标
let x = event.clientX // 鼠标点击处的 x 坐标
let y = event.clientY // 鼠标点击处的 y 坐标
const rect = event.target.getBoundingClientRect() // canvas 坐标信息
x = ((x - rect.left) - canvas.height/2) / (canvas.height/2)
y = (canvas.width/2 - (y - rect.top)) / (canvas.width/2)
// 将位置存储在一个数组中
g_points.push([x, y])
// 清空 <canvas>
gl.clear(gl.COLOR_BUFFER_BIT)
// 根据数组的每个元素,在相应的位置绘制点
for (let [x, y] of g_points) {
// 将点的位置传输到 a_Position attribute 变量中
gl.vertexAttrib3f(a_Position, x, y, 0.0)
// 绘制点
gl.drawArrays(gl.POINTS, 0, 1)
}
}
- 坐标转换:浏览器视口坐标 →
<canvas>
坐标 → WebGL 坐标 - 点击位置数组:为什么要存数组,而不是仅仅记录最近一次鼠标点击的位置
- 因为 WebGL 的绘制操作是在颜色缓冲区中进行绘制的,绘制结束后系统将缓冲区中的内容显示在屏幕上,然后颜色缓冲区就会被重置,其中的内容会丢失
- 因此,有必要将每次鼠标点击的位置都记录下来,鼠标每次点击之后,程序都重新绘制所有的点
- 绘制前先清空
<canvas>
- 在绘制之后,颜色缓冲区就被 WebGL 重置为了默认的颜色
(0.0, 0.0, 0.0, 0.0)
(透明) - 如果不希望这样,就需要在每次绘制之前都调用
gl.clear()
来用指定的背景色清空
- 在绘制之后,颜色缓冲区就被 WebGL 重置为了默认的颜色
改变点的颜色
使用 uniform
变量
uniform
变量用于在顶点着色器和片元着色器传输“一致的”(不变的)数据,可以任意类型(详见)
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float; // 精度限定符:中等精度
uniform vec4 u_FragColor; // 声明全局 uniform 变量,数据将从着色器外部传给该变量
void main() {
gl_FragColor = u_FragColor; // 使用 uniform 变量
}
`
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 获取 uniform 变量的存储位置
const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')
if (!u_FragColor) {
console.error(`uniform 变量 'u_FragColor' 位置获取失败`)
return
}
// 将数据从外部传输给该 uniform 变量
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0)
// 设置清除颜色并清空 canvas
// 绘制图形
- 指定变量的范围(最大值与最小值)和精度
- 未指定会报错:
Failed to compile shader: No precision specified for (float)
获取 uniform
变量的存储位置
gl.getUniformLocation(program, name)
- 基本同 gl.getAttribLocation(program, name)
- 唯一区别:获取失败返回
null
而不是-1
向 uniform
变量赋值
gl.uniform4f(location, v0, v1, v2, v3)
与 gl.vertexAttrib3f 相似
同族函数:
gl.uniform1f(location, v0)
gl.uniform2f(location, v0, v1)
gl.uniform3f(location, v0, v1, v2)
gl.uniform4f(location, v0, v1, v2, v3)