From 842c0d0e9e248328db422ae414b77a75aecc01b9 Mon Sep 17 00:00:00 2001
From: Fabian Aichele <aichele@fzi.de>
Date: Mon, 30 Nov 2015 16:58:13 +0100
Subject: [PATCH] [NRRPLT-2545] Merge JavaScript changes for client-side
 animated models into main gzweb repository. Added JS file with AnimatedModel
 class prototype.

Change-Id: I389248d98fd78a10ecf768d0b21757fba64c842e
---
 gz3d/src/gz3d-animated-models.js | 197 +++++++++++++++++++++++++++++++
 gz3d/src/gziface.js              |  65 +++++++++-
 2 files changed, 256 insertions(+), 6 deletions(-)
 create mode 100644 gz3d/src/gz3d-animated-models.js

diff --git a/gz3d/src/gz3d-animated-models.js b/gz3d/src/gz3d-animated-models.js
new file mode 100644
index 0000000..ade3e2c
--- /dev/null
+++ b/gz3d/src/gz3d-animated-models.js
@@ -0,0 +1,197 @@
+var GZ3D = GZ3D || {
+  REVISION : '1'
+};
+
+GZ3D.AnimatedModel = function(scene) {
+  this.scene = scene;
+  this.loader = null;
+
+  this.modelUri_animated = '';
+  this.animatedModelFound = false;
+  this.hideLinkVisuals = false;
+
+  this.modelName = '';
+
+  this.visualsToHide = [];
+
+  this.serverSideModel = null;
+};
+
+// Helper function to find Visuals that need to be hidden when loading a client-side-only animated model
+GZ3D.AnimatedModel.prototype.isVisualHidden = function(visual) {
+  if (visual.geometry && visual.geometry.mesh) {
+    for (var k = 0; k < this.visualsToHide.length; k++) {
+      if (this.visualsToHide[k] === visual.geometry.mesh.filename) {
+        return true;
+      }
+    }
+  }
+  return false;
+};
+
+// Function to check if a client-side-only file for an animated model is available
+GZ3D.AnimatedModel.prototype.animatedModelAvailable = function(model) {
+  var uriPath = GZ3D.assetsPath;
+
+  for (var j = 0; j < model.link.length; ++j) {
+    var link = model.link[j];
+
+    for (var k = 0; k < link.visual.length; k++) {
+      var geom = link.visual[k].geometry;
+      if (geom && geom.mesh) {
+        var meshUri = geom.mesh.filename;
+        // This saves all visuals of a model here because access to the loaded model after the loading
+        // process finished is more difficult to implement. The optimal solution would be to actually
+        // do this only after an animated model was found.
+        this.visualsToHide.push(geom.mesh.filename);
+        var uriType = meshUri.substring(0, meshUri.indexOf('://'));
+        if (uriType === 'file' || uriType === 'model') {
+          var modelUri = meshUri.substring(meshUri.indexOf('://') + 3);
+          var modelName = modelUri.substring(0, modelUri.indexOf('/'));
+          var modelUriCheck = uriPath + '/' + modelName + '/meshes/' + modelName + '_animated.dae';
+          var checkModel = new XMLHttpRequest();
+
+          checkModel.open('HEAD', modelUriCheck, false);
+          try {
+            checkModel.send();
+          } catch (err) {
+            console.log(modelUriCheck + ': no animated version');
+          }
+          if (checkModel.status !== 404) {
+            this.modelUri_animated = modelUriCheck;
+            this.animatedModelFound = true;
+            this.hideLinkVisuals = true;
+            this.modelName = model.name;
+            this.serverSideModel = model;
+            break;
+          }
+        }
+      }
+    }
+  }
+  return this.animatedModelFound;
+};
+
+GZ3D.AnimatedModel.prototype.loadAnimatedModel = function(modelName) {
+  this.loader = new THREE.ColladaLoader();
+
+  // Helper function to enable 'skinning' property so three.js treats meshes as deformable
+
+  var enableSkinning = function (skinnedMesh) {
+    var materials = skinnedMesh.material.materials;
+    if (materials !== null && materials !== undefined) {
+      for (var i = 0, length = materials.length; i < length; i++) {
+        var mat = materials[i];
+        mat.skinning = true;
+      }
+    }
+
+    if (skinnedMesh.material !== undefined && skinnedMesh.material !== null) {
+      skinnedMesh.material.skinning = true;
+    }
+  };
+
+  // Load animated model with separate COLLADA loader instance
+  var that = this;
+  this.loader.load(this.modelUri_animated, function (collada) {
+    var modelParent = new THREE.Object3D();
+    modelParent.name = modelName + '_animated';
+    var linkParent = new THREE.Object3D();
+
+    // Set gray, phong-shaded material for loaded model
+    // TODO: Texturing/GLSL shading; find out what is available in the COLLADA loader implementation
+    collada.scene.traverse (function (child) {
+      if (child instanceof THREE.Mesh) {
+        var transparentMaterial = new THREE.MeshPhongMaterial({color: 0x707070});
+        transparentMaterial.wireframe = false;
+        child.material = transparentMaterial;
+      }
+    });
+
+    // Enable skinning for all child meshes
+    collada.scene.traverse(function (child) {
+      if (child instanceof THREE.SkinnedMesh) {
+        enableSkinning(child);
+      }
+    });
+
+    linkParent.add(collada.scene);
+
+    // Hide model coordinate frames for the time being; remove as soon as position offset and rotation axis issues are fixed
+    /*var collada_scene_axes = new THREE.AxisHelper(2);
+     linkParent.add(collada_scene_axes);*/
+
+    modelParent.add(linkParent);
+
+    // Hide model coordinate frames for the time being; remove as soon as position offset and rotation axis issues are fixed
+    /*var model_parent_axes = new THREE.AxisHelper(4);
+     modelParent.add(model_parent_axes);*/
+
+    // Temporary fix for offset between mouse model defined in SDF and client-side COLLADA model; cause remains to be investigated
+    modelParent.position.y = modelParent.position.y + 5;
+    modelParent.position.z = modelParent.position.z + 0.62;
+
+    // Build list of bones in rig, and attach it to scene node (userData) for later retrieval in animation handling
+    var getBoneList = function (object) {
+      var boneList = [];
+
+      if (object instanceof THREE.Bone) {
+        boneList.push(object);
+      }
+
+      for (var i = 0; i < object.children.length; i++) {
+        boneList.push.apply(boneList, getBoneList(object.children[i]));
+      }
+
+      return boneList;
+    };
+
+    var boneList = getBoneList(collada.scene);
+    var boneHash = {};
+    for (var k = 0; k < boneList.length; k++) {
+      boneHash[boneList[k].name] = boneList[k];
+    }
+    // Skeleton visualization helper class
+    var helper = new THREE.SkeletonHelper(collada.scene);
+
+    boneHash['Skeleton_Visual_Helper'] = helper;
+    modelParent.userData = boneHash;
+
+    helper.material.linewidth = 3;
+    // Hide skeleton helper for the time being
+    // TODO: Make this configurable for visualizing the underlying skeleton
+    helper.visible = false;
+
+    that.scene.add(helper);
+    that.scene.add(modelParent);
+  });
+};
+
+GZ3D.AnimatedModel.prototype.updateJoint = function(robotName, jointName, jointValue, jointAxis) {
+  // Check if there is an animated model for it on the client side
+  var entity = this.scene.getByName(robotName + '_animated');
+  if (entity) {
+    if (entity.userData !== undefined && entity.userData !== null) {
+      var boneName = jointName;
+
+      // Retrieve bone instance from userData map of bone instances in animated model
+      if (entity.userData[boneName] !== undefined && entity.userData[boneName] !== null) {
+        var targetBone = entity.userData[boneName];
+        var rotationAxis = jointAxis;
+        var rotationAngle = jointValue;
+
+        var rotationMatrix = new THREE.Matrix4();
+        rotationMatrix.identity();
+        rotationMatrix.makeRotationAxis(rotationAxis, rotationAngle);
+
+        targetBone.setRotationFromMatrix(rotationMatrix);
+
+        // Update animation handler and skeleton helper
+        // TODO: Move this to the animation loop to synchronize animations with the actual frame rate.
+        // Alternative: Use the clock helper class from three.js for retrieving the actual frame rate delta.
+        entity.userData['Skeleton_Visual_Helper'].update(0.016);
+        THREE.AnimationHandler.update(0.016);
+      }
+    }
+  }
+};
diff --git a/gz3d/src/gziface.js b/gz3d/src/gziface.js
index 2471b3c..6248b22 100644
--- a/gz3d/src/gziface.js
+++ b/gz3d/src/gziface.js
@@ -14,6 +14,9 @@ GZ3D.GZIface = function(scene, gui)
   this.init();
   this.visualsToAdd = [];
 
+  // Stores AnimatedModel instances
+  this.animatedModels = {};
+  
   this.numConnectionTrials = 0;
   this.maxConnectionTrials = 30; // try to connect 30 times
   this.timeToSleepBtwTrials = 1000; // wait 1 second between connection trials
@@ -253,6 +256,27 @@ GZ3D.GZIface.prototype.onConnected = function()
   };
 
   poseTopic.subscribe(poseUpdate.bind(this));
+  
+  // ROS topic subscription for joint_state messages
+  // Requires gzserver version with support for joint state messages
+  var jointTopicSubscriber = new ROSLIB.Topic({ros: this.webSocket,
+                                              name: '~/joint_states',
+                                              message_type: 'jointstates',
+                                              throttle_rate: 1.0 / 25.0 * 1000.0,
+                                              });
+
+  // function for updating transformations of bones in a client-side-only animated model
+  var updateJoint = function(message)
+  {
+    if (message.robot_name in this.animatedModels)
+    {
+      var animatedModel = this.animatedModels[message.robot_name];
+      animatedModel.updateJoint(message.robot_name, message.name, message.position, message.axes);
+    }
+  };
+
+  // Subscription to joint update topic
+  jointTopicSubscriber.subscribe(updateJoint.bind(this));
 
   // Requests - for deleting models
   var requestTopic = new ROSLIB.Topic({
@@ -782,6 +806,21 @@ GZ3D.GZIface.prototype.createModelFromMsg = function(model)
   {
     this.scene.setPose(modelObj, model.pose.position, model.pose.orientation);
   }
+  
+  // Check for client-side-only animated model
+  var animatedModel = new GZ3D.AnimatedModel(this.scene);
+  var animatedModelExists = animatedModel.animatedModelAvailable(model);
+
+  if (animatedModelExists)
+  {
+    animatedModel.loadAnimatedModel(model.name);
+    this.animatedModels[model.name] = animatedModel;
+  }
+  else
+  {
+    animatedModel = null;
+  }
+  
   for (var j = 0; j < model.link.length; ++j)
   {
     var link = model.link[j];
@@ -804,7 +843,19 @@ GZ3D.GZIface.prototype.createModelFromMsg = function(model)
     for (var k = 0; k < link.visual.length; ++k)
     {
       var visual = link.visual[k];
-      var visualObj = this.createVisualFromMsg(visual);
+      // Check if the current visual needs to be hidden, i. e. is replaced by a client-side-only AnimatedModel
+      var isVisual_Hidden = false;
+      if (animatedModel !== null)
+      {
+        isVisual_Hidden = animatedModel.isVisualHidden(visual);
+      }
+
+      var visualObj = null;
+      if (!isVisual_Hidden) {
+        visualObj = this.createVisualFromMsg(visual);
+      }
+
+      // Hide object if it needs to be hidden: Don't add it to the scene in that case.
       if (visualObj && !visualObj.parent)
       {
         linkObj.add(visualObj);
@@ -816,11 +867,13 @@ GZ3D.GZIface.prototype.createModelFromMsg = function(model)
       var collision = link.collision[l];
       for (var m = 0; m < link.collision[l].visual.length; ++m)
       {
-        var collisionVisual = link.collision[l].visual[m];
-        var collisionVisualObj = this.createVisualFromMsg(collisionVisual);
-        if (collisionVisualObj && !collisionVisualObj.parent)
-        {
-          linkObj.add(collisionVisualObj);
+        // Only create collision visuals if this is not an animated model
+        if (!animatedModelExists) {
+          var collisionVisual = link.collision[l].visual[m];
+          var collisionVisualObj = this.createVisualFromMsg(collisionVisual);
+          if (collisionVisualObj && !collisionVisualObj.parent) {
+            linkObj.add(collisionVisualObj);
+          }
         }
       }
     }
-- 
GitLab