前言
WebGL是一种3D绘图协议,这种绘图协议允许javascript和openGL结合起来,WebGL可以为canvas提供3d硬件加速(使用GPU在浏览器渲染3d模型和场景)。WebGL是openGL的一种实现,是可以在支持WebGL的浏览器中进行3d渲染。由于浏览器的跨平台性,可以使用WebGL在多种设备多个平台展示3d效果。那么我们可以用WebGL做什么呢?
数据可视化
AR VR
3d游戏动画等
在现代前端技术发展中,使用WebGL创建3d效果是一种不可或缺的能力。由于webGL本身API只能绘制最普通的点、线、三角形,所以为了绘制大型场景和模型,我们需要选用一些框架帮助开发,比如three.js。
Three.js是对WebGL的API抽象化和封装化的js库,不必关心WebGL怎么渲染3d图形,并且在渲染中加入了各种优化,提高了性能。Three.js包含数学库,支持交互,扩展性强,还可以进行SVG,CSS3D等渲染,在WebGL不兼容的版本可以有回退解决方案。下图是three.js对WebGL做的封装:
勋章系统是对用户粘性提升的一种积极刺激行为,分为样式展示(简单图片)--行为触发(达到某种条件)--颁发勋章(提供3d勋章)--勋章升级(不同等级勋章提供不同展示)--趣味性(移动端控制旋转特效)。在开发勋章系统中有几个重点:
技术选型 因为是在app中展示,要考虑安卓和ios的差异性,同时还需要在web端展示,所以最好的选择就是用前端的WebGL方式来做3d勋章系统,在app中通过webview引入该勋章网页。在框架选择中,three.js的兼容性功能性都是接受过业界业务考验的,我们选择了three.js当我们的勋章系统框架。
模型选择 既然是3d勋章,3d模型的选择会影响开发周期和渲染效果,调研了three.js支持的3d模型格式:
Three.JS支持格式 | 特点 | 优缺点 |
---|---|---|
JSON(*.js/ *.json) | 专门为Three.js自己设计的JSON格式,你可以使用它以声明的方式定义模型,及模型材质和动画。 | 在app中的webview解析json比较慢 |
OBJ和MTL(*.obj/ *.mtl) | OBJ是一种简单的三维文件格式,只用来定义对象的几何体。 | 对象不支持动画,还需要搭配mtl来载入材质 |
Collada (*.dae) | 用来定义XML类文件中数字内容的格式。差不多所有的三维软件和渲染引擎都支持这个格式。 | 使用最广,兼容性好 |
STL (*.stl) | 立体成型术 。 广泛用于快速成型。 | 生成速度快,但是一般是3d打印机使用 |
FBX (*.fbx) | 在max、maya、softimage等软件间进行模型、材质、动作和摄影机信息的互导,复用性比较好。 | 多平台支持,但是生成在部分平台会被转为mesh |
CTM (*.ctm) | 由openCTM创建的格式。可以用来压缩存储表示三维网格的三角形面片。 | 文件压缩效果好,压缩算法理解难度较高 |
VTK(*.vtk) | Visualization Tookit定义的文件格式,用来指定顶点和面。 | 可支持节点比较多,但threejs仅支持旧格式 |
PLY (*.ply) | 多边形文件格式。 | 3d打印机使用 |
加上ui组使用c4d软件能轻松导出dae文件,最终选择了以xml为基础的dae的3d模型。
Three.js是3d渲染,在3d渲染编程中包括:场景,相机,渲染器(物体,光源,纹理)。下图是three.js的运行过程:
要想使用three.js很简单,通过简单的js引用或者npm包下载即可。然后通过加载相应的解析模块,来载入个人模型或者控制器。
import 'three';
import 'three/examples/js/loaders/ColladaLoader2';
import 'three/examples/js/loaders/MTLLoader';
在threejs中初始化场景,相机,灯光:
function init() {
initRender()
initScene()
initCamera()
initLight()
}
导入模型,并加载到场景:
function loadModel() {
mesh = new THREE.Mesh()
let mtl = new THREE.MTLLoader()
let loader = new THREE.ColladaLoader()
mtl.load("../model/yemaozi.mtl", function (result) {
result.preload()
let { materials } = result
loader.load("../model/yemaozi.dae", function (dae) {
for (let key in dae.library.materials) {
let name = dae.library.materials[key].name
if (materials[name]) {
Object.assign(dae.library.materials[key].build, materials[name])
}
if (name === 'font') {
dae.library.materials[key].build.blending = THREE.NoBlending
dae.library.materials[key].build.needsUpdate = true
}
}
mesh = dae.scene.children[0].clone()
scene.add(mesh)
})
})
}
最后是最关键的渲染(包含动效):
// 动效
function animate() {
requestAnimationFrame(animate)
mesh.rotation.y +=0.01
render()
}
展示效果如下:
勋章系统优化
支持换肤功能
多加入一个包含材质和纹理信息的mtl文件,通过不同的mtl材质提供换肤功能 ,接口传递的数据为:
{
// 材质文件
image3DMaterial: "http://xxx.mtl",
// gzip压缩过的模型文件
image3DMoudle: "http://xxx.gz",
// 已获得图片
imageGot: "http://xxx.png",
// 未获得图片
imageNotget: "http://xxx.png"
}
生成模型文件较大,下载时间过长
出于体验优化,一般大文件传输需要gzip处理,目前使用的cdn服务暂不支持文件的gzip(后期会迁移到新cdn)。所以前端用nodejs写一个预压缩脚本,将dae和mtl文件压缩并上传到cdn,同时同步更新到服务端的数据库中;拿到数据后,解压则是通过webWorker多线程,使用pako.js库进行gzip解压。
下载解析开销很大,并且3d模型占用内存过多
目前产品需求是只有等级5的勋章,分两步优化:
// 尝试从临时缓存中读取模型
if (models[currentMedalInfo.id]) {
scene.add(models[currentMedalInfo.id]);
modelsList.splice(modelsList.findIndex((id) => currentMedalInfo.id === id), 1);
modelsList.push(currentMedalInfo.id);
resolve();
return;
}
let medalDataMap, medalDataCache, modelData, materialData;
if (indexedDBSupport) {
medalDataMap = await localforage.getItem('medalDataMap');
medalDataMap = medalDataMap || {};
medalDataCache = medalDataMap[currentMedalInfo.id];
if (medalDataCache) {
// 如果材质地址没变则直接从indexedDB读取,否则清理掉
if (medalDataCache.image3DMaterial === materialUrl) {
materialData = await localforage.getItem(materialUrl);
} else {
localforage.removeItem(medalDataCache.image3DMaterial);
}
// 模型同上
if (medalDataCache.image3DMoudle === modelUrl) {
modelData = await localforage.getItem(modelUrl);
} else {
localforage.removeItem(medalDataCache.image3DMoudle);
}
}
// 储存最新的信息
medalDataMap[currentMedalInfo.id] = currentMedalInfo;
localforage.setItem('medalDataMap', medalDataMap);
}
// 如果不支持或indexedDB中没有则加载
if (!materialData) {
materialData = loadResource({ url: materialUrl });
}
if (!modelData) {
modelData = loadResource({
url: modelUrl, type: isDae ? 'text' : 'arraybuffer', onProgress: (progress) =>{
progressCB(progress.loaded / progress.total * (isDae ? 100 : 80));
}
});
}
内存中只会存储最近5次的解析过的模型,当有新的模型要加入,通过LRU策略,对于最长时间没有使用的模型内存会释放。
数据会存在IndexedDB,每次请求先从内存读取,内存中不存在从IndexedDB读取,都不存在走网络请求,并且会把新数据通过key-value存入IndexedDB。
有些过老机型不支持WebGL,模型加载失败
在获取数据时候,会返回两张图片(没获得等级勋章图片和获得等级勋章图片),通过图片展示当做优雅降级处理方法。
用户体验
增加重力感应和粒子特效。
最终效果
5G时代的来临,让网络传输和资源加载速度更胜一筹,在3D效果的制作可以采用WebGL这种跨平台兼容性高的方法。选用Three.js来快速开发WebGL项目,能为用户体验和业务需求提供多样化的选择。
孙森 2018年加入网易传媒,高级前端开发工程师,目前主要做pc,移动端,小程序等版本迭代和升级优化工作,热爱足球和coding。