4。 利用脚本编写自定义行为
在VRML中,利用Script节点(脚本节点)定义用户自定义行为,所谓定义即用脚本描述语言(Scripting Language)编写脚本的过程。VRML97支持的脚本描述语言目前有两种:Java和EMCAScript(这是JavaScript标准化后的名称),关于这两种语言本身,请参考相应参考书,VRML97标准中定义了它们和VRML的接口方法。应提请注意的是:VRML是基于节点的语言,所以脚本也是封装在Script这个特殊节点中的。这里我们不过多讨论脚本描述语言的细节,主要讨论把脚本集成到VRML文件中的方法。
上面我们曾把接触检测器touchBox 和视点view2直接通过路径连接起来,现在要定义我们指定的行为,就需要在二者之间插入一个脚本节点,也就是让路径绕个弯:
ROUTE touchBox.isActive TO touchScript.touchBoxIsActive
ROUTE touchScript.bindView2 TO view2.set_bind
其中的脚本节点touchScript有一个事件人口touchBoxIsActive和一个事件出口bind_View2,前者接收来自接触检测器touchBox的事件,然后经自己的脚本处理后,把结果发送给视点节点view2:
DEF touchScript Script {
eventIn SFBool touchBoxIsActive #入口
eventOut SFBool bindView2 #出口
url"javescript: #脚本
function touchBoxIsActive(active) { #与入口同名的函数被调用
bindView2= TRUE; #返回到出口
}"
}
小结:
本节建立的虚拟境界并不复杂,但涉及到了VRML2.0最基础性的功能和概念:利用检测器产生事件、利用路由传递事件以及利用脚本编写自定义行为,掌握了这些内容也就掌握了VRML2.0的核心。在后面的几节中,我们将探索一些专题性的有趣功能,而本节是基础,因而必须透彻理解。
本节的完整代码是:
#VRML V2.0 utf8
DEF view1 Viewpoint {
position 0 0 20
description "view1"
}
DEF view2 Viewpoint {
position 5 0 20
description "view2"
}
Group {
children [
DEF box Transform {
translation 5 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Box {}
}
DEF touchBox TouchSensor {}
]
}
DEF sphere Transform {
translation 0 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 0 1 0
}
}
geometry Sphere {}
}
]
}
DEF cone Tranform {
transaltion -5 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 0 0 1
}
}
geometry Cone {}
}
]
}
] #end of Group children
}
DEF touchScript Script {
eventIn SFBool touchBoxIsActive
eventOut SFBool bindView2
url "javascript :
function touchBoxIsActive (active) {
bindView2 = TRUE;
}"
}
ROUTE touchBox.isActive TO touchScript.touchBoxIsActive
ROUTE touchScript.bindView2 TO view2.set_bind
5.事件流程与小结
下面我们整理一下事件流程:
(1)用户在方块上按下鼠标左键。
(2)接触检测器发出一个“TRUE”事件。
(3)此事件进入脚本节点touchScript的事件入口touchBoxIsActive.
(4)调用脚本函数touchBoxIsActive(注意函数并没有判断入事件的值)。
(5)函数向touchScript的事件出口bindView2发送一个“TRUE”事件(还可以进行其它判断或执行其它事件)。
(6)view2节点收到“TRUE”事件,成为当前视点。按照VRML约定,“认为”上述事件是同时发生的,也就是这些事件的时间戳相同。
(7)若用户松开鼠标左键,则接触检测器发出一个“FALSE”事件,此事件同样引起脚本函数调用并发送“TRUE”事件,所以view2仍然保持为当前视点。
关于这个Script节点,请注意一下几点:
(1)它的事件入口touchBoxIsActive和事件出口bindView2是自定义的,其它VRML节点的域和事件都是固定的。
(2)这里定义的事件入口touchBoxIsActive(即入事件)和事件出口bindView2(即出事件)的类型都是SFBool(单值布尔型),它们与touchBox的事件出口isActive和view2的事件入口set_bind的类型保持一致。
(3)“url”是脚本节点的一个域,可以直接包含脚本,也可以包含一个或多个用URL地址指示的脚本,若有多个地址,则按照先后次序获取第一个可得到的脚本。
(4)脚本是以函数(function)的形式给出的,函数名touchBoxIsActive 与事件入口的名称相同,这是和ECMAScript语言的接口约定,表示相应事件入口收到事件后调用此函数进行处理。
3。事件传递
下面我们把触发(用鼠标箭头按动方块)和场景变化(视点切换)这两件事情联系起来,在场景图中,除节点构成的层次体系外,还有一个“事件体系”,事件体系由相互通讯的节点组成。能够接收事件的节点都应具有事件入口(eventIn),如果它要接收多种类型的事件(称为入事件),它就应该具有多个事件入口,也就是说,事件入口象节点的域一样是有类型的。同样,发送事件的节点应有事件出口(eventOut),事件出口也是有类型的。例如ViewPoint节点就有一个事件入口set_bind,当向此事件送入一个值“TRUE”(即所谓的入事件)时,该viewpoint节点成为当前视点。又如,接触检测器TouchSensor有一个事件出口isActive,当受到用户触发后它就从此出口送出一个“TRUE”(即所谓的出事件),补充一句,在下一个事件发送之前,此事件一直保存在事件出口中(作为记录)。
事件出口和事件入口通过路径相连,这就是VRML文件中除节点以外的另一基本组成部分:ROUTE 语句。ROUTE语句把事件出口和事件入口联系在一起,从而构成事件体系。在这里,我们是把接触检测器touchBox的事件出口isActive连接到视点节点view2的事件入口set_bind:
ROTUE touchBox.isActive TO view2.set_bind
好了!现在我们得到的VRML文件是:
#VRML V2.0 utf8
DEF view1 Viewpoint { #视点
position 0 0 20
description "view1"
}
DEF view2 Viewpoint {
position 5 0 20
description "view2"
}
Group {
children [
DEF box Transform {
translation 5 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Box {}
}
DEF touchBox TouchSensor {} #触感
]
}
DEF sphere Transform {
translation 0 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 0 1 0
}
}
geometry Sphere {}
}
]
}
DEF cone Transform {
translation -5 0 0
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 0 0 1
}
}
geometry Cone {}
}
]
}
] #end of Group children
}
ROUTE touchBox.isActive TO view2.set_bind #传递
把这个文件调入浏览器,然后把鼠标指向方块并按下左钮(先别松开!),可以看到视点已经变为view2,内部的机制我们已经很清楚:左钮按下时方块节点的接触检测器被触发,接着接触检测器从事件出口isActive送出一个事件“TRUE”,这个事件通过路由进入视点节点view2的事件入口set_bind,view2收到“TRUE”后成为当前视点,所以在我们眼前场景发生了变化。
2.视点
最常见的变化是视点的变化,在我们的第一个境界中你可能已经体验到视点变化:当你拖动鼠标或按动箭头键时(按照VRML术语,称为航行),虚拟境界就会旋转或缩放,这实际上是在调整你的视点位置或视角。在虚拟场景的重要位置可以定义视点节点(ViewPoint),它们是境界作者给用户推荐的上佳观赏方位,在CosmoPlayer浏览器中,用户就可以通过鼠标右键选择作者推荐的各个视点。这里我们定义两个视点节点:
DEF view1 Viewpoint { #“view1”是编程时引用的名字
position 0 0 20
description "View1" #“View1”是浏览器上显示的名字
}
DEF view2 Viewpoint {
position 5 0 20
description "view2"
}
我们的潜在目的是使用户可以通过触发开关节点来切换视点。现在先研究一下这两个视点节点,其中的坐标表示视点在场景中的位置,坐标的单位是米,这在前面已经提到过,视点的名称将会在浏览器菜单中提示出来供用户选择。把上述视点说明加入helloworld2.wrl中(放在Group节点之前),并把其中的方块节点修改成可触发节点:
DEF box Tranform {
children [
Shape { .... Box ...}
DEF touchBox TouchSensor {} #定义触感
]
}
把修改过的文件另存为“touchme.wrl”。
上一节我们学习了用几何体建立虚拟境界以及为几何体赋予色彩和材质的方法,这样建立的虚拟境界是静态的。这一节我们将使一个几何体(为了更具一般性,下面我们称之为对象)能够根据用户动作做出反应,即交互能力,这是VRML2.0最突出的特征。
1。检测器
在VRML中,检测器(Sensor)节点是交互能力的基础。检测器节点共九种。在场景图中,检测器节点一般是以其它节点的子节点的身份而存在的,它的父节点称为可触发节点,触发条件和时机由检测器节点类型确定。
接触检测器( TouchSensor)是最常用的检测器之一,最典型的应用例子是开关。其它检测器将在后续教程中陆续介绍。这里我们定义一个开关节点lightSwitch(这是一个组节点),并定义一个接触检测器作为它的子节点:
DEF lightSwitch Group {
children [
各几何造型子节点...
DEF touchSensor TouchSensor {} #接触检测器
]
}
这样开关节点lightSwitch就是一个可触发节点。当然,检测器存在的理由是它被触发时能够引起某种变化,所以在更深入讨论开关节点之前,我们先讨论一下场景变化。