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