>

教你用webgl火速创造一个小世界

- 编辑:至尊游戏网站 -

教你用webgl火速创造一个小世界

webgl世界 matrix入门

2017/01/18 · HTML5 · matrix, WebGL

原文出处: AlloyTeam   

这次没有带来游戏啦,本来还是打算用一个小游戏来介绍阴影,但是发现阴影这块想完完整整介绍一次太大了,涉及到很多,再加上业务时间的紧张,所以就暂时放弃了游戏,先好好介绍一遍webgl中的Matrix。

这篇文章算是webgl的基础知识,因为如果想不走马观花的说阴影的话,需要打牢一定的基础,文章中我尽力把知识点讲的更易懂。内容偏向刚上手webgl的同学,至少知道着色器是什么,webgl中drawElements这样的API会使用~

文章的标题是Matrix is magic,矩阵对于3D世界来说确实是魔法一般的存在,说到webgl中的矩阵,PMatrix/VMatrix/MMatrix这三个大家相信不会陌生,那就正文let’s go~

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

2017/03/25 · HTML5 · AlloyTeam

原文出处: AlloyTeam   

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

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

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

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

1/ 矩阵的来源

刚刚有说到PMatrix/VMatrix/MMatrix这三个词,他们中的Matrix就是矩阵的意思,矩阵是干什么的?用来改变顶点位置信息的,先牢记这句话,然后我们先从canvas2D入手相信一下我们有一个100*100的canvas画布,然后画一个矩形

XHTML

<canvas width="100" height="100"></canvas> ctx.rect(40, 40, 20, 20); ctx.fill();

1
2
3
<canvas width="100" height="100"></canvas>
ctx.rect(40, 40, 20, 20);
ctx.fill();

代码很简单,在画布中间画了一个矩形

现在我们希望将圆向左移动10px

JavaScript

ctx.rect(30, 40, 20, 20); ctx.fill();

1
2
ctx.rect(30, 40, 20, 20);
ctx.fill();

结果如下:

源码地址:
结果展示:图片 1

 

改变rect方法第一个参数就可以了,很简单,因为rect()对应的就是一个矩形,是一个对象,canvas2D是对象级别的画布操作,而webgl不是,webgl是片元级别的操作,我们操作的是顶点
用webgl如何画一个矩形?地址如下,可以直接查看源码

源码地址:
结果展示:

图片 2

这里我们可以看到position这个数组,这里面存的就是矩形4个点的顶点信息,我们可以通过操作改变其中点的值来改变位置(页面源码也可以看到实现),但是扪心自问这样不累吗?有没有可以一次性改变某个物体所有顶点的方式呢?
有,那就是矩阵,magic is coming

1  0  0  0
0  1  0  0
0  0  1  0
0  0  0  1

上面这个是一个单位矩阵(矩阵最基础的知识这里就不说了),我们用这个乘一个顶点(2,1,0)来看看
图片 3

并没有什么变化啊!那我们换一个矩阵来看

1  0  0  1
0  1  0  0
0  0  1  0
0  0  0  1

再乘之前那个顶点,发现顶点的x已经变化了!
图片 4

如果你再多用几个顶点试一下就会发现,无论我们用哪个顶点,都会得到这样的一个x坐标+1这样一个结果
来,回忆一下我们之前的目的,现在是不是有了一种一次性改变顶点位置的方式呢?

 

2/ 矩阵规律介绍
刚刚我们改变了矩阵16个值中的一个,就使得矩阵有改变顶点的能力,我们能否总结一下矩阵各个值的规律呢?当然是可以的,如下图

图片 5
这里红色的x,y,z分别对应三个方向上的偏移

图片 6
这里蓝色的x,y,z分别对应三个方向上的缩放

然后是经典的围绕各个轴的旋转矩阵(记忆的时候注意围绕y轴旋转时,几个三角函数的符号……)
图片 7

还有剪切(skew)效果的变换矩阵,这里用个x轴的例子来体现
图片 8

这里都是某一种单一效果的变化矩阵,可以相乘配合使用的,很简单。我们这里重点来找一下规律,似乎所有的操作都是围绕着红框这一块来的
图片 9
其实也比较好理解,因为矩阵这里每一行对应了个坐标
图片 10

那么问题来了,最下面那行干啥用的?
一个顶点,坐标(x,y,z),这个是在笛卡尔坐标系中的表示,在3D世界中我们会将其转换为齐次坐标系,也就是变成了(x,y,z,w),这样的形式(之前那么多图中w=1)
矩阵的最后一行也就代表着齐次坐标,那么齐次坐标有啥作用?很多书上都会说齐次坐标可以区分一个坐标是点还是向量,点的话齐次项是1,向量的话齐次项是0(所以之前图中w=1)
对于webgl中的Matrix来说齐次项有什么用处呢?或者说这个第四行改变了有什么好处呢?一句话概括(敲黑板,划重点)
它可以让物体有透视的效果
举个例子,大名鼎鼎的透视矩阵,如图
图片 11
在第四行的第三列就有值,而不像之前的是0;还有一个细节就是第四行的第四列是0,而不是之前的1

写到这里的时候我纠结了,要不要详细的把正视和透视投影矩阵推导写一下,但是考虑到篇幅,实在是不好放在这里了,否则这篇文章要太长了,因为后面还有内容
大部分3D程序开发者可能不是很关注透视矩阵(PMatrix),只是知道有这一回事,用上这个矩阵可以近大远小,然后代码上也就是glMatrix.setPerspective(……)一下就行了
所以决定后面单独再写一篇,专门说下正视透视矩阵的推导、矩阵的优化这些知识
这里就暂且打住,我们先只考虑红框部分的矩阵所带来的变化
图片 12

为什么说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);

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

圆:
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);

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

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

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

3/ webgl的坐标系

我们前面bb了那么多,可以总结一下就是“矩阵是用来改变顶点坐标位置的!”,可以这么理解对吧(不理解的再回去看下第二节里面的各种图)

那再看下文章开头说的PMatrix/VMatrix/MMatrix三个,这三个货都是矩阵啊,都是来改变顶点位置坐标的,再加上矩阵也是可以结合的啊,为什么这三个货要分开呢?

首先,这三个货分开说是为了方便理解,因为它们各司其职

MMatrix --->  模型矩阵(用于物体在世界中变化)
VMatrix --->  视图矩阵(用于世界中摄像机的变化)
PMatrix --->  透视矩阵

模型矩阵和视图矩阵具体的原理和关系我之前那篇射击小游戏文章里有说过,它们的改变的一般就是仿射变换,也就是平移、旋转之类的变化
这里稍微回忆一下原理,具体细节就不再说了
这两货一个是先旋转,后平移(MMatrix),另一个是先平移,后旋转(VMatrix)
但就这个小区别,让人感觉一个是物体本身在变化,一个是摄像机在变化

好啦,重点说下PMatrix。这里不是来推导出它如何有透视效果的,这里是讲它除了透视的另一大隐藏的功能
说到这里,先打一个断点,然后我们思考另一个问题

canvas2D中和webgl中画布的区别

它们在DOM中的宽高都是通过设置canvas标签上width和height属性来设置的,这很一致。但webgl中我们的坐标空间是-1 ~ 1

图片 15
(width=800,height=600中canvas2D中,矩形左顶点居中时,rect方法的前两个参数)

图片 16
(width=800,height=600中webgl中,矩形左顶点居中时,左顶点的坐标)

我们会发现x坐标小于-1或者大于1的的话就不会展示了(y同理),x和y很好理解,因为屏幕是2D的,画布是2D的,2D就只有x,y,也就是我们直观上所看到的东西
那z坐标靠什么来看到呢?

对比

首先至少有两个物体,它们的z坐标不同,这个z坐标会决定它们在屏幕上显示的位置(或者说覆盖)的情景,让我们试试看

JavaScript

var aPo = [ -0.2, -0.2, -0.5, 0.2, -0.2, -0.5, 0.2, 0.2, -0.5, -0.2, 0.2, -0.5 ]; 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, 1, 0, 0); webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer()); webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW); // 先画一个z轴是-0.5的矩形,颜色是红色 webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0); aPo = [ 0, -0.4, -0.8, 0.4, -0.4, -0.8, 0.4, 0, -0.8, 0, 0, -0.8 ]; 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, 1, 0); // 再画一个z轴是-0.8的矩形,颜色是绿色 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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var aPo = [
    -0.2, -0.2, -0.5,
    0.2, -0.2, -0.5,
    0.2, 0.2, -0.5,
    -0.2, 0.2, -0.5
];
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, 1, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
// 先画一个z轴是-0.5的矩形,颜色是红色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
aPo = [
    0, -0.4, -0.8,
    0.4, -0.4, -0.8,
    0.4, 0, -0.8,
    0, 0, -0.8
];
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, 1, 0);
// 再画一个z轴是-0.8的矩形,颜色是绿色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);

注意开启深度测试,否则就没戏啦
(不开启深度测试,计算机会无视顶点的z坐标信息,只关注drawElements(drawArrays)方法的调用顺序,最后画的一定是最上一层)

代码中A矩形(红色)的z值为-0.5, B矩形(绿色)的z值为-0.8,最终画布上谁会覆盖谁呢?
如果我问的是x=0.5和x=0.8之间,谁在左,谁在右,我相信每个人都确定知道,因为这太熟了,屏幕就是2D的,画布坐标x轴就是右大左小就是这样的嘛

那我们更深层的考虑下为什么x和y的位置没人怀疑,因为“左手坐标系”和“右手坐标系”中x,y轴是一样的,如图所示

图片 17

而左手坐标系和右手坐标系中的z轴正方向不同,一个是屏幕向内,一个是屏幕向外,所以可以认为
如果左手坐标系下,B矩形(z=-0.8)小于A矩形(z=-0.5),那么理应覆盖了A矩形,右手坐标系的话恰恰相反

事实胜于雄辩,我们所以运行一下代码

查看结果:

可以看到B矩形是覆盖了A矩形的,也就意味着webgl是左手坐标系

excuse me???所有文章说webgl都是右手坐标系啊,为什么这里居然是左手坐标系?

答案就是webgl中所说的右手坐标系,其实是一种规范,是希望开发者一起遵循的规范,但是webgl本身,是不在乎物体是左手坐标系还是右手坐标系的

可事实在眼前,webgl左手坐标系的证据大家也看到了,这是为什么?刚刚说的有点笼统,不应该是“webgl是左手坐标系”,而应该说“webgl的裁剪空间是按照左手坐标系来的”

裁剪空间词如其名,就是用来把超过坐标空间的东西切割掉(-1 ~ 1),其中裁剪空间的z坐标就是按照左手坐标系来的

代码中我们有操作这个裁剪空间吗?有!回到断点的位置!

就是PMatrix它除了达成透视效果的另一个能力!
其实无论是PMatrix(透视投影矩阵)还是OMatrix(正视投影矩阵),它们都会操作裁剪空间,其中有一步就是将左手坐标系给转换为右手坐标系

怎么转化的,来,我们用这个单位矩阵试一下

1  0  0  0
0  1  0  0
0  0  -1  0
0  0  0  1

只需要我们将z轴反转,就可以得到将裁剪空间由左手坐标系转变为右手坐标系了。用之前的矩形A和矩形B再试一次看看

地址:

果然如此!

这样我们就了解到了webgl世界中几个最为关键的Matrix了

简单分析一下这个obj文件

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

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

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

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

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

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

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

4/ 结语

至于具体的PMatrix和OMatrix是怎么来的,Matrix能否进行一些优化,我们下次再说~

有疑问和建议的欢迎留言一起讨论~

1 赞 1 收藏 评论

图片 23

具体实现

1、首先建模生成obj文件

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

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部分源码

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、旋转平移等变换

物体全部导入进去,剩下来的任务就是进行变换了,首先我们分析一下有哪些动画效果
因为我们模拟的是一个宇宙,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(模型矩阵)里面有三个矩阵,为什么有三个呢,它们的顺序有什么要求么?
因为矩阵不满足交换率,所以我们矩阵的平移和旋转的顺序十分重要,先平移再旋转和先旋转再平移有如下的差异
(下面图片来源于网络)
先旋转后平移:图片 25
先平移后旋转:图片 26
从图中明显看出来先旋转后平移是自转,而先平移后旋转是公转
所以我们矩阵的顺序一定是 公转 * 平移 * 自转 * 顶点信息(右乘)
具体矩阵为何这样写可见上一篇矩阵入门文章
这样一个3D文字的8大行星就形成啦

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; // 丢弃
    }
}

结语

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

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

1 赞 收藏 评论

图片 27

本文由IT-综合发布,转载请注明来源:教你用webgl火速创造一个小世界