Matrices

The HOOPS Web Viewer API uses matrices to encode geometric transformations for each node within the model tree. After a matrix is encoded with the appropriate transformation characteristics, which include translation, rotation, and scaling, it can be applied to one or more nodes, determining how they will be located and oriented when displayed. A matrix can also be used to transform 3D points and vectors as needed.

For the remainder of this section, it is expected that the reader has a basic understanding of 3D math concerning matrices and vectors. There are numerous books and web-articles that go into appropriate depth on these topics.

Points and vectors

Three-dimensional points and vectors are typically represented as floating-point values with components named x, y, and z. Even though both use the same mathematical representation, they have a conceptual difference - a point represents a position in three-dimensional space while a vector represents a direction. But since mathematical operations on points and vectors are identical, the Communicator API only has classes using the “Point” nomenclature.

To properly encode the full geometric transformations that we require, we must utilize a 4x4 matrix. However, the mathematics used to transform a point or vector using a 4x4 matrix requires that they have four components instead of the three that we’ve identified. The standard approach to this problem is to use homogeneous points which include a fourth component named w, which is typically set to 1.0. However, the Communicator API hides this mathematical requirement where possible and allows you to use and represent points and vectors with just the x, y, z coordinates.

More information can be found in the literature on homogenous coordinates, but for our purposes, we can effectively ignore the mathematical implications.

The Communicator API includes classes to represent points and vectors with two, three, and four coordinates which are named Point2, Point3, and Point4 respectively. This section will focus on the Communicator.Point3 class to represent standard 3D geometry.

To create a 3D point or a vector, use the Point3 class constructor.

    // Creates a 3D point with x=1,y=2,z=3
    var point1 = new Communicator.Point3(1, 2, 3);

    // Create a 3D vector, which still uses the point class
    var vec1 = new Communicator.Point3(1, 2, 3);

The Point3 class includes several common mathematical operations, some of which will modify the object while others may produce a new object or return a calculated value. Check the API documentation for specifics.

This example creates a vector and normalizes it.

    var vec1 = new Communicator.Point3(1, 1, 0);
    // vec1 is {x: 1, y: 1, z: 0}
    vec1.normalize(); // vec1 will be modified
    // vec1 is now {x: 0.7071067811865475, y: 0.7071067811865475, z: 0}

This example computes the cross product of two vectors using a static function of Point3 and returns the result as a newly created Point3 instance.

    var vec1 = new Communicator.Point3(1, 0, 0);
    var vec2 = new Communicator.Point3(0, 1, 0);
    var crossProd = Communicator.Point3.cross(vec1, vec2);
    // crossProd is {x: 0, y: 0, z: 1}

Copying

An important function included in many of Communicator API classes is copy(), which creates and returns a duplicate of the object it’s called on. For self-modifying functions like normalize(), it can often be desirable to use the copy() function such that the original vector is left unchanged.

    var vec1 = new Communicator.Point3(1, 1, 0);
    var vec1Norm = vec1.copy().normalize();
    // vec1 is {x: 1, y: 1, z: 0}
    // vec1Norm is {x: 0.7071067811865475, y: 0.7071067811865475, z: 0}

Matrices

A 4x4 matrix can encode a geometrical transformation that includes translation, rotation, and scaling. A matrix can be applied to a model-tree node or can be used to transform a point or a vector. The Communicator API includes the class Communicator.Matrix to represent a 4x4 transformation matrix.

Identity matrix

The identity matrix has no geometric transformations, thus applying an identity matrix to something will have no effect. The identity matrix looks like:

1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

Because it is used frequently as the starting point when combining matrices, the Matrix constructor takes no arguments and creates an identity matrix:

    var identity = new Communicator.Matrix();

Translation

A translation matrix is used to move a node, point, or vector in 3D space by a specific distance in a given direction. To create a translation matrix, use Matrix.setTranslationComponent(dx,dy,dz) where the parameters indicate the distance to move in each axis direction. This example shows how a point can be moved using a translation matrix:

    var m = new Communicator.Matrix();
    m.setTranslationComponent(1, -1, 2);
    var point = new Communicator.Point3(1, 1, 1);
    var moved = m.transform(point);
    // moved is {x: 2, y: 0, z: 3}

Rotation

A rotation matrix is used to rotate a node, point, or vector in 3D space by specific rotation amounts. The Communicator API supports a few ways to create a rotation matrix. One way is to use Matrix.createFromOffAxisRotation(axis, degrees). With this approach, you specify an arbitrary axis as a Point3 and the number of degrees about which to rotate around that axis in a counter-clockwise direction.

    var axis = new Communicator.Point3(0, 0, 1);
    var m = Communicator.Matrix.createFromOffAxisRotation(axis, 45);
    var point = new Communicator.Point3(1, 0, 0);
    var newPoint = m.transform(point);
    // newPoint is {x: 0.7071067811865476, y: 0.7071067811865475, z: 0}

Scaling

A scaling matrix is used to scale a node, point, or vector in 3D space by specified amounts for each axis. To create a scaling matrix, use Matrix.setScaleComponent(sx, sy, sz). While scaling supports components for each axis, it is often desirable to scale an object uniformly. For that case, simply use the same value for each axis parameter. This example creates a matrix that will scale a vector by a uniform value of 2.0:

    var m = new Communicator.Matrix();
    var scaleComp = 2.0;
    m.setScaleComponent(scaleComp, scaleComp, scaleComp);
    var vec = new Communicator.Point3(1, 2, 3);
    var newVec = m.transform(vec);
    // newVec is {x: 2, y: 4, z: 6}

Combining matrices

Different transformation matrices can be combined using matrix multiplication. The order in which the matrices are combined is critical in determining the results because matrix multiplication is not mathematically commutative. Therefore if you have two valid geometric transformation matrices A and B which are both not the identity matrix, then AB will not be equal to BA.

For 3D geometric operations, the order that the matrices are combined will mirror the order that the operations affect the result. For example, if you wanted to scale something, then translate it, you would combine the respective matrices in that order:

    var combined = Communicator.Matrix.multiply(scale, translate);

If instead, you wanted to translate the item first, then scale it, you would perform the multiplication in that order:

    var combined = Communicator.Matrix.multiply(translate, scale);

This can be shown with a simple example where we transform the point (0, 0, 0) in a couple of different ways. First, let’s rotate then translate it:

    var translate = new Communicator.Matrix();
    translate.setTranslationComponent(0, 5, 0);
    var rotate = Communicator.Matrix.createFromOffAxisRotation(
        new Communicator.Point3(0, 0, 1),
        45,
    );
    var combined = Communicator.Matrix.multiply(rotate, translate);
    var point = new Communicator.Point3(0, 0, 0);
    var newPoint = combined.transform(point);
    // newPoint is {x: 0, y: 5, z: 0}

The resulting point is now at (0, 5, 0) which makes sense because the first rotation operation will rotate the point about the origin, but since the point is at the origin, that operation results in the point still being (0, 0, 0). The next operation will be the translation by dx=0, dy=5, dz=0, thus yielding the final point of (0, 5, 0).

Next, let’s do the same thing but with the matrix combination order inverted. First, we will translate the point, then rotate it:

    var translate = new Communicator.Matrix();
    translate.setTranslationComponent(0, 5, 0);
    var rotate = Communicator.Matrix.createFromOffAxisRotation(
        new Communicator.Point3(0, 0, 1),
        90,
    );
    var combined = Communicator.Matrix.multiply(translate, rotate);
    var point = new Communicator.Point3(0, 0, 0);
    var newPoint = combined.transform(point);
    // newPoint is {x: -3.5355339059327373, y: 3.5355339059327378, z: 0}

The resulting point is now at (-3.5, 3.5, 0). First, the translation occurred which put the point at (0, 5, 0), then the rotation about the z-axis occurred next, but the center of rotation is still at (0, 0, 0), thus yielding the final result point.

The typical order of combination when placing objects in a scene is first scaling, then rotating, and finally translating. This is because object geometry definitions are often centered around the origin and may use a different unit scaling than the target scene. To place the object, it would therefore, make sense to first scale and rotate it since that rotation would be about the center of the object, then finally to use the translation to move the object to the desired location in the scene.

The next example will show this by creating a matrix that will scale uniformly by a factor of 2.0, then rotate 45 degrees around the z-axis, and finally move 5 units down the y-axis. Combining three matrices will take two calls to Matrix.multiply(). A vector will be transformed using this combined matrix.

    var scale = new Communicator.Matrix();
    scale.setScaleComponent(2.0, 2.0, 2.0);
    var translate = new Communicator.Matrix();
    translate.setTranslationComponent(0, 5, 0);
    var rotate = Communicator.Matrix.createFromOffAxisRotation(
        new Communicator.Point3(0, 0, 1),
        45,
    );
    var combined = Communicator.Matrix.multiply(scale, rotate);
    combined = Communicator.Matrix.multiply(combined, translate);
    var vec = new Communicator.Point3(1, 1, 1);
    var finalVec = combined.transform(vec);

Direct element access

The Matrix class exposes its internal elements as a data array named m which allows direct index-based element access. To work well with WebGL, the array data is stored as a 16-value array with base vectors laid out contiguously in memory. The translation values are found at element indices 12, 13, and 14. Thus an alternative way to set up a translation matrix with dx=1, dy=2, and dz=3 would be:

    var mat = new Communicator.Matrix();
    mat.m[12] = 1;
    mat.m[13] = 2;
    mat.m[14] = 3;
    // mat.m is [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]

Another possibility would be to create a new matrix with a fully formed array by using Matrix.createFromArray(array). This example creates the same matrix as the previous example:

    var mat = Communicator.Matrix.createFromArray([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]);
    // mat.m is [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]

Matrices and nodes

Each node within the model has an associated matrix that will determine the scaling, rotation, and position of the node’s associated geometry. And since a node’s matrix will also be applied to any of its child nodes, it is not necessary to apply the change matrix to any node other than the parent. As an example, you can move the entire model around simply by applying a translation matrix to the root node.

Relative geometry changes

It’s common to want to make slight changes to a node’s geometry based on its current orientation. This could include a scenario like “move the node 2 units down the x-axis”. Because you want a relative change, you must combine the change matrix with the node’s current matrix.

This example moves a node 2.0 units down the x-axis from its current position. It assumes a variable model exists as a valid Communicator.Model instance and that NodeId is a valid node identifier within the model. The node’s existing matrix is acquired, combined with the translation matrix, then applied back to the node.

    var translate = new Communicator.Matrix();
    translate.setTranslationComponent(2, 0, 0);
    var current = hwv.model.getNodeMatrix(nodeId);
    var change = Communicator.Matrix.multiply(current, translate);
    hwv.model.setNodeMatrix(nodeId, change);

Note that the last three lines can be combined into a single statement:

    hwv.model.setNodeMatrix(
        nodeId,
        Communicator.Matrix.multiply(hwv.model.getNodeMatrix(nodeId), translate),
    );