本文为读书笔记第五章
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
本章很长,讨论了前言所述的三个问题,三个由浅入深,帮助我们更细致地了解WebGL系统。虽然内容很长,此处也放在了一起进行呈现。(也许我们可以直接学习第三个问题,也有很多博客只给出了第三个关于纹理的内容,那些可能更适用于有一定经验的人取长补短,但对于初学者来说很难理解系统的机制。)
本章前两节主要讨论了缓冲区对象的使用,包括多个缓冲区对象和单个缓冲区对象的交错组织方式;
第三四节讨论了顶点着色器和片元着色器的工作流程
第五六节讨论了纹理(贴图)的相关内容,笔者在第五节末尾补充了非2的n次幂纹理的设置问题,但没有深入讨论,网上有很多相关的内容,有需要的学者可以自行了解,不排除之后补充的可能。。。。
书中每章的前言和小结都很有意义,言简意赅。笔记中将前言和小结完全复制下来以供参考。
在前几章研究了一些示例程序,通过绘制二维图形介绍了 WebGL的基础知识和关键概念。相信此时你已经对WebGL系统中处理单色几何图形的过程有了基本的理解。 这一章将在此基础上,深入讨论以下三个问题:
将顶点的其他(非坐标)数据——如颜色等——传入顶点着色器。
发生在顶点着色器和片元着色器之间的从图形到片元的转化,又称为图元光栅化 (rasterzation process)。
将图像(或称纹理)映射到图形或三维对象的表面上。
本章是最后一个关于WebGL关键性的基础知识的章节。在学习了本章之后,你将能够掌握在WebGL中使用颜色和纹理的方法,并具有足够的知识去创建精美的三维场景了。
相关内容:如标题所示,为顶点尺寸也建立一个缓冲区对象来进行操作
小结:多个缓冲区的操作只是将一个缓冲区的操作再复制一遍,没有新的函数和思路需要掌握,但其中揭示了WebGL处理缓冲区对象的一种方式,这种方式在初次使用缓冲区对象的时候有所介绍,此处表现得更加具体:WebGL系统每次只能处理一个缓冲区对象。具体见末尾笔者总结的部分——WebGL处理缓冲区对象的方式,感兴趣的读者也可以查找相关资料,也希望能互相交流。
为了探讨这一功能,本节示例程序为MultiAttributeSize
,分别绘制了三个不同尺寸(分别是10.0,20.0,30.0)的点,效果如下:
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
}
代码中都是以往应用的功能,此处我们在原绘制多个点的示例基础上,做了如下改变:
下图展示了示例程序刚运行完毕未清空颜色缓冲区时,WebGL系统内部的状态:
WebGL处理缓冲区对象的方式:
此处有一个细节不知道读者是否注意到,书中没有进行说明,笔者补充于此:
以上一些信息,说明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)
示例代码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属性代表了强类型数组中每个元素所占的字节数。
// 将缓冲区分配给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系统的内部行为如下:
相关内容:通过varying变量从顶点着色器向片元着色器传值
相关函数:varying小结:1. varying变量的作用:从顶点着色器向片元着色器传输数据;2. varying变量可以赋值的数据类型:float及相关数据类型;3. varying变量的行为(如何传值):末尾有图展示——在WebGL中,如果顶点着色器与片元着色器中又类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值会自动传入片元着色器。
本节示例为MultiAttributeColor
,顾名思义,本节示例希望在缓冲区对象中同时填充顶点坐标和颜色数据,然后分配给attribute变量用以处理颜色,最终呈现结果如下:
在此之前,我们只静态设置或统一设置(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变量,那么顶点着色器赋给该变量的值会自动传入片元着色器)。
注意:varying变量只能是float(以及相关的vec2,vec3,vec4,mat2,mat3,mat4)类型。
补充:片元着色器在声明新的变量的时候,需要在前面加上精度限定语句,此处为
precision mediump float
,这一语法在后面GLSL ES语言的介绍中会进行说明。
在第二章的笔记中,笔者曾经提到了一个问题:怎么判断绘制结束以清空颜色缓冲区,在之前地学习中,又逐渐发现了一个问题:片元着色器是如何和顶点着色器协同工作的(逐片元操作是什么意思)。值得高兴的是,这一节应该能给出两个问题的不少答案。
相关内容:着色器工作细节:顶点着色器-几何图像装配-光栅化-片元着色器;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)
示例程序在三角形表面产生了颜色平滑过渡的效果:
为了理解这种效果的产生方式,我们需要厘清顶点着色器和片元着色器之间的数据传输细节。
几何形状的装配和光栅化:
为了便于理解,此处采用第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
}
这里直接给出几何图形处理的细节过程:
如上图所示,顶点着色器和片元着色器之间有以下两个步骤:
gl.drawArrays()
第一个参数决定。WebGL系统中关于图形装配和光栅化的细节部分如下图所示:
需要注意以下几点:
gl_Position
是几何图形装配(geometric shape assembly)阶段输入的数据。所以,在HelloTriangle.js
示例中,顶点着色器会被执行三次,之后进行装配和光栅化,最后执行片元着色器,流程细节如下:
调用片元着色器:
光栅化之后,就可以逐片元调用片元着色器。
假如图形只有如下图所示实心正方形所示的10个片元,那么片元着色器会调用10次,每调用一次处理一个片元。对于每个片元(本身带有坐标信息),片元着色器计算出该片元的颜色,写入颜色缓冲区。
全部处理完毕后,浏览器会显示最终的结果。
对片元着色器内部操作:
前面说过,片元本身带有坐标信息,调用片元着色器时,这些坐标信息也随着片元传了过去,我们可以通过片元着色器内置变量来访问该坐标信息:
类型和变量名 | 描述 |
---|---|
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)
如此一来,可以通过片元坐标计算颜色值,效果如下:
varying作用过程:
在ColoredTriangel.js
中,我们指定了三个顶点的颜色,最后却得到了一个颜色渐变的三角形,这是因为varying变量在光栅化的过程中经过了内插操作,如下图所示:
内插之后,每个片元都根据三个顶点的v_Color变量得到了自己的v_Color变量,该变量和坐标信息(可能还有其他信息)一起保存在片元中。进入片元着色器后,新声明的varying变量v_Color接收原varying变量v_Color,然后赋值给gl_FragColor,最后会知道颜色缓冲区中。
所谓的内插过程(interpolation process)如下所示:
书中给出的内插操作均为线性内插,《计算机图形学》(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中,纹理映射的步骤如下:
准备好映射到几何图形上的纹理图像;
可以是浏览器支持的任意格式的图像
为几何图形配置纹理映射方式;
确定“几何图形的某个片元”的颜色如何取决于“纹理图像中哪个(或哪几个)像素”的问题(即前者到后者的映射)。此处利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标(texture coordinates)确定纹理图像的哪部分将覆盖到几何图形上。
加载纹理图像,对其进行一些配置,以在WebGL中使用它;
在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元。
纹理坐标与映射方式:
纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL系统中纹理坐标系统是二维的st坐标系统(GLSL ES中也是用st分量名),如下图所示:
我们将纹理坐标映射到WebGL系统顶点上,方式如下:
示例程序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)
}
呈现效果如下图所示:
示例代码越来越长,不过有很多是之前学习过的内容(包括顶点坐标赋值等),新加入的纹理相关操作流程可分为如下五个部分:
initVertexBuffers()
)initTextures()
)。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'
初看着色器设计,可以得到程序大致轮廓:
a_Position
用于存储顶点坐标信息,赋值给gl_Position
变量,通过顶点着色器绘制顶点;a_TexCoord
用于存储纹理坐标信息,赋值给varying变量v_TexCoord
。每一个顶点均有两个信息被赋予:a_Position
和a_TexCoord
,即顶点对应的坐标和纹理坐标。v_TexCoord
(插值处理)信息;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: 创建纹理对象失败。
错误: 无
创建纹理对象的过程如下图所示:
同样,删除纹理对象的方法如下:
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)
函数。
异步的过程在书中有一个很完整的图,如下:
配置纹理:
配置纹理的操作发生在图像加载完成之后,代码位于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)
}
下面进行分解:
gl.pixelStorei()
) // 对纹理图像进行Y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
WebGL中纹理坐标系统中t轴方向和PNG、BMP、JPG等格式图片的坐标系统Y轴方向是相反的,所以要先将图像Y轴进行反转才能正确映射(这一操作也可以放在着色器中手动进行)。
至此,我们已经接触了多个坐标系统:
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不是合法的值。
// 开启0号纹理单元
gl.activeTexture(gl.TEXTURE0)
WebGL通过一种称作纹理单元(texture unit)的机制同时使用多个纹理(单个纹理也遵循此规则),每个纹理单元有一个单元编号来管理一张纹理图像。
系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,但在默认情况下,WebGL至少支持8个纹理单元,一些其他的系统支持的个数更多。如下图所示,内置变量gl.TEXTURE0
、gl.TEXTURE1
······gl.TEXTURE7
各表示一个纹理单元。
使用纹理单元之前,还需要调用gl.activeTexture()
来激活它,该函数规范如下:
gl.activeTexture(texUnit):
激活texUnit指定的纹理单元。
参数:
texUnit: 指定准备激活的纹理单元:gl.TEXTURE0
、gl.TEXTURE1
······gl.TEXTURE7
。最后的数字表示纹理单元的编号
返回值: 无
错误:
INVALID_ENUM: texUnit不是合法的值。
激活纹理单元后,WebGL系统内如下图所示:
// 向target绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture)
与缓冲区对象类似,纹理对象在使用前也需要绑定到target,以确定类型,同时之后的配置操作也以target为对象(指针?)进行操作。
对这一部分的猜想:
WebGL或OpenGL的操作方法放在了target类下?显然,不同的target有不同的操作方法。
函数规范如下:
gl.bindTexture(target, texture):
开启texture指定的纹理对象,并将其绑定到target(目标)上。此外,如果已经通过gl.activeTexture()
激活了某个纹理单元,则纹理对象也会绑定到这个纹理单元上。
参数:
target: gl.TEXTURE_2D
或gl.TEXTURE_BUVE_MAP
texture: 表示绑定的纹理单元。
返回值: 无
错误:
INVALID_ENUM: target不是合法的值。
target的两个选项介绍如下:
纹理类型 | 描述 |
---|---|
gl.TEXTURE_2D | 二维纹理 |
gl.TEXTURE_CUBE_MAP | 立方体纹理(书中没有,可以参考OpenGL ES 2.0 Programming Guide一书) |
与缓冲区对象不同的是,此处完成了两个任务:开启纹理单元和绑定纹理对象。因为0号纹理单元已经激活,所以纹理对象也绑定到了纹理单元上。
在该方法执行完毕后,WebGL系统如下图所示:
结构已经搭建好了,接下来需要配置多个参数来进行使用。
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
配置纹理对象,就是设置纹理图像映射到图形上的具体方式:如何根据纹理坐标获取纹素颜色、按哪种方式重复填充纹理。
此处使用函数gl.texParamteri()
,函数规范如下:
gl.texParameteri(target, pname, param):
将param的值赋给绑定到目标的纹理对象的panme参数上。
参数:
target: gl.TEXTURE_2D
或gl.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 |
四种纹理参数的效果如下图所示:
可以赋值给gl.TEXTURE_MAG_FILTER
和gl.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_S
和gl.TEXTURE_WRAP_T
的常量:
值 | 描述 |
---|---|
gl.REPEAT | 平铺式的重复纹理 |
gl.MIRRORED_REPEAT | 镜像对称式的重复纹理 |
gl.CLAMP_TO_EDGE | 使用纹理图像的边缘值 |
在一些情况下,我们可以不使用gl.texParameteri()
方法配置纹理对象,而是保留其默认值。
示例程序进行到这一步后,WebGL系统如下图所示:
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_2D
或gl.TEXTURE_BUVE_MAP
level: 传入0(是为金字塔纹理准备的,本书不涉及)。
internalformat: 图像的内部格式。
format: 纹理数据的格式,必须使用与internalformat相同的值。
type: 纹理数据的类型。
image: 包含纹理图像的Image对象。
返回值: 无
错误:
INVALID_ENUM: target不是合法的值。
INVALID_OPERATION: 当前目标上没有绑定纹理对象
internalformat和format分别表示图像的内部格式和纹素的格式,一般情况下二者取值相同,如下表:
格式 | 描述 |
---|---|
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系统如下所示:
传输纹理单元给着色器(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
。
之后,片元着色器就可以访问纹理图像了:
着色器的行为
// 绘制矩形
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
之后我们使用gl.drawArrays()方法调用着色器绘制图形。因为涉及异步请求,绘制函数也需要放在图像加载的响应函数中。
根据着色器语言,纹理相关的着色器行为如下:
从顶点着色器向片元着色器传输纹理坐标
// 顶点着色器
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 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)
如果程序不需要平铺图像,可以接收非金字塔纹理的设置的话,可以采用该方法简单配置纹理对象。
效果如下:
方法二:采用DOM挂载的方法手动将图像设置为2的n次幕图像。
方法如下:
在配置纹理参数前,手动处理纹理图像,即:
把纹理图像加载到一个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’类型的上下文并需要把图像绘制其上。
效果如下:
新的OpenGL:
在OpenGL 2.0和之后的版本中,系统已经支持了非2的n次幂纹理的加载,不过这种纹理的渲染会大大降低显卡的性能,不推荐使用。
当然,WebGL似乎没有进行相关更新。
最后,我们修改一下顶点坐标,让图片更好看一点吧:
如果采用方法二,我们需要知道变化之后image(canvas)的长宽来设置顶点坐标。此处采用方法一,根据原图片长宽比例设置坐标,效果如下:
不如原图好看。
本节没有新的方法,只是在上一个示例的思路之上,增加了一个纹理图像,展示了方便地配置多幅纹理图像的一个思路,值得借鉴。
要点:1. 着色器语言中通过计算获得新的颜色;2. 图像加载响应函数中添加一个参数,以达到在多幅纹理图形同时使用的情况下该函数的复用。
纹理单元的设计就是为了方便WebGL使用多幅纹理,上一个示例程序使用了一幅纹理,在本节将使用两幅纹理进行绘制,形成如下效果:
示例程序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格式数据乘法如下:
在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,前几章其实都是蜻蜓点水般地带过而已。