【WebGL之巅】18-颜色与纹理-在矩形表面帖上图像

By yesmore on 2021-07-29
阅读时间 17 分钟
文章共 4.1k
阅读量

对应《WebGL编程指南》代码:20-TexturedQuad

要点:纹理映射、纹理坐标

知识点

一、纹理映射

​ 概念:将一张图像(像帖纸)映射(帖)到一个几何图形的表面上去。此时这张图片又可以称为纹理图像纹理(texture)。

​ 作用:根据纹理图像,为之前光栅化后的每个元素涂上合适的颜色。组成纹理图像的像素又被称为纹素,每一个纹素都使用RGB或RGBA格式编码。

二、纹理映射的步骤

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

​ 2、为几何图形配置纹理映射方式

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

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

三、纹理坐标

​ 第2步指定映射方式,就是确定“几何图形的某个片元”的颜色如何取决于“纹理图像中那个(或哪几个)像素”的问题(即前者到后者的映射)。我们利用图形顶点坐标来确定屏幕上那部分被纹理图像覆盖,使用纹理坐标来确定纹理图像的哪部分将覆盖到几何图形上。

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。
​ WebGL系统中的纹理坐标系统是二维的。
​ WebGL中使用st命名纹理坐标。(st坐标系统或uv坐标系统)

1

​ 纹理坐标很通用,因为坐标值与图像自身尺寸无关。

四、将纹理图像粘贴到几何图形上

​ 在WebGL中,我们通过纹理图像的纹理坐标与几何图形顶点坐标间的映射关系,来确定怎样将纹理图像贴上去。

2

​ 这里的映射关系为:

纹理坐标 顶点坐标
(0.0, 1.0) (-0.5, 0.5, 0.0)
(0.0, 0.0) (-0.5, -0.5, 0.0)
(1.0, 0.0) (0.5, -0.5, 0.0)
(1.0, 1.0) (0.5, 0.5, 0.0)

五、程序讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function initTextures(gl, n) {
var texture = gl.createTexture(); //创建纹理对象
if(!texture){
console.log('Failed to create the texture object');
return false;
}

//获取u_Sampler的存储位置
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
if (!u_Sampler) {
console.log('Failed to get the storage location of u_Sampler');
return false;
}

var image = new Image();//创建一个image对象
if (!image) {
console.log('Failed to create the image object');
return false;
}

//注册图像加载时间的响应函数
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
};

//浏览器开始加载图像
image.src = '../resources/sky.jpg';

return true;
}

function loadTexture(gl, n, texture, u_Sampler, image){
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);//对纹理图像进行y轴反转
//开启0号纹理单元
gl.activeTexture(gl.TEXTURE0);
//向target绑定纹理对象
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.MIRRORED_REPEAT);
//配置纹理图像
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);//绘制矩形
}

这段程序主要分五个部分:

1、顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器

2、片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元。

3、设置顶点的纹理坐标(initVertexBuffers()

1
2
3
4
5
6
7
8
var verticesTexCoords = new Float32Array(
[
-0.5, 0.5, 0.0, 1.0, // 左上 顶点(-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, // 右下
]
);

​ 将顶点坐标和纹理坐标写入缓冲区对象,将其中的顶点坐标分配给a_Position变量并开启之。

1
2
3
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);

​ 获取a_TexCoord变量的存储位置,将缓冲区中的纹理坐标分配给该变量更开启。

1
2
3
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);

4、配置和加载纹理,准备待加载的纹理图像,令浏览器读取它(initTextures()

①取样器

​ u_Sampler。“Sampler”意为取样器,因为从纹理图像中获取纹素颜色的过程,相当于从纹理图像中“取样”,即输入纹理坐标,返回颜色值。实际上,由于纹理像素也是有大小的,取样处的纹理坐标很可能并不在某个像素中心,所以取样通常并不是直接取纹理图像某个像素的颜色,而是通过附近的若干个像素共同计算而得。

1
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
②加载图像(异步加载)
1
2
3
4
5
6
7
8
9
10
11
var image = new Image();//创建一个image对象
// 注册图像加载时间的响应函数
image.onload = function () {
// 异步调用
loadTexture(gl, n, texture, u_Sampler, image);
};

// 浏览器开始异步加载图像,程序本身执行return true语句
// 然后,浏览器在某个时刻完成了对图像的加载,就会调用事件响应函数loadTexture()将加载得到的图像交给WebGL系统处理
image.src = '../resources/sky.jpg';
return true;

注意:出于安全性考虑,WebGL不允许使用跨域纹理图像。

异步加载图片过程:

​ [1]告诉浏览器在图像加载完成后调用loadTexture()

​ [2]浏览器开始加载图像

​ [3]向web服务器请求纹理图像

​ [4]服务器从数据库文件系统中获取图像

​ [5]服务器返回图像

​ [6]浏览器收到加载完成的图像

​ [7]调用loadTexture()函数

解析:上述步骤中,[1]和[2]是顺序执行的,而第[2]步到第[7]步不是。

5、监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理(loadTexture()

​ 为WebGL配置纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function loadTexture(gl, n, texture, u_Sampler, image){
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);//对纹理图像进行y轴反转
//开启0号纹理单元
gl.activeTexture(gl.TEXTURE0);
//向target绑定纹理对象
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.MIRRORED_REPEAT);
//配置纹理图像
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轴反转
1
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

​ 在使用图像前,必须对它进行Y轴反转。(WebGL纹理坐标中的t轴的方向和PNG、BMP、JPG等格式图片的坐标系统的Y轴方向是相反的。)

3

gl.pixelStorei()参数
pname 以下二者之一
gl.UNPACK_FLIP_Y_WEBGL 对图像进行Y轴反转,默认值为false
gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL 将图像RGB颜色值的每一个分量乘以A。默认false
param 指定非0或0。必须为整数
②激活纹理单元

​ WebGL通过纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。
​ 系统支持的纹理单元格数取决于硬件和浏览器的WebGL实现,但是在默认情况下,WebGL至少支持8个纹理单元,内置变量gl.TEXTURE0、gl.TEXTURE1…gl.TEXTURE7各表示一个纹理单元
​ 在使用纹理单元之前,还需要调用gl.aactiveTexture()来激活它。

1
gl.activeTexture(gl.TEXTURE0);
③绑定纹理对象(设置纹理的类型)
1
2
// 注意:该方法完成了两个任务:开启纹理对象,以及将纹理对象绑定到纹理单元上。
gl.bindTexture(gl.TEXTURE_2D, texture);

​ 解析:告诉WebGL系统纹理对象使用的是哪种类型的纹理。在对纹理对象进行操作之前,需要绑定纹理对象,这一点与缓冲区很像:在对缓冲区对象进行操作(写入数据)之前,也需要绑定缓冲区对象。如下:

1
2
3
4
// 将缓冲区对象保存到目标上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
// 向缓存对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

WebGL支持两种类型的纹理:

​ gl.TEXTURE_2D:二维纹理

​ gl.TEXTURE_CUBE_MAP:立方体纹理

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

④配置纹理对象的参数

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

gl.texParameteri()
参数1:target gl.TEXTURE_2D或gl.TEXTURE_CUBE_MAP
参数 2:pname 纹理参数(参见)
参数3:param 纹理参数的值

纹理参数:

4

纹理参数 描述 默认值 其他值
gl.TEXTURE_MAX_FILTER 纹理放大。当纹理的绘制范围比较纹理本身更大时,如何获取纹素颜色。(图像比空间小) gl.LINEAR: 见下图
gl.TEXTURE_MIN_FILTER 纹理缩小。当纹理的绘制范围比较纹理本身更小时,如何获取纹素颜色。(图像比空间大) gl.NEAREST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S 纹理水平填充 gl.REPEAT
gl.TEXTURE_WRAP_T 纹理垂直填充 gl.REPAET

5

​ 每个纹理参数都有一个默认值,不调用gl.texParameteri就使用默认值。本例修改了gl.TEXTURE_MIN_FILTER参数,它的默认值是一种特殊的、被称为MIPMAP(也称金字塔)的纹理类型。

1
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

注:曼哈顿距离,即直角距离,棋盘距离。如(x1,y1)和(x2,y2)的曼哈顿距离为|x1-x2|+|y1-y2|。

⑤将纹理图像分配给纹理对象(gl.texImage2D),同时允许你告诉WebGL系统关于该图像的一些特性。

6

​ 这时,Image对象中的图像就从JavaScript传入WebGL系统中,并存储在纹理对象中。

​ 纹理数据的格式:

7

format标示纹理数据的格式,必须根据纹理图像的格式来选择这个参数:
JPG => gl.RGB
PNG => gl.RGBA
BMP => gl.RGB

补充:

​ 1.在webgl中,internalformat必须和format一样。

​ 2.gl.LUMUNANCEgl.LUMINANCE_ALPHA 通常用在灰度图像上等等

​ 3.流明(luminance)标示我们感知到的物体表面的亮度。通常使用物体表面红、蓝 颜色分量指的加权平均来计算流明。

​ 纹理数据的数据格式:

type参数制定了纹理数据类型。通常使用gl.UNSIGNED_BYTE数据类型。

8

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

一旦将纹理图像传入了WebGL系统,就必须将其传入片元着色器并映射到图形的表面上去。

1
2
3
4
5
//片元着色器程序
var FSHADER_SOURCE =
...
'uniform sampler2D u_Sampler;\n' +
...

用于纹理对象的数据类型:

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

通过纹理单元编号(gl.TEXTUREn中的n)将纹理对象传给u_Sampler

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

执行完后,片元着色器就能够访问纹理图像了。

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

通过同名的varying变量传输数据:

​ 通过attribute变量a_TexCoord接收顶点的纹理坐标,将数据赋值给varying变量v_TexCoord并将纹理坐标传入片元着色器。

1
2
3
4
5
6
7
8
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';
⑧在片元着色器中获取纹理像素颜色(texture2D())
1
gl_FragColor = texture2D(u_Sampler, v_TexCoord);

9

​ 纹理放大和缩小方法的参数将决定WebGL系统将以何种方式内插出片元。我们将texture2D函数返回值赋给了gl_FragColor变量,然后片元着色器就将当前片元染成这个颜色。

实例1

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// TexturedQuad.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 =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\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() {
var canvas = document.getElementById("webgl");
if (!canvas) {
console.log("Failed to retrieve the <canvas> element");
return;
}

var 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;
}

//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}

// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 配置纹理
if (!initTextures(gl, n)) {
console.log('Failed to intialize the texture.');
return;
}
}

function initVertexBuffers(gl){
var 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,
// -0.5, 0.5, -0.3, 1.7,
// -0.5, -0.5, -0.3, -0.2,
// 0.5, 0.5, 1.7, 1.7,
// 0.5, -0.5, 1.7, -0.2
]
);
var n=4;// 顶点数目

// 创建缓冲区对象
var vertexTexCoordBuffer = gl.createBuffer();
if(!vertexTexCoordBuffer){
console.log("Failed to create thie buffer object");
return -1;
}

// 将顶点坐标和纹理坐标写入缓冲区对象
// 将缓冲区对象保存到目标上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
// 向缓存对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

var 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_Postion变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE*4, 0);
// 连接a_Postion变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);

// 将纹理坐标分配给a_TexCoord并开启它
var 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) {
var texture = gl.createTexture(); //创建纹理对象
if(!texture){
console.log('Failed to create the texture object');
return false;
}

// 从片元着色器中获取uniform变量u_Sampler(取样器)的存储位置
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
if (!u_Sampler) {
console.log('Failed to get the storage location of u_Sampler');
return false;
}

var image = new Image();//创建一个image对象
//注册图像加载时间的响应函数
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
};

//浏览器开始加载图像
image.src = '../resources/sky.jpg';

return true;
}

function loadTexture(gl, n, texture, u_Sampler, image){
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);//对纹理图像进行y轴反转
//开启0号纹理单元
gl.activeTexture(gl.TEXTURE0);
//向target绑定纹理对象
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.MIRRORED_REPEAT);
//配置纹理图像
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);//绘制矩形
}

效果

10

实例2

代码

TexturedQuad_Repeat

1
2
3
4
5
6
7
8
9
10
11
12
13
仅修改此处:
var 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,
-0.5, 0.5, -0.3, 1.7,
-0.5, -0.5, -0.3, -0.2,
0.5, 0.5, 1.7, 1.7,
0.5, -0.5, 1.7, -0.2
]
);

效果

​ 由于纹理图像不足以覆盖整个矩形,所以可以看到在本该空白的区域,纹理重复出现了,因为gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T都默认值为gl.REPEAT

11

实例3

代码

TexturedQuad_Clamp_Mirror

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
修改1
var 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,
-0.5, 0.5, -0.3, 1.7,
-0.5, -0.5, -0.3, -0.2,
0.5, 0.5, 1.7, 1.7,
0.5, -0.5, 1.7, -0.2
]
);

修改2
//配置纹理参数
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.MIRRORED_REPEAT);

效果

​ 修改纹理参数,可见s轴(水平轴)上,纹理外填充了最边缘纹素的颜色,而t轴(垂直轴)上镜像的重复填充纹理。

12


Tips: Please indicate the source and original author when reprinting or quoting this article.