赞
踩
目标:将一张图片贴在矩形上
结果:
在前一节中,我们了解了如何绘制彩色的图形,如何内插出平滑的颜色渐变效果。虽然这种方法很强大,但在更复杂的情况下仍然不够用。比如说,如果你想创建如下图的一堵逼真的砖墙,问题就来了。你可能会试图创建很多歌三角形,指定它们的颜色和位置来模拟墙面上的坑坑洼洼,如果你真这么做了,那就陷入了繁琐和无异议的苦海中。
你可能已经知道,在三维图形学中,有一项很重要的技术可以解决这个问题,那就是纹理映射。纹理映射其实非常简单,就是将一张图像(就像一张贴纸)映射(贴)到一个几何图形的表面上去。将一张真是世界的图片铁道一个由两个三角形组成的矩形上,这种矩形表面看上去就是这张图片。此时,这张图片又可以称为 纹理图像或纹理。
纹理映射的作用,就是根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色。组成纹理图像的像素又被称为纹素,每一个纹素的颜色都使用 RGB 或 RGBA 格式编码。
在 WebGL中,要进行纹理映射,需遵循以下四步:
我们来仔细研究上述第1步到第4步。第1步中准备的纹理图像,可以使浏览器支持的任意格式的图像。你可以使用任何照片,包括你自己拍摄的,当然你也可以使用本书示例代码中 resource 文件夹下的图像。
第2步指定映射方式,就是确定“几何图形的某个片元”的颜色如何取决于“纹理图像中哪个(或哪几个)像素”的问题(即前者到后者的映射)。我们利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标来确定纹理图像的哪部分将覆盖到几个图形上。纹理坐标是一套新的坐标系统,下面就来仔细研究一下。
纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹理颜色。WebGL 系统上的纹理坐标系统是二维的,如下图所示。为了将纹理坐标和广泛使用的 x 坐标和 y 坐标区分开来, WebGL 使用 s 和 t 命名纹理坐标。
纹理坐标很通用,因为坐标值与图像自身的尺寸无关,不管是 128x128还是 128x256 的图像,其右上角的纹理坐标始终是(1.0, 1.0)。
如前所述,在WebGL 中,我们通过纹理图像的纹理坐标与几何形体顶点坐标间的映射关系,来确定怎样将纹理图像贴上去。
在这里,我们将纹理坐标(0.0,1.0)映射到顶点坐标(-0.5,0.5,0.0)上,等等。通过建立矩阵死歌顶点与纹理坐标的对应关系,就获得了上图右边所示的结果。
纹理映射的过程需要顶点着色器和片元着色器二者的配合:首先在顶点着色器中为每个顶点指定纹理坐标,然后再片元着色器中根据每个片元的纹理坐标从纹理图像中抽取纹素颜色。
TexturedQuad.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec2 a_TexCoord;'+
'varying vec2 v_TexCoord;'+
'void main(){'+
'gl_Position = a_Position;'+
'v_TexCoord = a_TexCoord;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'precision mediump float;\n' +
'uniform sampler2D u_Sampler;'+
'varying vec2 v_TexCoord;'+
'void main(){'+
'gl_FragColor = texture2D(u_Sampler, v_TexCoord);'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the <canvas> element");
return;
}
//获取WebGL绘图上下文
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;
}
//指定清空<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
]);
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;
}
//获取 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.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); // Clear <canvas>
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}
这段程序主要分五个部分。
监听纹理图像的加载时间,一旦加载完成,就在WebGL 系统中使用纹理(loadTexture())。
让我们从第3部分(使用 initVertexBuffers()为每个顶点设置纹理坐标)开始。着色器(前两个部分)将在图像加载完成之后执行,所以最后再解释。
将纹理坐标传入顶点着色器,与将其他顶点数据(如颜色)传入顶点着色器的方法是相同的。我们可以将纹理坐标和顶点坐标写在同一缓冲区:定义数组 verticesTexCoords,成对记录每个顶点的顶点坐标和纹理坐标,如下所示:
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
]);
可见,第1个顶点(-0.5,0.5)对应的纹理坐标是(0.0,1.0),第2个顶点(-0.5,-0.5)对应的纹理坐标是(0.0,0.0),第3个顶点(0.5,0.5)对应的纹理坐标是(1.0,1.0),第4个顶点(0.5,-0.5)对应的纹理坐标是(1.0,0.0)。
然后我们将顶点坐标和纹理坐标写入缓冲区对象,将其中的顶点坐标分配黑 a_Position 变量并开启之。接着,获取 a_TexCoord 变量的存储位置,将缓冲区中的纹理坐标分配给该变量,并开启之。
//将纹理坐标分配给 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);
initTextures()函数负责配置和加载纹理:首先调用 gl.createTexture()创建纹理对象,纹理对象用来管理 WebGL 系统中的纹理。然后调用 gl.getUniformLocation()从片元着色器获取 uniform 变量 u_Sample(取样器)的存储位置,该变量用来接收纹理图像。
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");
gl.createTexture()方法可以创建纹理对象。
调用该函数将在 WebGL 系统中创建一个纹理对象,gl.TEXTIRE0 到 gl.TEXTURE7 是管理纹理图像的8个纹理单元,每一个都与 gl.TEXTURE_2D相关联,而后者就是绑定纹理时的纹理目标。
接下来,请求浏览器加载纹理图像供 WebGL 使用,该纹理图像将会映射到矩形上。为此,我们需要使用 Image 对象:
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';
这段代码创建了一个 Image 对象,然后为其注册了 onload 事件响应函数 loadTexture(),图像加载完成后就会调用该函数。最后通知浏览器开始加载图像。
必须使用 new 操作符新建 Image 对象,就像你新建一个 Array 对象或 Date 对象时一样。Image 是 JS 内置的一种对象类型,它通常被用来处理图像。
var image = new Image(); //创建一个image 对象
由于加载图像的过程是异步地,所以我们需要监听加载完成事件(onload):一旦浏览器完成了对图像的加载,就将加载得到的图像交给WebGL 系统。注册 onload 事件响应函数相当于告诉浏览器,在完成了对纹理图像的加载之后,异步调用loadTexture()函数。
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
}
loadTexture()函数接受5个参数,最后一个参数就是刚刚加载得到的图像(即 Image 对象)。第1个参数gl 是WebGL 绘图上下文,参数 n 是顶点的个数,参数 texture 是之前创建的纹理对象,而 u_Sample 是着色器中 uniform 变量 u_Sample 的存储位置。
就像 HTML 中的 <canvas>标签一样,我们为 Image 对象添加 src 属性,将该属性赋值为图像文件的路径和名称来告诉浏览器开始加载图像。注意,出去安全性考虑,WebGL 不允许使用跨域纹理图像:
image.src = '../resources/sky.jpg';
在执行完上一行代码后,浏览器开始异步加载图像,而程序本身则继续运行到 return 语句并退出。然后,浏览器在某个时刻完成了对图像的加载,就会滴哦用事件响应函数 loadTexture()将加载得到的图像交给 WebGL 系统处理。
loadTexture()函数的定义如下:
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.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); // Clear <canvas>
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}
该函数的主要任务是配置纹理供 WebGL 使用。使用纹理对象的方式和使用缓冲区类似,下面让我们研究一下。
在使用图像之前,你必须对它进行Y轴反转。
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
该方法对图像进行了Y轴翻转。如图所示,WebGL 纹理坐标系统中的 t 轴的方向和PNG、BMP、JPG等格式图片的坐标系统Y轴方向是相反的。因此,只有先将图像 Y 轴进行反转,才能够正确地将图像映射到图形上。
下面是 gl.pixelStorei()方法的规范:
WebGL 通过一种称为纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。即使你的程序只需要使用一张纹理图像,也得为其指定一个纹理单元。
系统支持的纹理单元个数取决于硬件和浏览器的WebGL 实现,但是在默认情况下,WebGL 至少支持8个纹理单元,一些其他的系统支持的个数更多。内置的变量 gl.TEXTURE0、gl.TEXTURE1……gl.TEXTURE7各表示一个纹理单元。
在使用纹理单元之前,还需要调用 gl.activeTexture()来激活它:
gl.activeTexture(gl.TEXTURE0);
接下来,你还需要告诉WebGL 系统纹理对象使用的是哪种类型的纹理。在对纹理对象进行操作之前,我们需要绑定纹理对象,这一点与换缓冲区很像:在对缓冲区对象进行操作之前,也需要绑定缓冲区对象。WebGL 支持两种类型的纹理,如图所示:
示例程序使用一张二维图形作为纹理,所以传入了 gl.TEXTURE_2D。立方体纹理的内容超出了本书的讨论范围。
gl.bindTexture(gl.TEXTURE_2D, texture);
注意,该方法完成了两个任务:开启纹理对象,以及将纹理对象绑定到纹理单元上。在本例中,因为0号纹理单元已经被激活了,所以在执行完上面的代码后,WebGL 系统内部状态如图所示:
这样,我们就制定了纹理对象的类型(gl.TEXTURE_2D)。本书将始终使用该类型的纹理。实际上,在WebGL 中,你没法直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象。
接下来,还需要配置纹理对象的参数,以此来设置纹理图像映射到图形上的具体方式:如何根据纹理坐标获取纹素颜色、按哪种方式重复填充纹理。我们使用通用函数 gl.texParameteri()来设置这些参数。
如图所示,通过 pname 可以指定4个纹理参数:
下表显示了每种纹理参数的默认值:
下表显示了可以赋给 gl.TEXTURE_MAG_FILTER 和 gl.TEXTURE_MIN_FILTER 的常量:
下表显示了可以赋给 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 的常量:
每个纹理参数都有一个默认值,通常你可以不调用 gl.texParameteri()就使用默认值。然后,本例修改了 gl.TEXTURE_MIN_FILTER 参数,它的默认只是一种特殊的、被称为 MIPMAP(也成金字塔)的纹理类型。MIPMAP 纹理实际上是一系列纹理,或者说是原始纹理图像的一系列不同分辨率的版本。本书不大会用刀这种类型,也不做详细介绍了。总之,我们把参数 gl.TEXTIRE_MIN_FILTER 设置为 gl.LINEAR。
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
纹理对象的参数都被设置好后,WebGL 系统的内部状态如图所示
接下来,我们将纹理图像分配给纹理对象。
我们使用 gl.texImage2D()方法将纹理图像分配给纹理对象,同时,该函数还允许你告诉WebGL 系统关于该图像的一些特性。
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
这时,Image 对象中的图像就从 JS 传入 WebGL 系统中,并存储在纹理对象中,如图所示:
快速看一下调用该方法时的每个参数的取值。level 参数直接用0就好了,因为我们没用到金字塔纹理。format 参数表示纹理数据的格式,具体取值如下表所示,你必须根据纹理图像的格式来选择这个参数。示例程序中使用的纹理图片是JPG 格式的,改格式将每个像素用RGB三个分量表示,所以我们将参数指定为 gl.RGB。对其他格式的图像,如PNG格式的图像,通常使用 gl.RGNA,BMP格式的图像通常使用 gl.RGB,而 gl.LUMINANCE 和 gl.LUMINANCE_ALPHA 通常用在灰度图像上等等。
这里的流明表示我们感知到的物体表面的亮度。通常使用物体表面红、绿、蓝颜色分量值的加权平均来计算流明。
fk.texImage2D()方法将纹理图像存储在了WebGL 系统中的纹理对象中。一旦存储,你必须通过 internalformal 参数告诉系统文理图像的格式类型。在 WebGL 中,internalformat 必须和 format 一样。
type参数指定了纹理数据类型。通常我们使用 gl.UNSIGNED_BYTE 数据类型。当然也可以使用其他数据类型。如gl.UNSIGNED_SHORT_5_6_5.后面的几种数据格式通常用来压缩数据,以减少浏览器加载图像的时间。
一旦将纹理图像传入了 WebGL 系统,就必须将其传入片元着色器并映射到图形的表面上去。如前所述,我们使用 uniform 变量来表示纹理,因为纹理图像不会随着片元变化。
var FSHADER_SOURCE=
'precision mediump float;\n' +
'uniform sampler2D u_Sampler;'+
'varying vec2 v_TexCoord;'+
'void main(){'+
'gl_FragColor = texture2D(u_Sampler, v_TexCoord);'+
'}';
必须将着色器中表示纹理对象的 uniform 变量声明为一种特殊的、专用于纹理对象的数据类型, 如下表所示。示例程序使用二维纹理 gl.TEXTURE_2D,所以该 uniform 变量的数据类型设为 sampler2D。
在 initTextures()函数中,我们获取了 uniform 变量 u_Sampler 的存储地址,并将其作为参数传给了 loadTexture()函数。我们必须通过指定纹理单元编号将纹理对象传给 u_Sampler。本例唯一的纹理对象被绑定在了 gl.TEXTURE0 上,所以调用 gl.uniformi()时,第2个参数为0。
gl.uniform1i(u_Sampler, 0);
执行完这一行后,WebGL 系统的内部状态如图所示,这样片元着色器就终于能够访问纹理图像了。
由于我们是通过 attribute 变量 a_TexCoord 接受顶点的纹理坐标,所以将数据赋值给 varying 变量 v_TexCoord 并将纹理坐标传入片元着色器是可行的。你应该还记得,片元着色器和顶点着色器内的同名、同类型的 varying 变量可用来在两者之间传输数据。顶点之间片元的纹理坐标会在光栅化的过程中内插出来,所以在片元着色器中,我们使用的是内插后的纹理坐标。
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec2 a_TexCoord;'+
'varying vec2 v_TexCoord;'+
'void main(){'+
'gl_Position = a_Position;'+
'v_TexCoord = a_TexCoord;'+
'}';
这样就完成了在 WebGL 系统中使用纹理的所有准备工作。
剩下的工作就是,根据片元的纹理坐标,从纹理图像上抽取出纹素的颜色,然后涂到当前的片元上。
片元着色器从纹理图像上获取纹素的颜色
'gl_FragColor = texture2D(u_Sampler, v_TexCoord);'+
使用 GLSL ES 内置函数 texture2D()来抽取纹素颜色。该函数很容易使用,只需要传入两个参数——纹理单元编号和纹理坐标,就可以取得纹理上的像素颜色。这个函数是内置的,留意一下其参数类型和返回值。
texture2D()的返回值
纹理放大和缩小方法的参数将决定 WebGL 系统将以何种方式内插出片元。我们将 texture2D()函数的返回值赋给了 gl_FragColor 变量,然后片元着色器就将当前片元染成这个颜色。最后纹理图像就被映射到了图形上,并最终被画了出来。
这已经是进行纹理映射的最后一步了。此时,纹理已经加载好、设置好,并映射到了图形上,就等你画出来了。
如你所见,在WebGL 中进行纹理映射是一个相对复杂的过程,一方面是因为你得让浏览器曲加载纹理图像;另一方面是因为,即使只有一个纹理,你也得使用纹理单元。但是一旦你掌握了这些基本的步骤,以后使用起来就会得心应手多了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。