【《WebGL编程指南》读书笔记-颜色与纹理】

本文为读书笔记第五章
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
本章很长,讨论了前言所述的三个问题,三个由浅入深,帮助我们更细致地了解WebGL系统。虽然内容很长,此处也放在了一起进行呈现。(也许我们可以直接学习第三个问题,也有很多博客只给出了第三个关于纹理的内容,那些可能更适用于有一定经验的人取长补短,但对于初学者来说很难理解系统的机制。)
本章前两节主要讨论了缓冲区对象的使用,包括多个缓冲区对象和单个缓冲区对象的交错组织方式;
第三四节讨论了顶点着色器和片元着色器的工作流程
第五六节讨论了纹理(贴图)的相关内容,笔者在第五节末尾补充了非2的n次幂纹理的设置问题,但没有深入讨论,网上有很多相关的内容,有需要的学者可以自行了解,不排除之后补充的可能。。。。

第5章 颜色与纹理

书中每章的前言和小结都很有意义,言简意赅。笔记中将前言和小结完全复制下来以供参考。

在前几章研究了一些示例程序,通过绘制二维图形介绍了 WebGL的基础知识和关键概念。相信此时你已经对WebGL系统中处理单色几何图形的过程有了基本的理解。 这一章将在此基础上,深入讨论以下三个问题:

  • 将顶点的其他(非坐标)数据——如颜色等——传入顶点着色器。

  • 发生在顶点着色器和片元着色器之间的从图形到片元的转化,又称为图元光栅化 (rasterzation process)。

  • 将图像(或称纹理)映射到图形或三维对象的表面上。

本章是最后一个关于WebGL关键性的基础知识的章节。在学习了本章之后,你将能够掌握在WebGL中使用颜色和纹理的方法,并具有足够的知识去创建精美的三维场景了。


创建多个缓冲区将非坐标数据传入顶点着色器

相关内容:如标题所示,为顶点尺寸也建立一个缓冲区对象来进行操作

小结:多个缓冲区的操作只是将一个缓冲区的操作再复制一遍,没有新的函数和思路需要掌握,但其中揭示了WebGL处理缓冲区对象的一种方式,这种方式在初次使用缓冲区对象的时候有所介绍,此处表现得更加具体:WebGL系统每次只能处理一个缓冲区对象。具体见末尾笔者总结的部分——WebGL处理缓冲区对象的方式,感兴趣的读者也可以查找相关资料,也希望能互相交流。

为了探讨这一功能,本节示例程序为MultiAttributeSize,分别绘制了三个不同尺寸(分别是10.0,20.0,30.0)的点,效果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第1张图片

MultiAttributeSize.js的完整代码如下:

// MultiAttributeSize.js
// 顶点着色器程序
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute float a_PointSize;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = a_PointSize;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main(){\n' + ' gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n' + '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点信息
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}

function initVertexBuffers(gl) {
  // 数据准备
  let vertices = new Float32Array([
    0.0,
    0.5,
    -0.5,
    -0.5,
    0.5,
    -0.5, // 点的位置
  ])
  let n = 3 // 点的数量
  let sizes = new Float32Array([
    10.0,
    20.0,
    30.0, // 点的尺寸
  ])
  // 创建缓冲区
  let vertexBuffer = gl.createBuffer()
  let sizeBuffer = gl.createBuffer()
  if (!vertexBuffer || !sizeBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }

  // 将顶点坐标写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) // 绑定缓冲区
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) // 向缓冲区存入数据
  // 将缓冲区分配给attribute变量
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_Position)

  // 将顶点尺寸写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW)
  let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize')
    return -1
  }
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(a_PointSize)

  return n
}

代码中都是以往应用的功能,此处我们在原绘制多个点的示例基础上,做了如下改变:

  • 新增了attribute变量a_PointSize,该变量为Float类型,负责接收JavaScript程序传入的顶点尺寸数据并被赋值给gl_PointSize变量,
  • 在initVertexBuffers()函数中也增加了a_PointSize相关内容。

下图展示了示例程序刚运行完毕未清空颜色缓冲区时,WebGL系统内部的状态:

【《WebGL编程指南》读书笔记-颜色与纹理】_第2张图片


WebGL处理缓冲区对象的方式:

此处有一个细节不知道读者是否注意到,书中没有进行说明,笔者补充于此:

  • 示例中除了创建缓冲区对象之外,其他包括绑定缓冲区、向缓冲区传输数据、将缓冲区分配给attribute变量,都是一个缓冲区一个缓冲区进行的;
  • gl.bufferData()是通过绑定的target识别缓冲区并向其传输数据;
  • gl.vertexAttribPointer()不需要缓冲区相关参数,只需要被分配的attribute即可。

以上一些信息,说明WebGL系统(最起码在已经了解的部分)只能够一个缓冲区对象一个缓冲区对象地逐个处理,主要以target来定位缓冲区对象的位置,target只会保留一个缓冲区对象的地址(指针),当位置分配给attribute变量之后,才可以通过attribute变量来处理缓冲区对象的内容。


一个缓冲区对象传输多类型信息——步进和偏移参数与交错组织

相关内容:通过gl.vertexAttribPointer()函数的stride(步进)参数和offset(偏移)参数,在一个缓冲区对象中存储并读取多类型数据,示例中一个缓冲区对象包含有顶点位置和大小两类数据。
相关函数:gl.vertexAttribPointer(), TypedArray.BYTES_PER_ELEMENT

小结:一方面熟悉了gl.vertexAttribPointer()中stride和offset参数,另一方面熟悉了TypedArray.BYTES_PER_ELEMENT的用法,总的来说了解了通过stride和offset参数在一个缓冲区对象中读取数据的方法,对于读取大量数据,从外部引用数据等有一定用处。

使用多个缓冲区对象向着色器传递多种数据,比较适合数据量不大的情况。当程序中的复杂三维图形具有成千上万个顶点时,维护所有的顶点数据是很困难的。然而,WebGL允许我们把顶点的坐标和尺寸数据打包到同一个缓冲区对象中(交错组织interleaving),并通过某种机制有差别地访问缓冲区对象中不同种类的数据,即使用gl.vertexAttribPointer()函数的第5个参数stride和第6个参数offset。

本节示例程序效果与上一节相同,完整代码如下:

// MultiAttributeSize_Interleaved.js
// 顶点着色器程序
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute float a_PointSize;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = a_PointSize;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main(){\n' + ' gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n' + '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点信息
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}

function initVertexBuffers(gl) {
  // 数据准备
  let verticesSizes = new Float32Array([
    // 顶点坐标和点的尺寸
    0.0,    0.5,    10.0, // 第一个点
    -0.5,    -0.5,    20.0, // 第二个点
    0.5,    -0.5,    30.0, // 第三个点
  ])
  let n = 3 // 点的数量
  // 创建缓冲区
  let vertexSizeBuffer = gl.createBuffer()
  if (!vertexSizeBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer)
  // 向缓冲区存入数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW)

  // 获取单个数据的大小
  let FSIZE = verticesSizes.BYTES_PER_ELEMENT

  // 获取a_Position的存储位置
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  // 将缓冲区分配给a_Position
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_Position)

  // 获取a_PointSize的存储位置
  let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
  if (a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize')
    return -1
  }
  // 将缓冲区分配给a_PointSize
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2)
  // 开启attribute变量(开启缓冲区)
  gl.enableVertexAttribArray(a_PointSize)

  return n
}

这段代码主要用到了gl.vertexAttribPointer()函数来对前一个示例进行改造,该函数规范再次声明如下:

gl.vertexAttribPointer(location, size, type, normalized, stride, offset):
将绑定到gl.ARRAY_BUFFER的缓冲区对象(实际上是其引用或指针)分配给由location指定的attribute变量。
参数:
location:指定待分配attribute变量的存储位置
size:指定缓冲区中每个顶点的分量个数(1到4)。若size比attribute变量需要的分量数小,缺失分量将按照与gl.vertexAttrib[1234]f()相同的规则补全。比如,如果size为1,那么第2、3分量自动设为0,第4分量为1。
type:用以下类型之一来指定数据格式
gl.UNSIGNED_BYTE 无符号字节,Uint8Array
gl.SHORT 短整型,Int16Array
gl.INT 整型,Int32Array
gl.UNSIGNED_INT 无符号整型,UInt32Array
gl.FLOAT 浮点型,Float32Array
normalize:传入true或false,表明是否将非浮点型的数据归一化到[0,1]或[-1,1]区间(正则化)
stride:指定相邻两个顶点间的字节数,默认为0
offset:指定缓冲区对象中的偏移量(以字节为单位),即attribute变量从缓冲区中的何处开始存储。如果是从起始位置开始的,offset设为0。
**返回值:**无
错误:
INVALID_OPERATION 不存在当前程序对象
INVALID_VALUE location大于等于attribute变量的最大数目(默认为8)。或者stride或offset是负值。

通过示例代码,我们多少知道了stride参数和offset参数的用法,如下图所示:(每个顶点有3个数据项,所以stride为FSIZE*3;offset表示需要的数据项距离首个元素的距离,a_Position为0,a_PointSize为FSIZE*2)

【《WebGL编程指南》读书笔记-颜色与纹理】_第3张图片

示例代码MultiAttributeSize_Interleaved.js不难理解,其与MultiAttributeSize.js的主要区别如下:

  • 缓冲区中的数据每一个顶点为一组,每组中先为位置数据,后为大小数据。
  let verticesSizes = new Float32Array([
    // 顶点坐标和点的尺寸
    0.0,    0.5,    10.0, // 第一个点
    -0.5,    -0.5,    20.0, // 第二个点
    0.5,    -0.5,    30.0, // 第三个点
  ])
  • 获取单个元素的大小(字节数)。
  // 获取单个数据的大小
  let FSIZE = verticesSizes.BYTES_PER_ELEMENT

TypedArray.BYTES_PER_ELEMENT属性代表了强类型数组中每个元素所占的字节数。

  • 每个attribute变量都要先获取位置,再分配缓冲区,最后开启变量,两个变量分配缓冲区的方式不同。
  // 将缓冲区分配给a_Position
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0)
  // 将缓冲区分配给a_PointSize
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2)

这一示例程序最后,WebGL系统的内部行为如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第4张图片


改变颜色(初识varying变量)——为顶点单独设置颜色

相关内容:通过varying变量从顶点着色器向片元着色器传值
相关函数:varying

小结:1. varying变量的作用:从顶点着色器向片元着色器传输数据;2. varying变量可以赋值的数据类型:float及相关数据类型;3. varying变量的行为(如何传值):末尾有图展示——在WebGL中,如果顶点着色器与片元着色器中又类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值会自动传入片元着色器。

本节示例为MultiAttributeColor,顾名思义,本节示例希望在缓冲区对象中同时填充顶点坐标和颜色数据,然后分配给attribute变量用以处理颜色,最终呈现结果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第5张图片

在此之前,我们只静态设置或统一设置(uniform变量)过顶点的颜色,许多操作都放在了顶点着色器中,还没有真正操作过片元着色器。而作为每个顶点单独的属性,attribute变量又只支持在顶点着色器中使用,所以,我们需要知道顶点着色器和片元着色器是如何交流的,这样才能使顶点着色器的数据进入片元着色器(也能明白为什么attribute变量只支持在顶点着色器中使用)。此处需要使用到一个新的变量:varying变量(varying variable),从顶点着色器向片元着色器传输数据。

示例程序完整代码如下:

// MultiAttributeColor.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  ' v_Color = a_Color;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_FragColor = v_Color;\n' +
  '}\n'
// 主程序
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 获取顶点位置
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the position of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空绘图区
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.POINTS, 0, n)
}
// 顶点位置相关函数
function initVertexBuffers(gl) {
  // 数据准备
  let 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,
  ])
  let n = 3
  // 创建缓冲区对象
  let vertexColorBuffer = gl.createBuffer()
  if (!vertexColorBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 向缓冲区存储数据
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)

  let FSIZE = verticesColors.BYTES_PER_ELEMENT
  // 获取a_Position的存储位置,分配缓冲区并开启
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
  gl.enableVertexAttribArray(a_Position) // Enable buffer assignment
  // 获取a_Color的存储位置,分配缓冲区并开启
  let a_Color = gl.getAttribLocation(gl.program, 'a_Color')
  if (a_Color < 0) {
    console.log('Failed to get the storage location of a_Color')
    return -1
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
  gl.enableVertexAttribArray(a_Color) // 开启缓冲区分配

  return n
}

该节重点在于着色器语言部分:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' gl_PointSize = 10.0;\n' +
  ' v_Color = a_Color;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_FragColor = v_Color;\n' +
  '}\n'

顶点着色器中声明了attribute变量a_Color用以接收颜色数据,同时在两个着色器中都声明了varying变量v_Color变量,该变量将值从顶点着色器传递到片元着色器,最终赋值给gl_FragColor(在WebGL中,如果顶点着色器与片元着色器中又类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值会自动传入片元着色器)。

【《WebGL编程指南》读书笔记-颜色与纹理】_第6张图片

注意:varying变量只能是float(以及相关的vec2,vec3,vec4,mat2,mat3,mat4)类型。

补充:片元着色器在声明新的变量的时候,需要在前面加上精度限定语句,此处为precision mediump float,这一语法在后面GLSL ES语言的介绍中会进行说明。


ColoredTriangle.js与几何图形装配、光栅化、执行片元着色器(varying变量的varying过程-内插)

在第二章的笔记中,笔者曾经提到了一个问题:怎么判断绘制结束以清空颜色缓冲区,在之前地学习中,又逐渐发现了一个问题:片元着色器是如何和顶点着色器协同工作的(逐片元操作是什么意思)。值得高兴的是,这一节应该能给出两个问题的不少答案。

相关内容:着色器工作细节:顶点着色器-几何图像装配-光栅化-片元着色器;varying变量的特性—传值和内插
相关函数:varying变量;片元着色器内置对象gl_FragCoord

小结:1. 顶点着色器在提供顶点坐标之后,会进入几何图像装配流程,根据gl.drawArrays()中的参数选择配装方式;2. 配装之后,光栅化将配装后的图像转化为片元,每个片元中保存有窗口坐标系统坐标等信息(目前已知);3. 片元着色器接收光栅化后的图像,为每个片元确定颜色,绘制到颜色缓冲区中;4. 通过gl_FragCoord,我们可以获得片元在canvas坐标系统(窗口坐标系统)下的坐标,据此对gl_FragColor进行操作,可以根据坐标控制片元的颜色

在上一节示例中,如果我们把gl.drawArrays()参数修改一下,就得到了ColoredTriangles.js,如下所示:

  // 绘制
  gl.drawArrays(gl.TRIANGLES, 0, n)

示例程序在三角形表面产生了颜色平滑过渡的效果:

【《WebGL编程指南》读书笔记-颜色与纹理】_第7张图片

为了理解这种效果的产生方式,我们需要厘清顶点着色器和片元着色器之间的数据传输细节。


几何形状的装配和光栅化:

为了便于理解,此处采用第3章中的示例HelloTriangle.js,绘制一个红色的三角形,来一步一步分析过程。

HelloTriangle.js代码如下:

// HelloTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main(){\n' +
  '   gl_Position = a_Position;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'void main() {\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取WebGL上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点位置
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  // 清空canvas
  gl.clear(gl.COLOR_BUFFER_BIT)
  // 绘制
  gl.drawArrays(gl.TRIANGLES, 0, n)
}

function initVertexBuffers(gl) {
  let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
  let n = 3 // 点的个数

  // 创建缓冲区对象
  let vertexBuffer = gl.createBuffer()
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 向缓冲区对象中写入数据
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
  // 将缓冲区对象分配给a_Position变量
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
  // 连接a_Position变量与分配给它的缓冲区对象
  gl.enableVertexAttribArray(a_Position)

  return n
}

这里直接给出几何图形处理的细节过程:

【《WebGL编程指南》读书笔记-颜色与纹理】_第8张图片

如上图所示,顶点着色器和片元着色器之间有以下两个步骤:

  • 图形装配过程:将孤立的顶点坐标装配成几何图形,几何图形的类别由gl.drawArrays()第一个参数决定。
  • 光栅化过程:将装配好的几何图形转化为片元。(于是就可以进行逐片元操作了)

WebGL系统中关于图形装配和光栅化的细节部分如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第9张图片

需要注意以下几点:

  • gl_Position几何图形装配(geometric shape assembly)阶段输入的数据。
  • 几何图形装配过程又被称为图元装配过程(primitive assembly process),装配出的基本图形(点,线,面)又被称为图元(primitives)。

所以,在HelloTriangle.js示例中,顶点着色器会被执行三次,之后进行装配和光栅化,最后执行片元着色器,流程细节如下:

  1. 执行顶点着色器,缓冲区对象中第一个坐标(0.0,0.5)传递到attribute变量a_Position(自动补全为(0.0,0.5,0.0,1.0)),然后赋值给gl_Position。一旦坐标被赋值给gl_Position,它就进入了图形装配区域并暂时储存在那里;
  2. 执行顶点着色器,传入第二个坐标(-0.5,-0.5,0.0,1.0);
  3. 执行顶点着色器,传入第三个坐标(0.5,-0.5,0.0,1.0);
  4. 开始装配图形。使用传入的点的坐标,根据gl.drawArrays()第一个参数决定如何装配,示例中为gl.TRIANGLES,使用三个顶点装配出一个三角形;
  5. 将图形转化为片元,即光栅化(rasterization),光栅化之后,我们就得到了组成这个三角形的所有片元,片元的数目实际上是三角形最终在屏幕上所覆盖的像素数

【《WebGL编程指南》读书笔记-颜色与纹理】_第10张图片


调用片元着色器:

光栅化之后,就可以逐片元调用片元着色器。

假如图形只有如下图所示实心正方形所示的10个片元,那么片元着色器会调用10次,每调用一次处理一个片元。对于每个片元(本身带有坐标信息),片元着色器计算出该片元的颜色,写入颜色缓冲区。

全部处理完毕后,浏览器会显示最终的结果。

【《WebGL编程指南》读书笔记-颜色与纹理】_第11张图片


对片元着色器内部操作:

前面说过,片元本身带有坐标信息,调用片元着色器时,这些坐标信息也随着片元传了过去,我们可以通过片元着色器内置变量来访问该坐标信息:

类型和变量名 描述
vec4 gl_FragCoord 该内置变量的第1个和第2个分量表示片元在坐标系统(窗口坐标系统)中的坐标值

通过片元的坐标数据,我们可以做到对每一个片元单独设置颜色。

我们此处对HelloTriangle.js示例进行如下修改:

  • 片元着色器引入新的变量并使用gl_FragCoord内置坐标

    片元着色器中坐标(窗口系统中的坐标)除以窗口宽或高是为了将颜色值压缩到0.0到1.0之间

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform float u_Width;\n' +
  'uniform float u_Height;\n' +
  'void main() {\n' + 
  ' gl_FragColor = vec4(gl_FragCoord.x/u_Width, 0.0, gl_FragCoord.y/u_Height, 1.0);\n' + 
  '}\n'
  • 新的uniform变量传值

    canvas.clientWidth和canvas.clientHeight是元素客户区的大小—width/height + padding

  // 新的uniform变量传值
  let u_Width = gl.getUniformLocation(gl.program, 'u_Width')
  if (!u_Width) {
    console.log('Failed to get the storage loaction of u_Width')
    return
  }
  gl.uniform1f(u_Width, canvas.clientWidth)
  let u_Height = gl.getUniformLocation(gl.program, 'u_Height')
  if (!u_Height) {
    console.log('Failed to get the storage location of u_Height')
    return
  }
  gl.uniform1f(u_Height, canvas.clientHeight)

如此一来,可以通过片元坐标计算颜色值,效果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第12张图片


varying作用过程:

ColoredTriangel.js中,我们指定了三个顶点的颜色,最后却得到了一个颜色渐变的三角形,这是因为varying变量在光栅化的过程中经过了内插操作,如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第13张图片

内插之后,每个片元都根据三个顶点的v_Color变量得到了自己的v_Color变量,该变量和坐标信息(可能还有其他信息)一起保存在片元中。进入片元着色器后,新声明的varying变量v_Color接收原varying变量v_Color,然后赋值给gl_FragColor,最后会知道颜色缓冲区中。

所谓的内插过程(interpolation process)如下所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第14张图片

书中给出的内插操作均为线性内插,《计算机图形学》(Computer Graphics)一书中可能有更系统的解释。

这就是varying变量为什么被称作“变化的”变量的原因。


贴图的方法——纹理

相关内容:WebGL中纹理映射的流程
相关函数:gl.createTexture(), gl.deleteTexture(), gl.pixelStorei(), gl.activeTexture(), gl.bindTexture(), gl.texParameteri(), gl.texImage2D(), gl.uniform1i(), texture2D()

小结:1.纹理映射主体流程:着色器设置-顶点坐标和对应纹理坐标设置-加载图像,加载完成后配置纹理;2. 纹理流程细节:创建纹理对象-获取变量存储地址-加载图像,加载完成后:纹理对象预处理(Y轴反转)-开启纹理单元-绑定并开启纹理对象-配置纹理参数-配置纹理图像-根据纹理单元传输纹理;3. 纹理相关扩展:非2的n次幂图像的呈现问题。

除了通过给定数值为片元着色,在三维图形学中,有一项重要的技术可以帮助我们使用现有的图像为图形着色,被称为纹理映射(texture mapping)。

纹理映射的作用,就是根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色。通俗说法,就是将一张图像(就像一张贴纸)映射(贴)到一个几何图形的表面上去。该图片又可以称为纹理图像(texture image)或纹理(texture),组成纹理图像的像素又被称为纹素(texels,texture elements),每一个纹素的颜色都是用RGB(jpg)或RGBA格式(png)编码。

【《WebGL编程指南》读书笔记-颜色与纹理】_第15张图片

在WebGL中,纹理映射的步骤如下:

  1. 准备好映射到几何图形上的纹理图像;

    可以是浏览器支持的任意格式的图像

  2. 为几何图形配置纹理映射方式;

    确定“几何图形的某个片元”的颜色如何取决于“纹理图像中哪个(或哪几个)像素”的问题(即前者到后者的映射)。此处利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标(texture coordinates)确定纹理图像的哪部分将覆盖到几何图形上。

  3. 加载纹理图像,对其进行一些配置,以在WebGL中使用它;

  4. 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元。


纹理坐标与映射方式:

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL系统中纹理坐标系统是二维的st坐标系统(GLSL ES中也是用st分量名),如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第16张图片

我们将纹理坐标映射到WebGL系统顶点上,方式如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第17张图片


示例程序TexturedQuad.js

// 顶点着色器
let VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'
// 片元着色器
let FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n' +
  '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }
  // 设置顶点坐标
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 设置背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0)

  // 配置纹理信息
  if (!initTextures(gl, n)) {
    console.log('Failed to configure the texture')
    return
  }
}
// 顶点坐标
function initVertexBuffers(gl) {
  // 数据准备
  let 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,
  ])
  let n = 4
  // 创建缓冲区
  let vertexTexCoordBuffer = gl.createBuffer()
  if (!vertexTexCoordBuffer) {
    console.log('Failed to create vertexTexCoordBuffer')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer)
  // 缓冲区存储数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW)

  // 顶点坐标相关配置
  let FSIZE = verticesTexCoords.BYTES_PER_ELEMENT
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
  gl.enableVertexAttribArray(a_Position)

  // 纹理坐标相关配置
  let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord')
    return -1
  }
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
  gl.enableVertexAttribArray(a_TexCoord)

  // 一切顺利,返回顶点数量
  return n
}
// 初始化纹理
function initTextures(gl, n) {
  // 创建纹理对象
  let texture = gl.createTexture()
  if (!texture) {
    console.log('Failed to create texture')
    return
  }
  // 获取u_Sampler(纹理图像)的存储位置
  let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
  if (!u_Sampler) {
    console.log('Failed to get the storage loaction of u_Sampler')
    return
  }
  // 创建一个image对象
  let image = new Image()
  // 注册图像加载事件的响应函数
  image.onload = function () {
    loadTexture(gl, n, texture, u_Sampler, image)
  }
  // 浏览器加载图像
  image.src = '../image/sky.jpg'

  return true
}

function loadTexture(gl, n, texture, u_Sampler, image) {
  // 对纹理图像进行Y轴反转
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
  // 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)
  // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
}

呈现效果如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第18张图片

示例代码越来越长,不过有很多是之前学习过的内容(包括顶点坐标赋值等),新加入的纹理相关操作流程可分为如下五个部分:

  1. 顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器(GLSL ES)。
  2. 片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元(GLSL ES)。
  3. 设置顶点的纹理坐标(initVertexBuffers()
  4. 准备待加载的纹理图像,令浏览器读取它(initTextures())。
  5. 监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理(loadTexture())。

后三部分在JavaScript代码中操作放置在main()函数中随dom加载执行,前两部分书写在着色器语言中并在图像加载完成之后执行。我们就纹理相关流程进行分解。


着色器初印象:

本示例的着色器设计更加复杂,对应于上面所说的1、2部分。

此处对其有个简单印象,方便后面JavaScript相关操作的进行,细致的内容随后讲解。

// 顶点着色器
let VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'
// 片元着色器
let FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n' +
  '}\n'

初看着色器设计,可以得到程序大致轮廓:

  • 顶点着色器中声明了两个attribute变量,其中a_Position用于存储顶点坐标信息,赋值给gl_Position变量,通过顶点着色器绘制顶点;a_TexCoord用于存储纹理坐标信息,赋值给varying变量v_TexCoord。每一个顶点均有两个信息被赋予:a_Positiona_TexCoord,即顶点对应的坐标和纹理坐标。
  • 顶点着色器中的信息经过图形配装和光栅化转化为众多片元,每个片元包括对应的坐标信息和varying变量v_TexCoord(插值处理)信息;
  • 片元着色器声明了uniform变量u_Sampler,该变量为sampler2D类型,为纹理图像。同时,片元着色器接收光栅化后的图形片元,片元本身有自己的位置信息,再接收varying变量v_TexCoord,通过texture2D()方法根据纹理坐标(v_TexCoord)对纹理取值(u_Sampler),绘制到颜色缓冲区。

所以,我们需要JavaScript程序来支持这一流程的组件配置和数据传输,下面对JavaScript程序进行分解。


设置顶点的纹理坐标:

设置纹理坐标的方式和顶点坐标类似,同样是从缓冲区到attribute变量的操作流程,相对基础,可见代码如下:

// 顶点坐标
function initVertexBuffers(gl) {
  // 数据准备
  let 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,
  ])
  let n = 4
  // 创建缓冲区
  let vertexTexCoordBuffer = gl.createBuffer()
  if (!vertexTexCoordBuffer) {
    console.log('Failed to create vertexTexCoordBuffer')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer)
  // 缓冲区存储数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW)

  // 顶点坐标相关配置
  let FSIZE = verticesTexCoords.BYTES_PER_ELEMENT
  ...

  // 纹理坐标相关配置
  let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord')
    return -1
  }
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
  gl.enableVertexAttribArray(a_TexCoord)
  
  ...
}

配置和加载纹理(initTextures()

坐标信息传输完毕后,开始对纹理进行配置和加载,主体在函数initTextures()中,代码如下:

// 初始化纹理
function initTextures(gl, n) {
  // 创建纹理对象
  let texture = gl.createTexture()
  if (!texture) {
    console.log('Failed to create texture')
    return
  }
  // 获取u_Sampler(纹理图像)的存储位置
  let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
  if (!u_Sampler) {
    console.log('Failed to get the storage loaction of u_Sampler')
    return
  }
  // 创建一个image对象
  let image = new Image()
  // 注册图像加载事件的响应函数
  image.onload = function () {
    loadTexture(gl, n, texture, u_Sampler, image)
  }
  // 浏览器加载图像
  image.src = '../image/sky.jpg'

  return true
}

纹理的相关操作与其它变量类似,分为创建对象、获取地址、配置等步骤,只是在配置之前需要浏览器加载纹理图像。

  • 创建纹理对象

创建纹理对象采用gl.createTexture()方法,该方法与缓冲区对象创建方法类似,函数规范如下:

gl.createTexture():
创建纹理对象以存储纹理图像。
参数:
返回值:
non-null: 新创建的纹理对象。
null: 创建纹理对象失败。
错误:

创建纹理对象的过程如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第19张图片

同样,删除纹理对象的方法如下:

gl.deleteTexture():
使用texture删除纹理对象。
参数:
texture: 待删除的纹理对象。
返回值:
错误:

  • 获取着色器中u_Sampler变量的地址

uniform变量u_Sampler常被称为取样器,在示例中,该变量被用来接收纹理图像,所以我们要先获取它的地址用于后续配置。

  // 获取u_Sampler(纹理图像)的存储位置
  let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
  if (!u_Sampler) {
    console.log('Failed to get the storage loaction of u_Sampler')
    return
  }
  • 加载纹理图像image

不同于OpenGL,WebGL是基于浏览器创建的,它本身不能读取本地文件,需要浏览器读取图片文件,再通过JavaScript将其传输到WebGL系统中(书中还提到,WebGL不允许使用跨域纹理图像)。

此处书中和许多教程中都提到了浏览器的跨域请求问题(如果出现此类问题,浏览器会报错,不知为何,笔者没有出现此类问题):从安全的角度考虑,在浏览器访问本地文件的行为默认禁止,如果要访问本地图片,需要开启浏览器的相关配置(用完后记得关闭)。另一种解决方案是从服务器发布图片。
两种方案在网上有很多教程,不属于WebGL的学习内容,故不再总结。

浏览器加载图像的过程是异步的,此处注册了图像加载事件的响应函数,在图片加载完成后自动调用该方法。

  // 创建一个image对象
  let image = new Image()
  // 注册图像加载事件的响应函数
  image.onload = function () {
    loadTexture(gl, n, texture, u_Sampler, image)
  }
  // 浏览器加载图像
  image.src = '../image/sky.jpg'

initTextures()函数最终会return退出,浏览器会等待image的onload事件发生,调用loadTexture(gl, n, texture, u_Sampler, image)函数。

异步的过程在书中有一个很完整的图,如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第20张图片


配置纹理:

配置纹理的操作发生在图像加载完成之后,代码位于loadTexture(gl, n, texture, u_Sampler, image)函数中。

该函数的参数包括gl上下文对象、n顶点数量、texture纹理对象、u_Sampler纹理图像存储位置、image新创建的Image对象(加载了纹理图像),函数的目的是将image图像分配给WebGL系统使用、定义使用中的各种参数。

因为是异步代码,所以绘制图形的操作也会放在该函数中。

function loadTexture(gl, n, texture, u_Sampler, image) {
  // 对纹理图像进行Y轴反转(此时图像是空的)
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
  // 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)
  // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
}

下面进行分解:

  • 图像Y轴旋转(gl.pixelStorei())
  // 对纹理图像进行Y轴反转
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)

WebGL中纹理坐标系统中t轴方向和PNG、BMP、JPG等格式图片的坐标系统Y轴方向是相反的,所以要先将图像Y轴进行反转才能正确映射(这一操作也可以放在着色器中手动进行)。

【《WebGL编程指南》读书笔记-颜色与纹理】_第21张图片

至此,我们已经接触了多个坐标系统:
WebGL坐标系统(几何图像装配和顶点坐标所用的坐标系统);canvas坐标系统(窗口坐标系统);WebGL纹理坐标系统(s-t, (0.0,0.0)-(1.0,1.0))

该函数规范如下:

gl.pixelStorei(pname, param):
使用pname和param指定的方式处理加载得到的图像。
参数:
pname: 可以是以下二者之一
gl.UNPACK_FLIP_Y_WEBGL 对图像进行Y轴反转,默认值为false
gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL 将图像RGB颜色值得每一个分量乘以A。默认值为false。
param 指定非0(true)或0(false)。必须为整数。
返回值:
错误:
INVALID_ENUM: pname不是合法的值。

  • 激活纹理单元(gl.activeTexture())
  // 开启0号纹理单元
  gl.activeTexture(gl.TEXTURE0)

WebGL通过一种称作纹理单元(texture unit)的机制同时使用多个纹理(单个纹理也遵循此规则),每个纹理单元有一个单元编号来管理一张纹理图像。

系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,但在默认情况下,WebGL至少支持8个纹理单元,一些其他的系统支持的个数更多。如下图所示,内置变量gl.TEXTURE0gl.TEXTURE1······gl.TEXTURE7各表示一个纹理单元。

【《WebGL编程指南》读书笔记-颜色与纹理】_第22张图片

使用纹理单元之前,还需要调用gl.activeTexture()来激活它,该函数规范如下:

gl.activeTexture(texUnit):
激活texUnit指定的纹理单元。
参数:
texUnit: 指定准备激活的纹理单元:gl.TEXTURE0gl.TEXTURE1······gl.TEXTURE7。最后的数字表示纹理单元的编号
返回值:
错误:
INVALID_ENUM: texUnit不是合法的值。

激活纹理单元后,WebGL系统内如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第23张图片

  • 绑定纹理对象(gl.bindTexture())
  // 向target绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

与缓冲区对象类似,纹理对象在使用前也需要绑定到target,以确定类型,同时之后的配置操作也以target为对象(指针?)进行操作。

对这一部分的猜想:
WebGL或OpenGL的操作方法放在了target类下?显然,不同的target有不同的操作方法。

函数规范如下:

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

返回值:
错误:
INVALID_ENUM: target不是合法的值。

target的两个选项介绍如下:

纹理类型 描述
gl.TEXTURE_2D 二维纹理
gl.TEXTURE_CUBE_MAP 立方体纹理(书中没有,可以参考OpenGL ES 2.0 Programming Guide一书)

与缓冲区对象不同的是,此处完成了两个任务:开启纹理单元和绑定纹理对象。因为0号纹理单元已经激活,所以纹理对象也绑定到了纹理单元上。

在该方法执行完毕后,WebGL系统如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第24张图片

结构已经搭建好了,接下来需要配置多个参数来进行使用。

  • 配置纹理对象的参数(gl.texParameteri())
  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

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

此处使用函数gl.texParamteri(),函数规范如下:

gl.texParameteri(target, pname, param):
将param的值赋给绑定到目标的纹理对象的panme参数上。
参数:
target: gl.TEXTURE_2Dgl.TEXTURE_BUVE_MAP
pname: 纹理参数。
param: 纹理参数的值。
返回值:
错误:
INVALID_ENUM: target不是合法的值。
INVALID_OPERATION: 当前目标上没有绑定纹理对象

纹理参数及其默认值如下表所示:

纹理参数 描述 默认值
gl.TEXTURE_MAG_FILTER 纹理放大 gl.LINEAR
gl.TEXTURE_MIN_FILTER 纹理缩小 gl.NEARST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S 纹理水平填充 gl.REPEAT
gl.TEXTURE_WRAP_T 纹理垂直填充 gl.REPEAT

四种纹理参数的效果如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第25张图片

可以赋值给gl.TEXTURE_MAG_FILTERgl.TEXTURE_MIN_FILTER的非金字塔纹理类型常量(金字塔纹理参见OpenGL ES 2.0 Programming Guide一书)如下表所示:

描述
gl.NEAREST 使用原纹理上距离映射后像素(新像素)中心最近的那个像素的颜色值,作为新像素的值(使用曼哈顿距离(直角距离))。
gl.LINEAR 使用距离新像素中心最近的四个像素的颜色值的加权平均,作为新像素的值(与gl.NEAREST相比,该方法图像质量更好,但是会有较大的开销)。

金字塔类型(MIPMAP)纹理实际上是一系列纹理,或者说是原始纹理图像的一系列不同分辨率的版本,在gis软件和webgis呈现中十分常见,gl.TEXTURE_MIN_FILTER的默认类型gl.NEARST_MIPMAP_LINEAR就是MIPMAP类型纹理。本书中没有对此进行过多介绍,笔者如果之后遇到再进行补充。

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

描述
gl.REPEAT 平铺式的重复纹理
gl.MIRRORED_REPEAT 镜像对称式的重复纹理
gl.CLAMP_TO_EDGE 使用纹理图像的边缘值

在一些情况下,我们可以不使用gl.texParameteri()方法配置纹理对象,而是保留其默认值。
示例程序进行到这一步后,WebGL系统如下图所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第26张图片

  • 将纹理图像分配给纹理对象(gl.texImage2D())
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)

这一函数的参数相对较多,其函数规范如下:

gl.texImage2D(target, level, internalformat, format, type, image):
将param的值赋给绑定到目标的纹理对象的panme参数上。
参数:
target: gl.TEXTURE_2Dgl.TEXTURE_BUVE_MAP
level: 传入0(是为金字塔纹理准备的,本书不涉及)。
internalformat: 图像的内部格式。
format: 纹理数据的格式,必须使用与internalformat相同的值。
type: 纹理数据的类型。
image: 包含纹理图像的Image对象。
返回值:
错误:
INVALID_ENUM: target不是合法的值。
INVALID_OPERATION: 当前目标上没有绑定纹理对象

internalformatformat分别表示图像的内部格式和纹素的格式,一般情况下二者取值相同,如下表:

格式 描述
gl.RGB 红、绿、蓝
gl.RGBA 红、绿、蓝、透明度
gl.ALPHA (0.0,0.0,0.0,透明度)
gl.LUMINANCE L、L、L、1L:流明
gl.LUMINANCE_ALPHA L、L、L,透明度

**补充一:**流明(luminance)表示我们感知到的物体表面的亮度。通常使用物体表面红、绿、蓝颜色分量值的加权平均来计算流明。
**补充二:**常见的JPG格式图片将每个像素用RGB三个分量来表示,属于gl.RGB格式;PNG格式图片属于gl.RGBA格式;BMP图片属于gl.RGB格式;gl.LUMINANCE和gl.LUMINANCE_ALPHA通常用于灰度图像。

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:RGB每个分量各占据5比特,A分量占据1比特

**补充一:**通常我们选用gl.UNSIGNED_BYTE数据类型,其它数据类型通常用于压缩数据以减少浏览器加载图像的时间。

经过这一过程后,WebGL系统如下所示:

【《WebGL编程指南》读书笔记-颜色与纹理】_第27张图片


传输纹理单元给着色器(gl.uniform1i()

通过以上方式,我们已经在WebGL系统中配置好了纹理,现在需要将纹理传输给着色器。

在着色器中,我们已经定义了u_Sampler变量(采样器)来存储纹理。

'uniform sampler2D u_Sampler;\n'

在WebGL中需要定义变量的类型,既然用于存储纹理数据,那么该变量的数据类型就与纹理的target有关。示例中使用的采样器的数据类型为sampler2D,对应于绑定到gl.TEXTURE_2D上的纹理数据类型。专用于纹理的数据类型如下:

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

所以,我们需要将之前配置的纹理传输给该采样器。

传输纹理使用纹理编号进行定位,采样器使用之前获得的采样器地址进行定位:

  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0)

该函数的作用是:分配正确的纹理单元给对应采样器,参数u_Sampler就是采样器地址,'0’表示0号纹理单元gl.TEXTURE0

之后,片元着色器就可以访问纹理图像了:

【《WebGL编程指南》读书笔记-颜色与纹理】_第28张图片


着色器的行为

  // 绘制矩形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)

之后我们使用gl.drawArrays()方法调用着色器绘制图形。因为涉及异步请求,绘制函数也需要放在图像加载的响应函数中。

根据着色器语言,纹理相关的着色器行为如下:

  • 从顶点着色器向片元着色器传输纹理坐标
  • 在片元着色器中获取纹理像素颜色(texture2D())

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

// 顶点着色器
let VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'

此处我们通过attribute变量a_TexCoord接收顶点的纹理坐标,将其赋值给varying变量v_TexCoord。

光栅化过程中varying变量会进行插值,在生成的片元中,每一个都包含片元坐标和v_TexCoord信息。

从片元着色器中获取纹理像素颜色(texture2D())

' gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n'

该步骤使用GLSL ES内置函数texture2D()来抽取纹素颜色,该函数规范如下:

gl.texture2D(sampler2D sampler, vec2 coord):
从sampler指定的纹理上获取coord指定的纹理坐标处的像素颜色。
参数:
sampler: 指定纹理单元编号
coord: 指定纹理坐标
返回值:
纹理坐标处像素的颜色值,其格式由gl.texImage2D()internalformat参数决定。如果由于某些原因导致纹理图像不可使用,就返回(0.0, 0.0, 0.0, 1.0)。

上述函数的返回值如下:

internalformat 返回值
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 指定亮度(indicates luminance)
gl.LUMINANCE_ALPHA (L, L, L, A)

纹理配置的相关参数决定WebGL系统如何插出片元。最后gl_FragColor变量接收了texture2D()的返回值,进行绘制。


扩展:非2的n次幂图像的纹理

非书中内容

为了优化显卡性能,OpenGL ES 2.0不支持非2的n次幂纹理(Non-power-of-two textures)的渲染,这也导致了在WebGL中对非2的n次幂图像的处理会出现问题。

【《WebGL编程指南》读书笔记-颜色与纹理】_第29张图片

WebGL warning: drawArraysInstanced: TEXTURE_2D at unit 0 is incomplete: Non-power-of-two textures must have a wrap mode of CLAMP_TO_EDGE.

(笔者进行相关试验时,错误提示在Chrome浏览器中不会出现,在FireFox浏览器中出现)

此处解决方法有两种。

方法一:通过纹理参数的配置来渲染图像。

这一方法也是错误提示中给出的方法。在设置纹理参数时,添加如下两个条件:

  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)

如果程序不需要平铺图像,可以接收非金字塔纹理的设置的话,可以采用该方法简单配置纹理对象。

效果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第30张图片

方法二:采用DOM挂载的方法手动将图像设置为2的n次幕图像。

方法如下:

在配置纹理参数前,手动处理纹理图像,即:

  1. 创建一个canvas元素,其长宽设为image长宽对应的最小二的n次方像素;
  2. 将image(Image对象)绘制到canvas元素绘图上下文(‘2d’)中;
  3. 将canvas赋值给image,然后依然操作image就可以了。

把纹理图像加载到一个canvas元素中,

  // 手动修改纹理图像
  if (!isPowerofTwo(image.width) || !isPowerofTwo(image.height)) {
    // Scale up the texture to the next highest power of two dimensions.
    let canvas = document.createElement('canvas')
    canvas.width = nextHighestPowerOfTwo(image.width)
    canvas.height = nextHighestPowerOfTwo(image.height)
    let ctx = canvas.getContext('2d')
    ctx.drawImage(image, 0, 0, image.width, image.height)
    image = canvas
  }
// x是否为2的n次幂,位运算符
function isPowerofTwo(x) {
  return (x & (x - 1)) == 0
}
// x最接近的大一点的2的n次幂
function nextHighestPowerOfTwo(x) {
  --x
  for (let i = 1; i < 32; i <<= 1) {
    x = x | (x >> i)
  }
  return x + 1
}

补充:从该方法可以看出,WebGL接收的纹理图像,可以是Image对象,也可以是canvas元素,其中canvas元素往往采用‘2d’类型的上下文并需要把图像绘制其上。

效果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第31张图片

新的OpenGL:

在OpenGL 2.0和之后的版本中,系统已经支持了非2的n次幂纹理的加载,不过这种纹理的渲染会大大降低显卡的性能,不推荐使用。

当然,WebGL似乎没有进行相关更新。

最后,我们修改一下顶点坐标,让图片更好看一点吧:

如果采用方法二,我们需要知道变化之后image(canvas)的长宽来设置顶点坐标。此处采用方法一,根据原图片长宽比例设置坐标,效果如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第32张图片

不如原图好看。


使用多幅纹理

本节没有新的方法,只是在上一个示例的思路之上,增加了一个纹理图像,展示了方便地配置多幅纹理图像的一个思路,值得借鉴。

要点:1. 着色器语言中通过计算获得新的颜色;2. 图像加载响应函数中添加一个参数,以达到在多幅纹理图形同时使用的情况下该函数的复用。

纹理单元的设计就是为了方便WebGL使用多幅纹理,上一个示例程序使用了一幅纹理,在本节将使用两幅纹理进行绘制,形成如下效果:

【《WebGL编程指南》读书笔记-颜色与纹理】_第33张图片

示例程序MultiTexture.js代码如下:

// MultiTexture.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  ' v_TexCoord = a_TexCoord;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler0;\n' +
  'uniform sampler2D u_Sampler1;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
  ' vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
  ' gl_FragColor = color0 * color1;\n' +
  '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取webgl上下文
  let gl = getWebGLContext(canvas)
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL')
    return
  }
  // 初始化着色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to initialize shaders')
    return
  }

  // 设置顶点位置
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 配置纹理信息
  if (!initTextures(gl, n)) {
    console.log('Failed to configure the texture')
    return
  }
}
// 设置顶点信息
function initVertexBuffers(gl) {
  // 准备数据
  let 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,
  ])
  let n = 4
  // 创建缓冲区
  let vertexTexCoordBuffer = gl.createBuffer()
  if (!vertexTexCoordBuffer) {
    console.log('Failed to create vertexTexCoordBuffer')
    return -1
  }
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer)
  // 缓冲区存储数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW)

  let FSIZE = verticesTexCoords.BYTES_PER_ELEMENT
  // 顶点坐标相关
  let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
  gl.enableVertexAttribArray(a_Position)
  // 纹理坐标相关
  let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord')
    return -1
  }
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
  gl.enableVertexAttribArray(a_TexCoord)

  return n
}
// 设置纹理信息
function initTextures(gl, n) {
  // 创建纹理对象
  let texture0 = gl.createTexture()
  if (!texture0) {
    console.log('Failed to create texture0')
    return false
  }
  let texture1 = gl.createTexture()
  if (!texture1) {
    console.log('Failed to create texture1')
    return false
  }
  // 获取u_Sampler0和u_Sampler1存储位置
  let u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0')
  if (!u_Sampler0) {
    console.log('Failed to get the storage loaction of u_Sampler0')
    return false
  }
  let u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1')
  if (!u_Sampler1) {
    console.log('Failed to get the storage loaction of u_Sampler1')
    return false
  }
  // 创建Image对象
  let image0 = new Image()
  let image1 = new Image()
  // 绑定加载图像响应事件
  image0.onload = function () {
    loadTexture(gl, n, texture0, u_Sampler0, image0, 0)
  }
  image1.onload = function () {
    loadTexture(gl, n, texture1, u_Sampler1, image1, 1)
  }

  // 加载图像
  image0.src = '../image/sky.jpg'
  image1.src = '../image/circle.gif'

  return true
}
// 标记纹理单元是否已经就绪
var g_texUnit0 = false,
  g_texUnit1 = false
function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
  // 图像Y轴反转
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
  // 激活纹理单元
  if (texUnit == 0) {
    gl.activeTexture(gl.TEXTURE0)
    g_texUnit0 = true
  } else {
    gl.activeTexture(gl.TEXTURE1)
    g_texUnit1 = true
  }
  // 绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  // 配置图像参数
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
  // 将纹理单元传递给着色器
  gl.uniform1i(u_Sampler, texUnit)

  // 绘制
  if (g_texUnit0 && g_texUnit1) {
    // 清空绘图区
    gl.clearColor(0.0, 0.0, 0.0, 1.0)
    gl.clear(gl.COLOR_BUFFER_BIT)
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
  }
}

代码很长,相比于上一个纹理相关代码,主要区别如下:

  • 片元着色器访问两个纹理,计算颜色。
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_Sampler0;\n' +
  'uniform sampler2D u_Sampler1;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main(){\n' +
  ' vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
  ' vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
  ' gl_FragColor = color0 * color1;\n' +
  '}\n'

此处两个纹理对象采用一样的坐标,所以在顶点着色器中没有区别。

在片元着色器中,color0和color1分别从u_Sampler0和u_Sampler1中根据v_TexCoord提供的坐标提取颜色,最后计算颜色的方式为二者相乘。vec4格式数据乘法如下:

【《WebGL编程指南》读书笔记-颜色与纹理】_第34张图片

  • 创建两个纹理对象并分配给两个纹理单元。

initTextures()函数中创建了两个纹理对象,每一个步骤都进行了两次,,因为有两个纹理图像,所以在响应函数loadTexture()中增加了一个参数来区别两个图像(主要为了判断是否全部加载完成以使用着色器):

// 设置纹理信息
function initTextures(gl, n) {
  // 创建纹理对象
  let texture0 = gl.createTexture()
  if (!texture0) {
    console.log('Failed to create texture0')
    return false
  }
  let texture1 = gl.createTexture()
  if (!texture1) {
    console.log('Failed to create texture1')
    return false
  }
  // 获取u_Sampler0和u_Sampler1存储位置
  let u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0')
  if (!u_Sampler0) {
    console.log('Failed to get the storage loaction of u_Sampler0')
    return false
  }
  let u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1')
  if (!u_Sampler1) {
    console.log('Failed to get the storage loaction of u_Sampler1')
    return false
  }
  // 创建Image对象
  let image0 = new Image()
  let image1 = new Image()
  // 绑定加载图像响应事件
  image0.onload = function () {
    loadTexture(gl, n, texture0, u_Sampler0, image0, 0)
  }
  image1.onload = function () {
    loadTexture(gl, n, texture1, u_Sampler1, image1, 1)
  }

  // 加载图像
  image0.src = '../image/sky.jpg'
  image1.src = '../image/circle.gif'

  return true
}

因为图像加载是异步的,所以在window下挂载了两个属性来表示图像加载的结果:

// 标记纹理单元是否已经就绪
var g_texUnit0 = false,
  g_texUnit1 = false

loadTexture()函数中,通过texUnit参数来区别两个纹理,做对应的处理,包括标记纹理单元是否已经被处理和将纹理单元传递给着色器,其它内容相同:

function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
  ...
  // 激活纹理单元
  if (texUnit == 0) {
    gl.activeTexture(gl.TEXTURE0)
    g_texUnit0 = true
  } else {
    gl.activeTexture(gl.TEXTURE1)
    g_texUnit1 = true
  }
  ...
  // 将纹理单元传递给着色器
  gl.uniform1i(u_Sampler, texUnit)
  ...
}

在纹理单元全部就绪后,可以调用着色器进行绘制:

  // 绘制
  if (g_texUnit0 && g_texUnit1) {
    // 清空绘图区
    gl.clearColor(0.0, 0.0, 0.0, 1.0)
    gl.clear(gl.COLOR_BUFFER_BIT)
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
  }

总结

WebGL纹理系统至此已经了解了很多

按照惯例,书中的总结如下:

本章深入地探索了WebGL的世界。现在,你已经掌握了 WebGL二维绘图的全部基本技能,也已经准备好开始下一段旅程:绘制三维对象。幸运的是,和三维对象打交道时,你会发现使用着色器的方式与进行二维绘图时非常相似,你可以迅速地用上迄今为止学到的所有知识。
本书接下来的部分将主要集中在与三维对象上有关的问题上。但是,在带你进入三维世界之前,你还需要熟悉一下OpenGL ES着色器语言(GLSL ES)的特性和功能。关于GLSL ES,前几章其实都是蜻蜓点水般地带过而已。

你可能感兴趣的:(WebGL基础,html5)