引入纹理
三维模型表面有了自己的纹理,桌子才成为桌子,房子才成为房子。
纹理是用体现表面特征的图片贴上去的,就像装修贴墙纸一样。图片可以拉伸扭曲,假设我们用一张图贴一面墙,给出图左上贴墙左上,图右上贴墙右上……四个角都对应上,图就贴上去了。而位置的关系,用坐标来表示。
这一节我们学习如何贴纹理,为了减少些工作量,先把上一节代码所有四面体有关的部分删除,来给正方体贴纹理。
效果如图5。
图5
function webGLStart()
{
//...
////initBuffers();
initTexture();
////gl.clearColor(0.0, 0.0, 0.0, 1.0);
////gl.enable(gl.DEPTH_TEST);
//setTimeout("tick()", 100);
//tick();
}
一开始加入纹理的初始化函数。tick()我们知道是执行动画的部分,直接用tick()的话,我看到了一个WARNING:
[.WebGLRenderingContext]RENDER WARNING: texture bound to texture unit 0 is not renderable. It maybe non-power-of-2 and have incompatible texture filtering or is not 'texture complete'
我猜应该是JS异步执行这个特点造成的,纹理加载需要时间,在纹理还没加载完成的时候就开始绘制了,于是有了这个提示。用JS的setTimeout简单处理了一下,大意就是等“一小会儿”再绘制。不过这个方法还是不稳定,网页响应慢的话还是会WARNING。最后把tick()改到下面读取纹理时加载图片的响应函数里了。
var myTexture;
function initTexture()
{
myTexture = gl.createTexture();
myTexture.image = new Image();
myTexture.image.onload = function()
{
handleLoadedTexture(myTexture);
tick();
}
myTexture.image.src = "/Public/image/mytexture.jpg";
}
先用createTexture()建立一个纹理对象给myTexture,再让myTexture随身携带它的纹理的图片,是一个JS的image对象。为这个image对象的onload事件指定触发我们处理纹理的函数handleLoadedTexture()。tick()是从webGLStart()挪过来的,保证加载图片后再tick(),避免那个WARNING。
之后设置image的地址,可以是自己服务器的相对地址也可以是网络上图片的地址。指定地址后浏览器就开始在后台异步获取这个图片,获取成功后就触发了onload事件。
function handleLoadedTexture(texture)
{
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindTexture和gl.bindBuffer相似,绑定texture为“当前纹理”,接下来没有新的绑定的话,对纹理的处理都是对texture的处理。
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.pixelstorei用于设置像素存储模式,第一个参数表示读入纹理的图片垂直翻转,这个是因为图片和纹理的坐标系统不同,翻过来就对应了,后面写纹理坐标会好写点。后一个参数表示存储器中每个像素行有1个字节对齐,我们先不用管。
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,
gl.UNSIGNED_BYTE, texture.image);
gl.texImage2D,将刚读入的图片传入显卡纹理缓存,参数从前往后为:使用图片类型、细节层次(以后说)、显卡中储存格式(重复两次,以后说)、每个通道的规格(存储R、G、B等的数据类型)、图像对象本身。
gl.texParameteri(gl.TEXTURE_2D,
gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER, gl.NEAREST);
这两句分别表示目标区域比纹理图片大了或小了的时候如何处理,TEXTURE_MAG_FILTER和TEXTURE_MIN_FILTER分别是放大和缩小。NEAREST是使用纹理坐标中最接近的像素颜色作为需要绘制的颜色,速度快,效果看起来可能会有好多“块块”,不平滑。找个尺寸小点的图片当纹理,对比下gl.LINEAR试试看。
gl.bindTexture(gl.TEXTURE_2D, null);
}
最后把“当前纹理”绑定为空。这是不必要的,但是一种好习惯。
接着我们要在initBuffers()中设置纹理坐标。既然有纹理贴图,就不需要之前的颜色了,把设置颜色有关的变量和函数都替换为纹理相关的。
var cubeVertexTextureCoordBuffer;
function initBuffers()
{
//...
cubeVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
textureCoords = [
// 正面
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// 背面
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// 顶部
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
// 底部
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
// 右侧面
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// 左侧面
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
cubeVertexTextureCoordBuffer.itemSize = 2;
cubeVertexTextureCoordBuffer.numItems = 24;
//...
}
和之前设置点坐标、点颜色类似,纹理坐标也是点的一个属性(attribute),纹理坐标看做纹理图片上的一个比例,左下角是(0, 0),右上角是(1, 1),每个三维点对应纹理图片上按比例换算的一个位置,点与点之间也在这个比例上自动插值,显卡就知道了纹理图片与三维形状每个位置的对应关系。
var xRot = 0;
var yRot = 0;
var zRot = 0;
function drawScene()
{
////mat4.translate(mvMatrix, mvMatrix, [0.0, 0.0, -5.0]);
mat4.rotate(mvMatrix, mvMatrix, degToRad(xRot), [1, 0, 0]);
mat4.rotate(mvMatrix, mvMatrix, degToRad(yRot), [0, 1, 0]);
mat4.rotate(mvMatrix, mvMatrix, degToRad(zRot), [0, 0, 1]);
////gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
////gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
//// cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute,
cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, myTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);
//...
}
改了一下旋转的方式,这个不做重点,应该可以理解。
对于textureCoordAttribute,和点坐标一样把纹理坐标派给shader中的对应变量。
还记得setMatrixUniforms()吗,那里我们用的uniformMatrix4fv(),uniform1i()也类似的,“uniform”后面的“1i”表示参数的类型,告诉显卡我们用的0号纹理,下面修改GLSL代码来看在shader里是怎样的。
<script id = "shader-vs" type = "x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;//new
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec2 vTextureCoord;//new
void main(void)
{
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;//new
}
</script>
顶点着色器中,我们用纹理代替了之前的颜色,和颜色属性(attribute)类似的,传给下一个着色器的过程中纹理坐标会自动插值。
<script id = "shader-fs" type = "x-shader/x-fragment">
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void)
{
gl_FragColor =
texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}
</script>
片元着色器中,得到了插值后的纹理坐标,通过texture2D()来通过纹理获取这个片元的颜色。uSampler是我们前面用uniform1i()给出的纹理编号,这里的s和t是别名,用x和y也是可以的。
于是我们完成了对模型的贴图。