15 Sep 2017

Know-How: Complex component transformations in Viewer - Part 1 (Basics)

Simple Clock

In some of our previous blog posts we touched the idea of component translation and rotation. More complex transformations, like rotation of one component around another one, are usually accompanied by frustration with translation/rotation matrices, matrix inverses, quaternions and other fun stuff that rapidly gets out of hand in case of some more complex hierarchical transformations.

In what follows, I would like to present an approach that will allow you to quickly setup complex transformations and bring some interactivity to your model.

The main purpose of this post is not only to show you a solution, but also to explain how and why it works. Thus, you should expect an abundance of details and to make it more manageable, I will break it into 3 parts:

  • In this first part, I will explain the basics of the proposed approach along with an illustration on a simple model, a simple rotation of a component around another one. 
  • In the second part, we will apply this approach on model that requires a hierarchical transformation - a very simple 4-axis robotic arm.
  • In the third part, we'll see how this approach can be applied for more complex transformations where some hierarchical transformations has to be ignored and for that we'll use a gyroscope like model.

 

Let us "Begin at the beginning" 

as Lewis Carroll said and understand in the first place what is the problem. To make it more appealing, we will start by analyzing a fairly simple model of desk clock:

and we can see that the hours, minutes and seconds arms of the clock are grouped at 12 o'clock and fail to expose the beauty or ugliness of their design and how the fit in the overall design.

Wouldn't be wonderful to have this model display the current time right in the Viewer? An excellent requirement for an online clock shop using the Viewer for illustration of available beauties.

After briefly examining the model, we can see that the task of showing the current time can be translated as "rotate HourArmMinuteArm and SecondsArm components around pin component (having on some data, but this is not important now).

Based on experience of previous blog posts and some samples, the course of actions would be the following:

  1. Get the NodeId of needed component:

    The NodeId of a needed component can be found by selecting that component and calling the viewer.getSelection() in the console, but for our purpose, based on knowledge from another post, this could be achieved be creating a helper function like this:

    function findNodeIdbyName(name) {
        let nodeList = Object.values(tree.nodeAccess.dbIdToIndex);
        for (let i = 0, len = nodeList.length; i < len; ++i) {
            if (tree.getNodeName(nodeList[i]) === name) {
                return nodeList[i];
            }
        }
        return null;
    }

     

  2. Get the fragments and fragment proxy corresponding to a node:

    This is usually achieved with a following code:

    let tree = viewer.model.getData().instanceTree
    
    tree.enumNodeFragments(nodeId, function (frag) {
        let fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
    
        //do some work with fragment proxy
    
    });

     

  3. Get transforms for each fragment proxy, modify them and "commit" the changes:

    ...
    fragProxy.getAnimTransform();
    fragProxy.position = some_new_position;
    fragProxy.quaternion = some_new_rotation;
    fragProxy.updateAnimTransform();
    ...

     

  4. Update the scene, by calling viewer.impl.sceneUpdated(true);

Thus, for our example, to rotate (around X axis) the SecondsArm, the code would look something like:

let secondsArm_Id = findNodeIdbyName('SecondsArm');
let tree = viewer.model.getData().instanceTree;

tree.enumNodeFragments(secondsArm_Id, function (frag) {
    var fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
    fragProxy.getAnimTransform();
    fragProxy.quaternion = new THREE.Quaternion().setFromAxisAngle(
                                    new THREE.Vector3(1,0,0), 
                                    -Math.PI/2);
    fragProxy.updateAnimTransform();
});

viewer.impl.sceneUpdated(true);

The result of straightforward (90 degrees) rotation of the SecondsArm will result in something like this:

As you can see, component rotation is not a big deal, but the problem is that the rotation is around its own "gravity center", while we would like to rotate it around another axis and in our example we are lucky that it is just around X axis, at pin coordinates.

For this case, any self respecting book on computer graphics will tell you that it is basically calculation of some matrices, and this is where the fun with translation matrix, rotation matrix and inverses come into play and the natural course of action is to abstract all this and come up with a system.

The funny thing is that sooner or later you'll realize that you are building your " ... own theme park, with blackjack and ...", while all this already exists in three.js library upon which Autodesk Viewer is based.

It is true, that your model is not integrated into three.js scene as you might expect, but this doesn't mean that we cannot use it's "logic" indirectly.

 

To better illustrate my point, let us start with a fairly simple three.js example, based on Three.js getting Started project:

In this very example both cubes were added to the scene (to the root node), the red one right in "the center of the Universe", while the blue one somewhere in the world:

...

scene.add(red_cube);
scene.add(blue_cube);

camera.position.z = 5;
blue_cube.position.x = 2;

var animate = function () {
    requestAnimationFrame(animate);

    red_cube.rotation.z += 0.01;

    renderer.render(scene, camera); 
};

...

Now, what it takes to rotate the the blue cube around the red one? Just one line:

...
    scene.add(red_cube);
    // scene.add(blue_cube);
    red_cube.add(blue_cube);
...

giving us:

Thus, instead of adding the blue cube to the scene "directly", we add it "indirectly" by adding it to the red one, thus creating a parent-child relation, where the red cube is the parent, while the blue one is a child, and all subsequent transformations on parent will influence its dependents.

Yeah, good to know, but how this could be useful in our case?

In one of our old posts, and in a newer one we illustrated how to add three.js objects to Viewer's scene.

Now will it work if we add another object, not to the scene, but as a child to the first one? Obviously the parent-child relation is working in the Viewer for the three.js objects, but still ... how this can be useful?

Well ... we cannot assign a component of our model to be a child of three.js object, but we can easily read the three.js transformation data and assign the needed ones to our model.

In other words, to rotate the SecondsArm component around the Pin component, we can do the following:

  1. create an empty three.js object and add it to the Viewer scene. Let us call it pivot_seconds object:

    For debugging purposes we will assign it a geometry and a material, just to illustrate it's position in our viewer, but in the end we can just create a new THREE.Mesh() to make it "invisible" and everything will continue to work without problems:

    let seconds_pivot = new THREE.Mesh(
                            new THREE.BoxGeometry(10, 10, 10),
                            new THREE.MeshBasicMaterial({ color: 0xff0000 }));
    
    
    viewer.impl.scene.add(seconds_pivot);

  2. move the pivot_seconds object to same position as Pin component position:

    For that, we will need to get the current pin position, which could be abstracted as:

    function getFragmentWorldMatrixByNodeId(nodeId) {
        let result = {
            fragId: [],
            matrix: [],
        }
        tree.enumNodeFragments(nodeId, function (frag) {
    
            let fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
            let matrix = new THREE.Matrix4();
    
            fragProxy.getWorldMatrix(matrix);
    
            result.fragId.push(frag);
            result.matrix.push(matrix);
        });
        return result;
    }
    
    //some nodes might contain several fragments, but in our case we know it has one fragment
    
    let pin_position = getFragmentWorldMatrixByNodeId(pin_Id).matrix[0].getPosition().clone();
    seconds_pivot.position.x = pin_position.x;
    seconds_pivot.position.y = pin_position.y;
    seconds_pivot.position.z = pin_position.z;

    And we have it exactly where we needed:

  3. add another empty three.js object and add it to the pivot_seconds object. Let us call it seconds_helper, as it helps us to get the needed position for the SecondsArm:

    let seconds_helper = new THREE.Mesh(
                                new THREE.BoxGeometry(5, 5, 5),
                                new THREE.MeshBasicMaterial({ color: 0x0000ff }));
    let secondsArm_position = getFragmentWorldMatrixByNodeId(secondsArm_Id).matrix[0].getPosition().clone();
    seconds_helper.position.x = secondsArm_position.x;
    seconds_helper.position.y = secondsArm_position.y;
    seconds_helper.position.z = secondsArm_position.z;

    Here comes the most important part: If we add seconds_helper directly to the scene by calling viewer.impl.scene.add(seconds_helper) we seems to get it at a right position, but we will see that this is not really what we want, as in case of a rotation, it still rotates around its own "gravity center":

    Let us investigate these all position by using a simple debugging piece of code:

    console.log("Pin position = " + JSON.stringify(seconds_pivot.position));
    console.log("SecondsArm position = " + JSON.stringify(secondsArm_position));
    console.log("Helper position = " + JSON.stringify(seconds_helper.position));

    we should receive something like this:

    Pin position = {"x":15.75,"y":-11.25,"z":-0.0030820071697235107}
    SecondsArm position = {"x":19.25,"y":7.5,"z":0}
    Helper position = {"x":19.25,"y":7.5,"z":0}
    

    Our helper will be at exact SecondsArm component's position, but it wouldn't be much of help.

    On the other hand, if we add it as a child of the seconds_pivot by calling seconds_pivot.add(seconds_helper), we end up with something different than expecting:

    If we check the debug output, we get the same positions as before, but the catch is that in case of seconds_helper, that position is relative to is parent and to check its world position, the following debugging snippet will help:

    seconds_pivot.updateMatrixWorld();
    var seconds_helper_wold_position = new THREE.Vector3();
    seconds_helper.localToWorld(seconds_helper_wold_position);
    console.log("Helper's World position = " + JSON.stringify(seconds_helper_wold_position));

    which give us the world position:

    Helper's World position = {"x":35,"y":-3.75,"z":-0.0030820071697235107}
    

    As you can see, that's explains why the blue box is not at expected coordinates. From this point there are several ways to approach what I intend to achieve, but the one that I'm using is through use of the offsets:

    By looking at the offset - the distance between the needed component and the wanted pivot, we see the following situations:

      Pin position SecondArm position Offset
    X 15.75 19.25 3.5
    Y -11.25 7.5 18.75
    Z ~ 0 ~ 0 ~ 0

    Knowing this offset, the position of our seconds_helper would be:

    - [SecondArm position] + [abs(Offset)]

    Why is that and why so complicated?

    The thing is that when you are assigning a new position to a fragment proxy, you are not assigning the World position, but a local one.

    To better illustrate this as a side note, let us assign 10 units to fragments of the "Holder" component and see how it affects the World position:

    By calling the scary construction:

    let Holder_Id = findNodeIdbyName('Holder');
    console.log(JSON.stringify(getFragmentWorldMatrixByNodeId(Holder_Id).matrix[0].getPosition()));

    we get:

    {"x":0,"y":81.74999237060547,"z":0}
    

    Now using the already mentioned approach and assign 10 to fragment's Y position:

    let secondsArm_Id = findNodeIdbyName('SecondsArm');
    let tree = viewer.model.getData().instanceTree;
    
    tree.enumNodeFragments(Holder_Id, function (frag) {
        var fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
        fragProxy.getAnimTransform();
        fragProxy.position.y = 10
        fragProxy.updateAnimTransform();
    });
    
    viewer.impl.sceneUpdated(true);

    we moved the component up:

    and it's world position now changed to

    {"x":0,"y":91.74999237060547,"z":0}
    

    Thus, the fragment proxy's transformations are just "portals" to modify the component's world position and not to set it (this is whay it is called a proxy after all).

    Now, back to our "offset thing". As I said, knowing this offset, the position of our seconds_helper would be:

    - [SecondArm position] + [abs(Offset)]

    or

    seconds_helper.position.x = - secondsArm_position.x + Math.abs(secondsArm_position.x - seconds_pivot.position.x);
    seconds_helper.position.y = - secondsArm_position.y + Math.abs(secondsArm_position.y - seconds_pivot.position.y);
    seconds_helper.position.z = - secondsArm_position.z + Math.abs(secondsArm_position.z - seconds_pivot.position.z);

    which in our debug output will give us

    PinPivot position = {"x":15.75,"y":-11.25,"z":-0.0030820071697235107}
    SecondsArm component position = {"x":19.25,"y":7.5,"z":0}
    Helper position = {"x":-15.75,"y":11.25,"z":0.0030820071697235107}
    

    Why this offset is need, will be explained in more details in the second part of this blog post, where we will deal with more complex transformation, but for now just believe me that these are the needed operations.

  4. apply seconds_helper transforms to the SecondsArm component's transform:

    This is done by decomposing the seconds_helper transformation into position, scale and rotation(quaternion) and could be stated as a following function:

    easily achieved by steps that could be embedded in a following :

    function assignTransformations(refererence_dummy, nodeId) {
        refererence_dummy.parent.updateMatrixWorld();
        var position = new THREE.Vector3();
        var rotation = new THREE.Quaternion();
        var scale = new THREE.Vector3();
        refererence_dummy.matrixWorld.decompose(position, rotation, scale);
    
        // console.log("decomposed matrix into position = " + JSON.stringify(position));
    
        tree.enumNodeFragments(nodeId, function (frag) {
            var fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
            fragProxy.getAnimTransform();
            fragProxy.position = position;
            fragProxy.quaternion = rotation;
            fragProxy.updateAnimTransform();
        });
    
    }

    After looking at all this, many of you might think that those matrix things might not be so scary after all and this method seems quite complicated, thus making this blog useless if not to say more.

These thoughts will disappear very fast when you realize that after a small setup (whose logic I tried to explain above), further component manipulations like rotation around another component, becomes super easy.

Thus in our case, the only thing we need now, to have the SecondsArm component rotate around the Pin component is to call:

    seconds_pivot.rotation.x = some_value;
    assignTransformations(seconds_helper, secondsArm_Id);
    viewer.impl.sceneUpdated();

which can be abstracted further to a single function, but these are already just technicalities.

Back to our clock thing, having setup like this:

/* ====================== SECONDS ================= */
let seconds_pivot = new THREE.Mesh();
let pin_position = getFragmentWorldMatrixByNodeId(pin_Id).matrix[0].getPosition().clone();
seconds_pivot.position.x = pin_position.x;
seconds_pivot.position.y = pin_position.y;
seconds_pivot.position.z = pin_position.z;

let seconds_helper = new THREE.Mesh();
let secondsArm_position = getFragmentWorldMatrixByNodeId(secondsArm_Id).matrix[0].getPosition().clone();
seconds_helper.position.x = - secondsArm_position.x + Math.abs(secondsArm_position.x - seconds_pivot.position.x);
seconds_helper.position.y = - secondsArm_position.y + Math.abs(secondsArm_position.y - seconds_pivot.position.y);
seconds_helper.position.z = - secondsArm_position.z + Math.abs(secondsArm_position.z - seconds_pivot.position.z);

viewer.impl.scene.add(seconds_pivot);
seconds_pivot.add(seconds_helper);

It is very easy to make the SecondsArm component show the almost realtime seconds, by just adding:

setInterval(function () {
    var timing = new Date();

    seconds_pivot.rotation.x = -1 * timing.getSeconds() * 2 * Math.PI / 60;
    assignTransformations(seconds_helper, secondsArm_Id);
    viewer.impl.sceneUpdated();

}, 1000);

That's it! By repeating same steps for MinuteArm and HourArm, you will have a model of a desk clock showing a real time, as illustrated here.

Wooden Clock

The most important thing is that even if I decide later to change the ugly design of those seconds, minutes, hours arms, everything will continue to work as long as I keep the same name for those components.

Now you should have the necessary ingredients to make you models even more appealing, by embedding small animations or functionality illustrations.

At least a clock shop is ready to go.

 

At this step, some of you may still look skeptical at this approach and wonder what it is a big deal about it, as it is not much easier than just creating a matrix out of 3 matrices.

Don't worry, you are perfectly right ... at some extent ... that till you have to deal with a more complex chained transformations and to illustrate this, I invite you to reinforce our knowledge on a more complex model in second part where we will see how this approach can be applied to hierarchical transformations.

Related Article