aframe-extras
aframe-extras copied to clipboard
Attach objects to GTLF model bones component
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)
}
});
});
}
}
}
});
@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.
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 do you have a working example anywhere?
Hey! Can't get it to work. Can you explain how to use it?