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):清除缓存区 ^clear
    • buffer:指定待清空的缓冲区,位操作符可用来指定多个缓冲区
      • 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 指定的方式绘制图形 ^1
    • mode:指定绘制的方式,可接收这些 常量符号
    • firstint,指定从哪个顶点开始绘制
    • countint,指定绘制需要用到多少个顶点
    • 错误:
      • INVALID_ENUM:传入的 mode 参数不是前述参数之一
      • INVALID_VALUE:参数 firstcount 是负数
    • 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 为例):

  1. 在顶点着色器中,声明 attribute 变量
  2. attribute 变量赋值给对应属性(如 gl_Position
  3. 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_ 前缀
  • 错误:
    • INVALID_OPERATION:程序对象未能成功连接
    • INVALID_VALUEname 参数的长度大于 attribute 变量名的最大长度(默认 256 宇节)

attribute 变量赋值

gl.vertexAttrib3f(location, v0, v1, v2):将数据 (v0, v1, v2) 传给由 location 参数指定的 attribute 变量

  • location:指定将要修改的 attribute 变量的存储位置
  • v0v1v2:填充的值,单精度浮点型(float32
  • 错误:
    • INVALID_OPERATION:没有当前的 program 对象
    • INVALID_VALUElocation 大于等于 attribute 变量的最大数目(默认为 8)
  • 三个浮点型如何填充 vec4?
    • 实际上,如果省略了第 4 个参数,这个方法就会默认地将第 4 个分量设置为 1.0。颜色值的第 4 个分量为 1.0 表示该颜色完全不透明,而齐次坐标的第 4 个分量为 1.0 使齐次坐标与三维坐标对应起来,所以 1.0 是一个“安全”的第 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 函数做什么:

  1. 通过鼠标点击位置,计算 WebGL 坐标
  2. 将位置存储在一个数组中
  3. 清空 <canvas>
  4. 根据数组的每个元素,在相应的位置绘制点
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() 来用指定的背景色清空

改变点的颜色

使用 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)

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)