aframe-extras icon indicating copy to clipboard operation
aframe-extras copied to clipboard

Attach objects to GTLF model bones component

Open rexraptor08 opened this issue 6 years ago • 4 comments
trafficstars

I wrote a component that lets you attach objects to GLTF model bones. It’s working great so far, but I was wondering if you wouldn’t mind taking a look at my code to see if there are any use cases I’m not considering or if it could be written more efficiently. Right now the component let’s you attach either another gltf model to a bone or an a-frame entity (like a-box for example). There’s even an option to trigger an animation clip to the attached gltf model.

It’s used like this:

<a-scene>

 <a-assets>

  <a-asset-item id="skeleton" src="models/glb/skeleton.glb"></a-asset-item>

<a-asset-item id="heart" src="models/glb/heart.glb"></a-asset-item>

  </a-assets>

 <a-entity                             

  gltf-model="#skeleton"

  animation-mixer="clip: idle"

  attach-to-bone="boneName: Armature_mixamorig_Spine2_2; isGLTF: true; addObj: #heart; objPos: 5 -6 2; objScale: 100 100 100; objRot: 0 0 0; hasAnimation: true; clip: heartbeat"

attach-to-bone__box="boneName: Armature_mixamorig_Head_2; isGLTF: false; addObj: #myBox; objPos: 0 0 0; objScale: 20 20 20; objRot: 0 45 0"

           > </a-entity>



<a-box id="myBox" position="0 0 0" rotation="0 0 0" color="#4CC3D9"></a-box>

</a-scene>

The way I get the bone name is by looking at the bone names in blender. The gltf that gets exported from blender adds the prefix “Armature_” to the bone name. So for example, even though it might say “mixamorig_Head_2” in blender, the real name is “Armature_mixamorig_Head_2”.

Let me know what you think. I’d love to get your feedback.

AFRAME.registerComponent("attach-to-bone", {
multiple: true,
  schema: {
boneName: {
  type: 'string'
},
mainModelLoaded: {
  type: 'boolean',
  default: false
},	  
addObj: {
  type: 'string'
},
objPos: {
  type: 'vec3',
  default: { x: 0, y: 0, z: 0 }
},
objScale: {
  type: 'vec3',
  default: { x: 2, y: 2, z: 2 }
},	  
	objRot: {
  type: 'vec3',
  default: { x: 0, y: 0, z: 0 }
},
isGLTF: {
  type: 'boolean',
  default: false
},	  
hasAnimation: {
  type: 'boolean',
  default: false
},	  
clip: {
  type: 'string',
  
},	  

},

	init: function () {
// Boolean to tell component if main model has already been loaded or not
    //Used to set the attribute later if you want to attach to model that already exists
		if(this.data.mainModelLoaded == false){
		this.el.addEventListener('model-loaded', () => {
			// Grab the mesh / scene.
			const obj = this.el.getObject3D('mesh');
			const newElement = document.createElement('a-entity');
			var entityEl;
			this.el.sceneEl.appendChild(newElement);
			if(this.data.isGLTF == true){
				
			newElement.setAttribute('gltf-model', this.data.addObj);	
				
				
			}else{
				
				entityEl = this.el.sceneEl.querySelector(this.data.addObj);
				newElement.appendChild(entityEl);
				
			}
			
			newElement.setAttribute('scale', this.data.objScale);
			newElement.setAttribute('position', this.data.objPos);
			newElement.setAttribute('rotation', this.data.objRot);
			newElement.setAttribute('id',this.id);
			
			if(this.data.hasAnimation == true){
				
				newElement.setAttribute("animation-mixer","clip:"+this.data.clip);
				
				
			}
			
			
			var boneObj = this.el.sceneEl.querySelector('#'+this.id).object3D;
			
			if(this.data.isGLTF == true){
			newElement.addEventListener('model-loaded', () => { //console.log("did I get the object mesh? "+boneObj);
	      										   
			// Go over the submeshes 
			obj.traverse(node => {
				if (node.name.indexOf(this.data.boneName) !== -1) {
					
					node.add(boneObj)
				
				
				}
			});
			
			
			});
			
		}else{
			
			newElement.addEventListener('loaded', () => { //console.log("did I get the object mesh? "+boneObj);
	      										   
			// Go over the submeshes 
			obj.traverse(node => {
				if (node.name.indexOf(this.data.boneName) !== -1) {
					
					node.add(boneObj)
				
				
				}
			});
			
			
			});
			
			
		}
			
		}); 
		
		} else{
			
					
			// Grab the mesh / scene.
			const obj = this.el.getObject3D('mesh');
			const newElement = document.createElement('a-entity');
			var entityEl;
			this.el.sceneEl.appendChild(newElement);
			if(this.data.isGLTF == true){
				
			newElement.setAttribute('gltf-model', this.data.addObj);	
				
				
			}else{
				
				entityEl = this.el.sceneEl.querySelector(this.data.addObj);
				newElement.appendChild(entityEl);
				
			}
			
			newElement.setAttribute('scale', this.data.objScale);
			newElement.setAttribute('position', this.data.objPos);
			newElement.setAttribute('rotation', this.data.objRot);
			newElement.setAttribute('id',this.id);
			
			if(this.data.hasAnimation == true){
				
				newElement.setAttribute("animation-mixer","clip:"+this.data.clip);
				
				
			}
			
			
			var boneObj = this.el.sceneEl.querySelector('#'+this.id).object3D;
			
			if(this.data.isGLTF == true){
			newElement.addEventListener('model-loaded', () => { //console.log("did I get the object mesh? "+boneObj);
	      										   
			// Go over the submeshes 
			obj.traverse(node => {
				if (node.name.indexOf(this.data.boneName) !== -1) {
					
					node.add(boneObj)
				
				
				}
			});
			
			
			});
			
		}else{
			
			newElement.addEventListener('loaded', () => { //console.log("did I get the object mesh? "+boneObj);
	      										   
			// Go over the submeshes 
			obj.traverse(node => {
				if (node.name.indexOf(this.data.boneName) !== -1) {
					
					node.add(boneObj)
				
				
				}
			});
			
			
			});
			
			
		}
						
			
			
		}
		
	}

    });

rexraptor08 avatar Apr 01 '19 19:04 rexraptor08

@wmurphyrd any thoughts on this? Do you know of a component that already does it, or think there should be one?

My first reaction would be that it's possibly better to have a component update the object's position on every frame without reparenting it, to match the bone's position. Similar to constraints in the physics system, but without the need for physics. Reparenting things dynamically has seemed more error prone and complex than needed in a lot of cases.

donmccurdy avatar Apr 07 '19 22:04 donmccurdy

Hi @donmccurdy. After attempting to use the above component in an actual project, it turns out that you were right that it was error prone and needlessly complicated. I took your advice and made it so that the objects I want to attach follow the position and rotation of the bones. It's a lot easier to work with. Here's the new one. Let me know if there's anything you think I can do to improve it. Thanks.

AFRAME.registerComponent('attach-to-bone', { multiple: true, schema: {

	addObj: {
		type: 'string'
	},
	target: {
		type: 'selector'
	},
	boneName: {
		type: 'string'
	},
	matchPosition: {
		type: 'boolean',
		default: true
	},
	positionObj: {
		type: 'boolean',
		default: true
	},
	matchRotation: {
		type: 'boolean',
		default: true
	},
	rotatateObj: {
		type: 'boolean',
		default: true
	},
}, //end schema
init: function () {
	this.el.addEventListener('object3dset', () => {
		const mesh = this.el.getObject3D('mesh');
		//this.data.matchRotation = this.data.matchRotation;

		mesh.traverse(node => {
			if (node.name.indexOf(this.data.boneName) !== -1) {
				var self = this;
				var el = this.el;

				this.data.target = node;
				self.data.rotateObj = self.data.matchRotation;
				self.data.positionObj = self.data.matchPosition;

				//  console.log("match rotation =="+this.data.matchRotation);
			}
		});
	});

},


tick: function (time, timeDelta) {
	var self = this;
	var el = this.el;
	var position = new THREE.Vector3();
	var rotation = new THREE.Euler();
	var targetPosition = self.data.target;
	var targetPos = targetPosition.getWorldPosition(position);
	var targetRot = targetPosition.getWorldQuaternion(rotation);
	var movePart = el.sceneEl.querySelector('#' + this.data.addObj).object3D;

	if (this.data.positionObj == true) {
		movePart.position.set(targetPos.x, targetPos.y, targetPos.z);
	}
	if (this.data.rotateObj == true) {

		movePart.rotation.set(targetRot.x, targetRot.y, targetRot.z);
	}
}

});

rexraptor08 avatar May 31 '19 14:05 rexraptor08

@rexraptor08 do you have a working example anywhere?

planetvoodoo avatar Jun 09 '20 05:06 planetvoodoo

Hey! Can't get it to work. Can you explain how to use it?

MELT9000 avatar Jul 06 '21 22:07 MELT9000