用 webgl 渲染的 3d 机房现在也不是什么新鲜事儿了,这篇文章的主要目的是说明一下,3d 机房中的 eye 和 center 的问题,刚好在项目中用上了,好生思考了一番,最终觉得这个例子最符合我的要求,就拿来作为记录。
效果图
这个 3d 机房的 demo 做的还不错,比较美观,基础的交互也都满足,接下来看看怎么实现。
代码生成
定义类
首先从 index.html 中调用的 js 路径顺序一个一个打开对应的 js,server.js 中自定义了一个 editor.server 类由 ht 封装的 ht.default.def 函数创建的(注意,创建的类名 editor.server 前面的 editor 不能用 e 来替代):
ht.default.def('editor.server', object, {//第一个参数为类名,如果为字符串,自动注册到ht的classmap中;第二个参数为此类要继承的父类;第三个参数为方法和变量的声明
addtodatamodel: function(dm) { //将节点添加进数据容器
dm.add(this._node);// ht 中的预定义函数,将节点通过 add 方法添加进数据容器中
},
sethost: function() { //设置吸附
this._node.sethost.apply(this._node, arguments);
},
s3: function() {//设置节点的大小
this._node.s3.apply(this._node, arguments);
},
setelevation: function() {//控制node图元中心位置所在3d坐标系的y轴位置
this._node.setelevation.apply(this._node, arguments);
}
});
创建 editor.server 类
这个类可以创建一个 ht.node 节点,并设置节点的颜色和前面贴图:
var s = e.server = function(obj) {//服务器组件
var color = obj.color,
frontimg = obj.frontimg;
var node = this._node = new ht.node();//创建节点
node.s({//设置节点的样式 s 为 setstyle 的缩写
'all.color': color,//设置节点六面的颜色
'front.image': frontimg //设置节点正面的图片
});
};
这样我在需要创建服务器组件的位置直接 new 一个新的服务器组件对象即可,并且能够直接调用我们上面声明的 sethost 等函数,很快我们就会用上。
接下来创建 editor.cabinet 机柜类 ,方法跟上面 editor.server 类的定义方法差不多:
ht.default.def('editor.cabinet', object, {
addtodatamodel: function(dm) {
dm.add(this._door);
dm.add(this._node);
this._serverlist.foreach(function(s) {
s.addtodatamodel(dm);
});
},
p3: function() {
this._node.p3.apply(this._node, arguments);//设置节点的 3d 坐标
}
});
创建 editor.cabinet 类
这个类相对于前面的 editor.server 服务器组件类要相对复杂一点,这个类中创建了一个柜身、柜门以及机柜内部的服务器组件:
var c = e.cabinet = function(obj) {
var color = obj.color,
doorfrontimg = obj.doorfrontimg,
doorbackimg = obj.doorbackimg,
s3 = obj.s3;
var node = this._node = new ht.node(); // 柜身
node.s3(s3);//设置节点的大小 为 setsize3d
node.a('cabinet', this);//自定义 cabinet 属性
node.s({//设置节点的样式 为 setstyle
'all.color': color,//设置节点六面的颜色
'front.visible': false//设置节点前面是否可见
});
if (math.random() > 0.5) {
node.addstyleicon('alarm', {//向节点上添加 icon 图标
names: ['icon 温度计'],//包含多个字符串的数组,每个字符串对应一张图片或矢量(通过ht.default.setimage注册)
face: 'top',//默认值为front,图标在3d下的朝向,可取值left|right|top|bottom|front|back|center
position: 17,//指定icons的位置
autorotate: 'y',//默认值为false,图标在3d下是否自动朝向眼睛的方向
t3: [0, 16, 0],//默认值为undefined,图标在3d下的偏移,格式为[x,y,z]
width: 37,//指定每个icon的宽度,默认根据注册图片时的宽度
height: 32,//指定每个icon的高度,默认根据注册图片时的高度
texturescale: 4,//默认值为2,该值代表内存实际生成贴图的倍数,不宜设置过大否则影响性能
visible: { func: function() { return !!e.alarmvisible; }}//表示该组图片是否显示
});
}
var door = this._door = new ht.doorwindow();//柜门
door.setwidth(s3[0]);//置图元在3d拓扑中的x轴方向的长度
door.setheight(1);//设置图元在3d拓扑中的z轴长度
door.settall(s3[1]);//控制node图元在y轴的长度
door.setelevation(0);//设置图元中心在3d坐标系中的y坐标
door.sety(s3[2] * 0.5);//设置节点在 y 轴的位置
door.sethost(node);//设置吸附
door.s({//设置节点样式 setstyle
'all.color': color,//设置节点六面颜色
'front.image': doorfrontimg,//设置节点正面图片
'front.transparent': true,//设置节点正面是否透明
'back.image': doorbackimg,//设置节点背面的图片
'back.uv': [1,0, 1,1, 0,1, 0,0],//自定义节点后面uv贴图,为空采用默认值[0,0, 0,1, 1,1, 1,0]
'dw.axis': 'right'//设置doorwindow图元展开和关闭操作的旋转轴,可取值left|right|top|bottom|v|h
});
var serverlist = this._serverlist = [];
var max = 6,
list = e.randomlist(max, math.floor(math.random() * (max - 2)) + 2); //global.js 中声明的获取随机数的函数
var server, h = s3[0] / 4;
list.foreach(function(r) {
var server = new e.server({ //服务器组件
color: 'rgb(51,49,49)',
frontimg: '服务器 组件精细'
});
server.s3(s3[0] - 2, h, s3[2] - 4);//设置节点大小
server.setelevation((r - max * 0.5) * (h + 2));//设置节点中心点在 y 轴的坐标
server.sethost(node);//设置节点的吸附
serverlist.push(server);//向 serverlist 中添加 server 节点
});
};
上面代码中唯一没提到的是 editor.randomlist 函数,这个函数是在 global.js 文件中声明的,声明如下:
var e = window.editor = {
leftwidth: 0,
topheight: 40,
randomlist: function(max, size) {
var list = [], ran;
while (list.length < size) {
ran = math.floor(math.random() * max);
if (list.indexof(ran) >= 0)
continue;
list.push(ran);
}
return list;
}
};
好了,场景中的各个部分的类都创建完成,那我们就该将场景创建起来,然后将这些图元都堆进去!
场景创建
如果熟悉的同学应该知道,用 ht 创建一个 3d 场景只需要 new 一个 3d 组件,再将通过 addtodom 函数将这个场景添加进 body 中即可:
var g3d = e.main = new ht.graph3d.graph3dview(); //3d 场景
main.js 文件中主要做的是在 3d 场景中一些必要的元素,比如墙面,地板,门,空调以及所有的机柜的生成和排放位置,还有非常重要的交互部分。
墙体,地板,门,空调和机柜的创建我就不贴代码出来了,有兴趣的请自行查看代码,这里主要说一下双击机柜以及与机柜有关的任何物体(柜门,服务器设备)则 3d 中 camera 的视线就会移动到双击的机柜的前方某个位置,而且这个移动是非常顺滑的,之前技艺不精,导致这个部分想了很久,最后参考了这个 demo 的实现方法。
为了能够重复地设置 eye 和 center,将设置这两个参数对应的内容封装为 seteye 和 setcenter 方法,setcenter 方法与 seteye 方法类似,这里不重复赘述:
// 设置眼睛位置
var seteye = function(eye, finish) {
if (!eye) return;
var e = g3d.geteye().slice(0),//获取当前 eye 的值
dx = eye[0] - e[0],
dy = eye[1] - e[1],
dz = eye[2] - e[2];
// 启动 500 毫秒的动画过度
ht.default.startanim({
duration: 500,
easing: easing,//动画缓动函数
finishfunc: finish || function() {}, //动画结束后调用的函数
action: function(v, t) {//设置动画v代表通过easing(t)函数运算后的值,t代表当前动画进行的进度[0~1],一般属性变化根据v参数进行
g3d.seteye([ //设置 3d 场景中的 eye 眼睛的值,为一个数组,分别对应 x,y,z 轴的值
e[0] + dx * v,
e[1] + dy * v,
e[2] + dz * v
]);
}
});
};
我没有重复声明 setcenter 函数不代表这个函数不重要,恰恰相反,这个函数在“视线”移动的过程中起到了决定性的作用,上面的 seteye 函数相当于我想走到我的目标位置的前面(至少我定义的时候是这种用途),而 scenter 的定义则是将我的视线移到了目标的位置(比如我可以站在我现在的位置看我右后方的物体,也可以走到我右后方去,站在那个物体前面看它),这点非常重要,请大家好好品味一下。
双击事件倒是简单,只要监听 ht 封装好的事件,判断事件类型,并作出相应的动作即可:
g3d.mi(function(e) {//addinteractorlistener 事件监听函数
if (e.kind !== 'doubleclickdata') //判断事件类型为双击节点
return;
var data = e.data, p3;
if (data.a('cabinet')) //机身
p3 = data.p3();
else {
host = data.gethost(); //获取点击节点的吸附对象
if (host && host.a('cabinet')) {//如果吸附对象为 cabinet
p3 = host.p3();
}
}
if (!p3) return;
setcenter(p3); //设置 center 目标的要移向位置为 cabinet 的位置
seteye([p3[0], 211, p3[2] + 247]); //设置 eye 眼睛要移向的位置
});
顶部导航栏
一开始看到这个例子的时候我在想,这人好厉害,我用 ht 这么久,用 ht 的 ht.widget.toolbar 还没能做出这么漂亮的效果,看着看着发现这原来是用 form 表单做的,厉害厉害,我真是太愚钝了。
var form = e.top = new ht.widget.formpane(); //顶部 表单组件
form.setrowheight(e.topheight);//设置行高
form.setvgap(-e.topheight);//设置表单组件水平间距 设置为行高的负值则可以使多行处于同一行
form.setvpadding(0);//设置表单顶部和顶部与组件内容的间距
form.addrow([null, {//向表单中添加一行组件,第一个参数为元素数组,元素可为字符串、json格式描述的组件参数信息、html元素或者为null
image: {
icon: './symbols/inputbg.json',
stretch: 'centeruniform'
}
}], [40, 260]);//第二个参数为每个元素宽度信息数组,宽度值大于1代表固定绝对值,小于等于1代表相对值,也可为80+0.3的组合
form.addrow([null, null, {
id: 'searchinput',
textfield: {}
}, {
element: '机房可视化管理系统',
color: 'white',
font: '18px arial, sans-serif'
}, null, {
button: {
// label: '视图切换',
icon: './symbols/viewchange.json',
background: null,
selectbackground: 'rgb(128,128,128)',
bordercolor: 'rgba(0, 0, 0, 0)',
onclicked: function() {
e.focusto();
}
}
}, null, {
button: {
// label: '告警',
icon: './symbols/alarm.json',
togglable: true,
selected: false,
background: null,
selectbackground: 'rgb(128,128,128)',
bordercolor: 'rgba(0, 0, 0, 0)',
onclicked: function(e) {
e.setalarmvisible(this.isselected());
}
}
}, null], [40, 42, 218, 300, 0.1, 50, 10, 50, 10]);
以上都只是能实现,但是并没有真正地添加进 html 标签中,也就意味着,现在界面上什么都没有!别忘了在页面加载的时候将 3d 场景添加进 body 中,同时也别忘了将 form 表单添加进 body 中,并且设置窗口大小变化事件时,form 表单也需要实时更新:
window.addeventlistener('load', function() {
g3d.addtodom(); //将 3d 场景添加进 body 中
document.body.appendchild(e.top.getview()); //将 form 表单组件底层 p 添加进 body 中
window.addeventlistener('resize', function() {//窗口大小变化事件监听
e.top.iv();//更新 form 表单的底层 p
});
});
这里说明一下 addtodom 函数,对于了解 ht 的机制非常重要。ht 的组件一般都会嵌入 borderpane、splitview 和 tabview 等容器中使用,而最外层的 ht 组件则需要用户手工将 getview() 返回的底层 p 元素添加到页面的 dom 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 borderpane 和 splitview 等这些 ht 预定义的容器组件,则 ht 的容器会自动递归调用孩子组件invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 ht 组件无法获知需要更新,因此最外层的 ht 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。
为了最外层组件加载填充满窗口的方便性,ht 的所有组件都有 addtodom 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写:
addtodom = function(){
var self = this,
view = self.getview(),
style = view.style;
document.body.appendchild(view); //将场景的底层 p 添加进 body 中
style.left = '0';//ht 默认将所有的组件底层p的position设置为absolute
style.right = '0';
style.top = '0';
style.bottom = '0';
window.addeventlistener('resize', function () { self.iv(); }, false); //窗口大小变化监听事件,通知组件变化更新
}
这样,所有的代码就结束了,可以自己右键“检查”,network 中可以获取相对应的 json 文件。
相关推荐:
打造最美html5 3d机房(mono哥强势归来,第四季惊艳发布)_html/css_web-itnose
以上就是html5 webgl实现的3d机房示例的详细内容。
