无插件纯 Web 3D机房,HTML5+WebGL倾力打造
作者 twaver 2015-07-08
前言
最近项目开发任务告一段落,刚好有时间整理这大半年的一些成果。使用 html5 时间还不久,对 js 的认识还不够深入。没办法,以前一直搞 java,对 js 的一些语言特性和概念一时还转换不过来。
这一回我想介绍一下项目中的一个亮点技术:html5 的 3D,以及如何用它打造精美的 3D 机房监控系统。
目标效果图
下图是客户给找的一张的效果参考图,希望机房至少能达到下面的 3D 效果。
懂的人都知道,这可是一张设计公司出的装修效果图啊,就算是用 max 建模,也需要大量的工作,何况咱可是程序员在做数据中心的可视化项目啊。强忍心中奔腾的万千头**马,静下心来思考,那就先从搭建一个 webGL 的场景开始吧。
WebGL 基本场景搭建
在 html5 里面使用 3D 已经不是什么高深技术,它的基础是 WebGL,一个 OpenGL 的浏览器子集,支持大部分主要 3D 功能接口。目前最新的浏览器都有比较好的支持,IE 需要到 11(是的,你没有看错)。
要检测你的浏览器是否支持 webGL,可直接访问网页 http://get.webgl.org/ 看是否能看到一个旋转的立方体。如果能看到,说明你的浏览器支持 webgGL,否则,可以下一个最新的 chrome 试试吧。相对来说 chrome 对 webGL 的支持最好,效率也很优秀。
要在浏览器里面使用 webGL,就要研究 webGL 相关的技术和用法。做 3D 应用并不是一件轻松的事。就算最简单的搭建一下 webGL 场景,也需要下面这些代码:
var width = window.innerWidth;
var height= window.innerHeight;
var container = document.createElement( 'div' );
document.body.appendChild( container );
var webglcanvas = document.createElement('canvas');
container.appendChild(webglcanvas);
var gl = webglcanvas.getContext("experimental-webgl");
function updateFrame () {
gl.viewport ( 0, 0, width, height );
gl.clearColor(0.4, 0.4, 0.7, 1);
gl.clear ( gl.COLOR_BUFFER_BIT );
setTimeout(
function(){
updateFrame()},
20);
}
setTimeout(
function(){
updateFrame();
},
20);
和html一样,需要先创建一个 canvas 元素,并获得其 webgl 上下文:
var gl = webglcanvas.getContext("experimental-webgl");
然后在一个 updateFrame
的函数中,像 html5 的 2D context 一样,去绘制 3D 的内容。
另外,要再起一个死循环,每隔 ** 毫秒调用一次这个 updateFrame
函数来重绘场景。和 2D 不同,3D 场景里面的变化是随时随地的,所以需要不停刷新,就像播放电影或视频,静止不动的画面基本没有,所以死循环刷新基本是必要的。不过实际项目使用中会有很多优化,尽量做到 “按需刷新”,节省 cpu 和移动设备电量。有感兴趣的同学,哥可以单独写文章介绍。这段程序基本上什么也没做,就画了一个静止不动的区域,如下图:
虽然看不见任何3D的内容,不过它已经是一个最简单的webgl程序了。我们的3D机房,也就是在这上面不断丰富而已。
对象封装
要做项目,搭建下去工作量太大了,时间周期也不允许。使用第三方辅助工具是不可避免的,像 Three.js, twaver.js 都是选择。这些工具都可以提供 3D 的基本对象和各种特效,当然这都不是最主要的,主要是如何利用它做出我想要的效果:好看。为了避免大量修改代码,在项目里做了一些封装,即把原始 3D 的立方体等对象进行进一步封装,让一个 json 数据就可以提供这些对象的定义。这样使用起来就比较方便了。json 大致结构如下:
var json={
objects: [{
name: '地板',
…
},{
…
}],
}
下面我们逐一来看这些 3D 对象是怎么进行美化的,过程可能稍显啰嗦,跬步千里,这次的基础打好了,以后的项目就手到擒来了。
地板和斜坡###
第一个要做的,也是应该比较简单的,就是地板对象。3D 中,地板应该是一个有些厚度、带上格子贴图的薄薄立方体平面。因此我对经过封装的立方体对象,用一段json对象定义如下:
{
name: '地板',
type: 'cube',
width: 1600,
height: 10,
depth: 1300,
style: {
'm.color': '#BEC9BE',
'm.ambient': '#BEC9BE',
}
}
通过定义,创建了一个13米*16米的地板块,这也是客户小型机房的实际尺寸:
看起来有那么点意思,就是颜色还不够,需要找一个地板砖纹理图。需要注意的是,纹理图的尺寸都需要是宽和高都是 2 的幂,例如 128x128、256*256 等,这样出来效果才会好。这也是 3D 软件一般所要求的。另外纹理要能连续拼接不露破绽,这样才好。例如下面我 google 出来的图:
在 style 里面添加:
'top.m.texture.image': 'images/floor.png',
'top.m.texture.repeat': new mono.Vec2(10,10),
效果如下:
有图片材质纹理,效果果然好多了。突然想到客户说,他们机房底面有一个方便运送设备的斜坡,必须要画出来。这……(╯-_-)╯
后来想到twaver里面的对象可以支持运算,比如可以定义一个斜的立方体,让地板剪掉立方体,就可以做到。于是继续定义json:
{
name: '地板切坡',
type: 'cube',
width: 200,
height: 20,
depth: 260,
translate: [-348,0,530],
rotate: [Math.PI/180*3, 0, 0],
op: '-',
style: {
…,
}
}
这里定义的一个倾斜的立方体,通过translate
定义位置,rotate
定义旋转角度,然后再通过op
定义运算符,这里是“减去”,就用“-”表示。被剪掉的立方体也可以设置材质、纹理、贴图、颜色…等等,和地板一样。看看效果:
第一步总算是有惊无险地搞定了。
走廊桌
下一步找了个简单的对象,按要求走廊要放一个接待桌。为了简单,我决定就偷懒做一个立方体表示。
{
name: '走廊板凳',
type: 'cube',
width: 300,
height: 50,
depth: 100,
translate: [350, 0, -500],
}
效果如下:
这里偷懒其实是有原因的。在3D里,最重视的就是效率,千万不要放一些很复杂的模型,尤其是这类非业务对象。就像这个桌子,尽管只是个简单的立方体,但只要和整体风格协调一致,再增加一点配色并启动阴影效果后,看着就好多了:
墙体
墙体是机房里很重要的一个部分,有好的光照、阴影的效果才能看起来更加逼真。由于墙体是不规则的路径,一段一段去生成还真挺麻烦的,还好引擎支持这种物体,甚至曲线路径都可以。这里只要在json里面定义一组数字的坐标,让这些数字依次连接,组成一个墙体,最后生成3D对象放入场景中就行啦。
json定义如下:
{
name: '主墙体',
type: 'path',
width: 20,
height: 200,
translate: [-500, 0, -500],
data:[
[0, 0],
[1000, 0],
[1000, 500],
[500, 500],
[500, 1000],
[0, 1000],
[0,0],
],
}
注意这里的类型变成了path
,data
中定义了一个二维坐标数组来描述墙体。由于墙都是从底面开始的,所以只定义它的平面的x、y坐标就行了。看看效果:
不过如前文所说,还是需要上色、上阴影,才能有更好的效果。这里我们启用阴影并咨询设计师美眉几个颜色值,加上去后再看下效果:
以及一些细节:
门
看着雪白的墙,是不是觉得少了点什么?对,就是门。在3D机房的监控系统里,门禁是很重要的一块,客户要求门应该与实际位置相对应,并且要有开门关门的动画效果。这样,实际的门禁信息采集上来后,就能在界面实时看到门的状态了。
这里,考虑到门如果直接放上去,会被墙盖住;如果比墙厚,又难看不符合实际。还是应该先定义一个门洞立方体,把门所在的位置挖掉:
{
name: '门洞',
type: 'cube',
width: 195,