从接触 WebGL,到入门 three.js,到各种修修改改,到活动上线,前后耗费了14个工作日。而后耗时3天又上了一个版本,整个十二月也就过去了。如果以后项目中没有用到这货估计也不会再接触了吧,安心搞自己乱七八糟的东西比较开心,写点东西纪念一下咯。

WebGL 是什么

WebGL 是一种 3D 绘图标准,允许把 JavaScript 和 OpenGL ES 2.0 结合在一起,为 Canvas 提供硬件3D加速渲染。一句话概括,通过 WebGL,可以在页端展示3D场景

three.js 是什么

three.js 是一个基于 Javascript 能够简化 WebGL 开发的库。通过 three.js 可以极大地提高开发效率,同时降低了开发门槛,就算不熟悉图形学也可以通过 three.js 愉快地玩耍了

如何愉快地玩耍

暂时忘记 WebGL 那些复杂的概念,基于 three.js,直接抛个简单的示例 jsbin

// 创建场景
scene = new THREE.Scene();
// 创建相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 1000;
// 创建模型
geometry = new THREE.BoxGeometry(200, 200, 200);
material = new THREE.MeshLambertMaterial({
color: 0x66ccff
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 创建灯光
light = new THREE.DirectionalLight(0xffffff, 0.8);
light.position.set(0, 0, 300);
scene.add(light);
// 创建渲染器
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x333333);
document.body.appendChild(renderer.domElement);

代码不多,可以看到的东西是创建了一个立方体,添加了光照,还有阴影。简单粗暴的文档如下,直接看文档方便一些

然后单独说一下模型,代码如下:

geometry = new THREE.BoxGeometry(200, 200, 200);
material = new THREE.MeshLambertMaterial({
color: 0x66ccff
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

geometry 即几何结构,包含了能够描述物体的三维模型的所有数据,具体到所有顶点坐标。这里使用的是 BoxGeometry,并提供了长宽高三个参数,three.js 会将其转换为顶点信息并提供给着色器,可在 geometry.vertices 看到

material 即材料,描述了物体的外表,具体到颜色、贴图、对光线的反射效果等。这里使用的是 MeshLambertMaterial,即兰伯特材质,表面能对光线产生均匀散射,继承自 Material,并提供了 color 参数。其他还可以提供纹理、透明度、叠加效果等参数,这里不具体巴拉

mesh 即网格模型,糅合了几何机构和材料在一起。将模型添加到已经创建的场景 scene 里面,摆好相机位置,渲染器 renderer 渲染场景,则可以在画布中看到添加的物体了。可以修改网格模型的坐标 position、旋转 rotation 等参数

以上,其他有什么想法可以直接到官网找示例看源码如何实现。官方文档有部分对象没有提到,谷歌就是了。

坐标系

一张图片就够了吧,右手坐标系

coordinates

踩过的坑

1. 移动端画面模糊,锯齿严重

移动端画面模糊,锯齿严重。跟着示例源码加了这样一句,发现没用。

renderer.setPixelRatio(window.devicePixelRatio);

继续查资料发现是 viewport 的问题。做这样的修改可以保证 Android 和 iOS 下显示清晰

<meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no" />

然后在锤子手机上又发现 viewport 出了问题,于是又做了这种奇怪的修改,保证了锤子手机能正确渲染。具体关于 viewport 之后再整理一下

<meta name="viewport" content="initial-scale=1,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no" />

2. 着色器控制

发现画面间歇性出现绘制异常,然后定位到自己写的着色器,有这部分代码

attribute float size;
attribute float idx; // 星星下标
uniform float timeline; // 时间轴,实现闪烁效果
...
void main() {
...
if(length(mvPosition.xyz) < 400.0) {
enlarge = (sin(timeline - idx) * 40.0);
} else {
enlarge = (sin(timeline - idx) * 10.0);
}
...
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = 1500.0 / length(mvPosition.xyz) * size + enlarge;
}

在计算中,enlarge 可能是负值,导致最终计算的 gl_PointSize 也可能是负值,然后导致了绘制的点直接呈现在相机前。加个判断修正就可以啦

if(enlarge < 0.1) {
enlarge = .0 - enlarge + 1.0;
}

3. 兼容问题

第一次尝试 WebGL,用身边有限的测试机子跑了一遍感觉兼容性出乎意料的好,为了兼容到旧的 Android 手机,这里使用 three.js r71 版本。可是在项目临近上线前发现兼容问题远比预想的严重。从线上返回的数据看,有接近三分之一的用户手机或浏览器因为不支持 WebGL 或者对其兼容有问题,而访问到降级页面。如果有打算将 WebGL 运用到项目里的话,需要在一开始关注如何平稳退化

压缩构建 three.js

想在移动端上面使用 three.js 的话,重新构建打包剔除不必要的代码是必须的。在 github 上的源码也提供了重新构建的方法,各个平台都考虑到了的样子,然后使用 node 构建的话主要参数如下:

// /utils/build/build.js
···
var parser = new argparse.ArgumentParser();
parser.addArgument( ['--include'], { action: 'append', required: true } );
parser.addArgument( ['--externs'], { action: 'append', defaultValue: ['./externs/common.js'] } );
parser.addArgument( ['--amd'], { action: 'storeTrue', defaultValue: false } );
parser.addArgument( ['--minify'], { action: 'storeTrue', defaultValue: false } );
parser.addArgument( ['--output'], { defaultValue: '../../build/three.js' } );
parser.addArgument( ['--sourcemaps'], { action: 'storeTrue', defaultValue: true } );
···

可以根据需要调整 utils/build/includes 中的文件列表,减少打包的文件,然后重新构建就可以了。common 中,可以剔除的文件大概有没用到的 lightloaders,还有 shaders 的部分文件,其他经常会报错,然后 extras 就直接保留使用到的功能就行了,根据项目实际需要吧

node build.js --include common --include extras --amd --minify

几何函数

简单的速度曲线变化用几何函数去处理路线就可以了,当然处理的结果有时会比较生硬,可以去 tween.js 扒一些东西下来。这里列几个简单的几何函数

EasingFunctions = {
linear: function (t) { return t },
easeInQuad: function (t) { return t*t },
easeOutQuad: function (t) { return t*(2-t) },
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
easeInCubic: function (t) { return t*t*t },
easeOutCubic: function (t) { return (--t)*t*t+1 },
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
easeInQuart: function (t) { return t*t*t*t },
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
easeInQuint: function (t) { return t*t*t*t*t },
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}

参考资料

学习过程中帮助很大的一些网站,大大的感谢~

http://webglfundamentals.org/
http://www.cnblogs.com/yiyezhai/category/446753.html
http://wiki.jikexueyuan.com/project/webgl/
https://oncemore2020.github.io/blog/homogeneous/
http://csgrandeur.github.io/
http://threejs.org/