App organisation in Plain English - michal-repo/three-mesh-ui GitHub Wiki

In this article we will see how three-mesh-ui works internally. It will be explained in Plain English, with however the assumption that you have at least a basic understanding of how three.js works and how to use it.

three-mesh-ui purpose

three-mesh-ui primary purpose is to provide an easy way to add user interfaces for virtual reality experiences built with three.js. It is designed to be integrated easily in an existing three.js project, as opposed to a framework like React360 or aFrame.

It doesn't make any assumption as to how the user interfaces can be interacted. That is, it is designed to produce THREE.Object3Ds, and from there the user can decide how to interact with this in a normal three.js workflow ( it can be tested for intersection with THREE.Raycaster ). This way, the user is free to integrate three-mesh-ui components in any kind of experience, involving any kind of controls (even VR hands controls).

How a component is built

A three-mesh-ui component is an object inheriting from THREE.Object3Ds. So far there is 4 of them : Block, InlineBlock, Text and Keyboard. Keyboard is different because it is the only component that is composed with other components. Therefore, it could be deemed a high-level component.

If you look at the code of these components, you will see that they are composed of several modules. For instance, this file is the main module of the Block component. Here, it composes the component with the several modules that are needed for its behaviour, over a THREE.Object3D instance. We will study these behaviour modules in the next chapter.

Every component has at at least three methods as its own properties : parseParams, updateLayout, and innerLayout. We talk about this more in length in the Components updates paragraph.

Components behaviour modules

Components behaviour modules are all placed in this directory.

BoxComponent

Behaviour of Block, InlineBlock and Keyboard, it is used for retrieving this component size (acccording to children dimension if undefined), and positioning children components inside itself. A BoxComponent (component with BoxComponent behaviour) is not responsible for positioning itself inside its parent, its parent is the only responsible for this.

InlineComponent

At the moment it does not add any behaviour to a component, but an isInline = true parameter, so that parents can know how to position this component. Used for the composition of InlineBlock and Text.

InlineManager

Responsible for positioning inline components children of a component. It only works if its children have a component.inlines property. This property is set by the children themselves in their parseParams function, it's an array containing objects with at least these properties : height, width, anchor and lineBreak.
width and height obviously contain the dimension of the element to position.
anchor is the distance between the lowest point of the element to position, and the line in which it's positioned. Principally responsible for offsetting letter like "p" or "q".
lineBreak is either null or 'possible' or 'mandatory', it indicates how much a line break is desirable on this element.

MaterialManager

Responsible for assigning and updating materials to its component. Materials are created once, then their parameters (shader uniforms) are updated when necessary. Material clipping planes are updated every frame for each component, in order to dynamically hide every part of this component which is overflowing out of ancestor components with hiddenOverflow === true.

TextManager

Only used in the composition of Text, its methods are called by Text for :

  • Determining the size of a given glyph
  • Creating individual MSDFGlyph and merging them to create a single text THREE.Mesh

MeshUIComponent

Most important behaviour module, it's common to all components in three-mesh-ui. This module is mostly a conductor, a getter-setter whose concern is to add/remove parameters (we should call them attributes) to a component, and register it for the right updates accordingly.

The user should not add properties directly to a component, instead they should call set, whose concern is to determine what type of updates should be called for this component depending on the updated attributes.
There is three types of updates :

  • parseParams, which parse input parameters than must be used by parents for a layout update
  • updateLayout, which updates things affecting the positioning of children
  • updateInner, which updates things that will affect only this component

We will elaborate on updates in the next paragraph.

When the user wants to repeatedly apply a given group of attributes to a component, they could use the set method, but the easiest way is to use setupState and setState methods. setupState stores predefined groups of attributes as states in the component, then the user can call setState with the state name as argument to apply the predefined attributes.

Components updates

We want to avoid useless duplicated update calls when the user adds/updates a lot of components in one go, which would lead to a huge drop in performance depending on how much components the user interface contains. Therefore, updates are not called immediately when the user calls set or setState. Instead, they are requested to UpdateManager.

UpdateManager is not used for module composition, it's a single module that knows all the components instantiated, since they are systematically registered to it.

When the user wishes to update all the components that where requested for updates, they call update, which is a function in the UpdateManager module assigned to the global three-mesh-ui object. It starts by calling all parseParams functions of requested components, from parents to children. Then it does the same for updateLayout functions, then updateInner.