将非坐标数据传入顶点着色器

三维图形不仅仅只有顶点坐标信息,还可能有一些其他的信息,包括顶点的颜色、顶点的尺寸(大小)等

创建多个缓冲区对象

第一种方案是不同的信息创建各自的缓冲区对象

// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position; // 顶点坐标信息
attribute float a_PointSize; // 顶点的尺寸(大小)
void main() {
  gl_Position = a_Position;
  gl_PointSize = a_PointSize;
}
`
 
// 片元着色器程序(GLSL ES)
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
 
// 设置顶点位置
const n = initVertexBuffers(gl)
if (n < 0) {
  console.error('设置顶点位置失败')
  return
}
 
// 设置清除颜色并清空 canvas
 
// 绘制图形(三个点)
gl.drawArrays(gl.POINTS, 0, n) // n 是 3
 
/**
 * 创建缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器
 */
function initVertexBuffers(gl) {
  // 点的个数
  const n = 3
  // 三个点的坐标信息
  const vertices = new Float32Array([
    0.0, 0.5, -0.5, -0.5, 0.5, -0.5
  ])
  // 三个点的尺寸信息
  const sizes = new Float32Array([
    10.0, 20.0, 30.0
  ])
 
  try {
    // 绑定坐标信息
    initBuffer(gl, 'a_Position', vertices, 2)
    // 绑定尺寸信息
    initBuffer(gl, 'a_PointSize', sizes, 1)
  } catch(e) {
    console.error(e.message)
    return -1
  }
 
  return n
}
 
/**
 * 初始化缓冲区对象,并传入数据,并绑定到对应 attribute 变量
 */
function initBuffer(gl, attributeName, data, size) {
  // 创建缓冲区对象
  const buffer = gl.createBuffer()
  if (!buffer) {
    throw new Error('创建缓冲区对象失败')
  }
  
  // 将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  
  // 向缓冲区对象中写入数据
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
  
  // 获取 attribute 变量的存储位置
  const attributeVar = gl.getAttribLocation(gl.program, attributeName)
  if (attributeVar < 0) {
    throw new Error(`attribute 变量 '${attributeName}' 位置获取失败`)
  }
  
  // 将缓冲区对象分配给 a_Position 变量
  gl.vertexAttribPointer(attributeVar, size, gl.FLOAT, false, 0, 0)
  
  // 开启 a_Position 变量与分配给它的缓冲区对象
  gl.enableVertexAttribArray(attributeVar)
}

只用一个缓冲区对象

第二种方案是只用一个缓冲区对象,根据 gl.vertexAttribPointer() 步进和偏移参数决定数据传入不同的 attribute 变量

// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position; // 顶点坐标信息
attribute float a_PointSize; // 顶点的尺寸(大小)
void main() {
  gl_Position = a_Position;
  gl_PointSize = a_PointSize;
}
`
 
// 片元着色器程序(GLSL ES)
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点位置
// 设置清除颜色并清空 canvas
// 绘制图形(三个点)
 
/**
 * 创建缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器
 */
function initVertexBuffers(gl) {
  // 点的个数
  const n = 3
  // 三个点的坐标和尺寸信息
  const verticesSizes = new Float32Array([
    0.0, 0.5, 10.0, // 第一个点
    -0.5, -0.5, 20.0, // 第二个点
    0.5, -0.5, 30.0 // 第三个点
  ])
 
  // 创建缓冲区对象
  const buffer = gl.createBuffer()
  if (!buffer) {
    console.error('创建缓冲区对象失败')
    return -1
  }
  
  // 将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  
  // 向缓冲区对象中写入数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW)
  
  // 获取 attribute 变量的存储位置
  const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.error(`attribute 变量 'a_Position' 位置获取失败`)
    return -1
  }
  const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.error(`attribute 变量 'a_PointSize' 位置获取失败`)
    return -1
  }
 
  const FSIZE = verticesSizes.BYTES_PER_ELEMENT // 类型化数组每个元素占用字节数
 
  // 将缓冲区对象分配给 a_Position 变量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0)
  // 开启 a_Position 变量与分配给它的缓冲区对象
  gl.enableVertexAttribArray(a_Position)
 
  // 将缓冲区对象分配给 a_PointSize 变量
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2)
  // 开启 a_PointSize 变量与分配给它的缓冲区对象
  gl.enableVertexAttribArray(a_PointSize)
 
  return n
}

gl.vertexAttribPointer() 方法的函数签名

修改颜色(varying 变量)

之前 使用了一个 uniform 变量来将颜色信息传入片元着色器

因为是个“一致的”(uniform),而不是“可变的”(varying)变量,没法为每个顶点都准备一个值,所以那个程序中的所有顶点都只能是同一个颜色

可以使用一种新的 varying 变量向片元着色器中传入数据。varying 变量的作用是从顶点着色器向片元着色器传输数据详见

// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color; // varying 变量
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;
  v_Color = a_Color; // 将数据传给片元着色器
}
`
 
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
varying vec4 v_Color; // 声明相同的 varying 变量,用于接收从顶点着色器传来的数据
void main() {
  gl_FragColor = v_Color;
}
`
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点位置
// 设置清除颜色并清空 canvas
// 绘制图形(三个点)
 
/**
 * 创建缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器
 */
function initVertexBuffers(gl) {
  // 点的个数
  const n = 3
  // 三个点的坐标和颜色信息
  const verticesColors = new Float32Array([
    0.0, 0.5, 1.0, 0.0, 0.0, // 第一个点
    -0.5, -0.5, 0.0, 1.0, 0.0, // 第二个点
    0.5, -0.5, 0.0, 0.0, 1.0 // 第三个点
  ])
 
  // 创建缓冲区对象
  // 将缓冲区对象绑定到目标
  // 向缓冲区对象中写入数据
  // 获取 attribute 变量的存储位置
 
  // 将缓冲区对象分配给 attribute 变量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
 
  // 开启 attribute 变量与分配给它的缓冲区对象
}

在 WebGL 中,如果顶点着色器与片元着色器中有类型和命名都相同的 varying 变量,那么顶点着色器赋给该变量的值就会被自动地传入片元着色器

彩色三角形

将上面代码的绘制三个点改成绘制三角形:

// 绘制图形(三角形)
// gl.drawArrays(gl.POINTS, 0, n) // n 是 3
gl.drawArrays(gl.TRIANGLES, 0, n)

会得到一个颜色平滑过渡的、三个角各是红、绿、蓝颜色的三角形:

只改变了一个参数,程序的运行结果却从三个不同颜色的孤立的点变成了一个颜色平滑过渡的三角形,到底发生了什么?

这需要理解片元着色器怎样进行所谓的逐片元操作

几何形状的装配和光栅化

  • 图形装配过程:将孤立的顶点坐标装配成几何图形。几何图形的类别由 gl.drawArrays() 函数的第一个参数 决定
  • 光栅化过程:将装配好的几何图形转化为片元,即:将矢量的几何图形转变为栅格化的片元(像素)

上例 中绘制彩色三角形,gl.drawArrays() 的参数 n3,顶点着色器将被执行 3 次

图形装配和光栅化过程是:

  1. 执行顶点着色器,缓冲区对象中的第一个坐标数据 (0.0, 0.5) 被传递给 attribute 变量 a_Position(z 分量和 w 分量使用默认值)。一旦一个顶点的坐标被赋值给了 gl_Position,它就进入了图形装配区域,并暂时储存在那里
  2. 再次执行顶点着色器,类似地,将第二个坐标 (-0.5, -0.5, 0.0, 1.0) 传入并储存在装配区
  3. 第 3 次执行顶点着色器,将第三个坐标 (0.5, -0.5, 0.0, 1.0) 传入并储存在装配区。现在,顶点着色器执行完毕,三个顶点坐标都已经处在装配区了
  4. 开始装配图形。使用传入的点坐标,根据 gl.drawArrays() 的第一个参数信息来决定如何装配(这里是 gl.TRIANGLES,三角形)
  5. 显示在屏幕上的三角形是由片元(像素)组成的,所以还需要将图形转化为片元,这个过程被称为光栅化(rasterization)。光栅化之后,就得到了组成这个三角形的所有片元

调用片元着色器

光栅化结束后,就开始逐片元调用片元着色器,每调用一次处理一个片元。对于每个片元,片元着色器计算出该片元的颜色,并写人颜色缓冲区。当所有片元被处理完成,浏览器就会显示出最终的结果

上图为了示意,只显示了 10 个片元,实际上,片元数目就是这个三角形最终在屏幕上所覆盖的像素数

根据片元位置确定颜色

做个试验:尝试根据片元的位置来确定片元颜色,这样可以证明片元着色器对每个片元都执行了一次

光栅化过程生成的片元都是带有坐标信息的,调用片元着色器时这些坐标信息也随着片元传了进去,可以通过片元着色器中的内置变量来访问片元的坐标

// 顶点着色器程序(GLSL ES)
 
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float; //  精度限定符:中等精度
uniform float u_Width;
uniform float u_Height;
void main() {
  gl_FragColor = vec4(gl_FragCoord.x / u_Width, gl_FragCoord.y / u_Height, 0.0, 1.0);
}
`
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点位置
// 获取 uniform 变量位置
 
// 给 uniform 变量传入 canvas 的宽高
gl.uniform1f(u_Width, gl.drawingBufferWidth)
gl.uniform1f(u_Height, gl.drawingBufferHeight)
 
// 设置清除颜色并清空 canvas
// 绘制图形(三角形)
  • vec4 gl_FragCoord:该内置变量的第 1 个和第 2 个分量表示片元在 <canvas> 坐标系统中的坐标值
    • 注意:<canvas> 中的 Y 轴方向和 WebGL 系统中的 Y 轴方向是相反的
  • WebGL 中的颜色分量值区间为 [0.0, 1.0],所以需要将 Y 轴坐标除以 <canvas> 元素的高度以将其压缩到 [0.0, 1.0] 之间。X 轴同理
    • gl.drawingBufferWidth:颜色缓冲区的宽度,即 <canvas> 元素的宽度
    • gl.drawingBufferHeight:颜色缓冲区的高度,即 <canvas> 元素的高度

varying 变量的作用和内插过程

回到之前的问题,为什么在顶点着色器中只是指定了每个顶点的颜色,最后得到了一个具有渐变色彩效果的三角形呢?

把顶点的颜色赋值给了顶点着色器中的 varying 变量 v_Color,它的值被传给片元着色器中的同名、同类型变量。但是,更准确地说,顶点着色器中的 v_Color 变量在传入片元着色器之前经过了内插过程。所以,片元着色器中的 v_Color 变量和顶点着色器中的 v_Color 变量实际上并不是一回事,这也正是将这种变量称为“varying”(变化的)变量的原因

例如,考虑一条两个端点的颜色不同的线段,一个端点为红色 (1.0, 0.0, 0.0),另一个端点为蓝色 (0.0, 0.0, 1.0)。在顶点着色器中向 varying 变量 v_Color 赋上这两个颜色,那么 WebGL 就会自动地计算出线段上的所有点(片元)的颜色,并赋值给片元着色器中的 varying 变量 v_Color。这个过程就被称为内插过程(interpolation process)

在矩形表面贴上图像

纹理映射(texture mapping):就是将一张图像(就像一张贴纸)映射(贴)到一个几何图形的表面上去

  • 这张图片可以称为纹理图像(texture image)或纹理(texture)
  • 纹理映射的作用:根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色
  • 组成纹理图像的像素被称为纹素(texels, texture elements),每一个纹素的颜色都使用 RGB 或 RGBA 格式编码

WebGL 中进行纹理映射的步骤:

  1. 准备纹理图像,可以是浏览器支持的任意格式的图像
  2. 指定映射方式,就是确定“几何图形的某个片元”的颜色如何取决于“纹理图像中哪个(或哪几个)像素”
    • 使用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖
    • 使用纹理坐标(texture coordinates)来确定纹理图像的哪部分将覆盖到几何图形上
  • 加载纹理图像,对其进行一些配置,以在 WebGL 中使用它
  • 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord; // 顶点纹理坐标
varying vec2 v_TexCoord; // 片元着色器接收顶点纹理坐标
void main() {
  gl_Position = a_Position;
  v_TexCoord = a_TexCoord;
}
`
 
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float; //  精度限定符:中等精度
uniform sampler2D u_Sampler; // 声明全局 uniform 变量,数据将从着色器外部传给该变量
varying vec2 v_TexCoord; // 片元着色器接收顶点纹理坐标
void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
 
// 设置顶点信息
const n = initVertexBuffers(gl)
if (n < 0) {
  console.error('设置顶点信息失败')
  return
}
 
// 设置清除颜色并清空 canvas
 
// 配置纹理
if (!initTextures(gl, n)) {
  console.error('配置纹理失败')
}

纹理映射的过程需要顶点着色器和片元着色器二者的配合:首先在顶点着色器中为每个顶点指定纹理坐标,然后在片元着色器中根据每个片元的纹理坐标从纹理图像中抽取纹素颜色

纹理坐标

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色

WebGL 系统中的纹理坐标系统是二维的。为了将纹理坐标和广泛使用的 xy 坐标区分开来, WebGL 使用 s 和 t 命名纹理坐标(另一种常用的命名习惯是用 u、v 为纹理坐标的名称)

纹理图像四个角的坐标为左下角 (0.0, 0.0),右下角 (1.0, 0.0),右上角 (1.0, 1.0) 和左上角 (0.0, 1.0)。这使得坐标值与图像自身的尺寸无关,不管是 128×128 还是 128×256 的图像,其右上角的纹理坐标始终是 (1.0, 1.0)

通过建立几何图形的矩形四个项点与纹理坐标的对应关系,就可以确定怎样将纹理图像贴上去

为每个顶点设置纹理坐标

将纹理坐标传入顶点着色器,与将其他顶点数据(如颜色)传人顶点着色器的方法是相同的,这里将纹理坐标和顶点坐标写在同一个缓冲区中

/**
 * 创建缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器
 */
function initVertexBuffers(gl) {
  // 点的个数
  const n = 4
  // 点的信息
  const verticesTexCoords = new Float32Array([
    // 顶点坐标,纹理坐标
    -0.5, 0.5, 0.0, 1.0, // 左上
    -0.5, -0.5, 0.0, 0.0, // 左下
    0.5, 0.5, 1.0, 1.0, // 右上
    0.5, -0.5, 1.0, 0.0, // 右下
  ])
 
  // 创建缓冲区对象
  // 将缓冲区对象绑定到目标
  // 向缓冲区对象中写入数据
  // 获取 attribute 变量的存储位置
 
  // 将缓冲区对象分配给 attribute 变量
  const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
 
  // 开启 attribute 变量与分配给它的缓冲区对象
}

创建和加载纹理

/**
 * 创建和加载纹理
 */
function initTextures(gl, n) {
  // 创建纹理对象
  const texture = gl.createTexture()
  if (!texture) {
    console.error('创建纹理对象失败')
    return false
  }
 
  // 获取 u_Sampler 的存储位置
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
  if (!u_Sampler) {
    console.error(`获取 uniform 变量 'u_Sampler' 失败`)
    return false
  }
 
  // 加载图片,配置纹理
  const img = new Image()
  img.onload = () => loadTexture(gl, n, texture, u_Sampler, img)
  img.onerror = () => console.error('图片加载失败')
  img.src = '../assets/sky.jpg'
 
  return true
}
  • gl.createTexture():创建纹理对象以存储纹理图像
    • 返回值:
      • null:新创建的纹理对象
      • null:创建纹理对象失败
  • gl.deleteTexture(texture):删除已创建的纹理对象
    • texture:待删除的纹理对象
  • 加载图片:
    • 出于安全性考虑,WebGL 不允许使用跨域纹理图像
    • 图像的加载过程是异步的

为 WebGL 配置纹理

/**
 * 为 WebGL 配置纹理
 */
function loadTexture(gl, n, texture, u_Sampler, img) {
  // 对纹理图像进行 y 轴反转
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
 
  // 激活 0 号纹理单元
  gl.activeTexture(gl.TEXTURE0)
 
  // 绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)
 
  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
 
  // 将纹理图像分配给纹理对象
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img)
 
  // 将 0 号纹理单元传递给片元着色器
  gl.uniform1i(u_Sampler, 0)
 
  // 清空 canvas
  gl.clear(gl.COLOR_BUFFER_BIT)
 
  // 绘制矩形
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
}

图像 Y 轴反转

WebGL 纹理坐标中的 t 轴和图片的坐标 y 轴方向是相反的,所以要对纹理图像进行 y 轴反转

gl.pixelStorei(pname, param):处理加载得到的图像

  • pname:可以是以下二者之一
    • gl.UNPACK_FLIP_Y_WEBGL:对图像进行 Y 轴反转,默认是 false
    • gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:将图像 RGB 颜色值的每一个分量乘以 A,默认是 false
  • param:指定 1(true)或 0(false),必须为整数
  • 错误:
    • INVALID_ENUMpname 不是合法的值

激活纹理单元

WebGL 通过纹理单元(texture unit)机制来同时使用多个纹理,每个纹理单元有一个单元编号来管理一张纹理图像

系统支持的纹理单元个数取决于硬件和浏览器的 WebGL 实现,默认情况下,WebGL 至少支持 8 个纹理单元,内置的变量 gl.TEXTURE0gl.TEXTURE1 …… gl.TEXTURE7 各表示一个纹理单元

在使用纹理单元之前,需要先激活它

gl.activeTexture(texUnit):激活指定纹理单元

  • texUnit:待激活的纹理单元,如:gl.TEXTURE0gl.TEXTURE1 …… gl.TEXTURE7
  • 错误
    • INVALID_ENUMtexUnit 不是合法的值

绑定纹理对象

绑定缓冲区对象类似,要告诉 WebGL 系统纹理对象使用的是哪种类型的纹理

gl.bindTexture(target, texture):开启 texture 指定的纹理对象,并将其绑定到 target(目标)上。此外,如果已经通过 gl.activeTexture() 激活了某个纹理单元,则纹理对象也会绑定到这个纹理单元上

  • target:目标,可以是以下二者之一:
    • gl.TEXTURE_2D:二维纹理
    • gl.TEXTURE_CUBE_MAP:立方体纹理(超出本书的讨论范围)
  • texture:待绑定的纹理对象
  • 错误
    • INVALID_ENUMtarget 不是合法的值

在 WebGL 中,没法直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象

配置纹理对象的参数

配置纹理对象的参数,以此来设置纹理图像映射到图形上的具体方式:如何根据纹理坐标获取纹素颜色、按哪种方式重复填充纹理等

gl.texParameteri(target, pname, param):将 param 的值赋给绑定到 target 的纹理对象的 pname 参数上

  • targetgl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP
  • pname:纹理参数
  • param:纹理参数的值

四种纹理参数:

纹理参数 pname描述默认值
gl.TEXTURE_MAG_FILTER纹理放大方法,当纹理的绘制范围比纹理本身更大时,如何获取纹素颜色gl.LINEAR
gl.TEXTURE_MIN_FILTER纹理缩小方法,当纹理的绘制范围比纹理本身更小时,如何获取纹素颜色gl.NEAREST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S纹理水平填充方法,如何对纹理图像左侧或右侧的区域进行填充gl.REPEAT
gl.TEXTURE_WRAP_T纹理垂直填充方法,如何对纹理图像上方和下方的区域进行填充gl.REPEAT

可以赋值给 gl.TEXTURE_MAG_FILTERgl.TEXTURE_MIN_FILTER 的非金字塔纹理类型常量:

意思描述
gl.NEAREST最近邻插值使用原纹理上距离([[047.A星、Floyd、Bellman-Ford 与 SPFA#mhdjl
gl.LINEAR双线性插值使用距离新像素中心最近的四个像素的颜色值的加权平均,作为新像素的值(与 NEAREST 相比,该方法图像质量更好,但是会有较大的开销)

gl.TEXTURE_MIN_FILTER 除了上述两种取值,还可以设置金字塔(MIPMAP)纹理,而 gl.TEXTURE_MAG_FILTER 只有上述两种取值

可以赋值给 gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T 的常量:

描述
gl.REPEAT平铺式的重复纹理
gl.MIRRORED_REPEAT镜像对称式的重复纹理
gl.CLAMP_TO_EDGE使用纹理图像边缘值
  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    • 默认情况下,gl.TEXTURE_MIN_FILTER 使用的是 gl.NEAREST_MIPMAP_LINEAR。如果你的纹理没有生成 MIPMAP(例如,使用 gl.generateMipmap),那么默认的 MIPMAP 过滤模式将无法正常工作,导致纹理无法显示,通常会显示为黑色
    • 显式设置为 gl.LINEAR 可以避免这种情况,因为这种模式不依赖 MIPMAP
  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    • 默认情况下,gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 使用的是 gl.REPEAT,这意味着纹理将重复平铺。如果纹理的尺寸不是 2 的幂次方(例如,非正方形或非 2 的幂次方大小的纹理),默认的重复平铺模式可能会导致纹理无法正确显示,通常会显示为黑色
    • 显式设置为 gl.CLAMP_TO_EDGE 可以避免这种情况,因为这种模式会将纹理坐标限制在 [0, 1] 范围内,并将边缘的像素扩展到纹理边界

将纹理图像分配给纹理对象

gl.texImage2D(target, level, internal_format, format, type, image):将 image 指定的图像分配给绑定到 target 上的纹理对象

  • targetgl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP
  • level:传入 0(实际上,该参数是为金字塔(MIPMAP)纹理准备的,本书不涉及)
  • internal_format:图像的内部格式,详见
  • format:纹理数据的格式,必须使用与 internal_format 相同的值
    • gl.RGB(R, G, B, 1.0)
    • gl.RGBA(R, G, B, A)
    • gl.ALPHA(0.0, 0.0, 0.0, A)
    • gl.LUMINANCE(L, L, L, 1.0),其中 L 为流明(luminance),表示感知到的物体表面的亮度。通常使用物体表面红、绿、蓝颜色分量值的加权平均来计算流明
    • gl.LUMINANCE_ALPHA(L, L, L, A)
  • type:纹理数据的类型
    • gl.UNSIGNED_BYTE:无符号整型,每个颜色分量占据 1 字节
    • gl.UNSIGNED_SHORT_5_6_5:RGB 每个分量分别占据 5、6、5 比特
    • gl.UNSIGNED_SHORT_4_4_4_4:RGBA 每个分量分别占据 4、4、4、4 比特
    • gl.UNSIGNED_SHORT_5_5_5_1:RGBA 每个分量分别占据 5、5、5、1 比特
  • image:包含纹理图像的 Image 对象
  • 错误
    • INVALID_ENUMtarget 不是合法的值
    • INVALID_OPERATION:当前 target 上没有绑定纹理对象

将纹理单元传递给片元着色器

片元着色器程序(GLSL ES)中 uniform sampler2D u_Sampler;

使用 uniform 变量来表示纹理,因为纹理图像不会随着片元变化,其类型为专用于纹理的数据类型:

  • sampler2D:绑定到 gl.TEXTURE_2D 上的纹理数据类型
  • samplerCube:绑定到 gl.TEXTURE_CUBE_MAP 上的纹理数据类型

Sampler 意为“取样器”,因为从纹理图像中获取纹素颜色的过程,相当于从纹理图像中“取样”

gl.uniform1i(location, v):将一个整数值传递给着色器中的 uniform 变量

  • locationuniform 变量储存位置
  • v:整数值,这里是纹理单元 0

从顶点着色器向片元着色器传输纹理坐标

// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord; // 顶点纹理坐标
varying vec2 v_TexCoord; // 片元着色器接收顶点纹理坐标
void main() {
  gl_Position = a_Position;
  v_TexCoord = a_TexCoord;
}
`
 
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
// ...
varying vec2 v_TexCoord; // 片元着色器接收顶点纹理坐标
// ...
`

在片元着色器中获取纹理像素颜色

// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float; //  精度限定符:中等精度
uniform sampler2D u_Sampler; // 声明全局 uniform 变量,数据将从着色器外部传给该变量
varying vec2 v_TexCoord; // 片元着色器接收顶点纹理坐标
void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`

vec4 texture2D(sampler2D sampler, vec2 coord):从 sampler 指定的纹理上获取 coord 指定的纹理坐标处的像素颜色

  • sampler:定纹理单元编号
  • coord:纹理坐标
  • 返回值:
    • 纹理坐标处像素的颜色值,其格式由 gl.texImage2D()internal_format 参数决定,详见
    • 如果由于某些原因导致纹理图像不可使用,那就返回 (0.0, 0.0, 0.0, 1.0)(黑色)

使用多幅纹理

WebGL 可以同时处理多幅纹理,纹理单元就是为了这个目的而设计的

// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
  gl_Position = a_Position;
  v_TexCoord = a_TexCoord;
}
`
 
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_Sampler0; // 纹理单元 0
uniform sampler2D u_Sampler1; // 纹理单元 1
varying vec2 v_TexCoord;
void main() {
  vec4 color0 = texture2D(u_Sampler0, v_TexCoord);
  vec4 color1 = texture2D(u_Sampler1, v_TexCoord);
  gl_FragColor = color0 * color1; // 分量乘法
}
`
 
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点信息
// 设置清除颜色并清空 canvas
// 配置纹理
 
/**
 * 创建和加载纹理
 */
function initTextures(gl, n) {
  // 创建纹理对象(两个)
  const texture0 = gl.createTexture()
  const texture1 = gl.createTexture()
  if (!texture0 || !texture1) {
    console.error('创建纹理对象失败')
    return false
  }
 
  // 获取 u_Sampler 的存储位置
  const u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0')
  const u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1')
  if (!u_Sampler0 || !u_Sampler1) {
    console.error(`获取 uniform 变量 'u_Sampler' 失败`)
    return false
  }
 
  // 加载图片,配置纹理
  const img0 = new Image()
  img0.onload = () => loadTexture(gl, n, texture0, u_Sampler0, img0, 0)
  img0.onerror = () => console.error(`图片 'sky.jpg' 加载失败`)
  img0.src = '../assets/sky.jpg'
  const img1 = new Image()
  img1.onload = () => loadTexture(gl, n, texture1, u_Sampler1, img1, 1)
  img1.onerror = () => console.error(`图片 'circle.gif' 加载失败`)
  img1.src = '../assets/circle.gif'
 
  return true
}
 
// 标记纹理单元是否已经就绪
let texUnit0Actioned = false, texUnit1Actioned = false
/**
 * 为 WebGL 配置纹理
 */
function loadTexture(gl, n, texture, u_Sampler, img, texUnit) {
  // 对纹理图像进行 y 轴反转
 
  // 激活纹理单元
  if (texUnit === 0) {
    gl.activeTexture(gl.TEXTURE0)
    texUnit0Actioned = true
  } else if (texUnit === 1) {
    gl.activeTexture(gl.TEXTURE1)
    texUnit1Actioned = true
  }
 
  // 绑定纹理对象
  // 配置纹理参数
 
  // 将纹理图像分配给纹理对象
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
 
  // 将纹理单元传递给片元着色器
  gl.uniform1i(u_Sampler, texUnit)
 
  // 两张图片是异步加载,需要都加载完成才绘制
  if (texUnit0Actioned && texUnit1Actioned) {
    // 清空 canvas
    gl.clear(gl.COLOR_BUFFER_BIT)
 
    // 绘制矩形
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
  }
}

使用两个纹素来计算最终的片元颜色有多种可能的方法(如:差集、排除、正片叠底、颜色加深等),这里采用简单的分量乘法