求问目前最好的引擎有哪些基于webgl的游戏引擎

用webgl打造一款简单第一人称射击游戏
不知道大家还记不记得上次那个,有同事吐槽说游戏中间有一个十字瞄准器,就感觉少了一把枪。好吧,那这次就带来一款第一人称射击游戏。写demo锻炼,所以依旧用的原生webgl,这次重点会说一下webgl中关于摄像头相关的知识,点开全文在线试玩~~
simpleFire在线试玩:https://vorshen.github.io/simpleFire/index.html(推荐开声音……否则没有打击感会觉得游戏有点呆……)
simpleFire源码地址:https://github.com/vorshen/simpleFire
做一款游戏和做一个项目是一样的,不能刚有想法了就直接开始撸代码。一个前端项目可能要考虑框架选型、选用何种构建、设计模式等等;而一款游戏,在确定游戏类型之后,要考虑游戏玩法,游戏场景,游戏关卡,游戏建模美术等等,而这些很多都是非代码技术层面的,在真正的游戏开发中会有专门那个领域的人去负责,所以一款好的游戏,每一个环节都不可或缺。
上面是关于游戏开发的碎碎念,下面开始真正的讲解simpleFire这款游戏。
试玩之后大家应该会发现游戏整个场景非常简单,一把枪,四面墙,墙上面有靶子,将所有的靶子都打掉则游戏结束,最终的游戏分数是: 击中靶子数 + 剩余时间转换。此时读者可能内心感受:这尼玛在逗我呢,这也太简单了吧。先别急,接下来说下游戏准备过程中遇到的坑点。
因为是3D游戏,而且涉及到了不同的物体在3D空间中存在(枪、靶子、墙),之前那3D迷宫准备工作之所以简单是空间中从头到尾就只有“墙”这一个东西。
要让枪、靶子、墙这些东西同处一个空间内很简单,把他们顶点信息写进shader就行了嘛
(在这里考虑到可能有没接触过webgl的同学,所以简单介绍一下,canvas是对象级别的画板操作,drawImage画图片,arc画弧度等,这些都是对象级别操作。而webgl是片元级操作,片元在这里可以先简单理解为像素,只是它比像素含有更多的信息。上面所说的把顶点信息写进shader,可以理解为把枪、靶子、墙这些东西的坐标位置画进canvas。先就这样理解着往下看吧~如果canvas也不知道那就没办法了。。。)
顶点信息从哪来?一般是设计师建模弄好了,导成相关文件给开发者,位置、颜色等等都有。但是……我这里没有任何相关信息,全部得自己来做。
自己跟前又没有专业的建模工具,那该如何生成顶点信息?用脑补 + 代码生成……事先声明,这是一种很不对很不对的方式,自己写点demo可以这样玩,但是生产中千万别这样。
这里就用生成枪来举例,我们知道普通制式手枪长180mm到220mm左右,在这里取20cm,并将其长度稍微小于视锥体近平面的长度,视锥体近平面也看作为屏幕中webgl画布的宽度。所以我们生成的枪理论上应该是这样的,如图所示:
好了,枪的比例确定之后就要结合webgl坐标系生成顶点信息了,webgl坐标系和canvas2D坐标系有很大的不同,如图:
因为是代码手动生成顶点信息,用-1~1写起来有点难受,所以这里我们先放大10倍,后面在把除回去,蛋疼吧,这就是不走正途的代价……
代码该怎么生成顶点信息呢?用代码画一把枪听起来很难,但是用代码画一条线、画一个圆、画一个正方体等,这些不难吧,因为这些是基本图形,有数学公式可以得到。一个复杂的模型,我们没法直接确定顶点信息,那就只好通过各种简单模型去拼凑了,下面这个页面就是简单的拆分了下枪的模型,可以看到是各个简单子模型拼凑而成的(说明:建模形成的也是拼凑,不过它的一块块子模型不是靠简单图形函数方法生成)。
手枪生成展示:https://vorshen.github.io/simpleFire/gun.html
这种方式有什么坏处:工作量大而且不好看、扩展性差、可控性差
这种方式有什么好处:锻炼空间想象力与数学函数应用吧……
介绍了这么多,其实就想说:这么恶心且吃力不讨好的活我都干下来了,真的走心了!
具体怎么用简单图形函数生成的子模型可以看代码,代码看起来还是比较简单,有一定立体几何空间想象力就好,这里不细讲,毕竟非常非常不推荐这样玩。
枪建模相关代码地址:https://github.com/vorshen/simpleFire/blob/master/js/gun.js
第一人称射击类游戏玩的是什么?就是谁开枪开的准,这个是永恒不变的,就算是OW,在大家套路都了解、可以见招拆招的情况下,最终也是比枪法谁更准。那么枪法准是如何体现的呢?就是通过移动鼠标的速度与准确度来体现(这里没有什么IE3.0……),对于玩家来说,手中移动的是鼠标,映射在屏幕上的是准心,对于开发者来说,移动的是视角,也就是3D世界中的摄像头!
先说下摄像头的基本概念和知识,webgl中默认的摄像头方向是朝着Z轴的负方向,随手画了图表示下(已知丑,轻吐槽)
摄像头位置不变,同一个物体在不同位置能给我们不同的感受,如下
摄像头位置改变,同一个物体位置不变,也能给我们不同的感受,如下
等等!这似乎并没有什么区别啊!感觉上就是物体发现了变化啊!确实如此,就好像你在车上,看窗外飞驰而过的风景那般道理。
摄像头的作用也就是改变物体在视锥体中的位置,物体移动的作用也是改变其在视锥体中的位置!
熟悉webgl的中的同学了解
gl_Position = uPMatrix * uVMatrix * uMMatrix * aP
对于不了解的同学,可以这样理解
gl_Position是最终屏幕上的顶点,aPosition是最初我们生成的模型顶点
uMMatrix是模型变换矩阵,比如我们想让物体移动、旋转等等操作,可以再次进行
uPMatrix是投影变换矩阵,就理解为3维物体能在2D屏幕上显示最为重要的一步
uVMatrix是视图变换矩阵,就是主角!我们用它来改变摄像头的位置
我们的重点也就是玩转uVMatrix视图矩阵!在这里,用过threejs或者glMatrix的同学肯定就很诧异了,这里有什么好研究的,直接lookAt不就不搞定了么?
确实lookAt就是用来操作视图矩阵的,考虑到没用过的用户,所以这里先说一下lookAt这个方法。
lookAt功能如其名,用来确认3D世界中的摄像机方向(操作视图矩阵),参数有3个,第一个是眼睛的位置,第二个是眼睛看向目标的位置,第三个是坐标的正上方向,可以想象成脑袋的朝上方向。
用图来展示的话就是如下图(已知丑,轻吐槽):
知道了lookAt的用法,接下来我们来看一下lookAt的原理与实现。lookAt既然对应着视图矩阵,将它的结果想象成矩阵VM
大家知道webgl中最初的坐标系是这样的
那么如果我们知道最终的坐标系,就可以逆推出矩阵VM了。这个不难计算,结果如下
来,回看一下lookAt第一个和第二个参数,眼睛的位置和眼睛看向目标的位置,有了这两个坐标,最终坐标系的Z是不是确定了!,最后一个参数是正上方向,是不是Y也确定了!
机智的同学看到有了Z和Y,立马想到可以用叉积算出X,不知道什么是叉积的可以搜索一下(学习webgl一定要对矩阵熟悉,这些知识是基础)
这样我们就很轻松愉快的得出了VM,但是!似乎有点不对劲
本身VM是没有问题的,关键在于这么使用它,比如说我直接lookAt(0,0,0, 1,0,0, 0,1,0)使用,可以知道此时我们的视线是X轴的正方向,但若是我鼠标随便晃一个位置,你能快速的知道这三个参数该如何传么?
所以现在的目标就是通过鼠标的偏移,来计算出lookAt的三个参数,先上代码~
var camera = {
toMatrix: function() {
var rx = this.
var ry = this.
var mx = this.
var my = this.
var mz = this.
var F = normalize3D([Math.sin(rx)*Math.cos(ry), Math.sin(ry), -Math.cos(rx) * Math.cos(ry)]);
var x = F[0];
var z = F[2];
var angle = getAngle([0, -1], [x, z]);
var R = [Math.cos(angle), 0, Math.sin(angle)];
var U = cross3D(R, F);
F[0] = -F[0];
F[1] = -F[1];
F[2] = -F[2];
var s = [];
s.push(R[0], U[0], F[0], 0);
s.push(R[1], U[1], F[1], 0);
s.push(R[2], U[2], F[2], 0);
这里封装了一个简单的camera对象,里面有rx对应鼠标在X方向上的移动,ry对应鼠标在Y方向上的移动,这个我们可以通过监听鼠标在canvas上的事件轻松得出。
var mouse = {
x: oC.width / 2,
y: oC.height / 2
oC.addEventListener('mousedown', function(e) {
if(!level.isStart) {
level.isStart =
level.start();
oC.requestPointerLock();
}, false);
oC.addEventListener("mousemove", function(event) {
if(document.pointerLockElement) {
camera.rx += (event.movementX / 200);
camera.ry += (-event.movementY / 200);
if(camera.ry &= Math.PI/2) {
camera.ry = Math.PI/2;
} else if(camera.ry &= -Math.PI/2) {
camera.ry = -Math.PI/2;
}, false);
lockMouse+momentX/Y对于游戏开发来说是真的好用啊!!否则自己来写超级蛋疼还可能会有点问题,安利一大家一波,用法也很简单。
鼠标在X方向上的移动,在3D空间中,其实就是围绕Y轴的旋转;鼠标在Y方向上的移动,其实就是围绕X轴的旋转,这个应该可以脑补出来吧
那么问题来了,围绕Z轴的旋转呢??这里我没有考虑围绕Z轴的旋转啊,因为游戏没用到嘛,第一人称射击的游戏很少会有围绕Z轴旋转的场景吧,那个一般是治疗颈椎病用的。虽然不考虑,但是原理都是一样的,可以推出来,有兴趣的小伙伴可以自己研究下。
我们将rx和ry拆看来看,首先就只看rx对初始视线(0, 0, -1)的影响,经过三角函数的变换之后应该是( Math.sin(rx), 0, -Math.cos(rx) ),这里就不画图解释了,三角函数基本知识
然后再考虑( Math.sin(rx), 0, -Math.cos(rx) )经过了ry的变换会如何,其实就是将( Math.sin(rx), 0, -Math.cos(rx) )与ry的变化映射到y-z坐标系上面,再用三角函数知识得出( Math.sin(rx)*Math.cos(ry), Math.sin(ry), -Math.cos(rx) * Math.cos(ry) )
一时理解不了的同学可以闭上眼睛好好脑部一下变换的画面……
经过这两步最终我们得到了经过变换之后的视线方向F(少了Z轴方向的旋转,其实就是再多一步),也就是lookAt函数中的前两个函数得出来的值,然后再计算一个值就ok了,代码中我们求的是X轴的正方向
代码在刚刚封装的camera中是这几行
var x = F[0];
var z = F[2];
var angle = getAngle([0, -1], [x, z]);
angle得出了最终的视角方向(-Z)和最初视线方向在x-z坐标系中的偏转角,因为是x-z坐标系,所以最初的X正方向和最终的X正方向偏移角也是angle
function getAngle(A, B) {
if(B[0] === 0 && A[0] === 0) {
var diffX = B[0] - A[0];
var diffY = B[1] - A[1];
var a = A[0] * B[0] + A[1] * B[1];
var b = Math.sqrt(A[0] * A[0] + A[1] * A[1]);
var c = Math.sqrt(B[0] * B[0] + B[1] * B[1]);
return (B[0] / Math.abs(B[0])) * Math.acos(a / b / c);
通过简单的三角函数得到了最终X轴的正方向R(注意:没考虑围绕Z轴的旋转,否则要繁琐一些)
再用叉积得到了最终Z轴的正方向U,然后不要忘记,之前F是视线方向,也就是Z轴正方向的相反方向,所以取反操作不要忘了
R、U、-F都得到了,也就得到了最终的VM视图矩阵!
其实吧,在没有平移的情况下,视图矩阵和模型变换矩阵也就是旋转方向不一致,所以以上的知识也可以用在推导模型变换矩阵里面。就算带上了平移也不麻烦,牢记模型变换矩阵需要先平移、再旋转,而视图变换矩阵是先旋转、再平移。
游戏中摄像机相关的知识就先讲到这里了,如果有不明白的同学可以留言讨论。
当然这不是唯一的方法,simpleFire这里没有考虑平移,不考虑平移的情况下,其实就是最终就是要生成一个3维旋转矩阵,只不过使用的是一种逆推的方法。此外还有一些欧拉角、依次2维旋转等等方式,都可以得到结果。不过这些都比较依赖矩阵和三角函数数学知识,是不是现在无比的怀恋当年的数学老师……
我们玩转了摄像头,然后就是开枪了,开枪本身很简单,但是得考虑到枪有没有打中人呀,这可是关于到用户得分甚至是敌我的死活。
我们要做的工作是判断子弹有没有击中目标,听起来像是碰撞检测有没有!来,回忆一下在2D中的碰撞检测,我们的检测都是按照AABB的方式检测的,也就是基于对象的包围框(对象top、left、width、height)形成,然后坐标(x, y)与其计算来判断碰撞情况。这种方法有一个缺陷,就是非矩形的检测可能有误差,比如圆、三角形等等,毕竟包围框是矩形的嘛。dntzhang所开发出的AlloyPage游戏引擎中有画家算法完美的解决了这个缺陷,将检测粒度由对象变成了像素,感兴趣的同学可以去研究一下~这里暂且不提,我们说的是3D检测
仔细想想3D世界中的物体也有包围框啊,更确切的说是包围盒,这样说来应该也可以用2D中AABB方式来检测啊。
确实可以,只要我们将触发鼠标事件得到的(x, y)坐标经过各种变换矩阵转换为3D世界中的坐标,然后和模型进行包围盒检测,也可以得到碰撞的结果。对开发者来说挺麻烦的,对CPU来说就更麻烦了,这里的计算量实在是太大了,如果世界中只有一两个物体还好,如果有一大票物体,那检测的计算量实在是太大了,很不可取。有没有更好的方法?
有,刚刚那种方式,是将2D中(x, y)经过矩阵转换到3D世界,还有一种方式,将3D世界中的东西转换到2D平面中来,这便是帧缓冲技术。帧缓冲可是一个好东西,3D世界中的阴影也得靠它来实现。
这里用一句话来直观的介绍帧缓冲给不了解的同学:将需要绘制在屏幕上的图像,更加灵活处理的后绘制在内存中
如图对比一下simpleFire中的帧缓冲图像是什么样的
正常游戏画面
帧缓冲下的画面
发现整个世界中只有靶子有颜色对不对!这样我们读取帧缓冲图像中某个点的rgba值,就知道对应的点是不是在靶子上了!实现了坐标碰撞检测!
之前说的更加灵活的处理,就是指渲染时对各个模型颜色的处理
检测代码如下:
oC.onclick = function(e) {
if(gun.firing) {
gun.fire();
var x = width / 2;
var y = height / 2;
webgl.uniform1i(uIsFrame, true);
webgl.bindFramebuffer(webgl.FRAMEBUFFER, framebuffer);
webgl.clear(webgl.COLOR_BUFFER_BIT | webgl.DEPTH_BUFFER_BIT);
targets.drawFrame();
var readout = new Uint8Array(1*1*4);
// webgl.bindFramebuffer(webgl.FRAMEBUFFER, framebuffer);
webgl.readPixels(x, y, 1, 1, webgl.RGBA, webgl.UNSIGNED_BYTE, readout);
webgl.bindFramebuffer(webgl.FRAMEBUFFER, null);
targets.check(readout);
webgl.uniform1i(uIsFrame, false);
/* targets下的check方法 */
check: function(arr) {
var r = '' + Math.floor(arr[0] / 255 * 100);
var g = '' + Math.floor(arr[1] / 255 * 100);
var b = '' + Math.floor(arr[2] / 255 * 100);
for(i = 0; i & this.ids. i++) {
if(Math.abs(this.ids[i][0] - r) &= 1 && Math.abs(this.ids[i][1] - g) &= 1 && Math.abs(this.ids[i][2] - b) &= 1) {
console.log('命中!');
id = this.ids[i][0] + this.ids[i][1] + this.ids[i][2];
this[id].leave();
score.add(1);
level.check();
而且这个方法很快,计算量都在GPU里面,这种数学计算的效率GPU是比CPU快的,GPU还是并行的!那传统的AABB法还有存在的意义么?
其实是有的,因为精确,可以在包围盒中计算得到具体的碰撞点位置,这是帧缓冲法所达不到的。
举个例子,第一人称射击游戏中的爆头行为,可以在帧缓冲中将人物模型中身体和头用不同颜色区分出来,这样可以检测出碰撞的是头还是身体。这种情景下帧缓冲方法还hold住。
那如果是想得到打靶中具体的位置,留下子弹的痕迹呢?这里帧缓冲方法就死也做不到了。
最佳实践就是在需要高精度复杂场景下的碰撞检测可以将两种方法结合使用:用帧缓冲去掉多余的物体,减少传统AABB法的计算量,最终得到具体位置。
simpleFire这里就没这么折腾了……只要射到靶上打哪都是得分~~~
关于simpleFire想讲的东西也就讲完了,本身也没有什么技术难点,文章的最后一节也聊一聊关于webgl
之前已经说了与canvas之间的区别,是从计算机层面的区别,这里说一下对于开发者的区别:
canvas2D是一块画布,在画布上作画,画中的东西一定是虚拟的
webgl是一个世界,你要在世界中创造,但也要满足世界的规则
这比喻有点夸大,都牵扯到了世界的规则。但事实就是如此,webgl比canvas2D复杂,而很大一块复杂的地方就是世界的规则 —— 光与阴影
这两块知识3D迷宫和simpleFire都没有用上,因为这应该是静态3D中最难啃的骨头了吧。说难吧,知道原理之后也不难,但就是恶心麻烦,加上光和阴影得多很多很多的代码。后面会详细讲解光和阴影相关知识的,也是用小游戏的方式。写一篇纯原理的文章感觉没啥意思,知识点一搜能搜到很多了
不看动画,纯看静态渲染方面的东西,2D和3D也就差不多,需要位置信息、颜色信息,平移旋转等等,3D也就是加上了光和阴影这样的世界规则,比2D还多了一些数学知识的要求。
所以webgl并不难~欢迎更多的人来到webgl的坑中来吧,但是推荐入坑的同学不要开始就过于依赖three、oak3D、PhiloGL等图形库,还是从原生入手比较好。
文章对simpleFire代码讲解的不是很多,源码也贴出来了,100%原生webgl的写法,看起来应该也不是很难。
? ? ? ? ? ? ? ?
延伸阅读(点击标题):
作者:AlloyTeam
原文:http://www.alloyteam.com/2016/11/with-webgl-to-build-a-simple-first-person-shooter-games/#prettyPhoto
点击“阅读原文”,看更多
责任编辑:
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
今日搜狐热点致力于游戏实用开发技术的心得与分享,好的东西就应该和大家一起分享!欢迎关注!...
【WebGL连载教程一】H5开发3D引擎:知识问答--前瞻篇
为什么要写这个教程?上次我们连载了三篇关于OpenGL的实战教程,发现现在学OpenGL的人比较少。或者说OpenGL方面的资料互联网上已经比较丰富了,再写这方面的教程已经没有意义了。所以,从今天开始,将改变一下。咱们从现在最火的H5开始。从零建立一个H5的3D渲染引擎。首先,进来看的人,需要了解的知识点:(1)什么是H5?
答:H5就是Html5的Web标准,不知道的请退出补习(2)没有OpenGL基础可以学吗?
答:完全可以,以后的连载都是从零开始(3)不会typescript可以学吗?
答:前提是你需要掌握一定的编程基础,会别的语言也行,因为编程都是通用的。(4)教程会提供源码下载吗?
答:会(5)通过学习,最终能有什么收获?
答:只要你勤奋努力愿意学习,进入3D游戏的开发殿堂之路肯定没问题选用什么语言开发H5?H5是基于Web浏览器标准的,主要采用javascript。由于javascript是一种动态无类型的语言。声明即可以使用,虽然方便简单。但在开发大型项目的时候,可能不是太方便,查找问题比较困难些。我们会选择现在比较流行的typescript语言来进行开发。typescript语言是一种强类型的语言,它是javascript的超集,提供了面向对象的编程方法。配合在vscode开发,有丰富快速的代码提示,错误检查能力,在浏览器中调试时,能像在源码中一样断点。最终typescript会被编译成js文件进行发布。什么是WebGL?WebGL (Web图形库) 是一种JavaScript API,用于在任何兼容的Web浏览器中呈现交互式3D和2D图形,而无需使用插件。WebGL通过引入一个与OpenGL ES 2.0紧密相符合的API,可以在HTML5 &canvas& 元素中使用。现在有哪些基于WebGL的渲染引擎?国内现在比较流行的引擎就是Laya,Egret。国外现在比较流行的引擎就是Babylon.JS , Three.js , Turbulenz , Famo.us , PlayCanvas.js 等等。既然有这么多引擎了,为什么还要写个WebGL渲染引擎?引擎只是一种工具封装,普通开发者无法了解它的底层是怎么样工作的。如果你只是单纯的想用工具,上面这些引擎已经完全满足你了。如果在开发过程中遇到底层问题,你可能无法下手,力不从心。如果你对底层都十分的了解,你在开发中将能得心应手。虽然最终不能和上面这些大引擎相比,但积累下来的的开发知识来说,完全足够了。这也是我们为什么要自己写WebGL的原因之一了。(注:本文展示图片纯为其它游戏截图,非本教程引擎效果)从下一节开始,咱们从零起步,一起来熟悉它!欢迎关注!
没有更多推荐了,现在开发H5游戏是不是都采用基于WebGL的引_百度知道
现在开发H5游戏是不是都采用基于WebGL的引
我有更好的答案
H5是一系列制作网页互动效果的技术集合,即H5就是移动端的web页面。而H5游戏,你可以看作是移动端的web游戏,无需下载软件即可体验,这就是H5在传播上的优势。H5是一系列制作网页互动效果的技术集合,即H5就是移动端的web页面。而H5游戏,你可以看作是移动端的web游戏,无需下载软件即可体验,这就是H5在传播上的优势。对于许多手游玩家来说,H5可能是个十分陌生的名词,其实H5就是HTML的高级版本,目前H5技术正在不断完善。当H5游戏在国内还处于萌芽状态时,专注移动游戏平台领域的公司就已经开始准备全面转型。在技术方面,利用H5开发移动小游戏的所需时间更少。你可以像写网页一样写游戏,而且有大量文档与插件可用。
采纳率:88%
为您推荐:
其他类似问题
您可能关注的内容
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。第 2 章 WebGL:实时 3D 渲染
第 2 章 WebGL:实时 3D 渲染
WebGL 是 Web 3D 图形的标准 API,它使得运行在浏览器中的 JavaScript 程序也可以充分利用 3D 渲染硬件的强大能力。在 WebGL 出现之前,为提供硬件加速的 3D 体验,开发者只能借助浏览器插件或编写需要用户下载安装的本地软件。
尽管 WebGL 不属于 HTML5 官方标准,但绝大多数支持 HTML5 的浏览器都支持 WebGL——正如支持 Web Workers、WebSockets 等并未被 W3C 官方作为标准采纳的技术一样。要想将浏览器打造成一流的应用平台,3D 是不可或缺的部分,这是 Google、Apple、Mozilla、Microsoft、Amazon、Opera、Intel、BlackBerry 等各大公司开发者们的共识。
主流的桌面浏览器和绝大多数手机浏览器都支持 WebGL1。WebGL 已经可以运行在类似于你的家用机器和办公机器的数百万台设备上。包括游戏、数据可视化、计算机辅助设备、3D 打印和零售行业在内的许多使用了 WebGL 技术的网站也在蓬勃发展。
1在本书写作时,iOS 上的 Mobile Safari 还不支持 WebGL,这是个很严重的问题。所幸利用某些适配工具包,我们可以将基于 HTML5 和 WebGL 的程序打包成本地应用在 iOS 平台上运行、研究,关于这个问题,在第 12 章中会有详细的说明。
WebGL 是一套底层绘图 API:它通过解析数据和着色器阵列 2 来进行绘制。它不像 2D Canvas API 那样具有高度封装的结构,这可能会令习惯 2D 图形接口的人感到困惑。不过很多开源的 JavaScript 工具包都提供了更加高级的封装方法,这些工具包让开发者可以用与操作传统图形库更为接近的方式来操作 WebGL 的 API。虽然有了这些工具包,3D 开发也还是有一定的难度,但至少利用它们,对 3D 开发没什么经验的人可以比较方便地入门,而有经验的 3D 开发者也可以节省大量时间。
2阵列,指排成行和列的数学元素。矩阵就是一种典型的阵列形式,此处姑且可以简单理解为一个数据 / 着色器二维数组。——译者注
为了让读者对 WebGL 有个基本印象,本章将简单介绍 WebGL 的底层基础。虽然我们在本书中使用的工具包可以让你不必去关注这些底层细节,但了解这些工具包是基于什么构建的也非常重要。所以,让我们从 WebGL 的核心概念和 API 开始学习。
 正如不支持许多 HTML5 新特性一样,你的电脑可能也不支持 WebGL。主流桌面浏览器中,一部分浏览器只有比较新的版本才支持 WebGL(例如 IE 只有 IE11 及其之后的版本才支持 WebGL)。还有一些老机器的图形处理器不支持 3D 硬件加速,在这些老机器上,浏览器会直接关闭 WebGL。如果你想了解你的目标机器、设备或浏览器是否支持 WebGL,请访问 ,键入“WebGL”关键字进行搜索,或直接访问这个链接:。
2.1 WebGL基础
WebGL 的雏形在 2006 年由 Mozilla 的工程师 Vladimir Vuki?evi? 提出。Vladimir Vuki?evi? 试图创建一套用于 Canvas 元素的 3D 绘图 API,作为已有的 2D Canvas API 的扩展。他基于 OpenGL ES(在移动端图形领域已经相当普及的 API 标准)设计了这套当时还被称为 Canvas 3D 的 API。到 2007 年,Mozilla 和 Opera 分别实现了各自浏览器上的 Canvas 3D 版本。
2009 年,来自 Opera、Apple 以及 Google 的其他参与者与 Vuki?evi? 共同创建了 WebGL 工作组,这个工作组隶属于 Khronos Group 团队,Khronos Group 团队还维护着 OpenGL、COLLADA 以及其他一些你或许已经耳熟能详的标准。如今 Khronos 仍然在继续维护 WebGL 标准。Vuki?evi? 担任 WebGL 小组负责人直到 2010 年,之后 Google 的 Kenneth Russell 接替了他。
以下是来自 Khronos 网站的 WebGL 官方描述:
WebGL 是一套免费、跨平台的 API,它在 HTML 中以 3D 绘图上下文的形式实现了 OpenGL ES 2.0 的功能,并以底层的文档对象模型(Document Object Model,DOM)接口的形式将开发接口暴露出来。它使用 OpenGL 着色器语言 GLSL ES,并且可以与页面上的其他内容(可以分层的形式叠加在 3D 绘图区域的上方或下方)无缝融合。它非常适应于使用 JavaScript 编程语言构建的 3D Web 动态应用,并将被现代浏览器完美支持。
这个定义包括几个要点,下面我们来分别说明。
WebGL 是一套 API。它通过一套专门的 JavaScript 编程接口来调用,不像 HTML 那样带有附带的标记。3D 渲染与 2D 绘制同样使用 Canvas 元素来作为绘图上下文,开发者通过对 JavaScript API 的调用,在 Canvas 中实现 3D 内容的绘制。事实上,对 WebGL 接口的访问是通过现有 Canvas 元素中的 3D 专用绘图上下文来实现的。
WebGL 基于 OpenGL ES 2.0。OpenGL ES 是长久以来的 3D 渲染标准 OpenGL 的一个适配方案。ES 代表“嵌入式系统”(embedded system),这表示它专用于小型计算设备,尤其是手机和平板电脑。OpenGL ES 是为 iPhone、iPad 和 Android 手机提供 3D 图形渲染能力的标准接口。WebGL 的设计者们认为,基于 OpenGL ES 更小的硬件占用量,可以更方便地提供一套统一的、跨平台跨浏览器的、用于 Web 的 3D API。
WebGL 可以与其他 Web 页面元素相结合。WebGL 可以以分层的形式置于其他页面内容的上方或下方。3D canvas 可以占据页面的一部分或整个页面,它也可以被包含在被设置了 z-index 属性的 &div& 元素中。这意味着你可以使用 WebGL 来构建 3D 图形,而使用你所熟悉的 HTML 特性来构建其他页面元素,并将它们(通过浏览器)无缝组合在同一个页面上展示给用户。
WebGL 为创建动态 Web 应用而生。WebGL 在设计过程中就考虑到了网络传输的需要。它始于 OpenGL ES,但它加入了许多与浏览器适配的特性。它使用 JavaScript 编写,并且对 Web 传播相当友好。
WebGL 是跨平台的。WebGL 可以运行在任意操作系统上,无论是手机、平板电脑还是台式电脑。
WebGL 是免费的。正如所有的开放 Web 标准那样,WebGL 可以免费使用。没有人会因为你使用了 WebGL 而要求你支付版权费用。
Chrome、Firefox、Safari 以及 Opera 的创造者们为发展和支持 WebGL 投入了众多的资源。来自这些浏览器开发组的工程师们同样也是 WebGL 标准组织的重要成员。WebGL 标准的 发展进程对 Khronos 小组的全体成员开放,同时也有对公众开放的邮件组。本书的附录中提供了邮件组和其他标准相关资源的详细信息。
2.2 WebGL API
WebGL 基于成熟的图形 API——OpenGL。WebGL 始于 20 世纪 80 年代末期,在经历了来自微软的 DirectX 的竞争威胁后,成为了 3D 图形编程无可争议的行业标准。
但所有的 OpenGL 版本都不尽相同。不同平台——包括台式电脑、电视机机顶盒、智能手机以及平板电脑——具有不同的特性,因此针对不同平台的 OpenGL 版本也由此发展起来。OpenGL ES 是针对机顶盒和智能手机这类小型设备的 OpenGL 版本,因此,它成为了 WebGL 的理想核心。它小而精,这不仅意味着它可以直接被浏览器实现,更意味着不同浏览器的开发者都可以很方便地在浏览器中实现对 WebGL 的支持,从而使得在一款浏览器中编写测试的 WebGL 应用可以直接运行在另一款浏览器中。
WebGL 非常精巧,这使得 WebGL 应用的开发者们需要做更多的工作。3D 场景本身不具备 DOM 结构 3,也没有原生支持的用于加载几何图形和动画的 3D 文件格式;除了几个底层的系统事件以外,3D canvas 并不具备内建的事件机制(例如,当你点击场景中的某个物体的时候,这个物体并不会触发鼠标点击事件)。对一般的 Web 开发者来说,WebGL 意味着陡峭的学习曲线和许多陌生的概念。
3可以理解为 3D 场景中的单个物体不具备单独的文档结构,即整个 canvas 中的绘图内容被视为一个整体。——译者注
有许多开源的代码库可以让 WebGL 开发变得不那么困难,正如 jQuery 或 Prototype.js 对于传统 Web 开发的意义那样——这个比喻或许比较粗糙。在接下来的几章我们会详细介绍这些库。不过现在我们先来简单了解一下底层的 WebGL,尽管在你的项目中你可能不会有机会去编写这些底层的 WebGL 代码,但了解一下引擎背后的实现机制,总归是有好处的。
2.3 WebGL应用剖析
说到底,WebGL 就是个绘图库,类似 2D canvas 那样,是被所有支持 HTML5 的浏览器所支持的另一种 canvas。事实上,WebGL 也使用了 Canvas 元素来作为在浏览器页面上绘制 3D 图形的容器。
为了在页面中渲染 WebGL,一个应用至少应当执行以下步骤:
(1) 创建一个 Canvas 元素;
(2) 获取 Canvas 元素中的绘图上下文;
(3) 初始化视口;
(4) 创建一个或多个包含待渲染数据(通常是顶点数据)的缓冲;
(5) 创建一个或多个定义顶点缓冲到屏幕空间转换规则的矩阵;
(6) 创建一个或多个实现绘制算法的着色器;
(7) 使用各项参数初始化着色器;
(8) 绘制。
下面我们用一些示例来说明这个流程。
2.4 一个简单的WebGL示例
为了说明 WebGL API 的基本工作机制,我们来编写一个非常简单的程序,在 canvas 上绘制一个白色正方形。文件 Chapter 2/example2-1.html 中有这个示例的完整代码,绘制结果如图 2-1 所示。
图 2-1:使用 WebGL 绘制的一个正方形
 本节大部分示例的灵感来源于 Learning WebGL()上的课程。Learning WebGL 由 Giles Thomas 创立,是一个出色的资源站点,它提供了一系列学习 WebGL 的教程。该网站还设有关于 WebGL 新应用的周刊,可供随时了解 WebGL 最新的发展动向。
2.4.1 Canvas元素和WebGL绘图上下文
所有的 WebGL 渲染都发生在一个上下文(context)中,这是一个提供了完整 WebGL API 的 DOM 对象。这个结构与 HTML5 Canvas 元素提供 2D 绘图上下文的模式相同。想要在页面中插入 WebGL 的内容,首先需要在页面上的某个位置创建一个 &canvas& 标签,获取与其相关的 DOM 对象(可以使用 document.getElementById()),并获取这个 DOM 对象的 WebGL 绘图上下文。
例 2-1 展示了如何从 canvas DOM 元素中获取 WebGL 绘图上下文。getContext() 方法支持两种表示上下文 id 的字符串参数,"2d" 参数用于获取 2D Canvas 绘图上下文(在第 7 章讲述),"webgl" 或 "experimental-webgl"(较老版本的浏览器)用于获取 WebGL 绘图上下文。新的浏览器同时兼容 "experimental-webgl" 和 "webgl" 参数。在示例代码中我们使用 "experimental-webgl",以确保它能够兼容不同版本的支持 WebGL 的浏览器。
例 2-1:从 canvas 中获取 WebGL 绘图上下文
function initWebGL(canvas) {
var msg = "Your browser does not support WebGL, " +
"or it is not enabled by default.";
gl = canvas.getContext("experimental-webgl");
msg = "Error creating WebGL Context!: " + e.toString();
alert(msg);
throw new Error(msg);
 注意示例中的 try/catch 代码块。它非常重要,因为某些浏览器,或者某些浏览器的旧版本并不支持 WebGL。即使浏览器支持 WebGL,浏览器运行的硬件设备也有可能由于太旧而无法提供 WebGL 绘图上下文。故而上述检测代码能帮助你在适当的时机加载降级方案(例如使用基于 2D canvas 的渲染方案),或者至少优雅地退出。
2.4.2 视口
当你从 canvas 中获取到一个 WebGL 绘图上下文时,需要定义一个绘制区域的矩形边界。在 WebGL 中,这个矩形边界被称为视口(viewport)。在 WebGL 中,设置视口非常简单,只需调用绘图上下文的 viewport() 方法,正如例 2-2 中那样。
例 2-2:设置 WebGL 视口
function initViewport(gl, canvas)
gl.viewport(0, 0, canvas.width, canvas.height);
注意这里的 gl 对象是由上面定义的 initWebGL() 函数生成的。在这里,我们将整个 canvas 区域都定义为 WebGL 的视口。
2.4.3 缓冲、缓冲数组和类型化数组
现在我们已经得到了等待绘制的 WebGL 绘图上下文。到目前为止,我们所做的事情和在 2D Canvas 上绘制图形所需的准备工作并没有多大区别。
WebGL 基于图元(primitive)进行图像绘制。所谓图元,是指不同类型的基本几何图形。WebGL 的图元包括三角形、点和线。三角形是最常用的图元类型,通常使用两种形式存储:三角形集(以数组形式存储的三角形)和三角形条带(triangle strip)4。图元以数组的形式存储数据,这个数组被称为缓冲(buffer),待绘制的顶点数据在缓冲中被定义。
4三角形条带是一种通过重复利用顶点数据来压缩数据体积的存储方式,关于这种存储方式,请参照维基百科词条 。——译者注
例 2-3 展示了如何为一个单位(1×1)正方形创建顶点缓冲。其结果在一个包含顶点缓冲数据的 JavaScript 对象中返回,包括顶点结构的长度(在这个示例中,表示顶点 x、y 和 z 值的每组数据用三个浮点数来存储),待绘制的顶点数量,绘制这个正方形时所使用的图元类型——在这个示例中,我们使用了三角形条带。一个三角形条带定义了一组连续的三角形,前三个顶点表示第一个三角形,后续的每个三角形都与前一个三角形共用其两个顶点。
例 2-3:创建顶点缓冲数据
// 构建用于绘制的正方形顶点数据
function createSquare(gl) {
var vertexB
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var verts = [
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
var square = {buffer:vertexBuffer, vertSize:3, nVerts:4,
primtype:gl.TRIANGLE_STRIP};
注意类型 Float32Array。这是一种浏览器专门为 WebGL 引入的新数据类型。Float32Array 是一类缓冲数组(ArrayBuffer),也称为类型化数组(typed array)。它是一种以二进制方式存储的 JavaScript 类型。你可以用与访问普通数组相同的方式来访问类型化数组,但访问类型化数组的速度更快,耗费的内存也更小。由于使用二进制数据存储,它们是解决性能瓶颈的理想存储方案。类型化数组可以被普遍使用(不仅仅在 WebGL 中),但它是因 WebGL 才被引入浏览器的。我们可以在 Khronos 组织的网站()上找到类型化数组的最新规范。
2.4.4 矩阵
在绘制正方形之前,我们首先要创建一对矩阵。一个矩阵用于定义正方形在 3D 坐标系统中的位置(相对于相机),这个矩阵被称为模型 - 视图矩阵(ModelView matrix),因为它同时包含模型矩阵(模型位置)和视图矩阵(相机位置)的信息。在我们的示例中,我 们沿着 z 轴负方向对正方形进行了平移(即将它移动到距离相机 -3.333 个单位长度的地方)5。第二个矩阵被称为投影矩阵(projection matrix),着色器使用它来执行从 3D 空间坐标到 2D 视口绘制空间坐标的转换。在这个示例中,投影矩阵定义了一个 45 度角视野的透视相机(如果你忘了什么是透视投影,请回顾第 1 章)。
5在 3D 图形处理中,当相机的位置被移动时,程序实际进行的处理是根据当前定义的相机位置去对整个场景进行平移,例如相机位于 [0, 0, 3] 位置时,实际上会被处理为整个场景进行了 [0, 0, -3] 的平移。——译者注
在 WebGL 中,矩阵以类型数组形式存储的一组数字来表示。例如一个 4×4 矩阵使用一个包含 16 个元素的 Float32Array 对象来表示。为了更方便地初始化矩阵和进行矩阵运算,我们将使用一个叫 glMatrix 的开源库(),这个库由现任 Google 工程师的 Brandon Jones 编写。矩阵初始化的代码如例 2-4 所示。glMatrix 的矩阵统一以 mat4 类型来表示,通过工厂函数 mat4.create() 来创建矩阵对象。函数 initMatrices() 创建了模型 - 视图矩阵和投影矩阵,并用全局变量 modelViewMatrix 和 projectionMatrix 来存储这两个矩阵。
例 2-4:初始化投影矩阵和模型 - 视图矩阵
var projectionMatrix, modelViewM
function initMatrices(canvas)
// 创建一个模型-视图矩阵,包含一个位于(0, 0, -3.333)的相机
modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -3.333]);
// 创建一个45度角视野的投影矩阵
projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI / 4,
canvas.width / canvas.height, 1, 10000);
2.4.5 着色器
场景绘制的准备工作已经差不多了。接下来我们要进行至关重要的初始化环节:着色器。正如先前所提到过的,着色器是一段使用 GLSL(一种类 C 的高级语言)编写的简短程序,它定义了 3D 对象的像素点实际绘制到屏幕上的方式。WebGL 要求开发者为每个待绘制的对象提供一个着色器。一个着色器可以应用于多个对象,因此在实际应用中,整个场景通常只需提供一个统一的着色器。通过设置不同的参数,可以在不同的几何形状上复用它。
一个着色器通常由两个部分组成:顶点着色器(vertex shader)和片段着色器(fragment shader,又称 pixel shader,像素着色器)。顶点着色器负责将物体的坐标转换为 2D 显示区域中的坐标;片段着色器负责计算转换好的顶点像素的最终颜色输出,其基于颜色、纹理、光照、材质等数值输入。我们简单示例中的顶点着色器包含顶点位置 vertexPos、模型 - 视图矩阵 modelViewMatrix 以及投影矩阵 projectionMatrix 的数值,这些数值与输入数值结合计算,构建出最终的、平移好的顶点数据,而片段着色器则简单地输出设定好的白色。
在 WebGL 中,初始化着色器需要进行一系列步骤,包括将独立的 GLSL 源代码片段编译到一起并建立链接。例 2-5 列出了着色器代码,让我们来通读这段代码。首先定义一个辅助函数 createShader(),这个函数调用 WebGL 提供的方法来编译顶点着色器和片段着色器的源代码。
例 2-5:着色器代码
function createShader(gl, str, type) {
if (type == "fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (type == "vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
GLSL 源代码以 JavaScript 字符串的形式定义,存储在全局变量vertexShaderSource 和 fragmentShaderSource 中:
var vertexShaderSource =
attribute vec3 vertexP\n" +
uniform mat4 modelViewM\n" +
uniform mat4 projectionM\n" +
void main(void) {\n" +
// 返回经过投影和变换的顶点值\n" +
gl_Position = projectionMatrix * modelViewMatrix * \n" +
vec4(vertexPos, 1.0);\n" +
var fragmentShaderSource =
void main(void) {\n" +
// 返回像素点的颜色:始终输出白色\n" +
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n" +
 GLSL 代码由存储在全局变量中的 JavaScript 字符串提供。这有点讨厌,因为我们不得不用加号来连接不同行,来保证代码格式。作为替代方案,我们可以采用先在外部文本文件中定义着色器,然后用 Ajax 的方式来加载这个文件。或者我们也可以创建隐藏的 DOM 节点,然后把代码写在 DOM 节点的文本内容中。为了便于说明,我们在示例代码中使用了这种最简单的形式。当真正编写代码的时候,你可以选择其他更优雅的方式。
当着色器的各个部分被编译完成,我们需要调用 WebGL 中的 gl.createProgram()、gl.attachShader() 以及 gl.linkProgram() 方法将它们链接到同一段程序中。随后我们需要调用 gl.getAttribLocation() 和 gl.getUniformLocation() 函数获取 GLSL 程序中定义的各个变量的句柄,从而可以用 JavaScript 中定义的数值来初始化这些变量。6 initShader() 函数的定义如下:
6这里描述的是将 GLSL 中的变量转换为 JavaScript 变量的过程,从而使得 JavaScript 可以向 GLSL 的变量注入一些配置参数。——译者注
var shaderProgram, shaderVertexPositionAttribute,
shaderProjectionMatrixUniform,
shaderModelViewMatrixU
function initShader(gl) {
// 加载并编译片段和顶点着色器
var fragmentShader = createShader(gl, fragmentShaderSource,
"fragment");
var vertexShader = createShader(gl, vertexShaderSource,
"vertex");
// 将它们链接到一段新的程序中
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// 获取指向着色器参数的指针
shaderVertexPositionAttribute =
gl.getAttribLocation(shaderProgram, "vertexPos");
gl.enableVertexAttribArray(shaderVertexPositionAttribute);
shaderProjectionMatrixUniform =
gl.getUniformLocation(shaderProgram, "projectionMatrix");
shaderModelViewMatrixUniform =
gl.getUniformLocation(shaderProgram, "modelViewMatrix");
if (!gl.getProgramParameter(shaderProgram,
gl.LINK_STATUS)) {
alert("Could not initialise shaders");
2.4.6 绘制图元
现在,我们已经为绘制正方形做好了全部的准备工作——我们创建了绘图上下文;设置了视口;顶点缓冲、矩阵和着色器也都已创建和初始化。下面我们定义一个函数 draw(),用它来绘制我们在上文中展示过的那个正方形。让我们来通读这个函数。
首先,draw() 函数以黑色背景填充的方式清空了整个画布,方法 gl.clearColor() 将黑色设为当前画布的“清空”颜色。这个方法携带四个参数,分别代表 RGBA(Red、Green、Blue、Alpha)颜色的四个分量。注意 WebGL 的 RGBA 值是用 0.0 到 1.0 范围的浮点数来表示的(这与用 0~255 的整数表示的 Web 颜色值不一样,例如在 CSS 中)。gl.clear() 使用定义的“清空”颜色来“清空”WebGL 颜色缓冲(color buffer),即 GPU 显存中用于渲染屏幕上像素点的区域。7[WebGL 使用多种类型的缓冲(buffer)来进行绘制,包括颜色缓冲和用于深度测试的深度缓冲(depth buffer)。关于深度缓冲,我们将在下一节予以说明。]
7指将颜色缓冲中的所有字节都设为指定的“清空”颜色。——译者注
其次,draw() 函数将正方形的顶点缓冲数据绑定到绘图上下文的缓冲,设定了图元绘制过程中将要使用的着色器,并建立顶点缓冲数据和矩阵与着色器之间的关联。
最后,我们调用 WebGL 的 drawArrays() 函数来绘制这个正方形。WebGL 通过传入的图元类型参数和图元的顶点数量参数,并结合之前预设的其他属性(顶点、矩阵、着色器)来得到最终的绘制结果。例 2-6 展示了整个流程。
例 2-6:绘制代码
function draw(gl, obj) {
// 清空背景(使用黑色填充)
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 设置待绘制的顶点缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer);
// 设置待用的着色器
gl.useProgram(shaderProgram);
// 建立着色器参数之间的关联:顶点和投影/模型矩阵
gl.vertexAttribPointer(shaderVertexPositionAttribute,
obj.vertSize, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false,
projectionMatrix);
gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false,
modelViewMatrix);
// 绘制物体
gl.drawArrays(obj.primtype, 0, obj.nVerts);
终于,我们完成了整个绘制流程。程序执行的结果是一个绘制在黑色背景上的白色正方形,如之前图 2-1 所示的那样。
2.5 创建3D几何体
上面绘制的正方形是一个尽可能简单的 WebGL 示例。显然它不怎么能引起你的兴趣,甚至还不是 3D 的——尽管为了绘制这个正方形,我们已经编写了将近 200 行代码。而实现同样效果的 2D Canvas 绘制代码顶多只需 30 行左右。在这一点上,WebGL 相对其他绘图 API 没有显示出优势。但别着急,现在我们来用 WebGL 做一些有趣的事情——真正的 3D 绘图。为了得到一个包含不同颜色的 3D 立方体,我们需要在正方形的基础上加一些线条,为此我们将对着色器和绘图函数做一些小改动。我们还要为这个立方体加上一个简单的动画,以便从各个角度去观察它。图 2-2 展示了一个旋转中立方体的屏幕截图。
图 2-2:一个包含不同颜色的立方体
为了创建和渲染这个立方体,我们将改进之前的示例代码。首先,我们将用于创建正方形的缓冲数据改为用于创建立方体的缓冲数据。其次,我们将使用与之前不同的 WebGL 函数来执行绘制过程。文件 Chapter 2/example2-2.html 中包含绘制立方体的完整代码。
例 2-7 展示了立方体的缓冲设置过程,它比绘制正方形的代码要复杂一些。立方体有更多的顶点,并且我们将为不同的面设置不同的颜色。首先,我们创建顶点缓冲数据,并将它存储在变量 vertexBuffer 中。
例 2-7:初始化立方体、颜色和索引缓冲的代码
// 为彩色的立方体构建顶点、颜色和索引数据
function createCube(gl) {
// 顶点数据
var vertexB
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var verts = [
-1.0, -1.0,
1.0, -1.0,
-1.0, -1.0, -1.0,
1.0, -1.0,
1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0,
1.0, -1.0,
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0,
-1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0,
1.0, -1.0,
-1.0, -1.0, -1.0,
-1.0, -1.0,
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
其次,创建颜色数据,为每个顶点设置一个四元色,并将其存储在变量 colorBuffer 中。faceColors 数组中是一系列定义好的 RGBA 颜色值。
// 颜色数据
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var faceColors = [
[1.0, 0.0, 0.0, 1.0], // 正面
[0.0, 1.0, 0.0, 1.0], // 背面
[0.0, 0.0, 1.0, 1.0], // 顶面
[1.0, 1.0, 0.0, 1.0], // 底面
[1.0, 0.0, 1.0, 1.0], // 右面
[0.0, 1.0, 1.0, 1.0]
var vertexColors = [];
for (var i in faceColors) {
var color = faceColors[i];
for (var j=0; j & 4; j++) {
vertexColors = vertexColors.concat(color);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexColors),
gl.STATIC_DRAW);
最后,我们要创建一类新型的缓冲——索引缓冲(index buffer),用于存储顶点数据的索引。我们将这些数据存储在变量 cubeIndexBuffer 中。之所以这里样做,是因为我们将在更新过的 draw() 函数中使用顶点集索引而非顶点本身来定义所有的三角形。这样做的理由是:3D 几何图形往往代表了连续封闭的区域,单个顶点常常由多个三角形共享,而索引缓冲能够避免数据重复,令数据存储更加紧凑。
// 索引数据(定义待绘制的三角形)
var cubeIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
var cubeIndices = [
8, 10, 11,
12, 13, 14,
12, 14, 15, // 底面
16, 17, 18,
16, 18, 19, // 右面
20, 21, 22,
20, 22, 23
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeIndices),
gl.STATIC_DRAW);
var cube = {buffer:vertexBuffer, colorBuffer:colorBuffer,
indices:cubeIndexBuffer,
vertSize:3, nVerts:24, colorSize:4, nColors: 24, nIndices:36,
primtype:gl.TRIANGLES};
为了绘制立方体的颜色,这些颜色必须被传递给着色器。例 2-8 展示了改进后的着色器代 码。注意加粗的代码行:我们声明了一个代表顶点颜色的属性。此外我们还需要声明一个
GLSL 中的 varying 变量——vColor,它用于将每个顶点的颜色信息从顶点着色器传递到 片段着色器。与之前出现过的并不逐个更改顶点数据的 uniform 变量(例如我们早先讨论 的矩阵)不同,varying 变量代表着色器会为每个顶点逐个输出不同的值。在这个示例中, 我们将存储在 vertexColor 变量中的颜色缓冲数据输入到变量 vColor 中。片段着色器直接 输出 vColor 中的原始颜色值。
例 2-8:用于渲染带颜色正方体的着色器代码
var vertexShaderSource =
attribute vec3 vertexP\n" +
attribute vec4 vertexC\n" +
uniform mat4 modelViewM\n" +
uniform mat4 projectionM\n" +
varying vec4 vC\n" +
void main(void) {\n" +
// 返回经过变换和投影的顶点值\n" +
gl_Position = projectionMatrix * modelViewMatrix * \n" +
vec4(vertexPos, 1.0);\n" +
// Output the vertexColor in vColor\n" +
vColor = vertexC\n" +
var fragmentShaderSource =
varying vec4 vC\n" +
void main(void) {\n" +
// 返回像素点颜色:始终输出白色\n" +
gl_FragColor = vC\n" +
 如果仅仅用于设置单一的颜色,这段代码看起来也许有点过于复杂。但一个复杂的着色器——例如一个实现了光照模型的着色器,或者一个实现了草地、水面动态纹理的着色器,等等——在输出最终色彩之前,会对 vColor 进行许多额外的运算处理。无疑,着色器提供了强大的视觉能力,但是正如 Ben Parker 的名言所述——能力越大,责任越大。
现在我们开始编写用于绘制的代码,如例 2-9 所示。为了绘制比正方形更为复杂的立方体,我们需要做一些不同的事。示例代码中加粗的部分标明了这些改动。首先,我们要开启深度测试,使得 WebGL 可以按深度排序来绘制 3D 物体。否则,WebGL 将无法保证将“在前方”的面按照我们的预期绘制在其他面的前方,“前方”和“后方”的面会混淆在一起。(如果想看看关闭深度测试会发生什么事,只需注释掉那行代码。你仍然会看到立方体的部分面,但不完整。)
其次,我们要将之前已经在 createCube() 函数中创建好的颜色和索引缓冲绑定到绘图上下文的缓冲。最后,我们调用 WebGL 方法 gl.drawElements() 而不是 gl.drawArray() 来绘制用索引缓冲来表示的图元信息。
例 2-9:更改后的立方体绘制代码
function draw(gl, obj) {
// 清空背景(使用黑色填充)
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 设置待用的着色器
gl.useProgram(shaderProgram);
// 建立着色器参数之间的关联:顶点和投影/模型矩阵
// 设置缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer);
gl.vertexAttribPointer(shaderVertexPositionAttribute,
obj.vertSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, obj.colorBuffer);
gl.vertexAttribPointer(shaderVertexColorAttribute,
obj.colorSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indices);
gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false,
projectionMatrix);
gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false,
modelViewMatrix);
// 绘制物体
gl.drawElements(obj.primtype, obj.nIndices, gl.UNSIGNED_SHORT, 0);
2.6 添加动画
如果希望看到立方体的 3D 效果而不是一个静止的 2D 图像,我们需要让它动起来。现在,我们来为这个立方体添加一个绕坐标轴旋转的简单动画。动画的代码如例 2-10 所示。在函数 animate() 中,立方体以五秒钟为周期围绕预先定义的旋转轴 rotationAxis 旋转。
animate() 由另一个函数 run() 循环调用,它调用一个新的浏览器函数 requestAnimationFrame() 来持续驱动动画。这个函数在每次浏览器重绘页面的时候调取一个回调函数。(关于这个函数以及其他动画相关的技术,后续的章节里有更详细的说明。)每次 animate() 函数被调用的时候,它都会计算当前时间和上一次调用该函数的时间之间的差值,并将其存储在变量 deltat 中,然后根据它计算出作用于矩阵变量 modelViewMatrix 的旋转角度。代码执行的结果是立方体以每五秒一圈的速度绕 rotationAxis 旋转。
例 2-10:为立方体添加动画
var duration = 5000; // 毫秒(ms)
var currentTime = Date.now();
function animate() {
var now = Date.now();
var deltat = now - currentT
currentTime =
var fract = deltat /
var angle = Math.PI * 2 *
mat4.rotate(modelViewMatrix, modelViewMatrix, angle, rotationAxis);
function run(gl, cube) {
requestAnimationFrame(function() { run(gl, cube); });
draw(gl, cube);
animate();
2.7 使用纹理映射
纹理映射是我们在本章要学习的最后一个 WebGL API 特性。纹理映射(texture map),或简称纹理,是指覆盖几何体表面显示的位图。WebGL 中使用 Image DOM 元素作为纹理数据的源,这表示,只需简单地更改 Image 元素的 src 属性,你就可以将 Web 图像格式,例如 JPEG 和 PNG,作为 WebGL 贴图了。
 WebGL 纹理并不一定要以图像文件为源来构建。2D canvas 元素也可以作为纹理的源,利用这个特性,我们可以使用 2D Canvas 绘图 API 在 3D 物体的表面绘制图案;纹理甚至还可以以 Vedio 元素作为源来构建,因此你可以在一个 3D 物体的表面播放视频。关于动态纹理的能力,在第 11 章有更详细的说明。
我们将前面的旋转立方体示例改为使用纹理映射而非表面色彩。如图 2-3 所示。
图 2-3:一个使用了纹理映射的立方体
这里需要提醒各位读者,如果你直接在文件系统里双击打开纹理映射的代码示例页面,它是无法正常运行的。它需要用一个 Web 服务器来加载,因为我们从一个 JPEG 文件中载入纹理,这是由于 WebGL 安全模型中的跨域访问安全限制,我们需要运行一个 Web 服务器而非直接通过 file:// URL 来访问这个文件。总的来说,本书中的大多数例子都需要通过 Web 服务器来访问。
我在 MacBook 上运行了一个标准本地版 LAMP 环境,不过你需要用到的仅仅是 LAMP 的一部分功能——像 Apache 这样的 Web 服务。或者如果你的机器上装了 Python,你也可以利用 Python 内置的 SimpleHTTPServer 模块来启动一个 Web 服务,使用命令行窗口定位到 examples 目录,然后输入:
python -m SimpleHTTPServer
这样你就可以通过 http://localhost:8000/ 这个地址来访问本书的示例了。如果希望获取更多关于这方面的技术支持,请访问 Linux Journal 网站()。
这个示例的完整代码在文件 Chapter 2/example2-3.html 中。例 2-11 展示了加载纹理的代码。首先,我们调用 gl.createTexture() 来创建一个新的 WebGL 纹理对象。然后将这个纹理对象的 image 属性设为一个新创建的 Image 对象。最后,将这个 Image 对象的 src 属性设为一个 JPEG 文件的路径——在这个示例中,我们加载了一个 256×256 的正方形 WebGL 官方 LOGO——不过首先我们要为图像的 onload 事件注册一个事件处理程序,以便在图像加载完毕的时候对 WebGL 纹理对象做一些处理。
例 2-11:从图像创建一个纹理映射
var okToRun =
function handleTextureLoaded(gl, texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
texture.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, null);
var webGLT
function initTexture(gl) {
webGLTexture = gl.createTexture();
webGLTexture.image = new Image();
webGLTexture.image.onload = function () {
handleTextureLoaded(gl, webGLTexture)
webGLTexture.image.src = "../images/webgl-logo-256.jpg";
在 onload 事件的回调函数 handleTextureLoaded() 中,我们做了下面这些事情。首先,我们调用 gl.bindTexture() 函数来指定 WebGL 在后续绘制过程中将要使用的纹理。被指定的纹理将在后续的整个绘制过程中生效,直到 gl.bindTexture() 再次被调用——在函数的末尾,我们将绑定纹理设置为空,以防止在后续操作中意外更改纹理存储区域的内容。
其次,我们调用 gl.pixelStorei() 函数来翻转所有纹理像素点的 y 坐标值。之所以需要进行这个操作,是因为在 WebGL 中,纹理坐标系的 y 轴是垂直向上的,而在 Web 图像本身的坐标系中,y 轴是垂直向下的。
 gl.pixelStorei() 函数名中的字母 i 代表整型数(integer),WebGL 的函数命名规范参照 OpenGL,通常以一个字母后缀来标识函数参数的类型。图像以整型数组的方式存储(RGB 或 RGBA 颜色),因此被标识为 i。
现在我们调用 texImage2D() 方法来将加载好的图像数据复制到 WebGL 纹理对象中。这个方法支持几种不同类型的参数,你可以查阅 WebGL 规范了解如何使用它创建不同类型的纹理。在这个示例中,我们在第 0 层创建了一个 2D 纹理——一个纹理中可以包含不同层级的纹理,这个技术被称为 mip-mapping,我们将在后面进行介绍——这个纹理采用了 RGBA 颜色模式,存储在一个无符号字节(unsigned byte)数组中。
我们还需要设置纹理过滤选项,这些选项用于控制图像随远近位置放大缩小时的纹理像素颜色计算。在我们的示例中,我们使用了最简单的过滤设置——gl.NEAREST,这个设置的策略是通过缩放图像本身来得出纹理像素点的颜色。在这个设置下,纹理在没有被过度缩放的前提下看起来效果还不错,但过近处(放大)会呈现块状和像素化效果,而过远处(缩小)会呈现锯齿和不平滑效果。WebGL 提供了另外两种纹理过滤能力:gl.LINEAR 是使用线性插值的方法来处理放大,使得放大后纹理的视觉效果更加平滑;gl.LINEAR_MIPMAP_NEAREST 用于添加 mip-map 过滤,使得远处物体的纹理看起来更平滑。
想要感受 gl.NEAREST 过滤的缺点,可以尝试调整立方体的位置。修改源文件 Chapter 2/example2-3.html 的第 47 行,修改立方体 z 坐标的值(当前为 -8),调整立方体的远近。
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -8]);
尝试将 -8 改为 ?4。当立方体更加靠近观察点,你可以观察到立方体的纹理明显变得像素化了(图 2-4)。
图 2-4:gl.NEAREST 过滤——近处物体上的纹理变得像素化
现在再尝试将 -8 改成 -32。当立方体远离观察点时候,可以看到纹理出现了明显的锯齿(图 2-5)。
图 2-5:gl.NEAREST 过滤——较远物体的纹理出现锯齿
现在,我们已经设置好了全部的纹理选项,并调用 gl.bindTexture() 方法来清空当前绑定的纹理。最后,我们将全局变量 okToRun 的值设为 true,以此通知 run() 函数纹理已经准备就绪,绘图程序可以开始运行了。
像往常一样,我们还需要对代码的其他部分进行更改:缓冲的创建代码,着色器代码,以及着色值的设置代码。首先,我们将创建颜色缓冲的代码替换为创建纹理缓冲的代码。纹理坐标(texture coordinate)用随顶点数据一同定义的、[0, 1] 范围内的一对浮点数来表示。这对浮点数对应位图上的 x、y 偏移量,正如下面着色器的代码中所展现的那样。对我们的立方体来说,纹理坐标值非常简单:我们把整张纹理分别贴到立方体的每个表面上,因此立方体每个面的转角坐标恰好对应纹理图像的转角坐标,如 [0, 0]、[0, 1]、[1, 0] 或 [1, 1]。
注意,这些纹理坐标值的顺序与顶点缓冲中的顶点顺序是相对应的。例 2-12 展示了创建纹理坐标缓冲的代码。
例 2-12:纹理映射立方体的缓冲创建代码
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
var textureCoords = [
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords),
gl.STATIC_DRAW);
我们还要将使用色彩的着色器更改为使用纹理的着色器。在顶点着色器中,我们定义了一个名为 texCoord 的顶点属性变量,顶点数据通过这个变量传入,以及一个 varying 类型的输出变量 vTexCoord,用于向片段着色器输出各个顶点的信息。片段着色器使用纹理坐标作为纹理映射数据的索引,纹理坐标通过 uniform 变量 uSampler 传入程序。我们调用 GLSL 函数 texture2D() 从纹理中取得像素信息,包括采样器和以 (x, y) 形式存储的 2D 顶点位置。更改后的着色器代码如例 2-13 所示。
例 2-13:纹理映射立方体的着色器代码
var vertexShaderSource =
attribute vec3 vertexP\n" +
attribute vec2 texC\n" +
uniform mat4 modelViewM\n" +
uniform mat4 projectionM\n" +
varying vec2 vTexC\n" +
void main(void) {\n" +
// 返回经过变换和投影的顶点值\n" +
gl_Position = projectionMatrix * modelViewMatrix * \n" +
vec4(vertexPos, 1.0);\n" +
// Output the texture coordinate in vTexCoord\n" +
vTexCoord = texC\n" +
var fragmentShaderSource =
varying vec2 vTexC\n" +
uniform sampler2D uS\n" +
void main(void) {\n" +
// 返回像素点的颜色:始终输出白色\n" +
gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s, vTexCoord.t));\n" +
为了将纹理贴到我们的立方体上,我们最后还要对绘制函数做一些小修改。例 2-14 展示了修改后的代码。我们将设置颜色缓冲的代码替换为设置纹理缓冲的代码,并将该纹理设为当前纹理,绑定到绘图上下文。
例 2-14:初始化绘制所需的纹理映射数据
gl.vertexAttribPointer(shaderTexCoordAttribute, obj.texCoordSize, gl.FLOAT,
false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indices);
gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix);
gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, webGLTexture);
gl.uniform1i(shaderSamplerUniform, 0);
本章讲述了如何使用 WebGL API 进行图形渲染。我们了解了编写一个 WebGL 应用的基本流程,包括创建绘图上下文、视口、缓冲、矩阵、着色器和图元的绘制。我们学习了如何创建 2D 和 3D 几何图形并用颜色和纹理来填充它们。我们还稍微借助了开源库 glMatrix 和 RequestAnimationFrame.js,它们都是 WebGL 开发的常用基本库。
显然,到目前为止我们接触到的 WebGL 底层编程是非常繁琐的。在读完本章之后,我们已经可以编写一些稍微复杂的、包括颜色和纹理的几何图形,尽管为此可能需要编写数百行代码。这提供了强大的能力——你可以精细地操作屏幕上的每一个顶点和像素,以令人炫目的硬件加速的速度进行。然而使用 WebGL 底层接口来编写代码需要大量繁重的工作。标准的设计者们采用了牺牲代码尺寸来换取更强大能力的思路。WebGL 的 API 小而简单,这就使得更多工作落在了应用开发者身上。
如果你是一名经验丰富的游戏或图形开发者,并且希望更好地掌控应用的性能和特性,那么直接使用 WebGL API 对你来说也许是最好的选择。如果你正在开发的应用对渲染有特别的需求,例如图像处理应用或 3D 建模工具,那么你应该深入地研究 WebGL 中的 metal 技术。又或许,你需要开发一些更顶层的应用,例如谁也不希望为了创建一个立方体而重复写那四十行相同的代码,在这个层次上你得完全靠自己,你需要理解和控制每一行代码。
尽管如此,如果你和大多数人一样对 3D 并不是特别熟悉,那么你应该以一种比 WebGL API 本身更高级的封装方式来编写应用,例如使用一些现成的工具。事实上这些工具已经存在:有许多基于 WebGL 的优秀开源库可供使用。我们将在后面的几章一一介绍它们。}

我要回帖

更多关于 守望先锋游戏引擎 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信