Math in XPresso
In the following chapter we will explain a couple of basic mathematical functions that you will often encounter when working in 3D and are therefore essential when working with XPresso.
You are most likely familiar with the term "vector". In 3D, the Vector is the building block for basically everything. In Cinema 4D a Vector consists of three floating point numbers (in the following referred to as x, y and z). These values can contain pretty much any property for which three digits are required: position, rotation, scale, direction or even colors (red, green and blue are also three floating point numbers). First we will explain how Vectors are used to define position and direction.
The following might appear somewhat strange - and in fact there is basically no difference between a position indication and a direction indication. Let’s take a look at a simple vector example: A wanderer is on his way to a hut. He reaches a guidepost that points in a certain direction and has a distance of 12 km written on it. The wanderer now knows that the hut is 12 km from his current position; and he also knows the direction in which it lies.
If we view the guidepost as a Vector it becomes clear that a position and direction Vector are basically one and the same. The guidepost can either mean that "the cabin lies 12 km in this direction" or "travel 12 kilometers in this direction and you will reach the cabin". Let’s apply this to 3D space: The Vector (10;10;10) can represent the position of an object. This position can however be reached by moving a distance of 10;10;10 from the coordinate system’s origin. Basically a position is nothing more than the result of a movement in a specific direction. The Vector’s three components (x,y,z) produce a direction and length of the movement. A Vector with a length of exactly 1 is called a normalized Vector.
Vectors can be combined with and modified by basic arithmetic calculations. For example, if the Vectors 8;10;0 and 6;6;0 are added the result would be a Vector of 2;16;0. In practice it is as if the translation of all Vectors took place consecutively.
The illustration above shows how an object’s global position in Cinema 4D is defined: it is the sum of the local (coordinate system) position of all superordinate objects. If a given object A lies at a position of 8;10;0 and a subordinate object B lies at a local (coordinate system) position of -6;6;0, a global (coordinate system) position of 2;16;0 will be carried over as a vector value to object B.
Vectors can also be scaled via multiplication (scalar product). Their length will be modified but their original orientation will be maintained.
Of course subtraction and division can also be applied, analogous to addition and subtraction: The subtraction reflects the addition of a vector with an opposing orientation; for a scaling division, a vector is divided by a number, through which its length will be modified accordingly.
Normals are Vectors with a length of 1. These can be seen when a Vector points only in one direction and the length is irrelevant, which for example applies to polygon Normals. A polygon’s Normals is used to display the direction in which the polygon faces. The same applies to the tangents of points along a Spline. Here the result is also always a Vector with a length of 1, i.e., a Normal. In XPresso Normals and Vectors are available as separate data types. A Normal’s output Port can be connected to a Vector’s input Port - the value will not change. If you connect a Vector’s output Port with a Normal’s input Port, XPresso will automatically normalize the Vector. This means that the Vector’s direction (orientation) is maintained but its length will be scaled to 1.
Normals can serve very practical purposes. Since their length is guaranteed to be 1 they can be used to accurately measure movement in a particular direction. If we want to move an object a specific distance in the direction in which a normal points all we have to do is multiply the polygon normal by the desired distance we want to move the object (the scalar product, as described above). The result is a Vector that has the same direction as the Normal but with the desired length. A Vector can also be scaled to a desired length by normalizing it prior to multiplication.
If the Vector were an atom the Matrix would be the molecule. A Matrix (plural: matrices) in Cinema 4D is made up of four Vectors: offset, v1, v2 and v3. Here we will also use an example directly from Cinema 4D: you already know what a Null Object is so you already understand what a Matrix is. A Null Object has a position, rotation and scaling value and no other functions. Every object in Cinema 4D has a Matrix in which its local position, rotation and scale are stored. The global position, rotation and scale values are never stored in an object but are a result of the sum of the matrices of all superordinate objects.
A Matrix in Cinema 4D has the same functionality as a Null Object. Its position is equal to the offset Vector. The Null Object’s three axes equal the Vectors v1, v2 and v3, respectively. The direction of these three Vectors reflect the direction of the corresponding axes. The Vectors’ length define the Matrix’ scale. Hence, a Matrix represents an object’s entire local coordinate system. A Matrix that lies exactly at the origin of the coordinate system and has a scale of 1;1;1 with no rotation would look like this:
A Matrix that is rotated 90° around its y axis looks like this:
If this Matrix is scaled to 200% (2;2;2) the following values would result:
As the rotation above shows, the direction in which the Vectors point was modified but not their length. When the Matrix was scaled only the length of the Vectors changed and their orientation remained unchanged. This may seem confusing but it is quite simple in practice. In the following, several Matrix characteristics will be examined more closely, after which an example scene will be used to practice working with matrices. In most cases functions can be applied to a Matrix (e.g., Nodes) in order to achieve the desired result, which means you don’t necessarily have to learn the math behind these operations.
We’ve shown that there is no secret to positioning a Matrix and the Offset Vector can be modified like any other Vector. The scaling of a Matrix however is a little different. We already know that a Matrix’ scaling is derived from the length of the three vector axes v1, v2 and v3. As we learned in the chapter about Vectors, they can be scales precisely to a desired length when they are first normalized and then multiplied by the desired length value. A Matrix can also be scaled in the same manner.
In the following example we will scale an object without changing its position. The Expressions will be somewhat more complex but we will do this step-by-step (we will not explain in which category the nodes can be found since they are self-explanatory for the most part).
In the Expression above an object’s Global Matrix (in this example a cube) is first broken down into its Vectors. Then the three axis Vectors are normalized using a simple trick: each of the axis Vectors is passed through a Spy node whose Data Type value is set to Normal (the Spy function merely serves the purpose displaying the current Expression’s input value in case an error should occur and passing this value unchanged to the output where it will be implemented by the specified Normal data type). Each of the three normalized Vectors is then multiplied by a scale value in a Mixed Math node that is set to Vector in the Attribute Manager. These three values originate from the top-most node row: A Constant vector defines the scaling along the X, Y and Z axes and a Vector to Real node breaks this vector down into the three required real numeric values. The results are then assembled to a Matrix (the Offset Vector that defines position is simply assumed unchanged) and passed into the Cube object. As a result of the Expression the scaling from the Constant Node is passed to the Cube object. The object’s position and rotation remain the unchanged.
Now that we know to define a Matrix’ position and scale we will take a look at how to define its rotation. In the following section the mathematical correlations will become increasingly more complex but, as we mentioned earlier, you don’t have to learn the math behind these operations.
In the following example we will continue the rotation of a Matrix. We will use a Vector to define the degree to which the Matrix will rotate. In order to rotate a Matrix we must generate a Rotation Matrix using the Vector containing the rotation angles. This Rotation Matrix will then be multiplied with the original Matrix. In the example below "Cube 2" will always be rotated a couple degrees ahead of "Cube 1".
A quick review of the first steps:
Add an XPresso tag to Cube 1 in the Object Manager. In the User menu, use Add User Data to create a vector and named Rotation. Drag the XPresso tag into the XPresso Editor and set the new XPresso node’s output port to User Data Rotation Rotation. This vector with the rotation angles must be converted to a rotation matrix. XPresso does not have a node with which this can be done so we will have to use a C.O.F.F.E.E. operator (XPresso/Script/C.O.F.F.E.E.). A detailed description of this programming code would take far too long for this tutorial so we will simply restrict ourselves to the following example. First, delete the input and output ports and add a Vector input port and a Matrix output port. Rename the input port InHPB and the output port OutMatrix. Rename the node HPBToMatrix. Click on the Open C.O.F.F.E.E. Editor button in the Attribute Manager and enter the following code in the Expression Editor:
The result is a Rotation Matrix that is multiplied by the "Cube 1" Matrix and rotates beyond the rotational angle defined for the "Cube 1" Matrix. Since the position of "Cube 1" is also passed on we have to replace this data with the position of "Cube 2" so that "Cube 2" can continue to be moved freely. Hence, the original position of "Cube 2" will be ascertained from its Matrix, combined with the results of the rotation to create a new Matrix and finally passed on to "Cube 2".
To test the Expressions’ function, drag both cubes to different positions in the Viewport, select the XPresso tag in the Object Manager and set the Rotation vector’s three components to 0° in the Attribute Manager’s User Data menu. If you now move Cube 1, the position of Cube 2 will remain unaffected. However, if you rotate Cube 1, Cube 2 will assume the exact same rotation. To test what this exercise was actually meant to achieve, enter any rotational value for the Rotation vector into the User Data menu. If everything works correctly, this rotation will be added to that already assumed by Cube 1.
Admittedly this Expression is very complex for such a minor effect but it shows how the rotation of a Matrix can be continued based on its original rotation. This Expression can, of course, be applied to other scenarios.
Now that we know how Matrices basically work and how to implement them we will turn our attention to another important aspect: The conversion of Vectors and Matrices between different coordinate systems.
A simple example: We want to position object "A" relative to object "B" without making object "A" a Child object of object "B".
A practical application would be a character’s hand (A) reaching for a given object (B). Hand and object are not hierarchically dependent but their movement (position) must be synchronized.
The following scene contains a Pyramid object and a Sphere object. The sphere is brought into a position relative to the pyramid’s local coordinate system without the sphere being made a Child object of the pyramid.
This was done by multiplying the local position at which we wanted the sphere to lie with the pyramid’s Matrix. The result of this multiplication is passed on to the sphere as a global position. The Expression for this is simple:
If we rotate the Pyramid object the Sphere object will remain at the same position relative to the pyramid. This demonstrates how local coordinates can be transformed into global coordinates by multiplying them by the subordinateglobal Matrix.
Global coordinates can just as easily be transformed into an object’s local coordinates. To do so, simply multiply the Vector with the global coordinates by the inverted global Matrix of the other object.
Below is a slightly modified version of the previous scene. The Sphere object has been made a Child object of a Null Object and we want to position the sphere using a position within the Null Object’s local coordinate system.
The Expression must be set up accordingly. The Null Object’s global Matrix must be ascertained, inverted and multiplied by the global position, which was determined in the previous example, using a MatrixMulVector Node. Finally, the sphere’s position in the pyramid’s local coordinate system is defined, then transformed into the global coordinates system and subsequently into the Null Object’s local coordinate system. In this manner a local coordinate system’s coordinates can be carried over into another coordinate system.
Click on the image below to view a movie that demonstrates this functionality:
Distorted Matrix and Singularity
Normally the Vectors v1, v2 and v3 lie at right angles to each other. With the appropriate values, however, the angle with which one or more Vectors lie to each other can be distorted. In the following example the X and Z axes no longer lie at a right angle to each other:
Such a distortion should definitely be avoided!
A distorted Matrix may result from a given calculation but should subsequently be corrected because working with distorted axes will produce incorrect results. Imagine the X and Z axes point in the same direction. This would result in an endless number of possibilities within the Matrix’ coordinate system for moving a Child object along the X or Y axis, whereby the results would be identical. This is called "singularity" and should be avoided when working with Matrices. Singularity also occurs in conjunction with "gimbal lock", something with which you my already be familiar.
Since the "danger" of singularity occurring is well known, XPresso offers functions (e.g., Cross Product) with which stable Matrices or individual right-angle axes can be created automatically.