开发指南 – TWaver HTML5 3D

TWaver
Published on 2024-11-21 / 325 Visits
0
0

第一章 前期准备

阅读之前

如果您准备使用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,否则就是有问题。

522

需要提醒您一点:网页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浏览器打开页面的效果:

520

如果您能看到截图上的图形,恭喜,您已经完成了第一个mono程序。

Note:
请确认默认浏览器程序版本符合mono的要求,尤其是IE,要求IE11或Edge,低版本IE不能支持webGL。如果不能正常看到上述程序,请检查浏览器程序及版本,是否符合mono要求。

如果发现程序异常,在浏览器中可以直接按F12查看异常信息,进行错误定位和调试:
523

程序说明

简单解释一下这个程序的内容。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。
f3d3572c11dfa9ecc8f784b362d0f703908fc1bd

向量的向量积性质:

  • ∣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矩阵。记作:
5d6034a85edf8db16911bfc90f23dd54564e7464
这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的思路把数据、绘制、交互三者进行了分离设计

24

具体说:

  • 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作为数据管理者和画布渲染者独立工作、各司其职。

26

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(如下图)。
33

开发工具及调试

JavaScript开发可以选择使用Eclipse、NetBeans、Visual Studio等大型IDE工具,也可以选择直接使用文本编辑工具如EditPlus、UltraEdit等。前者可以提供一些自动提示、集成调试等功能,缺点是程序比较庞大笨重。后者则轻量快速直接,但无自动提示等功能。开发者可以根据自身情况合理选择代码编辑工具。

对于开发测试浏览器,推荐使用Google的Chrome。Chrome速度快、对WebGL支持好、调试方便,是WebGL开发者首选浏览器。

在部署测试页面程序时,可以选择用IDE中的内置的调试方法,也可以直接放入如Tomcat等Web服务器中并用浏览器直接访问。

Chrome浏览器提供了内置的调试工具,非常方便。可以直接按F12打开开发工具窗口,其中提供了控制台、查看页面元素、断点跟踪、性能测试等功能(见下图)。
34
更多关于Chrome开发工具的功能介绍,请参考Google官方文档:https://developers.google.com/chrome-developer-tools/

理解坐标系

在mono中,3D场景的坐标是一个空间直角坐标系。直角坐标系是指在原点O,做三条互相垂直的数轴,它们都以O为原点,分别叫做x轴(横轴)、y轴(纵轴)、z轴(竖轴),统称坐标轴。在mono中,坐标系方向遵循右手法则,即以右手拇指、食指、中指相互交叉,拇指指向x轴正方向,食指指向y轴正方向,则中指指向的即是z轴正方向。

righthand

简单说:从左向右的水平轴是x轴,垂直向上的轴是y轴,从屏幕指向人面部的水平轴是z轴。再次示意如下图:

524

在mono中,空间的一个点由(x, y, z)三个数值组成。下图展示了不同数值其具体表示的点的位置。熟练掌握坐标点的位置,对mono开发具有重要的作用。请认真仔细观察以下图中空间几个点的位置和对应坐标,是否和您的理解一致。如果能够正确理解,则说明已经正确掌握了mono中3D的坐标概念。

points_in3d

除了位置以外,3D物体还有旋转角度。一个3D物体可以在x、y、z三个轴向分别进行旋转。

旋转角度的方向遵守“右手法则”:右手拇指指向要旋转的轴,其余四指的方向,就是角度旋转的方向。如下图:

525

如果旋转角度为正值,则沿图中箭头方向旋转;如为负值,则反方向旋转。

世界坐标系与本地坐标系

3D中的坐标系分为世界坐标系和本地坐标系。世界坐标系(World Coordinate System)是系统的绝对坐标系,是指所有3D物体所在的空间中全局的、绝对的坐标。而本地坐标系(Local Coordinate System)则是指相对以某一3D物体自身为原点的局部坐标系,物体的旋转或平移等操作都是围绕局部坐标系进行的。

例如一个飞速行走中的汽车,汽车的位置信息可以通过世界坐标系来定义其在地面空间的位置,而汽车雨刮的摆动位置和角度则是相对汽车的本地坐标系进行指定。这样有利于使用和理解的方便。

世界坐标系和本地坐标系可以进行转换。

local_global_axes

数值单位

在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>

执行代码后运行效果如图。
77

设置network背景颜色

默认network使用白色作为背景。修改network空白区域的背景颜色,可以使用函数network.setClearColor()完成。

var box = new mono.DataBox();
var network= new mono.Network3D(box, null, monoCanvas);
network.setClearColor('#39609B');

img3

自适应缩放

自适应缩放会自动调整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);
}

img14

设置雾霾效果和指数:

network.setUseFog(true);
network.setFogDensity(1);

img15

设置雾霾指数为2:

network.setUseFog(true);
network.setFogDensity(1);

img17

继续增大雾霾指数:

network.setUseFog(true);
network.setFogDensity(3);

img16

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]);

img1

如图,物体可以被选中、被拖拽,编辑交互已经生效。

可以通过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,点击物体后会被选中,显示为一个绿色高亮边框,如下图:

img6

方法一:设置交互

我们可以重新设置交互数组,不再设置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就是控制镜头的旋转速度。

img7

  • 作用:控制鼠标拖拽镜头旋转速度

  • 默认值:1

  • 建议值:0.5~10。越小拖拽反应越迟钝,越大拖拽反应越灵敏

  • 用法:interaction.rotateSpeed = 2 或 setRotateSpeed()/getRotateSpeed()

zoomSpeed

控制鼠标滚轮缩放场景的速度。鼠标滚轮的滚动,会导致镜头的拉近和拉远,表现为场景的放大和缩小。zoomSpeed就是控制镜头的拉近/拉远的速度。

img9

  • 作用:控制鼠标滚轮镜头拉动速度

  • 默认值:10

  • 建议值:1~20。越小镜头移动越迟钝,越大镜头移动越灵敏

  • 用法:interaction.zoomSpeed= 2 或 setZoomSpeed()/getZoomSpeed()

panSpeed

控制鼠标右键平移镜头的速度。鼠标右键拖拽会在垂直面内左右、上下平移镜头的位置。平移时,镜头的位置(position)和焦点(target)会同时移动,以保证镜头lookat的方向不变。panSpeed就是控制镜头平移的速度。

img10

  • 作用:控制鼠标右键拖拽平移镜头的速度

  • 默认值:3

  • 建议值:1~10。越小镜头移动越迟钝,越大镜头移动越灵敏

  • 用法:interaction.panSpeed= 2 或 setPanSpeed()/getPanSpeed()

minDistance/maxDistance

通过鼠标滚轮拉近/拉远镜头位置的极值。这是非常有用的两个参数,尤其是maxDistance。当滚轮不断拉远镜头位置,最终会导致场景和镜头距离过远而彻底消失,有时候这让用户感觉不舒服。所以,最好的使用方法是适当的设置镜头位置极值,避免过远或过近产生的视觉不适。

这两个极值没有什么绝对的标准,应当根据场景情况而定。例如,一个尺寸为10,000见方的场景,可以将maxDistance设置为20,000~50,000左右,是合适的。最小值也是一样,假如target焦点盯在一个设备面板上进行lookat,则最小值设置为50可以保证镜头不至于离目标过近,产生不适感或进入近切面盲区。

img11

  • 作用:控制鼠标移动镜头位置的最近/最远点

  • 默认值:minDistance为0,maxDistance为Infinity

  • 建议值:根据场景大小而定

  • 用法:interaction.minDistance = 20 或 setMinDistance()/getMinDistance()

yLowerLimitAngle/yUpLimitAngle

通过鼠标操作镜头在y轴位置的角度极值,也就是镜头可以俯视/仰视的角度极值,默认是正负90度(-Math.PI/2 ~ Math.PI/2)。0角度则是水平线位置。具体看下图:

img12

这两个参数中,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交互状态下的交互显示方式:
img1

Position:鼠标沿着三个轴箭头拖拽,可以将物体沿着轴进行移动。x、y、z三个轴向的箭头分别用不同的颜色表示,如下图:
img2
此外,拖拽三个直角扇形区域,也可以让物体沿着这个扇形区域所在的面进行移动。例如:蓝色扇形区域代表的是红色轴和绿色轴所组成的面,拖拽蓝色扇形即可让物体在这个面上自由拖拽移动,如下图:

img4

Rotation:鼠标沿着三个弧线拖拽,可以将物体沿着弧线对应的角度方向进行旋转。x、y、z三个轴向的旋转弧线分别用不通的颜色表示,如下图:
img3

Scale:鼠标拖住轴线根部的锥形体并沿着轴拖拽,可以将物体沿着轴对应的方向进行拉伸缩放。x、y、z三个轴向的锥形体分别用不通的颜色表示,如下图:
img6
除了单独在某个轴向进行拉伸之外,还可以拖拽最中心的灰色立方块进行三个轴向的等比例拉伸:
img12

此外,为了在拖拽过程中实时了解到位置、角度、拉伸缩放的数值变化,mono还在鼠标旁边显示了一个包含9个数值的、实时变化的表格,作为参考:
img7

参数控制EditInteraction

通过setShowHelpers(false)可以控制是否显示箭头、弧线、锥体这些交互用的视觉元素。如果关闭,则不能通鼠标进行交互操作。一般不会这样使用。

通过setScaleable(boolean)可以控制是否显示拉伸缩放的三个“锥体色块”。如果不想让用户修改scale,可以采用这种方法关闭scale。显示效果如下:
img8

通过setRotateable(boolean)可以控制是否显示拖拽旋转角度的弧形线。如果不想让用户修改物体的角度,可以采用这种方法关闭rotation。显示效果如下:
img9

通过setTranslateable(boolean)可以控制是否可以拖拽箭头或扇形角来移动物体的位置。如果不想让用户修改物体的位置,可以采用这种方法关闭position。显示效果如下:
img11

通过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事件的介绍,请参阅网络。

鼠标事件

属性

描述

onclick

当用户点击某个对象时调用的事件句柄。

oncontextmenu

在用户点击鼠标右键打开上下文菜单时触发

ondblclick

当用户双击某个对象时调用的事件句柄。

onmousedown

鼠标按钮被按下。

onmouseenter

当鼠标指针移动到元素上时触发。

onmouseleave

当鼠标指针移出元素时触发

onmousemove

鼠标被移动。

onmouseover

鼠标移到某元素之上。

onmouseout

鼠标从某元素移开。

onmouseup

鼠标按键被松开。

键盘事件

属性

描述

onkeydown

某个键盘按键被按下。

onkeypress

某个键盘按键被按下并松开。

onkeyup

某个键盘按键被松开。

拖动事件

事件

描述

ondrag

该事件在元素正在拖动时触发

ondragend

该事件在用户完成元素的拖动时触发

ondragenter

该事件在拖动的元素进入放置目标时触发

ondragleave

该事件在拖动元素离开放置目标时触发

ondragover

该事件在拖动元素在放置目标上时触发

ondragstart

该事件在用户开始拖动元素时触发

ondrop

该事件在拖动元素放置在目标区域时触发

常用技巧

双击到了哪个物体?

在3D场景中,经常会需要判断用户双击了哪一个物体。例如:双击一个门将其打开、双击摄像头播放一段视频,等等。要做到这些,首先要能判断用户双击了哪个3D物体。

当我们在network上随意位置进行双击,实际上是从这一点发出了一条无限远的“射线”,凡是在这条射线上的物体,都是可以被双击到的物体。因此双击一个点可能会点击到很多物体。通常我们会选取离眼睛(镜头)最近的作为目标,但也有例外:例如我们可能会忽略离眼睛很近的透明玻璃窗,而选取玻璃后面的机柜。这些业务逻辑,都需要我们用代码来进行判断。

好在network已经提供了network.getElementsByMouseEvent(event)方法,可以直接返回所有在双击射线上穿过的物体的数组,按从近到远的顺序。这样我们就可以直接处理了。

img16

下面的代码把双击到的全部物体拿到并打印出:

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场景。在隔着玻璃双击机柜门时,依旧可以实现打开机柜大门,而不受眼前的玻璃窗影响,实现了“隔山打牛”的效果。这也是利用了上面的逻辑判断实现的。

img18

做一个自己的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';
	}
});

显示效果如下图:
img19

当然这个tooltip并不完美。首先显示样式简陋,这个读者可以自行完善。其次,tip的显示非常“刚性”,没有像操作系统一般的tooltip那样有一个延迟的过程。要完善这一点,可以通过启动一个定时器,延迟一点时间(例如1~2秒)后再显示div。这里有一点复杂的是,当timer尚未触发,而新的事件又发生时,应该首先清除原来的定时器。具体代码不再列出,有兴趣的读者可以自行尝试。



Comment