第一章 前期准备
阅读之前
如果您准备使用TWaver HTML5 3D进行应用开发,那么本文档适合您阅读。请注意“HTML5”和“3D”两个关键字。如果关注的是Flex、Java、.NET等技术,或关注的是2D技术(例如拓扑图),则不在本文档讨论范围内,请阅读其他相关产品分支文档。
为了简化起见,除非明确说明,本文中对TWaver HTML5 3D简称为“twaver”,或“mono”。“mono”也是TWaver HTML5 3D的内部产品代号。请注意,“MONO DESIGN”是一个基于mono的3D编辑器,用于做机房、物体等的建模,和mono本身并不是一个产品,请注意区别。关于MONO DESIGN的使用,请阅读本站相关的使用手册。
“HTML5”本文中也常被简称为“H5”,也是业界常用的简称方式。
“JavaScript”本文中简称“js“。
一句话解释什么是mono
mono是基于h5技术的3d引擎,用它可以在网页上开发3d应用,而无需插件。h5的3d标准是webGL,一个简化版的openGL,允许使用js语言在浏览器中直接编写3D代码。由于webGL定义的接口非常底层,所以在浏览器中直接写webGL能识别的代码是无法想像的。mono则把webGL的底层接口进行二次封装,形成了一个“3D引擎”。开发者直接使用mono.js这个库,就可以在浏览器里面更容易的撰写3d代码。
所以,总结mono是:一个mono.js文件,一个3d引擎,一个3d开发sdk,一个webGL开发框架。
什么应用适合使用mono
mono产品的核心特点是:3D、Web、无插件、跨平台,适合创建轻量的跨平台的网页三维应用。如果您的应用符合下面的一项或多项需求,可考虑使用mono引擎:
在网页上显示3d场景
不想使用插件
页面能跨浏览器,跨PC、平板、手机显示
并非所有3d需求都适合mono。mono更适合简单、轻量的3d呈现,而不适合很“重”的3d呈现。如果您的应用有以下情形,则不建议使用mono,可转而考虑使用本地桌面3D技术:
硬件配置较差,例如无独立显卡,CPU、内存等配置远低于主流性能
浏览器有限制,例如只能使用低版本的IE浏览器、不允许安装Chrome等非IE浏览器
3d场景超大、超复杂(例如场景顶点数达千万级)、逼真度要求极高,需要加载大量素材资源,各种特效,刷新率及交互要求极高的应用。例如大型3D游戏、建筑业BIM设计、复杂的工业3D图纸设计等
目前,mono的典型应用行业场景有:
机房3D可视化呈现
电力变电站可视化呈现
智能园区/楼宇3D可视化
各类工业自动化监控系统可视化
3D库房管理
复杂大数据3D呈现
以上仅罗列目前mono客户的普遍需求。实际上由于mono是一个通用的3D引擎,它可以做各类行业的3D场景呈现。mono本身并没有太多限制,有限制的可能是我们的思维和创新意识。有新想法再加上mono,就一定能在不同行业创造出令人耳目一新的、全新用户体验的可视化系统。
软硬件的要求
硬件方面,所有的3D技术对硬件要求都较高,mono作为一个3D引擎也不例外。3D程序对硬件的要求体现在:显卡、CPU、内存几个方面。虽然很难用硬性指标来描述和约束硬件的具体要求,不过您还是可以参考以下几条主要的原则:
显卡:最好是具有独立显卡的电脑,显存容量越大越好
CPU:越快越好,主流的i5、i7处理器都可以很好的支撑3D应用
内存:内存和显存都是越多越好。1G的显存和2G的内存是需要的,2G的显存和4G的内存则可以更流畅的运行3D程序
软件方面的要求主要是指操作系统和浏览器。Mac OS和Windows 7及以上版本都可以很好的支持webGL。如果您的机器依旧使用Windows XP操作系统,估计硬件配置也不会很好,不推荐在这样的机器上运行3D程序。
浏览器方面,各种主流的新版浏览器基本都可以很好的支持webGL。包括Windows IE11及以上、Windows Edge、Chrome、Safari、FireFox、Opera等。
Microsoft IE v11及以上
Microsoft Edge
Safari (推荐)
Chrome (推荐)
Firefox
Opera
总结为一句话:IE必须11以上,Chrome最佳,其他没问题。
如果还不能确认您的浏览器是否支持webGL,可以直接用浏览器访问页面https://get.webgl.org/。如果能在页面上看到一个旋转的立方体,说明支持webGL,否则就是有问题。
需要提醒您一点:网页3D程序虽然是B/S架构下的产物,但它最终还是运行在您客户端本地机器上。所以上述软硬件要求也是指您桌面本机的配置要求,和服务器没有关系。再强大的服务器主机,对您浏览器的3D程序也是没有任何帮助的。
另外,有一些企业,开发者会在一个虚拟机中创建开发环境。此时,如果虚拟机的配置并没有设置显卡等资源,或配置较低达不到上述要求,也会出现各种问题。所以不建议在虚拟机中运行和开发mono程序。
对开发者的要求
学习mono对开发者的要求并不是很大。但是由于mono是关于“3D”的技术,对大部分开发者来说可能有一定的“恐惧感”。实践证明,克服这一恐惧感并不是难事。
mono是基于h5的,所以开发者必须熟悉js语言。事实上js语言是一个非常简单的脚本语言,几个小时就可以掌握它的基本语法。js语言对比C++、Java等高级面向对象语言有很多不同,建议能够熟悉和理解js中的对象、function和prototype等语言机理。
熟悉html标签和h5的canvas技术对使用好mono会有更多的帮助,但并非必须。
mono包含了什么
虽然mono的产品下载包可能很大,但本质上,mono的核心只有一个mono.js文件,大小只有几百k,无任何外部及第三方js依赖。mono的产品包中包含了文档、demo、MONO DESIGN编辑器等内容,所以体积更大。mono.js是一个经过混淆的js脚本文件,它暴露了引擎必要的API开发接口,而混淆了不必要暴露的内部代码实现。mono的源代码并不对开发者开放,是一个闭源的3d引擎商业产品。
mono的产品包中还包含了大量的demo程序。运行并研读这些程序代码,是非常有效的快速学习mono的手段。
mono产品包中还包含了可以运行的MONO DESIGN编辑器程序。它可以用来做房间和各种简单物体的建模、保存、导入导出等。具体使用方法请阅读本站相关文档。
第二章:第一个3D程序
目的
本节的任务和目的是用mono写一个最简单的网页3D程序。这一节花不了几分钟的时间,可以让开发者快速体验如何用mono做一个简单的网页3D程序。
准备工具
首先需要一个js的代码编辑工具。复杂的有微软的Visual Studio、Eclipse、NetBeans等IDE,也有相对简单流行的WebStorm等编辑工具。或者使用最简单的工具:写字板,例如UltraEditor、EditPlus、NotePlus等编辑工具,也足够了。复杂的IDE具有强大的功能和代码组织、调试能力,适合在大型项目中使用。如果只是js初学者,或简单尝试和体验mono的使用,使用写字板程序能更清晰的看清楚程序结构和逻辑,简单方便。
其次需要准备好t.js文件,它包含在mono产品包的libs目录中。
好了,就这些。
开始写程序
打开写字板,创建一个test.html文件。将下方代码粘贴进文件保存。注意,确保t.js和test.html两个文件一起放在某个目录下。
<!DOCTYPE html>
<html>
<head>
<title>Mono Test</title>
<script type="text/javascript" src = "t.js"></script>
<script type="text/javascript">
function init(){
var box = new mono.DataBox();
var network= new mono.Network3D(box, null, monoCanvas);
mono.Utils.autoAdjustNetworkBounds(network,document.documentElement,'clientWidth','clientHeight');
var pointLight = new mono.PointLight(0xFFFFFF,1.5);
pointLight.setPosition(1000,1000,1000);
box.add(pointLight);
box.add(new mono.AmbientLight(0x888888));
var cube = new mono.Cube(200, 200, 200);
cube.s({
'm.type': 'phong',
'm.color': 'red',
'm.ambient': 'red',
});
cube.setRotation(-Math.PI/10, -Math.PI/5, Math.PI/10);
box.add(cube);
}
</script>
</head>
<body onload = 'init()'>
<div>
<canvas id="monoCanvas"/>
</div>
</body>
</html>
直接双击test.html文件,即可用浏览器打开这个页面。下图是在Windows10上使用默认Edge浏览器打开页面的效果:
如果您能看到截图上的图形,恭喜,您已经完成了第一个mono程序。
Note:
请确认默认浏览器程序版本符合mono的要求,尤其是IE,要求IE11或Edge,低版本IE不能支持webGL。如果不能正常看到上述程序,请检查浏览器程序及版本,是否符合mono要求。
如果发现程序异常,在浏览器中可以直接按F12查看异常信息,进行错误定位和调试:
程序说明
简单解释一下这个程序的内容。test.html本质是一个html网页文件,里面只包含了一个普通的html必须的元素,包括title、body,body中放了一个div,div中放了一个canvas标签。另外,head中放了两个script标签,第一个引入了t.js文件,第二个则直接写了一个init全局函数,其中的代码来创建3D场景。init函数在body的onload中进行调用,也就是页面加载结束后,浏览器会立刻执行init函数。
init函数包含了mono最简单的使用方法。下面的注释逐行解释了每一句代码的作用:
<!DOCTYPE html>
<html>
<head>
<title>Mono Test</title>
<script type="text/javascript" src = "t.js"></script>
<script type="text/javascript">
function init(){
//创建数据容器,容纳所有3d对象
var box = new mono.DataBox();
//创建3d画布,显示3d场景。monoCanvas是html页面中canvas标签对象的id值
var network= new mono.Network3D(box, null, monoCanvas);
//当页面调整大小时,自动调整network画布宽度,铺满整个页面
mono.Utils.autoAdjustNetworkBounds(network,document.documentElement,'clientWidth','clientHeight');
//在坐标1000、1000、1000处放置一个颜色为0xFFFFFF色、强度为1.5的点光源
var pointLight = new mono.PointLight(0xFFFFFF,1.5);
pointLight.setPosition(1000,1000,1000);
box.add(pointLight);
//在场景中增加颜色为0x888888的环境光
box.add(new mono.AmbientLight(0x888888));
//创建一个大小为200、200、200的立方体
var cube = new mono.Cube(200, 200, 200);
//设置立方体材质、颜色等样式
cube.s({
'm.type': 'phong',
'm.color': 'red',
'm.ambient': 'red',
});
//设置立方体在x、y、z轴向的旋转角度
cube.setRotation(-Math.PI/10, -Math.PI/5, Math.PI/10);
//将立方体置入databox数据容器进行显示
box.add(cube);
}
</script>
</head>
<body onload = 'init()'>
<div>
<canvas id="monoCanvas"/>
</div>
</body>
</html>
程序主要分为几个步骤:
创建容器和画布。通过代码new一个mono.DataBox和mono.Network3D即可。databox是放置所有3D对象的容器,是使用mono必须创建的对象。即使不显式的new实例,在new mono.Network3D时其内部也会创建一个databox实例。您必须掌握的概念是:databox是装载所有3D对象的容器,而network则是显示3D场景的画布。两个对象一个负责数据管理,一个负责图形绘制,搭配使用,相互配合,缺一不可。程序中还使用了autoAdjustNetworkBounds这个工具函数保证network始终铺满页面
设置灯光。3D场景中,必须设置灯光才能看到场景,否则,3D场景会“漆黑一片”,这和现实世界的逻辑相同。灯光分多种,简单而言,常用的就2类:点光源和环境光。点光源模拟了一个类似“灯泡”一样的光源,环境光则模拟了我们现实世界的环境光。一般应同时搭配使用,增强3D场景的光照感和逼真度。代码中使用了一个点光源和一个环境光,并设置了光的颜色、强度等属性
添加3D物体。mono中3D物体都进行了面向对象化的封装,例如我们直接new mono.Cube就可以创建一个立方体。设置大小、颜色、位置、旋转、材质等信息,使用“box.add(cube)”的方式添加到数据容器中,就完成了3D物体的创建和显示
请认真阅读和理解上面代码中每一行代码的作用和含义。当然,html中的每一个标签的含义也应该做到完全理解。如果您理解了以上全部内容,就基本上理解了mono开发3D程序的基本原理和过程,可以做mono开发了。其他再复杂的3D场景,也是基于相同的基本原理和过程的,只不过数据更多、属性样式更复杂而已。
总结
可见,十几行js代码就能创建一个最简单的mono程序了,任何一个程序员在很短时间就可以掌握mono的开发。再次总结编写一个最简单的mono程序步骤:1、创建box容器和network画布;2、创建灯光;3、创建3D物体。
第三章:基本概念
向量、矩阵是数学中的基础概念。虽然大家在大学时候肯定都学习过《线性代数》高中就学习过向量,并可能考试得过高分,但很可能现在已经都还给了老师了。一般的程序开发可能用不到向量和矩阵,但在3D技术中,这些概念却是非常重要的基础知识。
这里我们不打算把整个《线性代数》重新学一遍,使用mono也不需要那么深入的数学知识。这里只是在使用mono过程中可能用到的基础概念,简单、快速的进行重新回顾,让大家重新掌握这些基础数学知识。
如果对这些知识还比较熟悉,或者还没有耐心复习这些枯燥的数学概念,可以先跳过本章节,待以后再阅读。
向量
向量的定义和基本概念
数量的定义:数学中,把只有大小但没有方向的量叫做数量(或纯量),物理中常称为标量。
向量的定义:既有大小又有方向的量叫做向量(亦称矢量)。在线性代数中的向量是指n个实数组成的有序数组,称为n维向量。α=(a1,a2,…,an)称为n维向量.其中ai称为向量α的第i个分量。(”a1″的”1″为a的下标,”ai”的”i”为a的下标,其他类推)。
几何表示:向量可以用有向线段来表示。有向线段的长度表示向量的大小,箭头所指的方向表示向量的方向。(若规定线段AB的端点A为起点,B为终点,则线段就具有了从起点A到终点B的方向和长度。这种具有方向和长度的线段叫做有向线段。)
坐标表示:在平面直角坐标系中,分别取与x轴、y轴方向相同的两个单位向量i,j作为基底。a为平面直角坐标系内的任意向量,以坐标原点O为起点作向量OP=a。由平面向量基本定理知,有且只有一对实数(x,y),使得a=向量OP=xi+yj,因此把实数对(x,y)叫做向量a的坐标,记作a=(x,y)。这就是向量a的坐标表示。其中(x,y)就是点P的坐标。向量OP称为点P的位置向量。
向量的模和向量的数量:向量的大小,也就是向量的长度(或称模)。向量a的模记作|a|。
注:
1、向量的模是非负实数,是可以比较大小的。
2、因为方向不能比较大小,所以向量也就不能比较大小。对于向量来说“大于”和“小于”的概念是没有意义的。例如,“向量AB>向量CD”是没有意义的。
单位向量:长度为单位1的向量,叫做单位向量.与向量a同向且长度为单位1的向量,叫做a方向上的单位向量,记作a0,a0=a/|a|。
零向量:长度为0的向量叫做零向量,记作0。零向量的始点和终点重合,所以零向量没有确定的方向,或说零向量的方向是任意的。
相等向量:长度相等且方向相同的向量叫做相等向量,向量a与b相等,记作a=b。所有的零向量都相等。
当用有向线段表示向量时,起点可以任意选取。任意两个相等的非零向量,都可用同一条有向线段来表示,并且与有向线段的起点无关.同向且等长的有向线段都表示同一向量。
自由向量:始点不固定的向量,它可以任意的平行移动,而且移动后的向量仍然代表原来的向量。在自由向量的意义下,相等的向量都看作是同一个向量。
相反向量:与a长度相等、方向相反的向量叫做a的相反向量,记作-a。有-(-a)=a;零向量的相反向量仍是零向量。
平行向量:方向相同或相反的非零向量叫做平行(或共线)向量.向量a、b平行(共线),记作a‖b。
向量的运算
以下介绍向量的基本运算。介绍之前,假设:
设a=(x,y),b=(x’,y’)
向量的加法
向量的加法满足平行四边形法则和三角形法则:
AB+BC=AC
a+b=(x+x’,y+y’)
a+0=0+a=a
向量加法的运算律:
交换律:a+b=b+a
结合律:(a+b)+c=a+(b+c)
mono中的相应函数,mono.Vec2/mono.Vec3/mono.Vec4均适用:
向量的减法
如果a、b是互为相反的向量,那么a=-b,b=-a,a+b=0
AB-AC=CB
a-b=(x-x’,y-y’)
mono中的相应函数,mono.Vec2/mono.Vec3/mono.Vec4均适用:
//向量减
mono.Vec2#sub
向量的数乘
实数λ和向量a的乘积是一个向量,记作λa,且∣λa∣=∣λ∣·∣a∣。
当λ>0时,λa与a同方向;
当λ<0时,λa与a反方向;
当λ=0时,λa=0,方向任意。
当a=0时,对于任意实数λ,都有λa=0。
注:
按定义知,如果λa=0,那么λ=0或a=0。
实数λ叫做向量a的系数,乘数向量λa的几何意义就是将表示向量a的有向线段伸长或压缩。
当∣λ∣>1时,表示向量a的有向线段在原方向(λ>0)或反方向(λ<0)上伸长为原来的∣λ∣倍;
当∣λ∣<1时,表示向量a的有向线段在原方向(λ>0)或反方向(λ<0)上缩短为原来的∣λ∣倍。
数与向量的乘法满足下面的运算律:
结合律:(λa)·b=λ(a·b)=(a·λb)。
向量对于数的分配律(第一分配律):(λ+μ)a=λa+μa.
数对于向量的分配律(第二分配律):λ(a+b)=λa+λb.
数乘向量的消去律:①如果实数λ≠0且λa=λb,那么a=b。②如果a≠0且λa=μa,那么λ=μ。
mono中的相应函数,mono.Vec2/mono.Vec3/mono.Vec4均适用:
//向量数乘
mono.Vec2#multiply
向量的点乘
数量积(dot product; scalar product,也称为点积)是接受在实数R上的两个向量并返回一个实数值标量的二元运算。
代数定义:
两个向量a = [a1, a2,…, an]和b = [b1, b2,…, bn]的点积定义为:
a·b=a1b1+a2b2+……+anbn。
几何定义:
两个向量的数量积(内积、点积)是一个数量,记作a·b。
a·b=|a|·|b|·cos〈a,b〉
由此可以得出:如果ab夹角为90度,则cos=0,点积也为零,向量AB*向量AC=向量AB的模*向量AC的模*cosΘ,因为垂直,所以Θ=π/2,cosπ/2=0。因此相互垂直的向量点积为零。
向量的数量积的坐标表示:a·b=x·x’+y·y’。
向量的数量积的运算律:
a·b=b·a(交换律)
(a+b)·c=a·c+b·c(分配律)
向量的数量积与实数运算的主要不同点:
向量的数量积不满足结合律,即:(a·b)·c≠a·(b·c);例如:(a·b)^2≠a^2·b^2。
向量的数量积不满足消去律,即:由a·b=a·c(a≠0),推不出b=c。
|a·b|≠|a|·|b|
由|a|=|b|,推不出a=b或a=-b。
mono中的相应函数,mono.Vec2/mono.Vec3/mono.Vec4均适用:
//向量点乘
mono.Vec2#dot
向量的叉乘
向量积,或称外积、叉积、叉乘,与点积不同,它的运算结果是一个向量而不是一个标量。并且两个向量的叉积与这两个向量的和垂直。定义为:
定义:两个向量a和b的向量积(外积、叉积)是一个向量,记作a×b。若a、b不共线,则a×b的模是:∣a×b∣=|a|·|b|·sin〈a,b〉;a×b的方向是:垂直于a和b,且a、b和a×b按这个次序构成右手系。若a、b共线,则a×b=0。
向量的向量积性质:
∣a×b∣是以a和b为边的平行四边形面积
a×a=0
a‖b〈=〉a×b=0
向量的向量积运算律:
a×b=-b×a
(λa)×b=λ(a×b)=a×(λb)
(a+b)×c=a×c+b×c
注:
向量没有除法,“向量AB/向量CD”是没有意义的。mono中的相应函数,mono.Vec2/mono.Vec3/mono.Vec4均适用:
//向量叉乘
mono.Vec2#cross
在mono中使用向量
在mono中,二维向量可以用mono.Vec2这个类,三维向量可以使用mono.Vec3。平面或空间的一个点、一个轴,都可以用一个二维或三维向量来表达。由于mono是3D引擎,因此使用mono.Vec3最为普遍。
例如,空间(x=100,y=200,z=300)这个点可以用如下向量表达:
var point = new mono.Vec3(100, 200, 300);
它是一个空间的点,也是一个从坐标原点到这个点的一个向量。
同样,如果一个轴线,例如y轴,可以用new mono.Vec3(0, 1, 0)来表示。
注意:
1、在mono中,一个空间的点、一根轴(也就是一个方向),都是一个向量。请务必理解这一概念。
2、在mono中,一个mono.Vec3所表达的是一个三维自由向量。和固定向量不同,自由向量只确定方向与大小,而不在意位置。因此Vec3所表达的向量起始点都是坐标原点。
在mono中,向量的运算都定义在了mono.Vec2/Vec3/Vec4中。罗列如下:
//加
mono.Vec2#add
//减
mono.Vec2#sub
//数乘
mono.Vec2#multiply
//点乘
mono.Vec2#dot
//叉乘
mono.Vec2#cross
同样,对于mono.Vec3、mono.Vec4以上函数也适用。
技巧:绕轴旋转的物体
任务:让一个物体绕指定的位置的一个轴做某个角度的旋转
思路:这一任务完全可以通过向量来解决。我们可以使用向量Vec3的内置函数rotateFromAxisAndCenter函数来实现。这个函数的作用是:让当前向量绕指定位置的指定轴进行指定角度进行旋转,并返回新的位置向量。解释如下:
//函数定义
mono.Vec3#rotateFromAxisAndCenter : axis, angle, center
//axis: 旋转的轴。例如new mono.Vec3(0, 1, 0)表示垂直轴。注意这个轴向量是一个自由向量,没有明确轴的具体位置。轴的位置要用center参数来确定。
//angle: 要旋转的角度。
//center: 旋转的轴的具体角度。例如new mono.Vec3(200, 300, 500)表示轴axis在x=200、y=300、z=500这个位置。
下面代码让立方体绕垂直y轴在x=50、z=100的位置逆时针不停旋转:
var node = new mono.Cube(100, 100, 100);
cube.s({
'm.type': 'phong',
'm.texture.image': 'box.jpg',
});
var rotationFunc = function(){
var position = cube.getPosition();
var axis = new mono.Vec3(0, 1, 0);
var angle = Math.PI/180;
var center = new mono.Vec3(50, 0, 100);
var newPosition = position.rotateFromAxisAndCenter(axis, angle, center);
cube.setPosition(newPosition);
}
setInterval(rotationFunc, 10);
此外,在mono的任意3D对象上都提供了Element#rotateFromAxis(axisDir, axisPosition, angle)方法,也可以实现完全相同的效果。仅仅是函数名称、参数略有不同而已。
/*
* 针对Element的操作。定轴旋转,返回旋转后的向量。
* axisDir: 轴的方向,比如 vec3(0,1,0) 表示垂直向上的y轴方向
* axisPosition: 表示轴的位置,比如vec3(1,0,0),则axis + center 表示的就是在x为1的位置,向上的一个轴
* angle: 要旋转的角度
*/
Element#rotateFromAxis(axisDir, axisPosition, angle)
矩阵
矩阵是3D中的重要基础概念。不过由于mono对于大部分相关运算都做了封装,所以开发者一般不需要直接使用矩阵API。但理解矩阵的概念和基本运算,会对更好的使用mono有积极的作用。
在数学中,矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合 ,最早来自于方程组的系数及常数所构成的方阵。这一概念由19世纪英国数学家凯利首先提出。 矩阵是高等代数学中的常见工具,也常见于统计分析等应用数学学科中。在物理学中,矩阵于电路学、力学、光学和量子物理中都有应用;计算机科学中,三维动画制作也需要用到矩阵。
定义
由 m × n 个数aij排成的m行n列的数表称为m行n列的矩阵,简称m × n矩阵。记作:
这m×n 个数称为矩阵A的元素,简称为元,数aij位于矩阵A的第i行第j列,称为矩阵A的(i,j)元,以数 aij为(i,j)元的矩阵可记为(aij)或(aij)m × n,m×n矩阵A也记作Amn
元素是实数的矩阵称为实矩阵,元素是复数的矩阵称为复矩阵。而行数与列数都等于n的矩阵称为n阶矩阵或n阶方阵[5]。
运算
矩阵的基本运算包括矩阵的加法,减法,数乘,转置,共轭和共轭转置。
mono中的矩阵
在mono中,用mono.Mat3/mono.Mat4表示2阶/3阶矩阵。它封装了矩阵的基本运算。
开发基本概念
MVC设计架构
您需要理解mono以及twaver产品的每一个分支都是基于“MVC”(或其变种)的设计架构。这对您日常使用mono进行开发非常重要。
这是一个“架构泛滥”的年代,各种类似MVC的热词满天飞,您可能很不明觉厉,或不以为然。其实不必大谈各种架构的理论和好处,我们只是用很朴素的想法来理解mono的MVC设计就行了:
mono采用MVC的思路把数据、绘制、交互三者进行了分离设计。
具体说:
M——Model,数据:它主要是指mono中的databox容器,及立方体/球体/圆柱体等等一系列基础数据对象。容器负责装载和管理这些3D数据对象。这些,都是纯粹的数据定义和管理,就和一个Array数组与一堆Object对象的意思一样,只是更复杂一些而已。
V——View,视图:它主要是指mono中的network画布对象,它代表了数据的一个“3D呈现的视图”,作用是“如何用一个角度去表达/绘制这些databox中的对象”。视图的主要任务是“绘制”,用自己的方式(例如3D)进行绘制,把绘制的细节封装起来,让使用者更简单。
C——Controller,控制器:控制器是指负责在视图和数据之间交互的控制部分。例如:数据变化后,如何让视图立刻实现通知和更新、怎么更新?用户拖拽了一个物体位置后,如何将数据的变化同步给databox?等等。控制器是负责数据的变化同步,以及用户和视图之间的各种交互。典型的部分是mono中Interaction相关的类和接口。
DataBox是一个对象管理容器。所有要显示的3D对象都放置在这个容器中统一管理。容器会负责3D对象的增、删、查等工作,同时监听数据的变化并通过事件监听机制进行广播通知。Network则是负责具体显示的画布,它必须连接到一个具体DataBox容器,并将容器中的数据绘制到canvas画布上。DataBox和Network作为数据管理者和画布渲染者独立工作、各司其职。
MVC的组织方式让程序更加的灵活,让类的组织更清晰、容易理解、容易扩展。反映到mono上,我们可以直接带来的灵活性和好处有:
可以一个databox连接多个network视图,实现一套对象多处呈现的要求。每个network可以显示不同的样式和风格,而内存中却只有一套数据和一个databox。
可以只关注业务数据的变化,而不关心绘制细节。例如,一个物体的颜色变了,我们直接“cube.setStyle(‘m.color’, ‘red’)”即可实现立刻更新,而不用关心“如何通知、如何重绘”等细节。
数据的各种变化,都可以实现灵活监听。例如一个对象的任何属性变化、场景中数据的创建和删除等,都可以通过监听容器完成。
可以灵活定制交互,而无需修改network或databox。例如,设置一个“可移动Interaction”就可以允许用户直接通过鼠标移动物体,设置“可选择Interaction”就可以允许选中物体,等等。
MVC的组织方式让程序更加的灵活,让类的组织更清晰、容易理解、容易扩展。反映到mono上,我们可以直接带来的灵活性和好处有:
可以一个databox连接多个network视图,实现一套对象多处呈现的要求。每个network可以显示不同的样式和风格,而内存中却只有一套数据和一个databox。
可以只关注业务数据的变化,而不关心绘制细节。例如,一个物体的颜色变了,我们直接“cube.setStyle(‘m.color’, ‘red’)”即可实现立刻更新,而不用关心“如何通知、如何重绘”等细节。
数据的各种变化,都可以实现灵活监听。例如一个对象的任何属性变化、场景中数据的创建和删除等,都可以通过监听容器完成。
可以灵活定制交互,而无需修改network或databox。例如,设置一个“可移动Interaction”就可以允许用户直接通过鼠标移动物体,设置“可选择Interaction”就可以允许选中物体,等等。
MVC带来的灵活性还有很多。无论我们是否喜欢,MVC都是无处不在,是整个软件行业的基础设计思路之一。mono中的MVC并不复杂,对于初学者,我们只要时刻掌握这样的概念即可:
databox是容器,network是视图,interaction是交互,分别代表M、V、C就行了,不必理会各种天花乱坠的“玄妙”定义。
支持浏览器
开发者需要在支持WebGL的浏览器上调试运行mono程序。包括:
Chrome v30及以上
IE v11及以上
Safari
FireFox
Opera
可以点击网址get.webgl.org来检查浏览器是否支持WebGL。如果能够看到一个旋转的立方体,则说明浏览器支持WebGL(如下图)。
开发工具及调试
JavaScript开发可以选择使用Eclipse、NetBeans、Visual Studio等大型IDE工具,也可以选择直接使用文本编辑工具如EditPlus、UltraEdit等。前者可以提供一些自动提示、集成调试等功能,缺点是程序比较庞大笨重。后者则轻量快速直接,但无自动提示等功能。开发者可以根据自身情况合理选择代码编辑工具。
对于开发测试浏览器,推荐使用Google的Chrome。Chrome速度快、对WebGL支持好、调试方便,是WebGL开发者首选浏览器。
在部署测试页面程序时,可以选择用IDE中的内置的调试方法,也可以直接放入如Tomcat等Web服务器中并用浏览器直接访问。
Chrome浏览器提供了内置的调试工具,非常方便。可以直接按F12打开开发工具窗口,其中提供了控制台、查看页面元素、断点跟踪、性能测试等功能(见下图)。
更多关于Chrome开发工具的功能介绍,请参考Google官方文档:https://developers.google.com/chrome-developer-tools/
理解坐标系
在mono中,3D场景的坐标是一个空间直角坐标系。直角坐标系是指在原点O,做三条互相垂直的数轴,它们都以O为原点,分别叫做x轴(横轴)、y轴(纵轴)、z轴(竖轴),统称坐标轴。在mono中,坐标系方向遵循右手法则,即以右手拇指、食指、中指相互交叉,拇指指向x轴正方向,食指指向y轴正方向,则中指指向的即是z轴正方向。
简单说:从左向右的水平轴是x轴,垂直向上的轴是y轴,从屏幕指向人面部的水平轴是z轴。再次示意如下图:
在mono中,空间的一个点由(x, y, z)三个数值组成。下图展示了不同数值其具体表示的点的位置。熟练掌握坐标点的位置,对mono开发具有重要的作用。请认真仔细观察以下图中空间几个点的位置和对应坐标,是否和您的理解一致。如果能够正确理解,则说明已经正确掌握了mono中3D的坐标概念。
除了位置以外,3D物体还有旋转角度。一个3D物体可以在x、y、z三个轴向分别进行旋转。
旋转角度的方向遵守“右手法则”:右手拇指指向要旋转的轴,其余四指的方向,就是角度旋转的方向。如下图:
如果旋转角度为正值,则沿图中箭头方向旋转;如为负值,则反方向旋转。
世界坐标系与本地坐标系
3D中的坐标系分为世界坐标系和本地坐标系。世界坐标系(World Coordinate System)是系统的绝对坐标系,是指所有3D物体所在的空间中全局的、绝对的坐标。而本地坐标系(Local Coordinate System)则是指相对以某一3D物体自身为原点的局部坐标系,物体的旋转或平移等操作都是围绕局部坐标系进行的。
例如一个飞速行走中的汽车,汽车的位置信息可以通过世界坐标系来定义其在地面空间的位置,而汽车雨刮的摆动位置和角度则是相对汽车的本地坐标系进行指定。这样有利于使用和理解的方便。
世界坐标系和本地坐标系可以进行转换。
数值单位
在mono中,所有的角度数值都统一用弧度表示。即:Math.PI表示180度,一周360度则为2*Math.PI。例如:
node.setRotation(Math.PI, Math.PI/2, Math.PI);
对于长度则没有具体单位。例如空间(100,200,300)的点表示其x值为100、y值为200、z值为300,但单位是厘米、米还是其他长度单位,mono是没有定义的,也没有实际意义,它只是逻辑上的数值而已。我们可以根据应用自行设定长度所代表的具体单位。
node.setPosition(100, 200, 300);
第四章:使用DataBox和Network
4.1 DataBox的作用
和twaver的2D产品概念类似,DataBox和Network是mono中最重要的基础对象。DataBox作为容器负责3D对象的管理,Network作为3D画布负责3D的渲染和呈现。
简单理解DataBox:它是一个不可见的内存容器对象,负责管理所有的3D对象,像一个复杂一点的数组,像一个简单一点的内存数据库。
使用DataBox
DataBox是管理所有数据对象的容器,在MVC框架中处于M(模型)层,它是视图的数据提供者,它的主要功能是对数据进行装载、卸载,并对数据元素的变化进行监听。
使用DataBox非常简单直接new一个mono.DataBox实例即可。下面的代码创建了一个DataBox和一个立方体对象,并添加到容器中:
var box = new mono.DataBox();
var cube = new mono.Cube(10, 10, 10);
box.add(cube);
需要理解的是:上面的代码并没有Network画布对象出现。DataBox也并不依赖Network而存在,反过来Network则必须依赖DataBox才能存在。这也容易理解:数据是客观存在的,不管是否显示出来。只有当数据需要被3D画布(Network)画到屏幕上的时候,才需要用到Network。
继续创建Network:
var box = new mono.DataBox();
var cube = new mono.Cube(10, 10, 10);
box.add(cube);
上面代码会创建一个network画布并和之前的box连接起来,渲染到HTML页面id为’myCanvas’的canvas标签上。
数据操作
DataBox最主要的任务是数据对象的增加、删除、查找、变化监听。其中,监听包括了数据出入的监听、每一个数据的属性变化的监听。
主要API罗列:
add(data, index) 往数据容器中添加一个数据对象。一般index不用设置,可忽略
addByDescendant(data) 把一个数据对象连其子孙对象一起添加到容器中
remove(data) 把一个数据对象从容器中删除
removeById(id) 删除指定id的数据对象
contains(data) 判断容器是否包含某个数据对象
containsById(id) 判断容器是否包含指定id的数据对象
getDataById(id) 获得容器中指定id的数据对象
getDatas() 返回容器所有数据对象
getDataAt(index) 返回容器指定index位置的数据对象。一般index不常用
size() 返回容器中数据对象的总数量
lightsSize() 返回数据容器中光源对象的总数量
getLights() 返回容器中所有的光源对象
clear() 清空数据容器。所有数据对象从容器中清除
removeSelection 把当前处于“选中状态”的数据对象从容器中清除
clearSelection 清除当前所有处于“选中状态”数据对象的选中状态,让它们处于非选中状态。注意数据对象不会从容器中删除(留意和removeSelection的区别)
clearEditing 取消所有当前正在处于“编辑状态”数据对象的编辑状态,让它们处于正常的非编辑状态
以下一段程序展示了对DataBox中数据的增删改查:
var box = new mono.DataBox();
for(var i = 0; i < 3; i++) {
var element = new mono.Element("element_" + i);
element.setName("element_" + i);
box.add(element);
}
box.forEach(function(element) {
console.log(element.getName()); //element_0,element_1,element_2
});
var element0 = box.getDataAt(0);
element0.setName("newElement");
console.log(element0.getName());//newElement
box.remove(element0);
console.log(box.size());//2
box.clear();
console.log(box.size());//0
事件监听
DataBox可以对容器中发生的事件进行监听。主要有两类事件:1、数据的变化;2、数据属性的变化。
数据的变化
数据的变化包括数据的添加、删除、清空。用box的addDataBoxChangeListener函数可以添加监听器对这些事件进行监听。下面代码展示了如何使用:
box.addDataBoxChangeListener(function(e){
if(e.kind === 'add') {
var newElement = e.data;
console.log('new element added: '+ newElement.getId());
}
});
监听器会收到事件对象e,事件上的kind是事件类型:
e.kind === ‘add’: 有对象被添加。新对象是e.data
e.kind === ‘remove’: 有对象被删除。删除的对象是e.data
e.kind === ‘clear’: 容器所有对象被清空。
通过removeDataBoxChangeListener函数删除监听器对象。
var listener = function(e){
//...
}
box.addDataBoxChangeListener(listener);
//...
box.removeDataBoxChangeListener(listener);
数据属性的变化
DataBox不但能监听数据的进出的变化,还能监听box中每一个数据对象的任意一个属性的变化。数据属性的变化包括name、tooltip等基本属性的变化、style属性的变化、client属性的变化。数据的变化包括数据的添加、删除、清空。用box的addDataPropertyChangeListener函数可以添加监听器对任意数据对象的任意属性变化进行监听。
属性变化监听器会传入一个事件对象e,它的主要属性有:
e.source:发生属性变化的数据对象,例如一个立方体对象;
e.property:变化的属性名称。例如立方体的名称发生变化,则property的值是’name’;
e.oldValue:变化前的数值;
e.newValue:变化后的数值;
有了这些信息,就可以方便的监听到某个数据发生的变化,并根据需要进行相应的动作。
当某些属性发生变化后,打印调试日志、提交后台进行保存等等,都是非常常见的典型用法。
下面代码利用这一方法,禁止用户修改一个物体的垂直位置,让它始终保持在地面上:
box.addDataPropertyChangeListener(function(e){
var element = e.source, property = e.property, oldValue = e.oldValue, newValue = e.newValue;
if(property == 'position' && oldValue.y != newValue.y){
element.setPositionY(oldValue.y);
}
});
每当y发生变化,就立刻再次把它设置回去。这样就阻止了y发生变化的可能。
导入导出json
DataBox支持把内部的对象导出为json字符串,也支持把json字符串定义的数据导入到DataBox中。使用mono.JsonSerializer这个类即可完成这一工作。
创建JsonSerializer:
//为指定box对象创建一个序列化器
var jsonSerializer = new mono.JsonSerializer(box);
序列化box中的数据:
//把box中的所有对象序列化到json字符串中
var json = jsonSerializer.serialize();
console.log(json);
打印结果类似下方格式:
{“v”:”2.0.6″,”platform”:”html5″,”dataBox”:{“class”:”TGL.DataBox”},”datas”:[{“class”:”TGL.PointLight”,”ref”:0,”p”:{“position”:{“x”:1000,”y”:1000,”z”:1000},”color”:{“r”:1,”g”:1,”b”:1},”ambient”:{},”diffuse”:{},”specular”:{},”intensity”:1.5}},{“class”:”TGL.PointLight”,”ref”:1,”p”:{“position”:{“x”:-1000,”y”:-1000,”z”:-1000},”color”:{“r”:1,”g”:1,”b”:1},”ambient”:{},”diffuse”:{},”specular”:{},”intensity”:2}},{“class”:”TGL.AmbientLight”,”ref”:2,”p”:{“color”:{“r”:0.5333333333333333,”g”:0.5333333333333333,”b”:0.5333333333333333},”ambient”:{},”diffuse”:{},”specular”:{}}},{“class”:”TGL.Cube”,”ref”:3,”p”:{“width”:100,”height”:100,”depth”:100},”s”:{“m.type”:”phong”,”m.texture.image”:”box.jpg”}}]}
反序列化json字符串。可以指定返回的对象放在某个对象的下方(孩子):
//把json字符串反序列化,并作为rootParent对象的孩子
jsonSerializer.deserialize(json,rootParent)
导出的json格式看上去很凌乱、不宜阅读。不过不必理会,json字符串的主要用途是直接被传输到后台数据库或文件进行保存,也可以作为javascript变量在代码中保存,实现数据的持久化。
4.2 使用Network
Network3D是一个用于交互的视图组件,可以展示3D场景,并实现用户和3D场景之间的交互,比如旋转镜头,选中3D对象,通过鼠标或键盘移动3D对象等。
在网页中添加Network
在网页中插入Network3D,首先自定义一个div,并创建一张画布canvas:
<div id = "mainDiv" >
<canvas id="myCanvas" width="800" height="800"/>
</div>
Network3D需要由一个与之关联的DataBox驱动,显示DataBox中的网元。当初始化Network3D时,DataBox默认会绑定在Network3D上。通过network3D构造方法创建network3D,并与DataBox、Camera和canvas绑定:
<div id = "mainDiv" >
<canvas id="myCanvas" width="800" height="800"/>
</div>
下面的例子在html中创建了一个network3D,并展现出一个3D物体。
<!DOCTYPE html>
<html>
<head>
<title>Room Inspection</title>
<script type="text/javascript" src = "libs/t.js"></script>
<script type="text/javascript" src = "libs/twaver.js"></script>
<script type="text/javascript">
var network, interaction;
function load(){
var box = new mono.DataBox();
var camera = new mono.PerspectiveCamera(30, 1.5, 0.1, 10000);
var target = new mono.Vec3(150,50,150);
camera.setPosition(1000,500,1000);
camera.lookAt(target);
network = new mono.Network3D(box, camera, myCanvas);
mono.Utils.autoAdjustNetworkBounds(network,document.documentElement,'clientWidth','clientHeight');
box.add(new mono.AmbientLight(0xffffff));
createBall(box);
}
function createBall(box){
var ball=new mono.Sphere(300,100);
ball.setStyle('m.texture.image','images/earth2.png');
ball.setStyle('m.type','phong');
ball.setStyle('m.texture.repeat',new mono.Vec2(1,1));
ball.setStyle('m.specularStrength',100);
box.add(ball);
}
</script>
</head>
<body onload = 'load()'>
<div id = "mainDiv" >
<canvas id="myCanvas" width="800" height="800"/>
</div>
</body>
</html>
设置network背景颜色
默认network使用白色作为背景。修改network空白区域的背景颜色,可以使用函数network.setClearColor()完成。
var box = new mono.DataBox();
var network= new mono.Network3D(box, null, monoCanvas);
network.setClearColor('#39609B');
自适应缩放
自适应缩放会自动调整Network3D的Bounds值,使3D物体在network中的呈现更加友善。可通过如下方法实现:
autoAdjustNetworkBounds = function(network, o, w, h, left, top) {}
鼠标键盘交互事件
mono中为我们提供了非常友善的用户鼠标键盘交互事件,下面列出常用的事件。
鼠标交互事件
按住左键移动:旋转物体
按住右键移动:平移物体
滚轴:缩放物体
键盘交互事件
pageup:沿着z轴正方向移动
pagedown:沿着z轴负方向移动
left:沿着x轴正方向移动
up:沿着x轴负方向移动
right:沿着y轴正方向移动
down:沿着y轴负方向移动
ctrl+A:全选
ctrl+C:复制
ctrl+V:粘贴
自动渲染
我们可以通过setRenderSelectFunction()方法过滤哪些选中的网元需要绘制选择边框,哪些不需要绘制边框。
setRenderSelectFunction = function(f) {}
我们还可以在分别监听到渲染前后,使用setBeforeRenderFunction()设置渲染Network3D前的执行方法,使用setRenderCallback()来设置渲染Network3D之后的回调方法。方法原型如下:
mono.Network3D.prototype.setRenderCallback = function(f) {}
//调用setRenderCallback方法
network.setRenderCallback(function(){
//do something
});
mono.Network3D.prototype.setBeforeRenderFunction = function(f) {}
//调用setBeforeRenderFunction方法
network.setBeforeRenderFunction(function(){
//do something
});
使用雾霾效果
雾霾是当代中国热词,mono自然应该支持这一特效功能。通过network的setUseFog(boolean)方法可以开启雾霾效果,setFogDensity(int)方法设置雾霾的等级。
下面代码随机创建了许多立方体:
for(var i=0;i<100;i++){
var cube=new mono.Cube(100, 100, 100);
cube.s({
'm.type': 'phong',
'm.texture.image': 'box.jpg',
});
cube.setPosition(Math.random()*1000-500, Math.random()*1000-500, Math.random()*200-100);
box.add(cube);
}
设置雾霾效果和指数:
network.setUseFog(true);
network.setFogDensity(1);
设置雾霾指数为2:
network.setUseFog(true);
network.setFogDensity(1);
继续增大雾霾指数:
network.setUseFog(true);
network.setFogDensity(3);
4.3 使用Interaction
理解Interaction
在mono的MVC结构中,DataBox提供了Model的能力,Network提供了View的能力,而Interaction则提供了Controller的能力。Interaction是在画布和数据之间提供的交互能力。例如通过键盘快捷键漫游、通过鼠标进行移动镜头等,都是交互的一部分。
每一个Network必须要设置Interaction才能有交互能力。一个没有任何Interaction的Network不会响应任何用户的交互,只能看,不能操作。Mono中已经封装了许多不同作用的Interaction,主要包括:
DefaultInteraction:默认交互,提供了默认的交互能力,例如键盘/鼠标镜头漫游等
SelectionInteraction:选中交互,提供了点击选中物体的能力,也支持ctrl+鼠标进行批量框选
EditInteraction:编辑交互,提供基于鼠标进行位置拖拽、角度拖拽、拉伸拖拽等综合编辑能力
Network可以同时设置多个Interaction,这些Interaction会同时起作用。例如,Network默认就内置了DefaultInteraction和SelectionInteraction这两个交互器,所以默认情况下Network可以响应鼠标/键盘漫游、点击选中等交互动作。
由于Network默认就内置了DefaultInteraction和SelectionInteraction这两个最常用的交互器,所以一般情况下,我们不需要特别为Network进行交互设置。如果要重新设置交互,可以把新的Interaction数组设置给network。
下面的例子为network增加了编辑交互器:
var network = new mono.Network3D();
var defaultInteraction = new mono.DefaultInteraction(network);
var selectionInteraction = mono.SelectionInteraction(network);
var editInteraction = new mono.EditInteraction(network);
network.setInteractions([defaultInteraction, selectionInteraction, editInteraction]);
如图,物体可以被选中、被拖拽,编辑交互已经生效。
可以通过network.getInteractions()获取到已经置入的交互器:
var interactions = network.getInteractions();
由于DefaultInteraction是最重要、最常见的交互器,为了方便获取,network提供了getDefaultInteraction()方法来特别获得DefaultInteraction对象:
var defaultInteraction = network.getDefaultInteraction();
可见,通过灵活组合和设置Interaction,就可以让画布实现各种不同的交互效果。下面通过Interaction来实现几个技巧。
技巧:禁止交互
有时候,我们希望将3D场景进行视频一样的自动漫游和回放,而不希望用户进行交互和干扰。此时我们可以暂时将交互器删掉(设置一个空数组即可):
var oldInteractions = network.getInteractions();
network.setInteractions([]);
等播放完毕需要恢复交互能力,将原来的交互器设置回去即可:
network.setInteractions(oldInteractions);
技巧:禁止物体选中
由于默认network内置了SelectionInteraction,点击物体后会被选中,显示为一个绿色高亮边框,如下图:
方法一:设置交互
我们可以重新设置交互数组,不再设置SelectionInteraction,即可去掉选中交互:
network.setInteractions([new mono.DefaultInteraction(network)]);
方法二:重载network.isSelectable函数
在不重新设置Interactions的情况下,也可以通过重写network的isSelection(element)函数,来更加精确的定义哪些物体可以选中、哪些物体不可以选中。isSelection函数会传入每一个3D对象element,返回true则表示可以选中,false表示不能选中。
network.isSelectable = function(element){
return false;
};
<pre>
上面的代码禁止一切物体选中。下面的代码则只允许场景中client的type为'rack'的机柜物体可以选中:
<pre>
network.isSelectable = function(element){
return element.getClient('type') === 'rack';
};
使用DefaultInteraction
DefaultInteraction是mono提供的最重要的一个交互器,它包含了常用的交互功能,包括可以在3D场景中旋转镜头,通过鼠标滚轮缩放镜头,键盘操作镜头等。以下介绍主要控制参数。
rotateSpeed
控制鼠标左键拖拽旋转镜头速度。默认在第三人称视角模式下,鼠标左键拖拽会让镜头绕焦点所在轴进行旋转。rotateSpeed就是控制镜头的旋转速度。
作用:控制鼠标拖拽镜头旋转速度
默认值:1
建议值:0.5~10。越小拖拽反应越迟钝,越大拖拽反应越灵敏
用法:interaction.rotateSpeed = 2 或 setRotateSpeed()/getRotateSpeed()
zoomSpeed
控制鼠标滚轮缩放场景的速度。鼠标滚轮的滚动,会导致镜头的拉近和拉远,表现为场景的放大和缩小。zoomSpeed就是控制镜头的拉近/拉远的速度。
作用:控制鼠标滚轮镜头拉动速度
默认值:10
建议值:1~20。越小镜头移动越迟钝,越大镜头移动越灵敏
用法:interaction.zoomSpeed= 2 或 setZoomSpeed()/getZoomSpeed()
panSpeed
控制鼠标右键平移镜头的速度。鼠标右键拖拽会在垂直面内左右、上下平移镜头的位置。平移时,镜头的位置(position)和焦点(target)会同时移动,以保证镜头lookat的方向不变。panSpeed就是控制镜头平移的速度。
作用:控制鼠标右键拖拽平移镜头的速度
默认值:3
建议值:1~10。越小镜头移动越迟钝,越大镜头移动越灵敏
用法:interaction.panSpeed= 2 或 setPanSpeed()/getPanSpeed()
minDistance/maxDistance
通过鼠标滚轮拉近/拉远镜头位置的极值。这是非常有用的两个参数,尤其是maxDistance。当滚轮不断拉远镜头位置,最终会导致场景和镜头距离过远而彻底消失,有时候这让用户感觉不舒服。所以,最好的使用方法是适当的设置镜头位置极值,避免过远或过近产生的视觉不适。
这两个极值没有什么绝对的标准,应当根据场景情况而定。例如,一个尺寸为10,000见方的场景,可以将maxDistance设置为20,000~50,000左右,是合适的。最小值也是一样,假如target焦点盯在一个设备面板上进行lookat,则最小值设置为50可以保证镜头不至于离目标过近,产生不适感或进入近切面盲区。
作用:控制鼠标移动镜头位置的最近/最远点
默认值:minDistance为0,maxDistance为Infinity
建议值:根据场景大小而定
用法:interaction.minDistance = 20 或 setMinDistance()/getMinDistance()
yLowerLimitAngle/yUpLimitAngle
通过鼠标操作镜头在y轴位置的角度极值,也就是镜头可以俯视/仰视的角度极值,默认是正负90度(-Math.PI/2 ~ Math.PI/2)。0角度则是水平线位置。具体看下图:
这两个参数中,yLowerLimitAngle更加常用,主要用于控制镜头是否可以移动到水平面的下方。例如,将yLowerLimitAngle设置为0,则镜头只能在水平面上方移动,而不能进入水平面下方。主要用于控制镜头不可以看到物体的“底部”。例如一个建筑物、一个房间等场景,从下方向上方仰视是没有什么意义的,用yLowerLimitAngle参数可以进行控制,让用户的感觉更舒适。
作用:控制鼠标移动镜头俯视/仰视角度
默认值:yLowerLimitAngle为-Math.PI/2,yUpLimitAngle为Math.PI/2
建议值:保持默认值,或选用yLowerLimitAngle=0、 yUpLimitAngle=Math.PI/2的设置(此时只能俯视不能仰视)
用法:interaction.yLowerLimitAngle= -Math.PI/4 或 用getY***/setY***方法()
使用EditInteraction
EditInteraction是一个负责编辑的交互器。用户对3D对象的编辑主要是指通过鼠标对位置、角度、拉伸缩放进行编辑。由于x、y、z三个轴都有位置、角度、拉伸缩放的数值,因此如何在界面上设计这9个交互,是一个很麻烦的事。好在mono已经提供了比较清晰完善的解决方案。
操作EditInteraction
下面的代码是如何给network添加EditInteraction:
var defaultInteraction = new mono.DefaultInteraction(network);
var selectionInteraction = new mono.SelectionInteraction(network);
var editInteraction = new mono.EditInteraction(network);
network.setInteractions([defaultInteraction, selectionInteraction, editInteraction]);
下图是EditInteraction交互状态下的交互显示方式:
Position:鼠标沿着三个轴箭头拖拽,可以将物体沿着轴进行移动。x、y、z三个轴向的箭头分别用不同的颜色表示,如下图:
此外,拖拽三个直角扇形区域,也可以让物体沿着这个扇形区域所在的面进行移动。例如:蓝色扇形区域代表的是红色轴和绿色轴所组成的面,拖拽蓝色扇形即可让物体在这个面上自由拖拽移动,如下图:
Rotation:鼠标沿着三个弧线拖拽,可以将物体沿着弧线对应的角度方向进行旋转。x、y、z三个轴向的旋转弧线分别用不通的颜色表示,如下图:
Scale:鼠标拖住轴线根部的锥形体并沿着轴拖拽,可以将物体沿着轴对应的方向进行拉伸缩放。x、y、z三个轴向的锥形体分别用不通的颜色表示,如下图:
除了单独在某个轴向进行拉伸之外,还可以拖拽最中心的灰色立方块进行三个轴向的等比例拉伸:
此外,为了在拖拽过程中实时了解到位置、角度、拉伸缩放的数值变化,mono还在鼠标旁边显示了一个包含9个数值的、实时变化的表格,作为参考:
参数控制EditInteraction
通过setShowHelpers(false)可以控制是否显示箭头、弧线、锥体这些交互用的视觉元素。如果关闭,则不能通鼠标进行交互操作。一般不会这样使用。
通过setScaleable(boolean)可以控制是否显示拉伸缩放的三个“锥体色块”。如果不想让用户修改scale,可以采用这种方法关闭scale。显示效果如下:
通过setRotateable(boolean)可以控制是否显示拖拽旋转角度的弧形线。如果不想让用户修改物体的角度,可以采用这种方法关闭rotation。显示效果如下:
通过setTranslateable(boolean)可以控制是否可以拖拽箭头或扇形角来移动物体的位置。如果不想让用户修改物体的位置,可以采用这种方法关闭position。显示效果如下:
通过EditInteraction.setScaleRate(scaleRate)函数可以控制拉伸缩放的比例速度。越大拉伸的就越快,越小拉伸的就越慢。
以上几个函数可以混合使用,控制更精细的编辑效果。
EditInteraction中的默认拖拽行为
上面介绍的是用户用鼠标操作编辑helper产生的编辑行为。其实,EditInteraction模式下,用鼠标直接拖拽物体本身进行移动,也可以产生编辑效果。只是这种操作会有很大的歧义:拖拽物体移动,是修改水平面的位移,还是垂直面的位移?是要旋转还是拉伸?为了定义此时的编辑模式,mono定义了EditInteraction.setDefaultMode(defaultMode)函数来设置鼠标拖拽要编辑的模式,其中defaultMode可以是:
移动位置,可以选一个轴和两个轴的组合,包括:TranslateX、TranslateY、TranslateZ、TranslateXY、TranslateXZ、TranslateYZ
旋转:可以绕三个轴任意一个旋转,包括:RotateX、RotateY、RotateZ
拉伸缩放:可以沿三个轴任意一个轴拉伸,包括:ScaleX、ScaleY、ScaleZ
下面设置拖拽物体沿着水平面(X和Z轴所在的平面)进行移动:
EditInteraction editInteraction = new mono.EditInteraction(network);
editInteraction.setDefaultMode('TranslateXZ');
EditInteraction中的快捷键
EditInteraction内置了一些用于编辑用的快捷键,包括:
del:删除选中的对象。会有确认消息框弹出;
技巧:如何控制对象一起或部分移动?
在编辑状态下,很多时候一个大的物体是由许多小的物体一起组成的。这些小的物体可能已经通过ComboNode进行运算变成了一个大的物体,也可能以parent父子关系的形式独立存在。对于前者,它们已经变成了一个物体,选中、移动都是一个对象,很容易理解。对于后者,如果要让物体的一部分移动,或整体一起移动,就需要考虑如何实现了。
原则:在mono中,当移动parent对象时,所有的children及其子孙,都会跟着移动;而移动孩子,父亲不会跟着移动。这一点请一定牢记。
基于这一原则:如果选中多个孩子进行移动,就可以实现物体“部分移动”的效果;如果选中父亲进行移动,就可以实现“整体移动”的效果。
这样说起来很容易理解,但实际操作中,最终用户怎么能够知道哪个对象是“父对象”,哪个对象是“子对象”呢?如果不能准确的选中最根的根对象,移动某个孩子对象很容易把整个物体拖“散架”。为了解决这个问题,可以用这样的思路:当用户要进行“整体移动”时,无论用户选择了物体的哪个部分,都自动判断其根对象,并选中根对象。用一个while循环一直找到父对象为null就可以了。
有时候,根父对象不一定是个头最大、最直观的那个对象。此时我们也可以自定义谁是整个物体的“移动代言人”,给它单独设置一个client标记。当物体任何部分选中后,都自动找到它,设置为选中。
技巧:物体如何整体删除?
当物体由多个对象组成并用parent关系进行组织时,如果没有选中全选中所有对象,删除物体会发生残留,因为没有被选中的对象会残留下来。此时可以自动找到根节点,并用DataBox的removeByDescendant方法删除这个节点的所有子孙,这样就不会发生残留了。
4.4 使用交互事件
理解HTML事件机制
鼠标事件主要指双击、单击、拖拽等。我们经常需要对用户的这些鼠标操作进行响应,此时需要使用鼠标事件处理机制。鼠标、键盘事件机制本身是浏览器定义的,和mono并无直接关系。这里主要介绍如何通过处理鼠标事件,来完成3D应用中常见的一些需求。
鼠标键盘事件是通过html定义的addEventListener和removeEventListener来完成的。它的定义如下:
监听HTML事件方法
element.addEventListener(event, function, useCapture);
event定义了事件的名称,function是事件的响应函数,useCapture是捕获还是冒泡时间执行响应。具体相关用法请参阅相关w3c标准和教程。
下面的代码给html页面上id为’myBtn’的按钮添加了一个单击事件,点击后将id为’demo’的元素内容改为”Hello World”:
document.getElementById("myBtn").addEventListener("click", function(){
document.getElementById("demo").innerHTML = "Hello World";
});
在mono中,network是一个js对象,它本身并不是一个显示在页面中的DOM元素。要为network的canvas元素添加事件,可以通过network.getRootView()函数获得network在html页面中对应的顶层DOM元素,然后再添加事件。
下面的代码在network上添加了一个单击事件,并在控制台打印消息:
network.getRootView().addEventListener('click', function(e){
console.log('network clicked');
});
以下列出了常用的鼠标和键盘事件。更多关于DOM事件的介绍,请参阅网络。
鼠标事件
键盘事件
拖动事件
常用技巧
双击到了哪个物体?
在3D场景中,经常会需要判断用户双击了哪一个物体。例如:双击一个门将其打开、双击摄像头播放一段视频,等等。要做到这些,首先要能判断用户双击了哪个3D物体。
当我们在network上随意位置进行双击,实际上是从这一点发出了一条无限远的“射线”,凡是在这条射线上的物体,都是可以被双击到的物体。因此双击一个点可能会点击到很多物体。通常我们会选取离眼睛(镜头)最近的作为目标,但也有例外:例如我们可能会忽略离眼睛很近的透明玻璃窗,而选取玻璃后面的机柜。这些业务逻辑,都需要我们用代码来进行判断。
好在network已经提供了network.getElementsByMouseEvent(event)方法,可以直接返回所有在双击射线上穿过的物体的数组,按从近到远的顺序。这样我们就可以直接处理了。
下面的代码把双击到的全部物体拿到并打印出:
network.getRootView().addEventListener('dblclick', function(e){
var objects= network.getElementsByMouseEvent(e);
console.log(objects);
});
需要注意的是:这里的数组中的object并不直接是3D对象,而是一个封装了包含3D对象在内更多双击点信息的数据结构,主要信息包括:
Object {
distance: 175.17295246548093 //双击点到镜头的距离
element: //双击点的3D对象
face: M //双击点所在的物体面
faceIndex: 4 //双击点物体面的索引值
side: -1 //双击点物体表面是外面还是内面
}
可以根据这些信息,做更复杂的双击点判断。例如根据距离、反正面等进行判断。下面代码继续改进,判断双击了哪个机柜(离眼睛最近的一个):
//模拟创建一个机柜的立方体
var cube=new mono.Cube(100, 100, 100);
cube.s({
'm.type': 'phong',
'm.texture.image': 'rack.jpg',
});
//为立方体添加type='rack'的client属性,作为机柜物体的标记
cube.setClient('type', 'rack');
box.add(cube);
//添加network的双击事件
network.getRootView().addEventListener('dblclick', function(e){
var objects = network.getElementsByMouseEvent(e);
var clickedElement;
if(objects){
for(var i=0;i<objects.length;i++){
var object=objects[i];
//注意:object并不是3D对象,而是一个包含了很多事件信息的对象。3D对象存储在object.element变量中
var element=object.element;
if(element && element.getClient('type') === 'rack'){
clickedElement=element;
break;
}
}
}
if(clickedElement){
console.log('clicked rack: ' + clickedElement);
}
});
下面的函数可以用于判断“第一个”被双击到的物体。其中,忽略了Billboard类型的物体。一般Billboard作为信息显示的辅助对象,不参与物理实体的碰撞检测。
findFirstObjectByMouse: function(network, e){
var objects = network.getElementsByMouseEvent(e);
if (objects.length) {
for(var i=0;i<objects.length;i++){
var first = objects[i];
var object3d = first.element;
if(! (object3d instanceof mono.Billboard)){
return first;
}
}
}
return null;
}
下面是一个实际应用的3D场景。在隔着玻璃双击机柜门时,依旧可以实现打开机柜大门,而不受眼前的玻璃窗影响,实现了“隔山打牛”的效果。这也是利用了上面的逻辑判断实现的。
做一个自己的Tooltip
在mono中,并没有内置的tooltip机制。但是根据上面介绍的事件监听机制,要自己完全动手做一个也并不难。大概思路是:
先在页面上创建一个不可见的div对象,作为tooltip文字显示标签;
为network添加一个mousemove事件,监听鼠标在画布上的移动;
当鼠标指向位置没有物体,继续隐藏div;
当鼠标指向一个3D物体,将3D物体的信息(例如name等)显示在div上,并将div位置移动到鼠标所在位置,设置为可见
代码很简单,可以看一下:
/随机产生100个立方体并设置不同的name以便进行测试
for(var i=0; i<100; i++){
var cube=new mono.Cube(10, 10, 10);
cube.setName('node '+i);
cube.s({
'm.type': 'phong',
'm.texture.image': 'box.jpg',
});
cube.setPosition(Math.random()*100-50, Math.random()*100-50, Math.random()*100-50);
box.add(cube);
}
//创建一个隐藏的div对象作为tooltip
var tipDiv=document.createElement('div');
tipDiv.style.display = 'block';
tipDiv.style['font-family']='Calibri';
tipDiv.style['font-size']='12px';
tipDiv.style.position = 'absolute';
tipDiv.style['padding']='5px';
tipDiv.style.background='rgba(144,254,144,0.85)';
tipDiv.style['border-radius']='5px';
tipDiv.style.visibility='hidden';
document.body.appendChild(tipDiv);
//函数用于找到鼠标指向的第一个物体
var findFirstElement = function(e){
var objects = network.getElementsByMouseEvent(e);
if (objects.length) {
return objects[0].element;
}
}
network.lastPointedElement;
//添加mouseover监听器
network.getRootView().addEventListener('mousemove',function(e){
tipDiv.style.top=(e.y+15)+'px';
tipDiv.style.left=(e.x+15)+'px';
var pointedElement=findFirstElement(e);
if(pointedElement===network.lastPointedElement) {
return;
}
network.lastPointedElement=pointedElement;
//如果有物体,显示div并显示其信息;否则,隐藏div
if(pointedElement){
tipDiv.innerHTML=pointedElement.getName();
tipDiv.style.visibility='visible';
}else{
tipDiv.style.visibility='hidden';
}
});
显示效果如下图:
当然这个tooltip并不完美。首先显示样式简陋,这个读者可以自行完善。其次,tip的显示非常“刚性”,没有像操作系统一般的tooltip那样有一个延迟的过程。要完善这一点,可以通过启动一个定时器,延迟一点时间(例如1~2秒)后再显示div。这里有一点复杂的是,当timer尚未触发,而新的事件又发生时,应该首先清除原来的定时器。具体代码不再列出,有兴趣的读者可以自行尝试。