WebGL教程10:优化代码结构实现多物体运动
来源:http://www.hiwebgl.com
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com
欢迎来到WebGL教程的第9课,这节课的内容是基于NeHe的OpenGL教程的第9节改编的。这节课中,我们将使用Javascript对象来实现3D场景中的多个独立的运动物体。我们还会涉及到如何更改加载纹理的颜色以及如何混合纹理。
下面的视频就是我们这节课将会完成的最终效果。
你会看到大量的不同颜色的星星,不断盘旋。
你可以点击画布下方的复选框来开启或关闭“闪光”效果(我们一会就会讲解到)。你还可以使用方向键来使星星围绕着X轴运动,并使用Page Up键和Page Down键来进行缩放。
下面我们来看看它是怎么工作的……
惯例声明:本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而可以快速地创建自己的3D Web页面。如果你还没有阅读前一课,请先阅读前一课的内容吧。因为本课中我只会讲解那些与前一课中不同的新知识。
另外,我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。
有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;或者点击这里下载我们为您准备好的压缩包。
要讲清楚这节课与上节课不同的地方,最好的办法就是从代码底部开始,从webGLStart函数开始,下面就是本节课中的webGLStart函数:
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
function webGLStart() { var canvas = document.getElementById("lesson09-canvas"); initGL(canvas); initShaders(); initBuffers(); initTexture(); initWorldObjects(); gl.clearColor(0.0, 0.0, 0.0, 1.0); document.onkeydown = handleKeyDown; document.onkeyup = handleKeyUp; tick(); } |
1 |
gl.enable(gl.DEPTH_TEST); |
下面一个重大的变化位于Animate函数中。之前我们一直使用这个函数来更新全局变量以便实现场景中的动画效果——例如,在绘制立方体之前旋转角度的全局变量。这节课中我们还是要做类似的事情,但不是直接更新变量,而是遍历场景中的物体,让它们自己运动。
368 369 370 371 372 373 374 375 376 377 378 379 |
function Animate() { var timeNow = new Date().getTime(); if (lastTime != 0) { var elapsed = timeNow - lastTime; for (var i in stars) { stars[i].Animate(elapsed); } } lastTime = timeNow; } |
345 346 347 348 349 |
function drawScene() { gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); pMatrix = okMat4Proj(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0); |
351 352 |
gl.blendFunc(gl.SRC_ALPHA, gl.ONE); gl.enable(gl.BLEND); |
这恰好给了我们想要的效果。
接下来的代码是:
354 355 |
mvMatrix = okMat4Trans(0.0, 0.0, zoom); mvMatrix.rotX(OAK.SPACE_LOCAL, tilt, true); |
357 |
var twinkle = document.getElementById("twinkle").checked; |
358 359 360 361 362 363 |
for (var i in stars) { stars[i].draw(tilt, spin, twinkle); spin += 0.1; } } |
334 335 336 337 338 339 340 341 342 |
var stars = []; function initWorldObjects() { var numStars = 50; for (var i=0; i < numStars; i++) { stars.push(new Star((i / numStars) * 5.0, i / numStars)); } } |
下面,我们要看一看如何定义星星的类。如果你对Javascript并不熟悉,那这些代码看起来会有些奇怪。(如果你已经非常了解Javascript,那请略过这一部分关于建立对象模型的讲解吧!)
Javascript对象模型与其它语言有很大的不同。你可以将js对象理解为一个字典容器(哈希表,关联数组),对象成员作为数据放在对象容器中,由对象名称索引。所以,在Javascript中,当你像其它语言一样使用foo.bar形式调用成员时,实际上,你是在访问foo这个字典容器中的以bar为索引关键字的数据,即foo[“bar”]。
对于每个Javascript函数,都有一个特殊的私有变量“this”, 表示当前函数的“拥有者”。对于全局函数,this指向网页中的“windows”对象。如果你在引用this的前方加new关键字,那它的意义就不再是指向当前函数的拥有者,而是创建一个新的对象。所以,如果你有一个函数,在函数体内部将this.foo设置为1,this.bar设置为一个函数,并确保在调用此函数前使用new关键字, 那它的功能基本上等同于带class定义的类构造函数。
接下来,我们需要注意,如果一个函数在调用时指明了调用者(例如foo.bar()),那此函数在调用过程中就绑定在了它的调用者身上,正像大多数开发者希望的那样,通过这种方式我们可以让函数调用的内容只作用于它的调用者。
最后,对于每个函数,都有一个特殊的属性:prototype。prototype是一个字典容器,它内部存储了当使用new关键字创建此对象的副本时,所需要创建的成员变量。这是一种良好的定义Javascript“类”对象通用成员的方式——比如,类方法。
好了,让我们来看看定义Star对象的代码。
265 266 267 268 269 270 271 272 |
function Star(startingDistance, rotationSpeed) { this.angle = 0; this.dist = startingDistance; this.rotationSpeed = rotationSpeed; // Set the colors to a starting value. this.randomiseColors(); } |
274 275 |
Star.prototype.draw = function (tilt, spin, twinkle) { mvPushMatrix(); |
277 278 279 |
// Move to the star's position mvMatrix.rotY(OAK.SPACE_LOCAL, this.angle, true); mvMatrix.translate(OAK.SPACE_LOCAL, this.dist, 0.0, 0.0, true); |
281 282 283 |
// Rotate back so that the star is facing the viewer mvMatrix.rotY(OAK.SPACE_LOCAL, -this.angle, true); mvMatrix.rotX(OAK.SPACE_LOCAL, -tilt, true); |
下面就要开始绘制星星了。
285 286 287 288 289 290 291 292 293 294 295 296 |
if (twinkle) { // Draw a non-rotating star in the alternate "twinkling" color gl.uniform3f(shaderProgram.colorUniform, this.twinkleR, |
现在来看看“闪光”的部分吧?好吧,每颗星星都有两个颜色,一个是正常的颜色,一个是闪光的颜色。在绘制闪光的星星前,我们先绘制了一个正常颜色下的星星。也就是说两个星星会混合在一起,实现了闪光;进一步说,第一次绘制的星星是不动的,而第二次绘制的星星是自转的,两者结合在一起,就给出了我们想要的闪光的效果。
好了,星星绘制完毕,我们要让模型视图矩阵出栈了。
298 299 |
mvPopMatrix(); }; |
302 303 |
var effectiveFPMS = 60 / 1000; Star.prototype.Animate = function (elapsedTime) { |
现在我们可以根据这个数字调整星星的角度了,也就是星星在围绕场景中心的轨道上可以走多远。
304 |
this.angle += this.rotationSpeed * effectiveFPMS * elapsedTime; |
306 307 308 309 310 311 312 313 314 |
// Decrease the distance, resetting the star to the outside of // the spiral if it's at the center. this.dist -= 0.01 * effectiveFPMS * elapsedTime; if (this.dist < 0.0) { this.dist += 5.0; this.randomiseColors(); } }; |
317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
Star.prototype.randomiseColors = function () { // Give the star a random color for normal // circumstances... this.r = Math.random(); this.g = Math.random(); this.b = Math.random(); // When the star is twinkling, we draw it twice, once // in the color below (not spinning) and then once in the // main color defined above. this.twinkleR = Math.random(); this.twinkleG = Math.random(); this.twinkleB = Math.random(); }; |
248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
function drawStar() { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, starTexture); gl.uniform1i(shaderProgram.samplerUniform, 0); gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, |
让我们直接看一下着色器部分的代码吧!你会看到这节课中的变化。所有与光照有关的代码都被我们移除出了顶点着色器,现在它的代码和第5课完全一致。而片元着色器则发生了一些有趣的变化。
10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
precision mediump float; varying vec2 vTextureCoord; uniform sampler2D uSampler; uniform vec3 uColor; void main(void) { vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); gl_FragColor = textureColor * vec4(uColor, 1.0); } |
以上就是本课的全部内容!在这节课中,你学会了如何建立Javascript对象来表示场景中的物体,然后赋予它们方法,允许它们独立地绘制和运动。
下节课中,我们将会展示如何从一个单一文件中载入场景,然后看一看如何操纵相机来穿越场景,配合这些制作出一个小的DOOM游戏!