const GLTFCache = {}; AFRAME.AGLTFEntity = { defaultInflator(el, componentName, componentData) { if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) { for (let i = 0; i < componentData.length; i++) { el.setAttribute(componentName + "__" + i, componentData[i]); } } else { el.setAttribute(componentName, componentData); } }, registerComponent(componentKey, componentName, inflator) { AFRAME.AGLTFEntity.components[componentKey] = { inflator: inflator || AFRAME.AGLTFEntity.defaultInflator, componentName }; }, components: {} }; // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573 function cloneGltf(gltf) { const skinnedMeshes = {}; gltf.scene.traverse(node => { if (!node.name) { node.name = node.uuid; } if (node.isSkinnedMesh) { skinnedMeshes[node.name] = node; } }); const clone = { animations: gltf.animations, scene: gltf.scene.clone(true) }; const cloneBones = {}; const cloneSkinnedMeshes = {}; clone.scene.traverse(node => { if (node.isBone) { cloneBones[node.name] = node; } if (node.isSkinnedMesh) { cloneSkinnedMeshes[node.name] = node; } }); for (const name in skinnedMeshes) { const skinnedMesh = skinnedMeshes[name]; const skeleton = skinnedMesh.skeleton; const cloneSkinnedMesh = cloneSkinnedMeshes[name]; const orderedCloneBones = []; for (let i = 0; i < skeleton.bones.length; ++i) { const cloneBone = cloneBones[skeleton.bones[i].name]; orderedCloneBones.push(cloneBone); } cloneSkinnedMesh.bind(new THREE.Skeleton(orderedCloneBones, skeleton.boneInverses), cloneSkinnedMesh.matrixWorld); cloneSkinnedMesh.material = skinnedMesh.material.clone(); } return clone; } const inflateEntities = function(classPrefix, parentEl, node) { // setObject3D mutates the node's parent, so we have to copy const children = node.children.slice(0); const el = document.createElement("a-entity"); // Remove invalid CSS class name characters. const className = (node.name || node.uuid).replace(/[^\w-]/g, ""); el.classList.add(classPrefix + className); parentEl.appendChild(el); // Copy over transform to the THREE.Group and reset the actual transform of the Object3D el.setAttribute("position", { x: node.position.x, y: node.position.y, z: node.position.z }); el.setAttribute("rotation", { x: node.rotation.x * THREE.Math.RAD2DEG, y: node.rotation.y * THREE.Math.RAD2DEG, z: node.rotation.z * THREE.Math.RAD2DEG }); el.setAttribute("scale", { x: node.scale.x, y: node.scale.y, z: node.scale.z }); node.position.set(0, 0, 0); node.rotation.set(0, 0, 0); node.scale.set(1, 1, 1); el.setObject3D(node.type.toLowerCase(), node); // Set the name of the `THREE.Group` to match the name of the node, // so that `THREE.PropertyBinding` will find (and later animate) // the group. See `PropertyBinding.findNode`: // https://github.com/mrdoob/three.js/blob/dev/src/animation/PropertyBinding.js#L211 el.object3D.name = node.name; if (node.animations) { // Pass animations up to the group object so that when we can pass the group as // the optional root in `THREE.AnimationMixer.clipAction` and use the hierarchy // preserved under the group (but not the node). Otherwise `clipArray` will be // `null` in `THREE.AnimationClip.findByName`. node.parent.animations = node.animations; } const entityComponents = node.userData.components; if (entityComponents) { for (const prop in entityComponents) { if (entityComponents.hasOwnProperty(prop)) { const { inflator, componentName } = AFRAME.AGLTFEntity.components[prop]; if (inflator) { inflator(el, componentName, entityComponents[prop]); } } } } children.forEach(childNode => { inflateEntities(classPrefix, el, childNode); }); }; function attachTemplate(templateEl) { const selector = templateEl.getAttribute("data-selector"); const targetEls = templateEl.parentNode.querySelectorAll(selector); const clone = document.importNode(templateEl.content, true); const templateRoot = clone.firstElementChild; for (const el of targetEls) { // Merge root element attributes with the target element for (const { name, value } of templateRoot.attributes) { el.setAttribute(name, value); } // Append all child elements for (const child of templateRoot.children) { el.appendChild(document.importNode(child, true)); } } } AFRAME.registerElement("a-gltf-entity", { prototype: Object.create(AFRAME.AEntity.prototype, { load: { value() { if (this.hasLoaded || !this.parentEl) { return; } // Get the src url. let src = this.getAttribute("src"); // If the src attribute is a selector, get the url from the asset item. if (src.charAt(0) === "#") { const assetEl = document.getElementById(src.substring(1)); const fallbackSrc = assetEl.getAttribute("src"); const highSrc = assetEl.getAttribute("high-src"); const lowSrc = assetEl.getAttribute("low-src"); if (highSrc && window.APP.quality === "high") { src = highSrc; } else if (lowSrc && window.APP.quality === "low") { src = lowSrc; } else { src = fallbackSrc; } } const onLoad = gltfModel => { if (!GLTFCache[src]) { // Store a cloned copy of the gltf model. GLTFCache[src] = cloneGltf(gltfModel); } this.model = gltfModel.scene || gltfModel.scenes[0]; this.model.animations = gltfModel.animations; this.setObject3D("mesh", this.model); this.emit("model-loaded", { format: "gltf", model: this.model }); if (this.getAttribute("inflate")) { inflate(this.model, finalizeLoad); } else { finalizeLoad(); } }; const inflate = (model, callback) => { inflateEntities("", this, model); this.querySelectorAll(":scope > template").forEach(attachTemplate); // Wait one tick for the appended custom elements to be connected before calling finalizeLoad setTimeout(callback, 0); }; const finalizeLoad = () => { AFRAME.ANode.prototype.load.call(this, () => { // Check if entity was detached while it was waiting to load. if (!this.parentEl) { return; } this.updateComponents(); if (this.isScene || this.parentEl.isPlaying) { this.play(); } }); }; // Load the gltf model from the cache if it exists. const gltf = GLTFCache[src]; if (gltf) { // Use a cloned copy of the cached model. const clonedGltf = cloneGltf(gltf); onLoad(clonedGltf); return; } // Otherwise load the new gltf model. new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => { // On glTF load error const message = error && error.message ? error.message : "Failed to load glTF model"; console.warn(message); this.emit("model-error", { format: "gltf", src }); }); } } }) });