WebGL教程7:键盘输入和纹理过滤
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com
欢迎来到WebGL教程的第六课,这节课的内容是基于NeHe OpenGL教程的第七节改编的。在这节课上,我们将会介绍如何在WeblGL页面上实现键盘输入。我们将用键盘输入来控制贴上纹理贴图的立方体的旋转方向和旋转速度;并且,我们还可以改变纹理过滤的方式,你可以选择加载速度快但图像质量低,或者加载速度慢但图像质量高的表现形式。在NeHe的OpenGL教程的第七节中,不光介绍了这些,还包括光线;因为光线在WebGL中比OpenGL有更多的作用,所以我在这节课上先不讲,以后拿出来单独讲。
下面的视频就是我们这节课将会完成的最终效果。
加载完成后,你可以使用Page Up 和 Page Down键来进行缩放;你还可以用方向键来改变立方体的旋转方向(按下的时间越长,旋转的速度越快)。你还可以点击F键在三个不同的纹理过滤之间切换。
下面我们来看看它是怎么工作的……
惯例声明:本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而可以快速地创建自己的3D Web页面。如果你还没有阅读前一课,请先阅读前一课的内容吧。因为本课中我只会讲解那些与前一课中不同的新知识。
另外,我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。
有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;或者点击这里下载我们为您准备好的压缩包。
这节课和之前最大的不同在于,我们需要把重心转到键盘上,我们先来看一看代码,这样的话,比较容易理解我们的课程。在示例代码中,你会发现我们定义了如下的全局变量:
192 193 194 195 196 197 198 199 200 |
var xRot = 0; var xSpeed = 0; var yRot = 0; var ySpeed = 0; var z = -5.0; var filter = 0; |
我们来看一下驱动纹理过滤的代码。第一处的改变在于加载纹理的代码,这段代码位于示例代码自上到下三分之一处。这一部分代码和以前有很大不同,因此,我就不标红任何东西了。但是,你们应该还是十分熟悉这样的代码形式。
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 |
function handleLoadedTexture(textures) { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[0].image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.bindTexture(gl.TEXTURE_2D, textures[1]); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[1].image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.bindTexture(gl.TEXTURE_2D, textures[2]); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[2].image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); gl.generateMipmap(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, null); } var crateTextures = Array(); function initTexture() { var crateImage = new Image(); for (var i=0; i < 3; i++) { var texture = gl.createTexture(); texture.image = crateImage; crateTextures.push(texture); } crateImage.onload = function () { handleLoadedTexture(crateTextures) } crateImage.src = "crate.gif"; } |
handleLoadedTexture 也并没有发生任何复杂的变化。之前,我们只是在初始化了一个带有图像数据的WebGL纹理物体,并且设置了两个参数gl.TEXTURE_MAG_FILTER 和gl.TEXTURE_MIN_FILTER ,他们都被设置为 gl.NEAREST。现在,我们要初始化数组中的3个带有相同图像的纹理,但是各自的参数是不同的,并且最后一个纹理还附加了一些其他的代码。以下就是这些纹理过滤方式具体是在哪些方面不同的。
Nearest(最近点采样过滤)
第一个纹理的gl.TEXTURE_MAG_FILTER 和gl.TEXTURE_MIN_FILTER参数都被设置为gl.NEAREST。这个和我们原来的设置是一样的,也就是说,当纹理被按比例放大或者缩小时,WebGL会在原始图像上寻找最近的点来决定指定点的颜色。在没有缩放的情况下,纹理看其来还是不错的;缩小后,图像看起来还过得去;但是纹理图像被放大时,看起来会有很多“马赛克”,因为这种算法实际上只是简单地放大了原始图像的像素,并没有做其他任何优化。
Linear(线性过滤)
对于第二个纹理来说,gl.TEXTURE_MAG_FILTER 和gl.TEXTURE_MIN_FILTER参数都被设置为gl.LINEAR。这里,我们在放大缩小时还是使用同样的过滤方式。但是,线性算法能够在纹理被放大时更好的表现物体;基本可以说,它对原始纹理图像上的像素进行了线性插值。说的再大概一下,在一个白的和一个黑的像素之间的像素会被输出为灰色。这样的话,我们看到的画面效果就更加平滑,但是必然原本锐利的边缘部分会看起来有点模糊。(公平的说,放大图像时,图像看起来都不会那么完美,因为你无法看到原始图像中本来就没有的细节。)
Mipmaps(多级渐进纹理过滤)
对于第三个纹理来说,gl.TEXTURE_MAG_FILTER参数被设置为gl.LINEAR,而gl.TEXTURE_MIN_FILTER被设置为gl.LINEAR_MIPMAP_NEAREST。这个是三种纹理过滤方式中最复杂的一个。
Linear过滤在图像被放大时,给出了比较好的效果,但是在图像被缩小时,它的表现不如Nearest过滤。事实上,两种过滤方法都会产生难看的锯齿效果。要想看一看效果是什么样的,请重新加载示例图像,这样它使用的就是Nearest(或者,点击刷新键,你就能看到它原来的状态);然后,按住Page Up键一会,来缩小图像。当立方体产生改变时,在某些点上,你会发现图像产生了“扭曲”,垂直的线条看起来时有时无。当你观察到这种现象时,稍微放大或缩小一下图像,观察一下扭曲的程度,接着按F键切换到Linear过滤,把立方体拉前推后,你会发现产生的扭曲效果和之前一样。再按一下F键,使用Mipmaps过滤,再次放大和缩小图像,你会发现,扭曲效果被消除了,至少是减少了。
当立方体离我们比较远时,比如在宽度和高度是Canvas画布的十分之一时,请让它在这个位置旋转然后切换纹理过滤方式。当使用Nearest过滤或者Linear过滤时,你会发现,在某些地方组成木质纹理的暗色线条十分清晰,然而在另外一些地方,这些线条缺消失了,图像看起来污渍斑斑的。这种情况在使用Nearest过滤时,十分严重,在使用Linear 过滤时也好不到哪儿去。只有在使用Mipmaps过滤时,才看的过去。
Nearest过滤和Linear过滤发生问题的原因是,当纹理被缩小为原来十分之一的尺寸时,纹理过滤将在原始图像的每十个像素中,使用一个来拼凑成缩小的纹理图像。实例中纹理是一个木质颗粒的图案,纹理的整体是浅棕色的,并且在垂直方向上有一些暗色的细线条。假设每个颗粒是10个像素大小,或者换句话说,水平方向上每十个像素就有一个暗棕色像素。当纹理图像被缩小为原图十分之一时,任何一个像素都会有十分之一的几率变成暗棕色,而十分之九的几率是光亮的。换句话说,只有十分之一的暗色的线条看起来会和原始尺寸的纹理图像一样清晰,其他的则都被隐藏了。这样就造成了看起来斑斑点点的效果,并且当纹理图像被放大缩小时,会产生扭曲。
我们需要做的是,在我们需要将纹理图像缩放为原来十分之一时候和场合,缩小后的图像中的每个像素的颜色,都根据一个10×10像素的像素块的平均值来计算。但是这种处理方式在真实使用时会耗费大量计算开销,因此,我们就有了Mipmap过滤。
Mipmaps过滤通过为纹理图像生成许多被称为mip level的子图像的方法解决了这个问题。这些图像分别是原图尺寸大小、四分之一大小、十六分之一大小……直到1×1像素大小。所有这些子图像的集合被称为mipmap。每一个mip level都是上一级大一点的mip level的平均值,这样,就很容易为当前缩放规格找出合适的图像版本:这一算法依赖于gl.TEXTURE_MIN_FILTER的值, 它所做的就是根据当前缩放规格,找到最合适的mip level,然后运用Linear过滤获得适合的像素。
这样就解释的很清楚了。另外,我们给第三个纹理加上了一行代码
137 |
gl.generateMipmap(gl.TEXTURE_2D); |
显然,对于Mipmaps我已经讲了非常多了,应当很清楚了,如果还有什么不理解的地方,请留下评论联系我。
让我们再次回到代码中。目前,我们已经看过了全局变量,并且了解了纹理是如何被加载和设置的。现在,我们来看一下全局变量和纹理在实际场景绘制中的应用。
drawScene函数位于示例代码的三分之一处,这里只有三处变化。第一处是我们在绘制立方体时,我们使用的是全局变量z,而不是一个固定的点:
362 |
mvMatrix = okMat4Trans(0.0, 0.0, z); |
364 365 |
mvMatrix.rotX(OAK.SPACE_LOCAL, xRot, true); mvMatrix.rotY(OAK.SPACE_LOCAL, yRot, true); |
376 |
gl.bindTexture(gl.TEXTURE_2D, crateTextures[filter]); |
392 393 |
xRot += (xSpeed * elapsed) / 1000.0; yRot += (ySpeed * elapsed) / 1000.0; |
出现的第一个相关的变化就在下面,在webGLStart函数中,我们加入了第418行和第419行这两行新的代码。
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 |
function webGLStart() { var canvas = document.getElementById("lesson06-canvas"); initGL(canvas); initShaders(); initBuffers(); initTexture(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); document.onkeydown = handleKeyDown; document.onkeyup = handleKeyUp; tick(); } |
接着我们看一下这些函数。他们位于示例代码的中间位置,就在我们先前看过的全局变量的下面。他们长这样:
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
var currentlyPressedKeys = {}; function handleKeyDown(event) { currentlyPressedKeys[event.keyCode] = true; if (String.fromCharCode(event.keyCode) == "F") { filter += 1; if (filter == 3) { filter = 0; } } } function handleKeyUp(event) { currentlyPressedKeys[event.keyCode] = false; } |
除了这个之外,我们在处理按键被按下的事件中还增加了另外一些东西,这些东西是为当F键被按下时所准备的。在这段代码中,每次按下F键时,filter全局变量的值都会在0,1,2 这三个数字之间循环改变。
这里很值得花些时间讲一讲为什么我们用两种不同的方法处理不同的按键。在电脑游戏中,或者在其他类似的3D系统中,按键能以以下两种形式工作:
一种是立即做出反应。比如发射激光枪!按键后,按某种固定速率自动重复,比如每秒发射两次。
另一种是根据按键时间长短期发生作用。例如,按方向键向前走,直到松手之后,才会停下。
对于第二种按键方式来说,当你按住一个键时,你可能还会想按其他的键,这样你就能比如说向前跑、转弯,或者在移动中射击。这样的话,和通常处理文字时的按键解读方式不同,这是一种完全不同的按键解读方法。例如,你在一个文字处理器中按住A键,会出现一长串的A,但是当你按住A的同时时按了B,B会出现,但是一长串A也会停止。如果同样的事情发生在游戏中,你在跑步或者转弯时,就会受阻。这很显然是非常让人厌恶的。
所以,我们这里只需要将F键按照第一种按键方式处理。字典将在处理第二种按键方式的代码中使用,它会不断记录过程中同时按下的所有按键,而不只是最后一个被按下的键。
这个字典实际上会被各种不同的函数调用,比如handleKeys。在这之前,请先跳到代码末尾处,我们会看到tick函数也调用了它,就像drawScene和Animate一样:
399 400 401 402 403 404 |
function tick() { okRequestAnimationFrame(tick); handleKeys(); drawScene(); Animate(); } |
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
function handleKeys() { if (currentlyPressedKeys[33]) { // Page Up z -= 0.05; } if (currentlyPressedKeys[34]) { // Page Down z += 0.05; } if (currentlyPressedKeys[37]) { // Left cursor key ySpeed -= 1; } if (currentlyPressedKeys[39]) { // Right cursor key ySpeed += 1; } if (currentlyPressedKeys[38]) { // Up cursor key xSpeed -= 1; } if (currentlyPressedKeys[40]) { // Down cursor key xSpeed += 1; } } |
好了,以上就是本节课的全部内容。在本节课中,你学到了各种不同的纹理过滤方式在处理缩放时的不同,并且还学会了如何在3D动画中读取用户的键盘输入。
如果你有任何的问题、评论或者修正,都请留下评论。在下节课中,我们将会开始加入光线。