欢迎来到WebGL系列课程的第9课,基于NeHe OpenGL课程的[第9课](http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=09)编写而成。在这节课中,我们将使用JavaScript对象,以便在3D场景中,能够支持许多的独立运动物体。同时我们还会探讨如何更改一个已加载纹理的颜色,以及将纹理混合在一起时将会发生什么。
以下是在支持WebGL的浏览器上运行程序的表现:
https://youtu.be/Ti3jbuY4VlI
[点击这里可以看到demo演示](http://learningwebgl.com/lessons/lesson09/index.html),如果你的浏览器支持WebGL,将会看到大量的不同颜色的星星,在场景中盘旋。
你可以使用画布下方的复选框,切换“闪烁”效果。同时你也可以使用上下方向键,使动画绕X轴旋转;还可以使用PageUp和PageDown键来缩放整个动画。
下面是具体实现...
>[info] 惯例声明:本课程面向具有一定编程基础,但在3D图形学方面无实际经验人群,目标是使学习者运行并了解WebGL代码,以便快速构造自己的3D页面。如果你还没有阅读之前的课程,建议先读完它们 -- 这里只会讲述和之前不同,以及新增的部分
>在教程中难免会有bug和错误,如果你发现了它,请告诉我,我会尽快改正它
本课的源码可以查看:https://github.com/gracefung/webgl-codes/tree/master/lesson09 【这是繁体译文对应的github源码,感觉还不错】
区分本课示例与第8课有哪些不同的最好方式是,从文件底部开始然后逐渐向上,首先是webGLStart()函数。如下是当前该函数代码:
~~~
function webGLStart() {
var canvas = document.getElementById("lesson09-canvas");
initGL(canvas);
initShaders();
initTexture();
initBuffers();
initWorldObjects();
gl.clearColor(0.0, 0.0, 0.0, 1.0);
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
tick();
}
~~~
这里调用了一个新的函数initWorldObjects()。这个函数创建JavaScript对象来代表场景,我们待会再去看它,这里还有一个变更:之前几课webGLStart()函数都会有一行设置深度测试的代码:
~~~
gl.enable(gl.DEPTH_TEST);
~~~
但在本课中被移除了。你应该还记得上节课说颜色混合与深度测试不能很好的兼容,而此课我们会一直使用颜色混合。在WebGL中深度测试是默认关闭的,所以我们在这里简单移除这一行就行。
下一个重大变化是在animate()函数中。之前我们使用它来更新一些代表场景变化的全局变量 -- 比如,立方体在下一帧绘制之前需要旋转的角度。这里的变更我们处理的很简单,不再直接更新变量,而是遍历场景中所有对象,然后告诉它们该作何处理。
~~~
var lastTime = 0;
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;
}
~~~
继续向上看,是drawScene()函数。这里面改变比较多,我们一点一点看。首先:
~~~
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
~~~
这些还是常用的初始化代码,从第1课都没有改动过。
~~~
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.enable(gl.BLEND);
~~~
接下来,我们开启了颜色混合。使用的混合方式和上节课一样;你应该记得这种方式允许物体间相互“通透”。还有,它还意味着物体黑色的部分被渲染的像是透明的效果;如果你不太记得这其中的原理了,可以翻看上节课的相关内容。也就是说,当我们绘制场景中的星星时,黑色部分看上去会是透明的;事实确实是这样,星星中亮的部分越少,那么看上去就越透明。
我们绘制星星用的是如下的纹理贴图:
![](https://box.kancloud.cn/7dbaac4bdeaf679e7be457e9e9628ff0_128x128.gif)
这个效果正是我们想要的
继续看代码:
~~~
mat4.identity(mvMatrix);
mat4.translate(mvMatrix, [0.0, 0.0, zoom]);
mat4.rotate(mvMatrix, degToRad(tilt), [1.0, 0.0, 0.0]);
~~~
这里我们移动到场景中心,并且进行适当的缩放。同时我们还绕着X轴对场景做了一定倾斜;缩放值和倾斜度都还是全局变量,且通过键盘控制。现在我们已经准备好绘制场景了,首先检查一下“闪烁”复选框是否被勾选:
~~~
var twinkle = document.getElementById("twinkle").checked;
~~~
接着,类似于我们在animate()函数中所做的,我们遍历星星列表,然后告诉它们把自己给绘制出来。传入当前的倾斜值,和闪烁值。同时还告诉它当前的自转角度是多少 -- 这个值是用来使星星在轨道上沿着场景中心公转同时,还能够环绕着自己的中心自转。
~~~
for (var i in stars) {
stars[i].draw(tilt, spin, twinkle);
spin += 0.1;
}
}
~~~
以上就是drawScene()函数。我们现在可以看出,星星已经能够自己绘制并且运动了;接下来的代码用来创造它们:
~~~
var stars = [];
function initWorldObjects() {
var numStars = 50;
for (var i=0; i < numStars; i++) {
stars.push(new Star((i / numStars) * 5.0, i / numStars));
}
}
~~~
一个简单的循环创建50颗星星(你可能会想试着调整这个数值)。每颗星星传入的第一个参数是表示和场景中心的初始距离,第二个参数是表示环绕场景中心的公转速度,这2个参数值都由它们在星星列表中的序号决定。
下面的代码关注定义星星的类。如果你不太熟悉JavaScript,看上去会觉得很怪异。但如果你很懂JavaScript,可以略过直接看后续部分。
JavaScript的对象模型和其它语言有很大区别。我能找到最简单易懂的解释方式是,每个对象都是作为字典被创建(或者是哈希表,或者是关联数组【即映射】),然后塞入各种值便成为一个成熟齐全的对象。对象的属性就是字典中可以映射值的简单条目【键】,对象的方法则是映射函数的条目。现在,我们就清楚了单词键的真实含义,比如```foo.bar```语法是```foo["bar"]```的合法简写,你可以从这个截然不同的开端,看看如何找到与其它语言的相似处。
接下来,在每一个JavaScript方法内部,都有一个被称做```this```的隐式绑定变量,它指向了方法的“所有者”。对于全局方法,```this```是每个页面的“window”对象,但如果你在方法的前面放了关键字```new```,那么它就不再是一个方法,而是一个全新的对象。所以,如果你有一个方法A,其内部设置了```this.foo```为1, 以及```this.bar```为一个方法,并且你能保证每次调用方法A时,都会用```new```关键字,那么它基本上就和类定义中的构造函数是一样的了。
后续我们可以看到如果一个方法,以类似于对象方法调用的语法被调用时(比如```foo.bar()```),那么该方法就会被绑定到这个方法的拥有者上(```foo```),就像我们希望的那样,这样对象的方法可以操作对象本身。
最后,有一个很特别的,与方法有关的属性,```prototype```。这是一个与该方法```new```出来的每一个对象都相关的字典值;通过它可以很方便对这个“class”的每个对象都设置相同的值 -- 比如,类方法。
让我们来看看对于场景中一颗星星对象的定义方法。
~~~
function Star(startingDistance, rotationSpeed) {
this.angle = 0;
this.dist = startingDistance;
this.rotationSpeed = rotationSpeed;
// Set the colors to a starting value.
this.randomiseColors();
}
~~~
在构造方法中,使用提供的参数值和初始角度0来初始化对象,然后调用了一个方法。下一步是将一些方法绑定到Star()函数的```prototype```中,以便使所有新的Star对象都有同样的方法,首先,绑定draw()方法:
~~~
Star.prototype.draw = function(tilt, spin, twinkle) {
mvPushMatrix();
~~~
draw()方法被定义为将我们传入的参数,传回到drawScene()函数中。首先将当前的模型视图矩阵推入矩阵栈中,这样我们就可以任意操作,而不需要担心哪里有副作用的影响。
~~~
// Move to the star's position
mat4.rotate(mvMatrix, degToRad(this.angle), [0.0, 1.0, 0.0]);
mat4.translate(mvMatrix, [this.dist, 0.0, 0.0]);
~~~
紧接着,我们【这里的“我们”可以看做是绘制笔触】绕Y轴,按照星星自己的角度旋转;然后从场景中心向外移动星星的初始距离值。这样我们就来到了绘制星星的正确位置。
~~~
// Rotate back so that the star is facing the viewer
mat4.rotate(mvMatrix, degToRad(-this.angle), [0.0, 1.0, 0.0]);
mat4.rotate(mvMatrix, degToRad(-tilt), [1.0, 0.0, 0.0]);
~~~
上面代码都是必需的,这样我们才可以使用方向键来改变场景的倾斜度时,星星的位置还能够保持正确。星星是在一个方形上绘制2D纹理表现出来的,当我们垂直观看时表现的很正常,但如果将场景倾斜,从边上观看时,就只能看到一条线。类似于这样的原因,我们还要还原用户指定的倾斜度,来放置星星。当你像这样撤销旋转时,你需要按照之前做的顺序,反向还原操作。所以首先我们在当前位置上做撤销旋转的操作,然后撤销倾斜度(这些都是在drawScene()函数中完成的)