# Animation

## Quickstart UI example

The Quickstart server contains an Animation example at: http://localhost:11180/animation.html?viewer=csr.

The example provides a sandbox environment with a basic user-interface applying the concepts learned in this article. We encourage you to review the corresponding code found in web_viewer/examples/animation/js/animation/ of the HOOPS Communicator package.

## Overview

Beginning with version 2021 SP1, HOOPS Communicator has the ability to perform simple animations. These animations can be used to demonstrate how assemblies fit together, better show complex parts within a single view, demonstrate how an object changes over time, and more.

Note

As the developer, you have a certain amount control over the complexity and structure of your own scenes, and HOOPS Communicator will render frames as fast as the hardware is able. However, please note that while Communicator can perform simple animations reasonably well, it is first and foremost an engineering visualization engine. High performance, computationally intensive animation involving a large amount of geometry and post-processing is theoretically possible, but is not the goal of the animation API. It follows that such performance should not necessarily be expected. If you require such capability, please contact support to determine the best options for your project.

We have designed the API with the glTF format <https://www.khronos.org/gltf/> in mind. HOOPS Communicator has a few different properties that can be animated, including scale, rotation, movement, and attributes. Programmatically speaking, animations are composed of a number of objects which describe how the animation changes over time. From lowest level to highest, these objects are arranged as follows:

Keyframes > KeyframeBuffer > Sampler > Animation > Manager > Player

- Keyframe: Like in traditional animation, a keyframe defines start and end points of an animation, or portions of an animation. In HOOPS Communicator, there are a number of different keyframe types, depending on what kind of animation you’re creating. See KeyType for more details.

- KeyframeBuffer: A collection of keyframes the sampler will use to create the animation.

- Sampler: Samples an animation based on keyframes in order to interpolate geometry between its start and end position.

- Animation: Uses one or more animation channels which describe what kind of animation will be occurring in the scene. An animation can have a CameraChannel or NodeChannel.

- Manager: Used to create and manage a Player.

- Player: Plays a specific animation based on a Manager.

In the next section, we’ll look at how these parts fit together. You can find the full source code in our HOOPS Communicator Quickstart Samples repository.

## Translation animation

Let’s take a look at a code example that will animate several nodes along the X axis using the microengine model. At this point, our goal is to remove four red screws from the engine.

To start, let’s create an Animation and corresponding Player.

    // Create the Animation object with a friendly name
    let animation = new Communicator.Animation.Animation("my_sample_animation");

    // Create the player
    let player = viewer.animationManager.createPlayer(animation);

We need five different data points to create the animation, and will provide them to a function that contains all of our translation animation logic:

1. The ID of the node(s) we are animating

2. Start time of the animation

3. Duration of the animation

4. Translation vector

5. Translation distance

    var nodes = [75, 77, 79, 81]; // 75 = topLeft, 77 = topRight, 79 = bottomLeft, 81 = bottomRight
    var startTime = 1;
    var duration = 2;
    var translationVector = new Communicator.Point3(-1, 0, 0);
    var translationDistance = 30;

    addTranslationAnimation(nodes, startTime, duration, translationVector, translationDistance);

We will animate nodeIDs `75, 77, 79, 81`, the animation will start on the 1st second of the animation, and will have a duration of 2 seconds. We will leave the timeframe 0 to 1 for the camera animation.

This function will contain all the logic to set up an animation for a given set of nodes.

    function addTranslationAnimation(nodes, startTime, duration, translationVector, translationDistance) {
            for (const nodeId of nodes) {
                    // ...
            }
    }

For each node, we will do the following:

1. Create a NodeChannel

2. Set a start keyframe for the node’s initial position

3. Convert the translation vector in the local coordinate

4. Set a keyframe for the node’s end position

### 1. Create a NodeChannel

We will define a function that will create a `NodeChannel` with the appropriate settings for a linear translation.

    function createTranslationChannel(nodeId, startTime) {
            // Create a node channel
            const channelName = `Translate-{nodeId}`;
            const buffer = new Communicator.Animation.KeyframeBuffer(Communicator.Animation.KeyType.Vec3);
            const sampler = new Communicator.Animation.Sampler(buffer, Communicator.Animation.InterpolationType.Linear);
            const channel = animationManager.createNodeChannel(channelName, nodeId, Communicator.Animation.NodeProperty.Translation, sampler);
    }

Let’s unpack this workflow.

Create a new KeyframeBuffer. We define the KeyType of the animation to be Vec3.

Create a Sampler. We chose Linear for the InterpolationType and pass the newly created KeyframeBuffer.

Create a NodeChannel. The last step is to put it all together. Pass createNodeChannel() a friendly name for the channel being created, the nodeID of the node that will be animated, the NodeProperty we are animating, and the Sampler created earlier.

Note the unique name we give to the channel we are creating. The example user-interface provided with the Quickstart server creates a list of animation details new channels are created and uses a similar naming convention to easily identify the node and type of animation.

### 2. Set the initial keyframe

Keyframes are maintained in an buffer array so we can insert or append our keyframe. For this example, we will use insertVec3Keyframe(). Note the append function matches the KeyType used when we created the `KeyframeBuffer` in the previous step.

    function createTranslationChannel(nodeId, startTime) {
            // Create a node channel
            const channelName = `Translate-${nodeId}`;
            const buffer = new Communicator.Animation.KeyframeBuffer(Communicator.Animation.KeyType.Vec3);
            const sampler = new Communicator.Animation.Sampler(buffer, Communicator.Animation.InterpolationType.Linear);
            const channel = viewer.animationManager.createNodeChannel(channelName, nodeId, Communicator.Animation.NodeProperty.Translation, sampler);

            // Get initial node matrix
            const nodeMatrix = this._viewer.model.getNodeMatrix(nodeId);

            // Create the start keyframe
            channel.sampler.buffer.insertVec3Keyframe(startTime, nodeMatrix.m[12], nodeMatrix.m[13], nodeMatrix.m[14]);

            return channel;
    }

We can now call our function to create the `NodeChannel` for our translation animation.

    function addTranslationAnimation(nodes, startTime, duration, translationVector, translationDistance) {
            for (const nodeId of nodes) {
                    // Create a node channel
                    const translationChannel = createTranslationChannel(nodeId, startTime);
            }
    }

The Animation API is designed for only one channel and property type per node. That is, now that we have created a `NodeChannel` with `NodeProperty.Translation` for these nodes we do not want to create another `NodeChannel` with `NodeProperty.Translation` for these nodes. Rather, just add additional keyframes to the channel already created.

Earlier we selected body nodes rather than part nodes when calling the `addTranslationAnimation()` function. If you are using the ID from a part node you will need to set the rotation channel for the node at this step as well. See the Rotation animation example for implementation details.

### 3. Convert the translation vector in the local coordinate

We must consider the node’s local coordinate when we create the translation matrix. We have provided a helper function to make things easier. This is necessary for any part that has already undergone a translation/rotation or if a parent has.

    function addTranslationAnimation(nodes, startTime, duration, translationVector, translationDistance) {
            for (const nodeId of nodes) {
                    // Create a node channel for translation
                    const translationChannel = createTranslationChannel(nodeId, startTime);

                    // Create a node channel for rotation
                    const rotationChannel = createRotationChannel(nodeId, startTime);

                    // helper function
                    function _convertLocalVector(nodeId, vector) {
                            // Compute inverse matrix of the parent node
                            const parentNode = this._viewer.model.getNodeParent(nodeId);
                            const netMatrix = this._viewer.model.getNodeNetMatrix(parentNode);
                            const inverseMatrix = new Communicator.Matrix.inverse(netMatrix);

                            // Convert vector of the parent node
                            const localOrg = Communicator.Point3.zero();
                            inverseMatrix.transform(Communicator.Point3.zero(), localOrg);

                            const localVector = Communicator.Point3.zero();
                            inverseMatrix.transform(vector, localVector);

                            localVector.subtract(localOrg);

                            // Return the local vector
                            return localVector;
                    }

                    // Convert the translation vector in the local coordinate
                    const localVector = _convertLocalVector(nodeId, translationVector);
                    localVector.scale(translationDistance);
            }
    }

### 4. Set keyframe for the node’s end position

Last, we calculate and set the ending keyframe given the node’s initial matrix and local vector:

    function addTranslationAnimation(nodes, startTime, duration, translationVector, translationDistance) {
            for (const nodeId of nodes) {
                    // Create a node channel for translation
                    const translationChannel = createTranslationChannel(nodeId, startTime);

                    // Create a node channel for rotation
                    const rotationChannel = createRotationChannel(nodeId, startTime);

                    // helper function
                    function _convertLocalVector(nodeId, vector) {
                            // Compute inverse matrix of the parent node
                            const parentNode = this._viewer.model.getNodeParent(nodeId);
                            const netMatrix = this._viewer.model.getNodeNetMatrix(parentNode);
                            const inverseMatrix = new Communicator.Matrix.inverse(netMatrix);

                            // Convert vector of the parent node
                            const localOrg = Communicator.Point3.zero();
                            inverseMatrix.transform(Communicator.Point3.zero(), localOrg);

                            const localVector = Communicator.Point3.zero();
                            inverseMatrix.transform(vector, localVector);

                            localVector.subtract(localOrg);

                            // Return the local vector
                            return localVector;
                    }

                    // Convert the translation vector in the local coordinate
                    const localVector = _convertLocalVector(nodeId, translationVector);
                    localVector.scale(translationDistance);

                    // Update node matrix and set to buffer
                    const translationMatrix = new Communicator.Matrix();
                    translationMatrix.setTranslationComponent(localVector.x, localVector.y, localVector.z);
                    const matrix = Communicator.Matrix.multiply(nodeMatrix, translationMatrix);

                    // Create the end keyframe
                    channel.sampler.buffer.insertVec3Keyframe(startTime + duration, matrix.m[12], matrix.m[13], matrix.m[14]);
            }
    }

The only step left is to play the animation using the Player created earlier! We will reload the the Player to ensure it starts at the beginning of the animation.

    player.reload();
    player.play();

The result:

Rendering speed HOOPS Communicator will do its best to keep your animation running according to your specification. However, updates scheduled by the animation API are not interrupted if they cannot be completed before the next frame is scheduled. Therefore, if the hardware is unable to keep up with the rendering demands of the scene, frames will be skipped. For example, let’s say you set a “milliseconds-per-tick” value of 1000 with ten keyframes which occur at ticks 1-10 (keyframe value changes occur every second), but the hardware needs two seconds to complete each update. In this case, you would see the animation render every other tick.

Player methods The animation player object can be paused, stopped, resumed, and waited. Consult the API Reference Manual for more information about Player and Player states.

## Camera animation

The goal of this example is to animate the camera into a better viewing position of the microengine model before the screws are removed. We need three different data points to animate the camera:

1. End camera position

2. Start time

3. Duration

Earlier, we started animating the screws after the animation played for one second. This time, we will start and end the camera animation before the animation for the screws starts.

     // Load camera infromation from JSON with end position
    const camaraJson = JSON.parse('{"position":{"x":-40.79396992044921,"y":82.7693393081149,"z":13.664941852404809},"target":{"x":16.655386566906614,"y":6.510726189166171,"z":-41.14015999906124},"up":{"x":0.3404564032582882,"y":-0.36597162248928816,"z":0.8661144318235252},"width":110.08816117336428,"height":110.08816117336428,"projection":0,"nearLimit":0.01,"className":"Communicator.Camera"}');
    const camera = Communicator.Camera.fromJson(camaraJson);

    // Animate the current camera to the new camera provided
    const starTime = 0;
    const duration = 1;

    addCameraAnimation(camera, starTime, duration);

Next, let’s build all the logic to set up the camera animation.

    function addCameraAnimation(camera, startTime, duration) {
            // Returns an array camera channels for each camera property
            const channels = Communicator.Animation.Util.createCameraChannels(animation, "Camera", Communicator.Animation.InterpolationType.Linear);

            // Get current camera from current view
            var currentCamera = this._viewer.view.getCamera();

            // Add initial camera keyframes using a convenience method that will update keyframe buffers for animation channels created with "createCameraChannels()" from above
            Communicator.Animation.Util.keyframeCamera(startTime, currentCamera, animation);

            // Add end camera at startTime+duration
            if (0 < duration) {
                    Communicator.Animation.Util.keyframeCamera(startTime + duration, camera, animation);
            }

            this._currentCamera = camera;
    }

The result:

The utility function createCameraChannels() will create a CameraChannel for each of the camera properties that can be animated. Likewise, we use the utility function keyframeCamera() to create a keyframe for each property at the same time.

## Rotation animation

This example will rotate a node of the microengine model.

By default, a node will use its center as the pivot point but in this example we will set a custom pivot point. For this animation we need six different data points:

- The ID of the node(s) we are rotating

- Start time of the animation

- Duration of the animation

- Rotation axis

- Rotation center

- Rotation angle

This animation will begin after the camera and screws have finished their animation.

    const nodes = [6];
    const startTime = 3;
    const duration = 1;

    // Rotation parameters in the world coordinate
    const rotationAxis = new Communicator.Point3(1, 0, 0);
    const rotationCenter = new Communicator.Point3(5, 16, -7.5);
    const rotationAngle = -180;

    addRotationAnimation(nodes, startTime, duration, rotationAxis, rotationCenter, rotationAngle)

For each node, we will do the following:

1. Create a NodeChannel

2. Set a start keyframe for the node’s initial position

3. Convert the rotation parameters in the local coordinate

4. Set a pivot point

5. Set the quaternion axis angle

6. Set a keyframe for the node’s end position

These steps should be familiar if you have reviewed the node animation example above. For this example, we will present all the code at once and review each step at the end. Our goal is to rotate the purple backing with the top-right screw position as the pivot point.

    function createRotationChannel(nodeId, startTime) {
            // Create a node channel
            const channelName = `Rotate-${nodeId}`;
            const buffer = new Communicator.Animation.KeyframeBuffer(Communicator.Animation.KeyType.Quat);
            const sampler = new Communicator.Animation.Sampler(buffer, Communicator.Animation.InterpolationType.Linear);
            const channel = animation.createNodeChannel(channelName, nodeId, Communicator.Animation.NodeProperty.Rotation, sampler);

            // Get initial node matrix
            const nodeMatrix = this._viewer.model.getNodeMatrix(nodeId);

            // Set initial keyframe
            const rotation = Communicator.Quaternion.createFromMatrix(nodeMatrix);
            channel.sampler.buffer.appendQuatKeyframe(startTime, rotation.x, rotation.y, rotation.z, rotation.w);

            return channel;
    }

    function addRotationAnimation(nodes, startTime, duration, rotationAxsis, rotationCenter, rotationAngle) {
            for (const nodeId of nodes) {
                    // Create a node channel for rotation
                    const rotationChannel = createRotationChannel(nodeId, startTime);

                    // Create a node channel for translation
                    const translationChannel = createTranslationChannel(nodeId, startTime);

                    // helper function, consider a node's local coordinate for rotation
                    _convertLocalRotation(nodeId, beforeMatrix, rotationAxsis, rotationCenter, rotationAngle) {
                            // Compute inverse matrix of the parent node
                            const parentNode = this._viewer.model.getNodeParent(nodeId);
                            const netMatrix = this._viewer.model.getNodeNetMatrix(parentNode);
                            const inverseMatrix = new Communicator.Matrix.inverse(netMatrix);

                            // Conpute rotatation vector in the parent node
                            const localAxis0 = Communicator.Point3.zero();
                            inverseMatrix.transform(Communicator.Point3.zero(), localAxis0);

                            const localAxis = Communicator.Point3.zero();
                            inverseMatrix.transform(rotationAxsis, localAxis);

                            localAxis.subtract(localAxis0);

                            // Create local rotation matrix
                            const rotationMatrix = Communicator.Matrix.createFromOffAxisRotation(localAxis, rotationAngle);

                            // Node matrix * rotation matrix
                            const multiplyMatrix = Communicator.Matrix.multiply(beforeMatrix, rotationMatrix);

                            // Compute local center point
                            const localCenter = Communicator.Point3.zero();
                            inverseMatrix.transform(rotationCenter, localCenter);

                            // Compute local center point after rotation
                            const rotatePoint = Communicator.Point3.zero();
                            rotationMatrix.transform(localCenter, rotatePoint);

                            // Create translation matrix to shift the node arond rotation center after rotation
                            const translationMatrix = new Communicator.Matrix();
                            translationMatrix.setTranslationComponent(localCenter.x - rotatePoint.x, localCenter.y - rotatePoint.y, localCenter.z - rotatePoint.z);

                            // Compute the node matrix of after rotation (multiplyMatrix * translationMatrix)
                            const afterMatrix = Communicator.Matrix.multiply(multiplyMatrix, translationMatrix);

                            return {
                                    localAxsis: localAxis,
                                    localCenter: localCenter
                            };
                    }

                    // Convert the rotation parameters in the local coordinate
                    const localRotation = _convertLocalRotation(nodeId, nodeMatrix[nodeId], rotationAxsis, rotationCenter, rotationAngle);

                    // Set rotation center point as pivotPoints
                    animation.pivotPoints.set(nodeId, localRotation.localCenter);

                    // Rotate the node counter-clockwise a half-turn
                    function _setQuatAxisAngle(out, axis, rad) {
                                    rad = rad * 0.5;
                                    let s = Math.sin(rad);
                                    out.x = s * axis.x;
                                    out.y = s * axis.y;
                                    out.z = s * axis.z;
                                    out.w = Math.cos(rad);

                                    return out;
                    }

                    // Set the quaternion axis angle
                    const q = Communicator.Quaternion.identity();
                    _setQuatAxisAngle(q, localRotation.localAxsis, Math.PI * rotationAngle / 180);

                    // Set the end keyframe
                    channel.sampler.buffer.appendQuatKeyframe(startTime + duration,  q.x, q.y, q.z, q.w);
            }
    }

1. Create a NodeChannel

Notice the KeyType.Quat for the KeyframeBuffer.

2. Set the initial rotation keyframe

Using the node’s initial matrix, we set the initial keyframe of the rotation animation with the help of `Communicator.Quaternion.createFromMatrix()`.

3. Convert the rotation parameters in the local coordinate

We will use another helper function to consider the local coordinates of the rotating node. The function will get the local axis and center for us.

4. Set a pivot point

Now that we have the local center we would like to rotate the node about we can set the pivot point.

5. Set quaternion axis angle

We set the quaternion axis angle with the local axis provided by our helper function.

6. Set keyframe for the node’s end position

Last, we set the ending keyframe.

The following animation does not set a custom pivot point, rather it uses the default center of the rotating node.

## Export/Import

Two functions are provided with the Animation class that allow you to export and import your animations. You can provide exportAnimations() an array of `Animation` objects and it will return a JSON object that can be serialized with JSON.stringify().

You can provide importAnimations() the JSON object created from exportAnimations() to recreate the `Animation` objects. And don’t forget to use JSON.parse() if you serialized the exported JSON object.