前言
最近学习了一下《WebGL 编程指南》这本书,为了检验自己的学习成果,模仿 GitHub Skyline 实现了一个乞丐版(代码)的:
对比原版:
显然,差距很大:
- 原版用的是点光源,乞丐版是平行光源
- 缺少 Github 的 Logo
- 缺少各种动画效果
- 原版的纹理等各方面明显更加好看 😂
抛开这些差距不谈,让我们来看看用原生 WebGL 是怎么实现这个东西的吧。
实现过程介绍
接下来会从三个方向来介绍实现的过程:
- 模型,即物体以及物体的变换效果。
- 光源,包括光源方向、颜色,以及物体如何反射光。
- 观察者,从哪个方向观察物体,使用什么投影观察物体。
模型
本文将模型分成了四大部分,即最下面的网格,往上的梯形体,梯形体上的长方体(表示每天的 contributions),梯形体上的文字。
网格
绘制网格是最简单的,绘制一批横竖相间的线条即可,不过这里为了方便,在横竖两个方向上分别只定义了一条线的数据,其他线都是通过修改模型的变换矩阵实现的(其他地方也都是采取的类似的方法):
1 | const points = new Float32Array([0, 0.0, 1.0, 0, 0.0, -1.0]) |
梯形体及长方体
梯形体以及长方体的绘制实际上可以统一为绘制一个六面体,需要先定义好每个面的顶点、顶点的法向量、顶点的颜色:
1 | v6----- v5 |
然后通过顶点索引的方式来绘制物体:
1 | // 每个面对应的顶点的索引 |
不过,长方体每个顶点的法向量可以心算得到,但是梯形的话还得通过向量叉乘计算得到:
1 | v1------v0 |
比如,上面这个梯形的法向量就可以通过向量 v0v1
叉乘 v0v3
得到。
每个长方体代表的 contributions 记录数据可以通过这个接口获取:https://skyline.github.com/{name}/{year}.json
。本来想着把这个工具发布到 Github 个人主页上去,但是由于这个接口没有设置 CORS 等响应头,所以只能通过代理在本地使用了。如果有大佬知道 Github 官方有提供类似的接口的,麻烦告知一下,小弟不胜感激!
文字
文字的绘制是最难的一环,而且书里面也没有提到。并且 WebGL 也没有提供类似 Canvas 2D 绘制文字的 fillText
和 strokeText
等 API。
既然这样,那只能面向 Google 编程了。搜索 webgl draw text
得到的第一条记录是 WebGLFundamentals 网站上的文章,看样子是一个非常靠谱的网站,而且效果都有了:
抄过来不就 OK 了么!
但是,当我仔细研究了一下代码后发现,人家这里绘制的文字不是那个 F
,而是 F
脚下那个黑色的数字。搞了半天,这里还是只介绍了如何绘制 2D 文字。
如果有个地方可以下载到所有字母的顶点以及法线数据就好了,这样我们就能跟绘制长方体一样把文字绘制出来了。
既然这样,那何不我们自己创造这个数据?刚好书里提到了 Blender 导出的模型数据的解析方法,而且还有现成的代码可以参考。那我们就用 Blender 这个工具来生成:
然后导出 .obj
格式的数据即可。
这样,最难的一部分也搞定了。
光照
根据书中的理论,物体(非镜面)反射的光颜色计算公式为:
1 | 反射光颜色=漫反射光颜色+环境反射光颜色 |
而漫反射光颜色
又等于:
1 | // θ 是入射光与法向量的夹角 |
而环境反射光颜色
等于:
1 | 环境反射光颜色=环境光颜色*表面基底色 |
代码表示如下:
1 | // 法向量归一化 |
观察者
本文采用透视投影:
如图所示,透视投影会形成一个梯形体,在这个里面的物体会投影到近裁剪面,然后将近裁剪面缩放到 1:1 的 WebGL 坐标系中得到的最后的图形就是我们看到的结果。我们可以通过如下方式来设置投影矩阵:
1 | // 参数分别是垂直视角、宽高比、近裁剪面距离,远裁剪面距离 |
这里要注意的是宽高比需要和 canvas 的保持一致,否则会出现图形变形的问题。比如原本预期的是正方形,投影后成了矩形。
研究 Github Skyline 发现,用户可以拖动鼠标来从不同的角度观察物理,所以我们单独定义了一个视图矩阵:
1 | const viewMatrix = new Matrix4() |
同时,我们还可以左右上下调整我们的视角,也就是对视图矩阵进行旋转:
1 | const angle = (Math.PI * this.rotateY) / 180 |
注意这里为了保持跟 Github Skyline 一样的交互效果,绕 Y 轴旋转后,需要计算出原来的 X 轴的方向后再绕其进行旋转。
最后每个顶点最终的坐标就由投影矩阵、视图矩阵、模型矩阵乘以顶点的原始坐标得到:
1 | gl_Position = uProjMatrix * uViewMatrix * uModelMatrix * aPosition; |
总结
实现完这么一个东西以后,对 WebGL 最深的体会就是,实在是太麻烦了。难怪会有那么多如 Three.js
,Babylon.js
等 3D 引擎库出现,否则开发一个 3D 产品出来的门槛未免也太高了。不过理解了 WebGL 的基础知识后对使用这些工具会提供更多的帮助,所以学习一下还是有必要的。
另外,《WebGL 编程指南》这本书写得非常不错,对新手很友好,强烈推荐。而且我发现竟然是我研究生同学(非同班)翻译的,原来我现在学的东西竟然是人家 8 年前就会的,差距真大,再次体会了那句话:“少壮不努力,老大徒伤悲”。