tuo-three2引入MVC架构的应用

简述

tuo-three2的发展其实已经遇见了瓶颈,tuo-three2的定位是3D交互和渲染引擎。我们给tuo-three2添加了很多基础插件,然而在准备把tuo-three2的应用做进一步拓展时,有一些插件对新应用的兼容性就开始表现得十分吃力了。这里指的十分吃力是指,它往往牵一发而动全身,我们越来越需要付出更大的代价来维护一个插件的可靠性。例如,就拿简单的平移一个对象来说,不同背景的3D对象在做移动时,有时候会需要做不同的操作。在有的应用中,一个3D对象可能和其他3D对象是绑定在一起的,按照该应用的逻辑,当你移动A时,可能你就必须移动B。这种情况下,如果我们只是单纯的移动A,可能就会导致对整个应用关系的一种破坏。

转变设计模式

当然我们也可以考虑给tuo-three2中负责平移的插件添加代码,让它能够针对特殊情况特殊处理。但如果遇见的应用类别越来越多时,维护这个平移插件就会很吃力了。因此,我们准备采取的方案是,让应用各自做自己插件。我们放开对不同应用的数据统一性的要求,让不同的应用维护自己的数据结构。与此同时,让这些应用自己来维护自己的交互插件。 tuo-three2要做的是提供做插件的一些基本工具和原料,然后再开放相应接口就行了。tuo-three2所有的插件都是可插拔的,有特殊要求的应用完全可以把tuo-three2的一些原始插件全部放弃,去重写自己的插件。

对于tuo-three2来说,我们做了一个设计模式的改变。我们会更多的开始去应用插件的动态插拔。当进入某些应用的不同模式时,我们可以去调整整体插件的配置(例如,增加一些插件,减少一些插件),而不再是去改变某一个插件的配置。

由应用自身去维护自己的应用数据结构和交互插件。虽然tuo-three2自身能够直接处理很多3D可视对象的交互问题,但从今天开始,对于一切引用tuo-three2的应用,我们鼓励他们去创建一层自己的数据结构。让tuo-three2仅仅负责做3个事情:

  1. 3D渲染
  2. 插件管理员
  3. 插件公共原材料仓库

以上三条是我们对tuo-three2的最新定位。

整体架构

引用tuo-three2的应用推荐采用如下的应用架构:

tuo-three2的wireframe插件

tuo-three2的wireframe插件的设计考虑到其可能引入与很多插件间的干涉问题,(因为它涉及到隐藏3D对象的操作,而我们规定了可视性操作只能由visibility插件负责)因此本插件将充分引用幻象的概念来进行设计。

对3D主对象设置wireframe时,需要面临2种情况。第一,是被处理对象的coupled === true时,我们需要检索到其第一个coupled !== true的父对象。并看其父对象是否配置了selfwireframe = ture。如果配置了,则通过生成A类幻象来解决问题。如果没有则参照下一条策略。第二,对被处理对象包括其子对象中的每一个mesh对象生成一个B类幻象。下文将解释A,B类幻象。

A类幻象直接与一个group对象关联,其几何体和材质来自该group对象的第一个line子对象(isLine === true)。同时,它也会监听该line对象的材质的color属性,和group对象的tuoVisible属性。A类幻象会吧raycaster触发点传递给该group

B类幻象直接与一个mesh对象关联,其几何体和材质都来自该mesh,不同点在与它的材质的wireframe = true。B类对象将会监听这个mesh的材质的color属性和tuoVisible属性。B类对象会把raycaster触发点传递给该mesh

tuo-three2的visibility插件

tuo-three2的visibility插件设计需要考虑以下几个要点。

  1. 存在其他插件对3D对象可视性操作的情况,不能与其他插件的可视性操作冲突。
  2. 存在幻象的机制,因此对物体可视性配置时需要充分考虑幻象的设计准则。
  3. tuo-three2重新定义了对group对象的visibility操作(可以参考这里的第11条)。
  4. visibility插件需要对自己每一个把visibility操作置为false的操作做记录,以便于对其进行恢复。
  5. visibility插件需要监听对象的移除事件,以此来更新自己在4中的记录。每一个对象被移除,visibility插件都需要遍历其所有的非group子对象,并移除其设置了visible = false / tuoVisible = false的记录。
  6. 除了visibility插件外,禁止其他插件对主3D对象做可视性操作。

tuo-three2主对象中3D对象之间的关系

主对象中的一些兄弟结点有可能存在相互关联的关系。也就是说,它们不独立。例如一个基于topo结构的正方体,其线,点,面其实是相关联的。至少位姿是必须同步的。我们给3D对象定义了一个coupled属性来标志这种关联。例如,如果一个对象的coupled === true,那就意味着它的几何属性与位姿属性都与它的某个或某些兄弟结点相耦合。当有的插件需要操作这样coupled === true的对象时,就需要注意了。因为你需要留意其兄弟结点可能也应该跟着变化。

有的插件需要对一个对象做操作时,需要首先检查其是否存在coupled === true.如果存在这样的情况,则需要反向去寻找其第一个coupled !== true的父结点。去检查这个父结点是否存在响应此操作的特殊配置。

tuo-three2引入幻象概念

基于好几种客观存在的需求,我们有必要对3D对象的可视性做一个重新定义。用以区分渲染层面上的可视性和应用上的可视性。例如,当我们引入“渲染代理”概念的时候,当使用“替代3D辅助对象“的时候,我们都需要将主3D对象设置为“不可见”。但对于其它操作,却都是可见的,因为此时此刻的该对象可能有了一个“代理”,有了一个“替身”,或者说“幻象”。我们出于各种目的,给一个3D对象设置了“幻象”。这有可能是为了节省渲染资源,有可能为了突出该对象的某个方式的渲染特效(例如动态变形,实体线框),这时候,它的“幻象”就是用户真正看到的图像。

“幻象”的实现方式类似“3D辅助对象”,相比于“3D辅助对象”,它主要是应用场景不一样。“3D辅助对象”主要是附加在3D主对象上,而“幻象”是替换掉了3D主对象来执行渲染运算。“幻象”对象的设计规范相比于“3D辅助对象”多出来几条规定。

3D渲染代理(幻象)对象设计规范

  1. 所有幻象对象都会配置一个isVirtual标志。即object.isVirtual = true。
  2. 系统通过utils.util.getReal函数提供获取真实对象的方法。此函数参照第3条规定做检索算法。
  3. 幻象对象可以通过以下规则来检索到其相对应的主对象,第一,通过其realObject的属性值。第二,如果第一条失效,则通过冒泡算法迭代的检索其父对象,直到找到一个有配置realObject属性的对象,或者一个非辅助对象为止(object.isVirtual === undefined)。
  4. 幻象对象需要自己定义在被选中时传递给主对象被选中位置的方法。此方法定义在函数object.convertRaycaster中。此函数接受THREE.Raycaster.intersectObject函数的返回值的单位元素为参数,然后返回一个新的类THREE.Raycaster.intersect函数返回值的对象。如果辅助对象不存在convertRaycaster函数,则被认为此辅助对象不响应主raycaster插件的检测,并被直接无视掉。即主raycaster插件不会发布hoverObject消息。
  5. 幻象对象每次被渲染的时候,都会检查主对象的一些属性的值,然后根据这些值来对自身的属性做一些调整。
  6. 与幻象对应的主对象用一个新的属性名tuoVisible来标识可视性。幻象对象可以通过监控这个属性来决定自身的可视性。因此,对于tuo-three2中的主对象有一个这样的规定:如果该对象的tuoVisible定义了,那么需要根据tuoVisible的值来判断其可视性。否则,按该对向的visible属性来判断。即
    realVisible = object.tuoVisible === undefined ? object.visible : object.tuoVisible
    与此同时,要真正的把某个对象置为不可见,也需要首先查看该对象是否存在tuoVisible属性。即,如果该对象的tuoVisible !== undefined, 则需要设置此属性的值,然后再判断其是否存在幻象,来判断是否需要继续设置visible的值。
  7. 存在幻象的主对象的virtualStack存在且长度大于零。即Array.isArray(object.virtualStack) === true && object.virtualStack.length > 0。6中,可以通过主对象的此属性来判断其是否存在幻象。
  8. virtualStack是用来管理一个主对象存在多个幻象的情况而存在的。我们规定,每一个主对象在同一时间只能激活一个幻象。而且每次被激活的幻象都是virtualStack中最新加入的幻象。
  9. 当一个幻象被移除时,需要首先检查主对象的virtualStack中是否还存在幻象,如果存在则使用上一个最新加入的幻象。如果没有则以主对象的“本象”来显示。
  10. 创建新幻象时,需要通过继承/core/VirtualObject来创建。并且需要定义里面的attach函数和detach函数。这两个函数分别定义幻象3D对象的创建和移除方法。实现新的VirtualObject的定义后,再通过VirtualObject.link函数为主对象添加幻象。在需要移除幻象时,可以通过VirtualObject.unlink函数来实现。
  11. 本系统在需要直接配置一个Group对象的可视性时。除了对该Group对象可视性进行设置外,还需要遍历其子结点,然后单独对每一个对象的可视性进行配置。这么做的目的是为了减少幻象对象可视性系统的复杂度。

注意1:幻象对象与3D辅助对象的主要差别是,一个主对象的幻象一旦存在,则该对象一定为渲染性的不可见。即object.visible === false为真。

注意2:幻象对象的生命周期由插件来管理。某个插件处于某个目的为主对象创建了幻象,那么当幻象需要移除时,也由该插件定义移除方法。

tuo-three2基于3D对象的raycaster

不同于raycaster插件是基于view且面向所有主对象而存在,这里提到的raycaster只面向特定几个对象做检测。基于3D对象的raycaster是由个别插件对辅助3D对象的需求,而这些辅助对象需要单独特别处理,因而我们有了基于对象的raycaster。

基于3D对象的raycaster的运行周期通常不长,仅仅在插件的某些特定状态下使用。它需要能够灵活的被创建和消除。它由插件调用实例TuoScene的setObjectsRaycaster函数来激活,并使用TuoScene.removeObjectsRaycaster函数删除。

基于对象的raycaster使用流程如下:

  1. 某个插件进入了一个状态,需要基于特定对象的raycaster支持。
  2. 该插件创建一个新的core/ObjectsRaycaster 对象,并监听其'hoverObject', 'unHoverObject', 和'clickObject'事件。
  3. 插件调用TuoScene.setObjectsRaycaster函数,使能此ObjectsRaycaster。TuoScene会将此ObjectsRaycaster安装到与之相关的所有TuoView上。
  4. 该插件退出该状态,取消对之前创建的ObjectsRaycaster对象事件的监听,并调用TuoScene.removeObjectsRaycaster注销掉ObjectsRaycaster

tuo-three2的stack插件

stack插件主要用于实现一些特定操作的“撤回”和“前进”操作。tuo-three2系统将要定义一组标准操作,在此,命名这个标准操作为"StandardOperation"。stack插件将会监听 TuoScene, 和每一个根结点对象的StandardOperation事件。根据自己的配置,stack插件能保存这些StandardOperation消息,并按消息中的内容,在监听到"GoBackStandardOperation"或"GoForwardStandardOperation"消息时,执行相应的消息发布。

在此,定义StandardOperation的规范如下:

<Root_or_TuoScene>.dispatchEvent({
  type: 'standardOperation',
  stackable: bool,
  payload: {
    backword: {
      type: <event_type_to_backword_this_operation>,
      payload: {<payload_needed_by_backword_this_operation>}
    },
    forward: {
      type: <event_type_to_forward_this_operation>,
      payload: {<payload_needed_by_forward_this_operation>}
    }
  }
})

其中的stackable用来控制此StandardOperation是否可以放入stack插件。这是为了防止,stack的事件再次触发stack的入栈操作。

tuo-three2对于3D对象修改操作的规范

本文档将定义tuo-three2对3D对象修改操作的规范,tuo-three2系统中将具有以下修改操作:

  1. changeGeometry, 所有对3D对象几何体变化的操作。此操作发生后,需要在该对象的root结点,发布'geometryChanged'消息。此消息的规范定义如下:
    root.dispatchEvent({type: 'geometryChanged', payload:{
    object: Object3D,
    }})
  2. changeMatrix, 所有对3D对象位姿矩阵变化的操作。此操作发生后,需要对该对象的root结点,发布'matrixChanged'消息。此消息的规范定义如下:
    root.dispatchEvent({type: 'matrixChanged', payload:{
    object: object,
    oldMatrix: Matrix4,
    newMatrix: Matrix4
    }})
  3. changeMatrix, 所有对3D对象材质的变化操作。此操作发生后,需要对该对象的root结点,发布'materialChanged'消息。此消息的规范定义如下:
    root.dispatchEvent({type: 'materialChanged',payload:{
    object: Object3D,
    oldMaterial: Material,
    newMaterial: Material
    }})
  4. updated, 某个3D对象发生了未知(不确定,或多种类型的变化)。此操作发生后需要对该对象的root结点,发布'updated'消息。此消息的规范定义如下:
    root.dispatchEvent({type: 'updated', payload: {
    object: Object3D
    }})
  5. addObject, 新对象的添加操作。此操作发生后,需要在相应TuoScene实例发布'addObject'消息。此消息的规范定义如下:
    TuoScene.dispatchEvent({type: 'addObject', payload: {
    object: Object3D,
    oldObject: Object3D, //optional
    group: String
    }})
  6. removeObject, 对象的移除操作。此操作发生后,需要在相应的TuoScene实例发布'removeObject'消息。此消息的规范定义如下:
    TuoScene.dispatchEvent({type: 'removeObject', payload: {
    object: Object3D
    }})
  7. addSubObject,新添加子对象的操作。需要在相应的root结点发布'addSubObject'事件。此消息的规范定义如下:
    root.dispatchEvent({type: 'addSubObject', payload: {
    object: Object3D,
    name: String, //optional, name of subObject
    }})
  8. removeSubObject, 移除子对象的操作。需要在相应的root结点发布'removeSubObject'事件。此消息的规范定义如下:
    root.dispatchEvent({type: 'removeSubObject', payload:{
    object: Object3D
    }})