对应《WebGL编程指南》代码:20-TexturedQuad
要点:纹理映射、纹理坐标
知识点
一、纹理映射
概念:将一张图像(像帖纸)映射(帖)到一个几何图形的表面上去。此时这张图片又可以称为纹理图像 或纹理 (texture)。
作用:根据纹理图像,为之前光栅化后的每个元素涂上合适的颜色。组成纹理图像的像素又被称为纹素 ,每一个纹素都使用RGB或RGBA格式编码。
二、纹理映射的步骤
1、准备好映射到几何图形上的纹理图像
2、为几何图形配置纹理映射方式
3、加载纹理图像,对其进行一些配置,以在WebGL中使用它
4、在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋值给片元
三、纹理坐标
第2步指定映射方式,就是确定“几何图形的某个片元”的颜色如何取决于“纹理图像中那个(或哪几个)像素”的问题(即前者到后者的映射)。我们利用图形顶点坐标来确定屏幕上那部分被纹理图像覆盖,使用纹理坐标 来确定纹理图像的哪部分将覆盖到几何图形上。
纹理坐标
是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。
WebGL系统中的纹理坐标系统是二维 的。
WebGL中使用s
和t
命名纹理坐标。(st坐标系统或uv坐标系统)
纹理坐标很通用,因为坐标值与图像自身尺寸无关。
四、将纹理图像粘贴到几何图形上
在WebGL中,我们通过纹理图像的纹理坐标与几何图形顶点坐标间的映射关系,来确定怎样将纹理图像贴上去。
这里的映射关系为:
纹理坐标
顶点坐标
(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 ; } 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(); 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 ); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); gl.texImage2D(gl.TEXTURE_2D, 0 , gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 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 , 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.onload = function ( ) { loadTexture(gl, n, texture, u_Sampler, image); }; 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 ); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); gl.texImage2D(gl.TEXTURE_2D, 0 , gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 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轴方向是相反的。)
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
纹理参数的值
纹理参数:
纹理参数
描述
默认值
其他值
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
每个纹理参数都有一个默认值,不调用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系统关于该图像的一些特性。
这时,Image对象中的图像就从JavaScript传入WebGL系统中,并存储在纹理对象中。
纹理数据的格式:
format标示纹理数据的格式,必须根据纹理图像的格式来选择这个参数:
JPG => gl.RGB
PNG => gl.RGBA
BMP => gl.RGB
补充:
1.在webgl中,internalformat
必须和format
一样。
2.gl.LUMUNANCE
和 gl.LUMINANCE_ALPHA
通常用在灰度图像上等等
3.流明
(luminance)标示我们感知到的物体表面的亮度 。通常使用物体表面红、蓝 颜色分量指的加权平均来计算流明。
纹理数据的数据格式:
type
参数制定了纹理数据类型。通常使用gl.UNSIGNED_BYTE
数据类型。
⑥将纹理单元传递给片元着色器
一旦将纹理图像传入了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 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);
纹理放大和缩小方法的参数将决定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 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 ; } 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 , ] ); 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 ; } gl.vertexAttribPointer(a_Position, 2 , gl.FLOAT, false , FSIZE*4 , 0 ); gl.enableVertexAttribArray(a_Position); 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 ; } 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.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 ); gl.activeTexture(gl.TEXTURE0); 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); gl.uniform1i(u_Sampler, 0 ); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLE_STRIP, 0 , n); }
效果
实例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.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_S
和gl.TEXTURE_WRAP_T
都默认值为gl.REPEAT
。
实例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.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轴(垂直轴)上镜像的重复填充纹理。
Tips:
Please indicate the source and original author when reprinting or quoting this article.