用webgl打造一款简单第一人称射击游戏

具体实现

用webgl打造一款简单第一人称射击游戏

2016/11/03 · HTML5 · 1
评论 ·
WebGL

原文出处:
AlloyTeam   

背景:不知道大家还记不记得上次那个3D迷宫游戏,有同事吐槽说游戏中间有一个十字瞄准器,就感觉少了一把枪。好吧,那这次就带来一款第一人称射击游戏。写demo锻炼,所以依旧用的原生webgl,这次重点会说一下webgl中关于摄像头相关的知识,点开全文在线试玩~~

 

simpleFire在线试玩:

simpleFire源码地址:

说明:

游戏比较简单(所以叫simpleFire)……不过也勉强算是一款第一人称射击游戏啦~

由于时间非常有限,这次真的不是懒!!相信我!!所以界面比较丑,见谅见谅(讲良心说,这比3D迷宫真的走心多了……)

上次3D迷宫文章重点介绍了迷宫的几种算法,webgl没怎么讲,这篇文章会重点讲下webgl中摄像机相关的知识,webgl基础知识会简单带一下

最后贴一下上次3D迷宫的地址:

 

1、游戏准备:

做一款游戏和做一个项目是一样的,不能刚有想法了就直接开始撸代码。一个前端项目可能要考虑框架选型、选用何种构建、设计模式等等;而一款游戏,在确定游戏类型之后,要考虑游戏玩法,游戏场景,游戏关卡,游戏建模美术等等,而这些很多都是非代码技术层面的,在真正的游戏开发中会有专门那个领域的人去负责,所以一款好的游戏,每一个环节都不可或缺。

上面是关于游戏开发的碎碎念,下面开始真正的讲解simpleFire这款游戏。

试玩之后大家应该会发现游戏整个场景非常简单,一把枪,四面墙,墙上面有靶子,将所有的靶子都打掉则游戏结束,最终的游戏分数是:
击中靶子数 +
剩余时间转换。此时读者可能内心感受:这尼玛在逗我呢,这也太简单了吧。先别急,接下来说下游戏准备过程中遇到的坑点

因为是3D游戏,而且涉及到了不同的物体在3D空间中存在(枪、靶子、墙),之前那3D迷宫准备工作之所以简单是空间中从头到尾就只有“墙”这一个东西。

要让枪、靶子、墙这些东西同处一个空间内很简单,把他们顶点信息写进shader就行了嘛

(在这里考虑到可能有没接触过webgl的同学,所以简单介绍一下,canvas是对象级别的画板操作,drawImage画图片,arc画弧度等,这些都是对象级别操作。而webgl是片元级操作,片元在这里可以先简单理解为像素,只是它比像素含有更多的信息。上面所说的把顶点信息写进shader,可以理解为把枪、靶子、墙这些东西的坐标位置画进canvas。先就这样理解着往下看吧~如果canvas也不知道那就没办法了。。。)

顶点信息从哪来?一般是设计师建模弄好了,导成相关文件给开发者,位置、颜色等等都有。但是……我这里没有任何相关信息,全部得自己来做。

自己跟前又没有专业的建模工具,那该如何生成顶点信息?用脑补 +
代码生成……事先声明,这是一种很不对很不对的方式,自己写点demo可以这样玩,但是生产中千万别这样。

这里就用生成枪来举例,我们知道普通制式手枪长180mm到220mm左右,在这里取20cm,并将其长度稍微小于视锥体近平面的长度,视锥体近平面也看作为屏幕中webgl画布的宽度。所以我们生成的枪理论上应该是这样的,如图所示:

图片 1

好了,枪的比例确定之后就要结合webgl坐标系生成顶点信息了,webgl坐标系和canvas2D坐标系有很大的不同,如图:

图片 2

因为是代码手动生成顶点信息,用-1~1写起来有点难受,所以这里我们先放大10倍,后面在把除回去,蛋疼吧,这就是不走正途的代价……

代码该怎么生成顶点信息呢?用代码画一把枪听起来很难,但是用代码画一条线、画一个圆、画一个正方体等,这些不难吧,因为这些是基本图形,有数学公式可以得到。一个复杂的模型,我们没法直接确定顶点信息,那就只好通过各种简单模型去拼凑了,下面这个页面就是简单的拆分了下枪的模型,可以看到是各个简单子模型拼凑而成的(说明:建模形成的也是拼凑,不过它的一块块子模型不是靠简单图形函数方法生成)。

手枪生成展示:

这种方式有什么坏处:工作量大而且不好看、扩展性差、可控性差

这种方式有什么好处:锻炼空间想象力与数学函数应用吧……

介绍了这么多,其实就想说:这么恶心且吃力不讨好的活我都干下来了,真的走心了!

具体怎么用简单图形函数生成的子模型可以看代码,代码看起来还是比较简单,有一定立体几何空间想象力就好,这里不细讲,毕竟非常非常不推荐这样玩。

枪建模相关代码地址:

 

2、游戏视角

第一人称射击类游戏玩的是什么?就是谁开枪开的准,这个是永恒不变的,就算是OW,在大家套路都了解、可以见招拆招的情况下,最终也是比枪法谁更准。那么枪法准是如何体现的呢?就是通过移动鼠标的速度与准确度来体现(这里没有什么IE3.0……),对于玩家来说,手中移动的是鼠标,映射在屏幕上的是准心,对于开发者来说,移动的是视角,也就是3D世界中的摄像头!

先说下摄像头的基本概念和知识,webgl中默认的摄像头方向是朝着Z轴的负方向,随手画了图表示下(已知丑,轻吐槽)

图片 3

摄像头位置不变,同一个物体在不同位置能给我们不同的感受,如下

图片 4 图片 5

摄像头位置改变,同一个物体位置不变,也能给我们不同的感受,如下

图片 6 图片 7

等等!这似乎并没有什么区别啊!感觉上就是物体发现了变化啊!确实如此,就好像你在车上,看窗外飞驰而过的风景那般道理。

摄像头的作用也就是改变物体在视锥体中的位置,物体移动的作用也是改变其在视锥体中的位置!

熟悉webgl的中的同学了解

JavaScript

gl_Position = uPMatrix * uVMatrix * uMMatrix * aPosition;

1
gl_Position = uPMatrix * uVMatrix * uMMatrix * aPosition;

对于不了解的同学,可以这样理解

gl_Position是最终屏幕上的顶点,aPosition是最初我们生成的模型顶点

uMMatrix是模型变换矩阵,比如我们想让物体移动、旋转等等操作,可以再次进行

uPMatrix是投影变换矩阵,就理解为3维物体能在2D屏幕上显示最为重要的一步

uVMatrix是视图变换矩阵,就是主角!我们用它来改变摄像头的位置

我们的重点也就是玩转uVMatrix视图矩阵!在这里,用过threejs或者glMatrix的同学肯定就很诧异了,这里有什么好研究的,直接lookAt不就不搞定了么?

确实lookAt就是用来操作视图矩阵的,考虑到没用过的用户,所以这里先说一下lookAt这个方法。

lookAt功能如其名,用来确认3D世界中的摄像机方向(操作视图矩阵),参数有3个,第一个是眼睛的位置,第二个是眼睛看向目标的位置,第三个是坐标的正上方向,可以想象成脑袋的朝上方向。

用图来展示的话就是如下图(已知丑,轻吐槽):

图片 8

知道了lookAt的用法,接下来我们来看一下lookAt的原理与实现。lookAt既然对应着视图矩阵,将它的结果想象成矩阵VM

大家知道webgl中最初的坐标系是这样的

图片 9

那么如果我们知道最终的坐标系,就可以逆推出矩阵VM了。这个不难计算,结果如下

图片 10

来,回看一下lookAt第一个和第二个参数,眼睛的位置眼睛看向目标的位置,有了这两个坐标,最终坐标系的Z是不是确定了!,最后一个参数是正上方向,是不是Y也确定了!

机智的同学看到有了Z和Y,立马想到可以用叉积算出X,不知道什么是叉积的可以搜索一下(学习webgl一定要对矩阵熟悉,这些知识是基础)

这样我们就很轻松愉快的得出了VM,但是!似乎有点不对劲

本身VM是没有问题的,关键在于这么使用它,比如说我直接lookAt(0,0,0, 1,0,0,
0,1,0)使用,可以知道此时我们的视线是X轴的正方向,但若是我鼠标随便晃一个位置,你能快速的知道这三个参数该如何传么?

所以现在的目标就是通过鼠标的偏移,来计算出lookAt的三个参数,先上代码~

JavaScript

var camera = {     rx: 0,     ry: 0,     mx: 0,     my: 0,     mz: 0,
    toMatrix: function() {         var rx = this.rx;         var ry =
this.ry;         var mx = this.mx;         var my = this.my;         var
mz = this.mz;           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);           s.push(
            0,             0,             0,             1         );  
        return s;     } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var camera = {
    rx: 0,
    ry: 0,
    mx: 0,
    my: 0,
    mz: 0,
    toMatrix: function() {
        var rx = this.rx;
        var ry = this.ry;
        var mx = this.mx;
        var my = this.my;
        var mz = this.mz;
 
        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);
 
        s.push(
            0,
            0,
            0,
            1
        );
 
        return s;
    }
};

这里封装了一个简单的camera对象,里面有rx对应鼠标在X方向上的移动,ry对应鼠标在Y方向上的移动,这个我们可以通过监听鼠标在canvas上的事件轻松得出。

JavaScript

var mouse = {     x: oC.width / 2,     y: oC.height / 2 };  
oC.addEventListener(‘mousedown’, function(e) {     if(!level.isStart) {
        level.isStart = true;         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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var mouse = {
    x: oC.width / 2,
    y: oC.height / 2
};
 
oC.addEventListener(‘mousedown’, function(e) {
    if(!level.isStart) {
        level.isStart = true;
        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中是这几行

JavaScript

var x = F[0]; var z = F[2];   var angle = getAngle([0, -1], [x,
z]);

1
2
3
4
var x = F[0];
var z = F[2];
 
var angle = getAngle([0, -1], [x, z]);

angle得出了最终的视角方向(-Z)和最初视线方向在x-z坐标系中的偏转角,因为是x-z坐标系,所以最初的X正方向和最终的X正方向偏移角也是angle

JavaScript

function getAngle(A, B) {     if(B[0] === 0 && A[0] === 0) {
        return 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); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getAngle(A, B) {
    if(B[0] === 0 && A[0] === 0) {
        return 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维旋转等等方式,都可以得到结果。不过这些都比较依赖矩阵和三角函数数学知识,是不是现在无比的怀恋当年的数学老师……

 

3、命中检测

我们玩转了摄像头,然后就是开枪了,开枪本身很简单,但是得考虑到枪有没有打中人呀,这可是关于到用户得分甚至是敌我的死活。

我们要做的工作是判断子弹有没有击中目标,听起来像是碰撞检测有没有!来,回忆一下在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中的帧缓冲图像是什么样的

图片 11正常游戏画面

图片 12帧缓冲下的画面

发现整个世界中只有靶子有颜色对不对!这样我们读取帧缓冲图像中某个点的rgba值,就知道对应的点是不是在靶子上了!实现了坐标碰撞检测!

之前说的更加灵活的处理,就是指渲染时对各个模型颜色的处理

检测代码如下:

JavaScript

oC.onclick = function(e) {     if(gun.firing) {         return ;     }
    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);
    var i;     var id;       for(i = 0; i < this.ids.length; 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();             break ;         }     } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
oC.onclick = function(e) {
    if(gun.firing) {
        return ;
    }
    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);
    var i;
    var id;
 
    for(i = 0; i < this.ids.length; 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();
            break ;
        }
    }
}

而且这个方法很快,计算量都在GPU里面,这种数学计算的效率GPU是比CPU快的,GPU还是并行的!那传统的AABB法还有存在的意义么?

其实是有的,因为精确,可以在包围盒中计算得到具体的碰撞点位置,这是帧缓冲法所达不到的

举个例子,第一人称射击游戏中的爆头行为,可以在帧缓冲中将人物模型中身体和头用不同颜色区分出来,这样可以检测出碰撞的是头还是身体。这种情景下帧缓冲方法还hold住

那如果是想得到打靶中具体的位置,留下子弹的痕迹呢?这里帧缓冲方法就死也做不到了。

最佳实践就是在需要高精度复杂场景下的碰撞检测可以将两种方法结合使用:用帧缓冲去掉多余的物体,减少传统AABB法的计算量,最终得到具体位置。

simpleFire这里就没这么折腾了……只要射到靶上打哪都是得分~~~

 

4、碎碎念

关于simpleFire想讲的东西也就讲完了,本身也没有什么技术难点,文章的最后一节也聊一聊关于webgl

之前已经说了与canvas之间的区别,是从计算机层面的区别,这里说一下对于开发者的区别:

canvas2D是一块画布,在画布上作画,画中的东西一定是虚拟的

webgl是一个世界,你要在世界中创造,但也要满足世界的规则

这比喻有点夸大,都牵扯到了世界的规则。但事实就是如此,webgl比canvas2D复杂,而很大一块复杂的地方就是世界的规则
—— 光与阴影

这两块知识3D迷宫和simpleFire都没有用上,因为这应该是静态3D中最难啃的骨头了吧。说难吧,知道原理之后也不难,但就是恶心麻烦,加上光和阴影得多很多很多的代码。后面会详细讲解光和阴影相关知识的,也是用小游戏的方式。写一篇纯原理的文章感觉没啥意思,知识点一搜能搜到很多了

不看动画,纯看静态渲染方面的东西,2D和3D也就差不多,需要位置信息、颜色信息,平移旋转等等,3D也就是加上了光和阴影这样的世界规则,比2D还多了一些数学知识的要求

所以webgl并不难~欢迎更多的人来到webgl的坑中来吧,但是推荐入坑的同学不要开始就过于依赖three、oak3D、PhiloGL等图形库,还是从原生入手比较好

文章对simpleFire代码讲解的不是很多,源码也贴出来了,100%原生webgl的写法,看起来应该也不是很难

 

结语:

下次带来的不一定是3D小游戏,3D小游戏写起来还是挺累的,素材什么的比2D麻烦很多

这篇文章也就到此结束啦,写的好累T_T。。有问题和建议的小伙伴欢迎留言一起探讨~

1 赞 5 收藏 1
评论

图片 13

3-11 排除对象数组某些项 

3、将obj中数据真正的运用3D对象中去

JavaScript

Text3d.prototype.addFace = function(data) {
    this.addIndex(+data[1], +data[4], +data[7], +data[10]);
    this.addUv(+data[2], +data[5], +data[8], +data[11]);
    this.addNormal(+data[3], +data[6], +data[9], +data[12]); };
  Text3d.prototype.addIndex = function(a, b, c, d) {     if(!d) {
        this.index.push(a, b, c);     } else {
        this.index.push(a, b, c, a, c, d);     } };  
Text3d.prototype.addNormal = function(a, b, c, d) {     if(!d) {
        this.normal.push(             3 * this.normalArr[a], 3 *
this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,             3 *
this.normalArr[b], 3 * this.normalArr[b] + 1, 3 *
this.normalArr[b] + 2,             3 * this.normalArr[c], 3 *
this.normalArr[c] + 1, 3 * this.normalArr[c] + 2         );     }
else {         this.normal.push(             3 * this.normalArr[a], 3
* this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,             3
* this.normalArr[b], 3 * this.normalArr[b] + 1, 3 *
this.normalArr[b] + 2,             3 * this.normalArr[c], 3 *
this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,             3 *
this.normalArr[a], 3 * this.normalArr[a] + 1, 3 *
this.normalArr[a] + 2,             3 * this.normalArr[c], 3 *
this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,             3 *
this.normalArr[d], 3 * this.normalArr[d] + 1, 3 *
this.normalArr[d] + 2         );     } };   Text3d.prototype.addUv =
function(a, b, c, d) {     if(!d) {         this.uv.push(2 *
this.uvArr[a], 2 * this.uvArr[a] + 1);         this.uv.push(2 *
this.uvArr[b], 2 * this.uvArr[b] + 1);         this.uv.push(2 *
this.uvArr[c], 2 * this.uvArr[c] + 1);     } else {
        this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
        this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
        this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
        this.uv.push(2 * this.uvArr[d], 2 * this.uvArr[d] + 1);
    } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Text3d.prototype.addFace = function(data) {
    this.addIndex(+data[1], +data[4], +data[7], +data[10]);
    this.addUv(+data[2], +data[5], +data[8], +data[11]);
    this.addNormal(+data[3], +data[6], +data[9], +data[12]);
};
 
Text3d.prototype.addIndex = function(a, b, c, d) {
    if(!d) {
        this.index.push(a, b, c);
    } else {
        this.index.push(a, b, c, a, c, d);
    }
};
 
Text3d.prototype.addNormal = function(a, b, c, d) {
    if(!d) {
        this.normal.push(
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2
        );
    } else {
        this.normal.push(
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
            3 * this.normalArr[d], 3 * this.normalArr[d] + 1, 3 * this.normalArr[d] + 2
        );
    }
};
 
Text3d.prototype.addUv = function(a, b, c, d) {
    if(!d) {
        this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
        this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
        this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
    } else {
        this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
        this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
        this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
        this.uv.push(2 * this.uvArr[d], 2 * this.uvArr[d] + 1);
    }
};

这里我们考虑到兼容obj文件中f(ace)行中4个值的情况,导出obj文件中可以强行选择只有三角面,不过我们在代码中兼容一下比较稳妥

4-7设置文本内容

4、旋转平移等变换

物体全部导入进去,剩下来的任务就是进行变换了,首先我们分析一下有哪些动画效果
因为我们模拟的是一个宇宙,3D文字就像是星球一样,有公转和自转;还有就是我们导入的obj文件都是基于(0,0,0)点的,所以我们还需要把它们进行平移操作
先上核心代码~

JavaScript

…… this.angle += this.rotate; // 自转的角度   var s =
Math.sin(this.angle); var c = Math.cos(this.angle);   // 公转相关数据
var gs = Math.sin(globalTime * this.revolution); //
globalTime是全局的时间 var gc = Math.cos(globalTime * this.revolution);
    webgl.uniformMatrix4fv(     this.program.uMMatrix, false,
mat4.multiply([             gc,0,-gs,0,             0,1,0,0,
            gs,0,gc,0,             0,0,0,1         ], mat4.multiply(
            [                 1,0,0,0,                 0,1,0,0,
                0,0,1,0,                 this.x,this.y,this.z,1 //
x,y,z是偏移的位置             ],[                 c,0,-s,0,
                0,1,0,0,                 s,0,c,0,
                0,0,0,1             ]         )     ) );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
……
this.angle += this.rotate; // 自转的角度
 
var s = Math.sin(this.angle);
var c = Math.cos(this.angle);
 
// 公转相关数据
var gs = Math.sin(globalTime * this.revolution); // globalTime是全局的时间
var gc = Math.cos(globalTime * this.revolution);
 
 
webgl.uniformMatrix4fv(
    this.program.uMMatrix, false, mat4.multiply([
            gc,0,-gs,0,
            0,1,0,0,
            gs,0,gc,0,
            0,0,0,1
        ], mat4.multiply(
            [
                1,0,0,0,
                0,1,0,0,
                0,0,1,0,
                this.x,this.y,this.z,1 // x,y,z是偏移的位置
            ],[
                c,0,-s,0,
                0,1,0,0,
                s,0,c,0,
                0,0,0,1
            ]
        )
    )
);

一眼望去uMMatrix(模型矩阵)里面有三个矩阵,为什么有三个呢,它们的顺序有什么要求么?
因为矩阵不满足交换率,所以我们矩阵的平移和旋转的顺序十分重要,先平移再旋转和先旋转再平移有如下的差异
(下面图片来源于网络)
先旋转后平移:图片 14
先平移后旋转:图片 15
从图中明显看出来先旋转后平移是自转,而先平移后旋转是公转
所以我们矩阵的顺序一定是 公转 * 平移 * 自转 * 顶点信息(右乘)
具体矩阵为何这样写可见上一篇矩阵入门文章
这样一个3D文字的8大行星就形成啦

browserInfo:function(type) {    switch (type)
{case’android’:returnnavigator.userAgent.toLowerCase().indexOf(‘android’)
!==
-1case’iphone’:returnnavigator.userAgent.toLowerCase().indexOf(‘iphone’)
!== -1case’ipad’:returnnavigator.userAgent.toLowerCase().indexOf(‘ipad’)
!==
-1case’weixin’:returnnavigator.userAgent.toLowerCase().indexOf(‘micromessenger’)
!== -1        default:returnnavigator.userAgent.toLowerCase()    }}

简单分析一下这个obj文件

图片 16
前两行看到#符号就知道这个是注释了,该obj文件是用blender导出的。Blender是一款很好用的建模软件,最主要的它是免费的!

图片 17
Mtllib(material library)指的是该obj文件所使用的材质库文件(.mtl)
单纯的obj生成的模型是白模的,它只含有纹理坐标的信息,但没有贴图,有纹理坐标也没用

图片 18
V 顶点vertex
Vt 贴图坐标点
Vn 顶点法线

图片 19
Usemtl 使用材质库文件中具体哪一个材质

图片 20
F是面,后面分别对应 顶点索引 / 纹理坐标索引 / 法线索引

这里大部分也都是我们非常常用的属性了,还有一些其他的,这里就不多说,可以google搜一下,很多介绍很详细的文章。
如果有了obj文件,那我们的工作也就是将obj文件导入,然后读取内容并且按行解析就可以了。
先放出最后的结果,一个模拟银河系的3D文字效果。
在线地址查看:

在这里顺便说一下,2D文字是可以通过分析获得3D文字模型数据的,将文字写到canvas上之后读取像素,获取路径。我们这里没有采用该方法,因为虽然这样理论上任何2D文字都能转3D,还能做出类似input输入文字,3D展示的效果。但是本文是教大家快速搭建一个小世界,所以我们还是采用blender去建模。

/* 封装ajax函数 * @param {string}obj.type
http连接的方式,包括POST和GET两种方式 * @param {string}obj.url
发送请求的url * @param {boolean}obj.async
是否为异步请求,true为异步的,false为同步的 * @param {object}obj.data
发送的参数,格式为对象类型 * @param {function}obj.success
ajax发送并接收成功调用的回调函数 * @param {function}obj.error
ajax发送失败或者接收失败调用的回调函数 */// 
ecDo.ajax({//type:’get’,//      url:’xxx’,//      data:{//         
id:’111’//      },//      success:function(res){//         
console.log(res)//      }//  })ajax:function(obj) {    obj = obj || {}; 
  obj.type = obj.type.toUpperCase() ||’POST’;    obj.url = obj.url
||”;    obj.async = obj.async ||true;    obj.data = obj.data || null; 
  obj.success = obj.success ||function() {        };    obj.error =
obj.error ||function() {        };    var xmlHttp =
null;if(XMLHttpRequest) {        xmlHttp = new XMLHttpRequest();   
}else{        xmlHttp = new ActiveXObject(‘Microsoft.XMLHTTP’);    }   
var params = [];for(var keyinobj.data) {        params.push(key +’=’+
obj.data[key]);    }    var postData =
params.join(‘&’);if(obj.type.toUpperCase() ===’POST’) {       
xmlHttp.open(obj.type, obj.url, obj.async);       
xmlHttp.setRequestHeader(‘Content-Type’,’application/x-www-form-urlencoded;charset=utf-8′); 
      xmlHttp.send(postData);    }elseif(obj.type.toUpperCase()
===’GET’) {        xmlHttp.open(obj.type, obj.url +’?’+ postData,
obj.async);        xmlHttp.send(null);    }   
xmlHttp.onreadystatechange =function() {if(xmlHttp.readyState == 4 &&
xmlHttp.status == 200) {            obj.success(xmlHttp.responseText); 
      }else{            obj.error(xmlHttp.responseText);        }    };}

4、装饰星星

光秃秃的几个文字肯定不够,所以我们还需要一点点缀,就用几个点当作星星,非常简单
注意默认渲染webgl.POINTS是方形的,所以我们得在fragment
shader中加工处理一下

JavaScript

precision highp float;   void main() {     float dist =
distance(gl_PointCoord, vec2(0.5, 0.5)); // 计算距离     if(dist <
0.5) {         gl_FragColor = vec4(0.9, 0.9, 0.8, pow((1.0 – dist *
2.0), 3.0));     } else {         discard; // 丢弃     } }

1
2
3
4
5
6
7
8
9
10
precision highp float;
 
void main() {
    float dist = distance(gl_PointCoord, vec2(0.5, 0.5)); // 计算距离
    if(dist < 0.5) {
        gl_FragColor = vec4(0.9, 0.9, 0.8, pow((1.0 – dist * 2.0), 3.0));
    } else {
        discard; // 丢弃
    }
}

4-5获取兄弟节点

2、读取分析obj文件

JavaScript

var regex = { // 这里正则只去匹配了我们obj文件中用到数据
    vertex_pattern:
/^vs+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/,
// 顶点     normal_pattern:
/^vns+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/,
// 法线     uv_pattern:
/^vts+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/, //
纹理坐标     face_vertex_uv_normal:
/^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/,
// 面信息     material_library_pattern:
/^mtllibs+([d|w|.]+)/, // 依赖哪一个mtl文件
    material_use_pattern: /^usemtls+([S]+)/ };   function
loadFile(src, cb) {     var xhr = new XMLHttpRequest();  
    xhr.open(‘get’, src, false);       xhr.onreadystatechange =
function() {         if(xhr.readyState === 4) {  
            cb(xhr.responseText);         }     };       xhr.send(); }  
function handleLine(str) {     var result = [];     result =
str.split(‘n’);       for(var i = 0; i < result.length; i++) {
        if(/^#/.test(result[i]) || !result[i]) { // 注释部分过滤掉
            result.splice(i, 1);               i–;         }     }  
    return result; }   function handleWord(str, obj) {     var firstChar
= str.charAt(0);     var secondChar;     var result;       if(firstChar
=== ‘v’) {           secondChar = str.charAt(1);           if(secondChar
=== ‘ ‘ && (result = regex.vertex_pattern.exec(str)) !== null) {
            obj.position.push(+result[1], +result[2], +result[3]);
// 加入到3D对象顶点数组         } else if(secondChar === ‘n’ && (result
= regex.normal_pattern.exec(str)) !== null) {
            obj.normalArr.push(+result[1], +result[2],
+result[3]); // 加入到3D对象法线数组         } else if(secondChar ===
‘t’ && (result = regex.uv_pattern.exec(str)) !== null) {
            obj.uvArr.push(+result[1], +result[2]); //
加入到3D对象纹理坐标数组         }       } else if(firstChar === ‘f’) {
        if((result = regex.face_vertex_uv_normal.exec(str)) !== null)
{             obj.addFace(result); // 将顶点、发现、纹理坐标数组变成面
        }     } else if((result =
regex.material_library_pattern.exec(str)) !== null) {
        obj.loadMtl(result[1]); // 加载mtl文件     } else if((result =
regex.material_use_pattern.exec(str)) !== null) {
        obj.loadImg(result[1]); // 加载图片     } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var regex = { // 这里正则只去匹配了我们obj文件中用到数据
    vertex_pattern: /^vs+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/, // 顶点
    normal_pattern: /^vns+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/, // 法线
    uv_pattern: /^vts+([d|.|+|-|e|E]+)s+([d|.|+|-|e|E]+)/, // 纹理坐标
    face_vertex_uv_normal: /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/, // 面信息
    material_library_pattern: /^mtllibs+([d|w|.]+)/, // 依赖哪一个mtl文件
    material_use_pattern: /^usemtls+([S]+)/
};
 
function loadFile(src, cb) {
    var xhr = new XMLHttpRequest();
 
    xhr.open(‘get’, src, false);
 
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4) {
 
            cb(xhr.responseText);
        }
    };
 
    xhr.send();
}
 
function handleLine(str) {
    var result = [];
    result = str.split(‘n’);
 
    for(var i = 0; i < result.length; i++) {
        if(/^#/.test(result[i]) || !result[i]) { // 注释部分过滤掉
            result.splice(i, 1);
 
            i–;
        }
    }
 
    return result;
}
 
function handleWord(str, obj) {
    var firstChar = str.charAt(0);
    var secondChar;
    var result;
 
    if(firstChar === ‘v’) {
 
        secondChar = str.charAt(1);
 
        if(secondChar === ‘ ‘ && (result = regex.vertex_pattern.exec(str)) !== null) {
            obj.position.push(+result[1], +result[2], +result[3]); // 加入到3D对象顶点数组
        } else if(secondChar === ‘n’ && (result = regex.normal_pattern.exec(str)) !== null) {
            obj.normalArr.push(+result[1], +result[2], +result[3]); // 加入到3D对象法线数组
        } else if(secondChar === ‘t’ && (result = regex.uv_pattern.exec(str)) !== null) {
            obj.uvArr.push(+result[1], +result[2]); // 加入到3D对象纹理坐标数组
        }
 
    } else if(firstChar === ‘f’) {
        if((result = regex.face_vertex_uv_normal.exec(str)) !== null) {
            obj.addFace(result); // 将顶点、发现、纹理坐标数组变成面
        }
    } else if((result = regex.material_library_pattern.exec(str)) !== null) {
        obj.loadMtl(result[1]); // 加载mtl文件
    } else if((result = regex.material_use_pattern.exec(str)) !== null) {
        obj.loadImg(result[1]); // 加载图片
    }
}

代码核心的地方都进行了注释,注意这里的正则只去匹配我们obj文件中含有的字段,其他信息没有去匹配,如果有对obj文件所有可能含有的信息完成匹配的同学可以去看下Threejs中objLoad部分源码

            //是否有哪些特殊符号需要保留

结语

需要关注的是这里我用了另外一对shader,此时就涉及到了关于是用多个program
shader还是在同一个shader中使用if
statements,这两者性能如何,有什么区别
这里将放在下一篇webgl相关优化中去说

本文就到这里啦,有问题和建议的小伙伴欢迎留言一起讨论~!

1 赞 收藏
评论

图片 13

1.下面代码,我放的是es5版本的,如果大家需要看es6版本的,请移步ec-do2.0.0.js

1、首先建模生成obj文件

这里我们使用blender生成文字
图片 22

            else {

为什么说webgl生成物体麻烦

我们先稍微对比下基本图形的创建代码
矩形:
canvas2D

JavaScript

ctx1.rect(50, 50, 100, 100); ctx1.fill();

1
2
ctx1.rect(50, 50, 100, 100);
ctx1.fill();

webgl(shader和webgl环境代码忽略)

JavaScript

var aPo = [     -0.5, -0.5, 0,     0.5, -0.5, 0,     0.5, 0.5, 0,
    -0.5, 0.5, 0 ];   var aIndex = [0, 1, 2, 0, 2, 3];  
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo),
webgl.STATIC_DRAW); webgl.vertexAttribPointer(aPosition, 3,
webgl.FLOAT, false, 0, 0);   webgl.vertexAttrib3f(aColor, 0, 0, 0);  
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex),
webgl.STATIC_DRAW);   webgl.drawElements(webgl.TRIANGLES, 6,
webgl.UNSIGNED_SHORT, 0);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var aPo = [
    -0.5, -0.5, 0,
    0.5, -0.5, 0,
    0.5, 0.5, 0,
    -0.5, 0.5, 0
];
 
var aIndex = [0, 1, 2, 0, 2, 3];
 
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
 
webgl.vertexAttrib3f(aColor, 0, 0, 0);
 
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
 
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);

完整代码地址:
结果:
图片 23

圆:
canvas2D

JavaScript

ctx1.arc(100, 100, 50, 0, Math.PI * 2, false); ctx1.fill();

1
2
ctx1.arc(100, 100, 50, 0, Math.PI * 2, false);
ctx1.fill();

webgl

JavaScript

var angle; var x, y; var aPo = [0, 0, 0]; var aIndex = []; var s =
1; for(var i = 1; i <= 36; i++) {     angle = Math.PI * 2 * (i /
36);     x = Math.cos(angle) * 0.5;     y = Math.sin(angle) * 0.5;  
    aPo.push(x, y, 0);       aIndex.push(0, s, s+1);       s++; }  
aIndex[aIndex.length – 1] = 1; // hack一下  
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo),
webgl.STATIC_DRAW); webgl.vertexAttribPointer(aPosition, 3,
webgl.FLOAT, false, 0, 0);   webgl.vertexAttrib3f(aColor, 0, 0, 0);  
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex),
webgl.STATIC_DRAW);   webgl.drawElements(webgl.TRIANGLES,
aIndex.length, webgl.UNSIGNED_SHORT, 0);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var angle;
var x, y;
var aPo = [0, 0, 0];
var aIndex = [];
var s = 1;
for(var i = 1; i <= 36; i++) {
    angle = Math.PI * 2 * (i / 36);
    x = Math.cos(angle) * 0.5;
    y = Math.sin(angle) * 0.5;
 
    aPo.push(x, y, 0);
 
    aIndex.push(0, s, s+1);
 
    s++;
}
 
aIndex[aIndex.length – 1] = 1; // hack一下
 
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
 
webgl.vertexAttrib3f(aColor, 0, 0, 0);
 
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
 
webgl.drawElements(webgl.TRIANGLES, aIndex.length, webgl.UNSIGNED_SHORT, 0);

完整代码地址:
结果:
图片 24

总结:我们抛开shader中的代码和webgl初始化环境的代码,发现webgl比canvas2D就是麻烦很多啊。光是两种基本图形就多了这么多行代码,抓其根本多的原因就是因为我们需要顶点信息。简单如矩形我们可以直接写出它的顶点,但是复杂一点的圆,我们还得用数学方式去生成,明显阻碍了人类文明的进步。
相比较数学方式生成,如果我们能直接获得顶点信息那应该是最好的,有没有快捷的方式获取顶点信息呢?
有,使用建模软件生成obj文件。

Obj文件简单来说就是包含一个3D模型信息的文件,这里信息包含:顶点、纹理、法线以及该3D模型中纹理所使用的贴图
下面这个是一个obj文件的地址:

>need-to-insert-img

教你用webgl快速创建一个小世界

2017/03/25 · HTML5 ·
AlloyTeam

原文出处:
AlloyTeam   

Webgl的魅力在于可以创造一个自己的3D世界,但相比较canvas2D来说,除了物体的移动旋转变换完全依赖矩阵增加了复杂度,就连生成一个物体都变得很复杂。

什么?!为什么不用Threejs?Threejs等库确实可以很大程度的提高开发效率,而且各方面封装的非常棒,但是不推荐初学者直接依赖Threejs,最好是把webgl各方面都学会,再去拥抱Three等相关库。

上篇矩阵入门中介绍了矩阵的基本知识,让大家了解到了基本的仿射变换矩阵,可以对物体进行移动旋转等变化,而这篇文章将教大家快速生成一个物体,并且结合变换矩阵在物体在你的世界里动起来。

注:本文适合稍微有点webgl基础的人同学,至少知道shader,知道如何画一个物体在webgl画布中

2-5替换* 

5-3现金额大写转换函数

3-1数组去重

//result:[“test1”, “test2”, “aaa”] 
//数组元素的值全等于’test’才被删除

//这一块的封装,主要是针对数字类型的数组//求和sumArr:function(arr)
{returnarr.reduce(function(pre, cur) {returnpre + cur   
})}//数组平均值,小数点可能会有很多位,这里不做处理,处理了使用就不灵活!covArr:function(arr)
{returnthis.sumArr(arr) / arr.length;},

//删除值为’val’的数组元素//ecDo.removeArrayForValue([‘test’,’test1′,’test2′,’test’,’aaa’],’test’,’)

                break;

                _regText += ‘]’

我自己封装这个,并不是我有造轮子的习惯,而是:

//ecDo.longestWord(‘Find the Longest word in a
String’)//result:7//ecDo.longestWord(‘Find|the|Longest|word|in|a|String’,’|’)//result:7longestWord:function(str,
splitType) {    var _splitType = splitType || /s+/g,        _max =
0,_item=”;    var strArr = str.split(_splitType);   
strArr.forEach(function(item) {if(_max < item.length) {           
_max = item.length            _item=item;        }   
})return{el:_item,max:_max};}

….//data-src储存src的数据,到需要加载的时候把data-src的值赋值给src属性,图片就会加载。//详细可以查看testLoadImg.html//window.onload
=function() {//    loadImg(‘load-img’,100);//    window.onscroll
=function() {//        ecDo.loadImg(‘load-img’,100);//       
}//}loadImg:function(className, num, errorUrl) {    var _className =
className ||’ec-load-img’, _num = num || 0, _this =
this,_errorUrl=errorUrl||null;    var oImgLoad =
document.getElementsByClassName(_className);for(var i = 0, len =
oImgLoad.length; i < len; i++) {       
//如果图片已经滚动到指定的高度if(document.documentElement.clientHeight +
document.documentElement.scrollTop > oImgLoad[i].offsetTop – _num
&& !oImgLoad[i].isLoad) {            //记录图片是否已经加载           
oImgLoad[i].isLoad =true;           
//设置过渡,当图片下来的时候有一个图片透明度变化           
oImgLoad[i].style.cssText =”transition: ”; opacity:
0;”if(oImgLoad[i].dataset) {               
this.aftLoadImg(oImgLoad[i], oImgLoad[i].dataset.src,
_errorUrl,function(o) {                   
//添加定时器,确保图片已经加载完了,再把图片指定的的class,清掉,避免重复编辑setTimeout(function()
{if(o.isLoad) {                            _this.removeClass(o,
_className);                            o.style.cssText =””;           
            }                    }, 1000)                });           
}else{                this.aftLoadImg(oImgLoad[i],
oImgLoad[i].getAttribute(“data-src”), _errorUrl,function(o) {       
           
//添加定时器,确保图片已经加载完了,再把图片指定的的class,清掉,避免重复编辑setTimeout(function()
{if(o.isLoad) {                            _this.removeClass(o,
_className);                            o.style.cssText =””;           
            }                    }, 1000)                });           
}            (function(i) {setTimeout(function() {                   
oImgLoad[i].style.cssText =”transition:all 1s; opacity: 1;”;         
      }, 16)            })(i);        }    }}

源码都放在github上了,大家想以后以后有什么修改或者增加的,欢迎大家来star一下ec-do。

String.prototype.trim=function(type){    switch
(type){case1:returnthis.replace(/s+/g,””);case2:returnthis.replace(/(^s*)|(s*$)/g,””);case3:returnthis.replace(/(^s*)/g,””);case4:returnthis.replace(/(s*$)/g,””); 
      default:returnthis;    }}//’  12345 6 8 96 
‘.trim(1)//”123456896″//比这样trim(‘  12345 6 8 96 
‘,1)调用方便。//但是,这样是不推荐的做法,这样就污染了原生对象String,别人创建的String也会被污染,造成不必要的开销。//更可怕的是,万一自己命名的跟原生的方法重名了,就被覆盖原来的方法了//String.prototype.substr=function(){console.log(‘asdasd’)} 
//’asdasdwe46546′.substr()//asdasd
//substr方法有什么作用,大家应该知道,不知道的可以去w3c看下

            case ‘chinese’:

2-8随机码(toString详解)

//ecDo.removeArrayForValue([‘test’,’test1′,’test2′,’test’,’aaa’],’test’)

5-6随进产生颜色

比如下面的栗子

5-7Date日期时间部分

//cookie//设置cookiesetCookie:function(name, value, iDay) {    var oDate
= new Date();    oDate.setDate(oDate.getDate() + iDay);   
document.cookie = name +’=’+ value +’;expires=’+
oDate;},//获取cookiegetCookie:function(name) {    var arr =
document.cookie.split(‘; ‘);for(var i = 0; i < arr.length; i++) {   
    var arr2 = arr[i].split(‘=’);if(arr2[0] == name)
{returnarr2[1];        }   
}return”;},//删除cookieremoveCookie:function(name) {   
this.setCookie(name, 1, -1);},

                break;

//var
arr=[{a:1,b:2,c:9},{a:2,b:3,c:5},{a:5,b:9},{a:4,b:2,c:5},{a:4,b:5,c:7}]//ecDo.arraySort(arr,’a,b’)a是第一排序条件,b是第二排序条件//result:[{“a”:1,”b”:2,”c”:9},{“a”:2,”b”:3,”c”:5},{“a”:4,”b”:2,”c”:5},{“a”:4,”b”:5,”c”:7},{“a”:5,”b”:9}]arraySort:function(arr,
sortText) {if(!sortText) {returnarr    }    var _sortText =
sortText.split(‘,’).reverse(), _arr = arr.slice(0);for(var i = 0, len =
_sortText.length; i < len; i++) {        _arr.sort(function(n1, n2)
{returnn1[_sortText[i]] – n2[_sortText[i]]        })   
}return_arr;}

2-2字母大小写切换

2-3字符串循环复制

            }

大家在开发的时候应该知道,有很多常见的实例操作。比如数组去重,关键词高亮,打乱数组等。这些操作,代码一般不会很多,实现的逻辑也不会很难,下面的代码,我解释就不解释太多了,打上注释,相信大家就会懂了。但是,用的地方会比较,如果项目有哪个地方需要用,如果重复写的话,就是代码沉余,开发效率也不用,复用基本就是复制粘贴!这样是一个很不好的习惯,大家可以考虑一下把一些常见的操作封装成函数,调用的时候,直接调用就好!

4-3删除类名

}

                _str =
_str.replace(/[^u4e00-u9fa5|u0000-u00ff|u3002|uFF1F|uFF01|uff0c|u3001|uff1b|uff1a|u3008-u300f|u2018|u2019|u201c|u201d|uff08|uff09|u2014|u2026|u2013|uff0e]/g,
_restr);

这个适配的方法很多,我就写我自己用的方法。大家也可以去我回答过得一个问题那里看更详细的说明!移动端适配问题

replaceClass:function(obj, newName, oldName) {    this.removeClass(obj,
oldName);    this.addClass(obj, newName);}

4-2 添加类名

这篇文章,写了很久了,几个小时了,因为我写这篇文章,我也是重新改我以前代码的,因为我以前写的代码,功能一样,代码比较多,现在是边想边改边写,还要自己测试(之前的代码for循环很多,现在有很多简洁的写法代替)。加上最近公司比较忙,所以这一篇文章也是花了几天才整理完成。

2.字符串操作

var ecDo={    trim:function(){..},    changeCase:function(){..}…}

            case ‘word’:

3-7返回数组(字符串)出现最多的几次元素和出现次数 ###

2-6检测字符串

3-3数组最大值最小值

//设置url参数//ecDo.setUrlPrmt({‘a’:1,’b’:2})//result:a=1&b=2setUrlPrmt:function(obj)
{    var _rs = [];for(var pinobj) {if(obj[p] != null && obj[p]
!=”) {            _rs.push(p +’=’+ obj[p])        }   
}return_rs.join(‘&’);},//获取url参数//ecDo.getUrlPrmt(‘test.com/write?draftId=122000011938’)//result:Object{draftId:”122000011938″}getUrlPrmt:function(url)
{    url = url ? url : window.location.href;    var _pa =
url.substring(url.indexOf(‘?’) + 1),        _arrS = _pa.split(‘&’),   
    _rs = {};for(var i = 0, _len = _arrS.length; i < _len; i++)
{        var pos = _arrS[i].indexOf(‘=’);if(pos == -1) {continue;   
    }        var name = _arrS[i].substring(0, pos),            value
= window.decodeURIComponent(_arrS[i].substring(pos + 1));       
_rs[name] = value;    }return_rs;}

编写自己的代码库(javascript常用实例的实现与封装)

4.基础DOM操作

            }

                    else {

2,因为零散的小实例,涉及到的有字符串,数组,对象等类型,就算找到插件,在项目引入的很有可能不止一个插件。

//var
strTest=’sad44654blog5a1sd67as9dablog4s5d16zxc4sdweasjkblogwqepaskdkblogahseiuadbhjcibloguyeajzxkcabloguyiwezxc967’//ecDo.countStr(strTest,’blog’)//result:6countStr:function(str,
strSplit) {returnstr.split(strSplit).length – 1}

                    }

源码都放在github上了,大家想以后以后有什么修改或者增加的,欢迎大家来star一下ec-do。

                }

2-9查找字符串

removeRepeatArray:function(arr) {returnarr.filter(function(item, index,
self) {returnself.indexOf(item) === index;    });}

//

                        _regText += _spstr[j];

4-1检测对象是否有哪个类名

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website