UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

255 lines (216 loc) 9.17 kB
import {Group, Material, Mesh, Object3D, Texture} from 'three'; import {GLTF as ThreeGLTF, GLTFReference, GLTFReferenceType} from 'three/examples/jsm/loaders/GLTFLoader.js'; import {GLTF, GLTFElement} from '../../three-components/gltf-instance/gltf-2.0.js'; export type ThreeSceneObject = Object3D|Material|Texture; type ThreeSceneObjectCallback = (a: ThreeSceneObject, b: ThreeSceneObject) => void; export type ThreeObjectSet = Set<ThreeSceneObject>; export type GLTFElementToThreeObjectMap = Map<GLTFElement, ThreeObjectSet>; export type ThreeObjectToGLTFElementHandleMap = Map<ThreeSceneObject, GLTFReference>; const $threeGLTF = Symbol('threeGLTF'); const $gltf = Symbol('gltf'); const $gltfElementMap = Symbol('gltfElementMap'); const $threeObjectMap = Symbol('threeObjectMap'); const $parallelTraverseThreeScene = Symbol('parallelTraverseThreeScene'); const $correlateOriginalThreeGLTF = Symbol('correlateOriginalThreeGLTF'); const $correlateCloneThreeGLTF = Symbol('correlateCloneThreeGLTF'); /** * The Three.js GLTFLoader provides us with an in-memory representation * of a glTF in terms of Three.js constructs. It also provides us with a copy * of the deserialized glTF without any Three.js decoration, and a mapping of * glTF elements to their corresponding Three.js constructs. * * A CorrelatedSceneGraph exposes a synchronously available mapping of glTF * element references to their corresponding Three.js constructs. */ export class CorrelatedSceneGraph { /** * Produce a CorrelatedSceneGraph from a naturally generated Three.js GLTF. * Such GLTFs are produced by Three.js' GLTFLoader, and contain cached * details that expedite the correlation step. * * If a CorrelatedSceneGraph is provided as the second argument, re-correlates * a cloned Three.js GLTF with a clone of the glTF hierarchy used to produce * the upstream Three.js GLTF that the clone was created from. The result * CorrelatedSceneGraph is representative of the cloned hierarchy. */ static from( threeGLTF: ThreeGLTF, upstreamCorrelatedSceneGraph?: CorrelatedSceneGraph): CorrelatedSceneGraph { if (upstreamCorrelatedSceneGraph != null) { return this[$correlateCloneThreeGLTF]( threeGLTF, upstreamCorrelatedSceneGraph); } else { return this[$correlateOriginalThreeGLTF](threeGLTF); } } private static[$correlateOriginalThreeGLTF](threeGLTF: ThreeGLTF): CorrelatedSceneGraph { const gltf = threeGLTF.parser.json as GLTF; const associations = threeGLTF.parser.associations as Map<ThreeSceneObject, GLTFReference>; const gltfElementMap: GLTFElementToThreeObjectMap = new Map(); const defaultMaterial = {name: 'Default'} as Material; const defaultReference = {type: 'materials', index: -1}; for (const threeMaterial of associations.keys()) { // Note: GLTFLoader creates a "default" material that has no // corresponding glTF element in the case that no materials are // specified in the source glTF. In this case we append a default // material to allow this to be operated upon. if (threeMaterial instanceof Material && associations.get(threeMaterial) == null) { if (defaultReference.index < 0) { if (gltf.materials == null) { gltf.materials = []; } defaultReference.index = gltf.materials.length; gltf.materials.push(defaultMaterial); } threeMaterial.name = defaultMaterial.name; associations.set(threeMaterial, {materials: defaultReference.index}); } } // Creates a reverse look up map (gltf-object to Three-object) for (const [threeObject, gltfMappings] of associations) { if (gltfMappings) { threeObject.userData = threeObject.userData || {}; threeObject.userData.associations = gltfMappings; } for (const mapping in gltfMappings) { if (mapping != null && mapping !== 'primitives') { const type = mapping as GLTFReferenceType; const elementArray = gltf[type] || []; const gltfElement = elementArray[gltfMappings[type]!]; if (gltfElement == null) { // TODO: Maybe throw here... continue; } let threeObjects = gltfElementMap.get(gltfElement); if (threeObjects == null) { threeObjects = new Set(); gltfElementMap.set(gltfElement, threeObjects); } threeObjects.add(threeObject); } } } return new CorrelatedSceneGraph( threeGLTF, gltf, associations, gltfElementMap); } /** * Transfers the association between a raw glTF and a Three.js scene graph * to a clone of the Three.js scene graph, resolved as a new * CorrelatedSceneGraph instance. */ private static[$correlateCloneThreeGLTF]( cloneThreeGLTF: ThreeGLTF, upstreamCorrelatedSceneGraph: CorrelatedSceneGraph): CorrelatedSceneGraph { const originalThreeGLTF = upstreamCorrelatedSceneGraph.threeGLTF; const originalGLTF = upstreamCorrelatedSceneGraph.gltf; const cloneGLTF: GLTF = JSON.parse(JSON.stringify(originalGLTF)); const cloneThreeObjectMap: ThreeObjectToGLTFElementHandleMap = new Map(); const cloneGLTFElementMap: GLTFElementToThreeObjectMap = new Map(); for (let i = 0; i < originalThreeGLTF.scenes.length; i++) { this[$parallelTraverseThreeScene]( originalThreeGLTF.scenes[i], cloneThreeGLTF.scenes[i], (object: ThreeSceneObject, cloneObject: ThreeSceneObject) => { const elementReference = upstreamCorrelatedSceneGraph.threeObjectMap.get(object); if (elementReference == null) { return; } for (const mapping in elementReference) { if (mapping != null && mapping !== 'primitives') { const type = mapping as GLTFReferenceType; const index = elementReference[type]!; const cloneElement = cloneGLTF[type]![index]; const mappings = cloneThreeObjectMap.get(cloneObject) || {} as GLTFReference; mappings[type] = index; cloneThreeObjectMap.set(cloneObject, mappings); const cloneObjects: Set<typeof cloneObject> = cloneGLTFElementMap.get(cloneElement) || new Set(); cloneObjects.add(cloneObject); cloneGLTFElementMap.set(cloneElement, cloneObjects); } } }); } return new CorrelatedSceneGraph( cloneThreeGLTF, cloneGLTF, cloneThreeObjectMap, cloneGLTFElementMap); } /** * Traverses two presumably identical Three.js scenes, and invokes a * callback for each Object3D or Material encountered, including the initial * scene. Adapted from * https://github.com/mrdoob/three.js/blob/7c1424c5819ab622a346dd630ee4e6431388021e/examples/jsm/utils/SkeletonUtils.js#L586-L596 */ private static[$parallelTraverseThreeScene]( sceneOne: Group, sceneTwo: Group, callback: ThreeSceneObjectCallback) { const traverse = (a: Object3D, b: Object3D) => { callback(a, b); if (a.isObject3D) { const meshA = a as Mesh; const meshB = b as Mesh; if (meshA.material) { if (Array.isArray(meshA.material)) { for (let i = 0; i < meshA.material.length; ++i) { callback(meshA.material[i], (meshB.material as Material[])[i]); } } else { callback(meshA.material, meshB.material as Material); } } for (let i = 0; i < a.children.length; ++i) { traverse(a.children[i], b.children[i]); } } }; traverse(sceneOne, sceneTwo); } private[$threeGLTF]: ThreeGLTF; private[$gltf]: GLTF; private[$gltfElementMap]: GLTFElementToThreeObjectMap; private[$threeObjectMap]: ThreeObjectToGLTFElementHandleMap; /** * The source Three.js GLTF result given to us by a Three.js GLTFLoader. */ get threeGLTF(): ThreeGLTF { return this[$threeGLTF]; } /** * The in-memory deserialized source glTF. */ get gltf(): GLTF { return this[$gltf]; } /** * A Map of glTF element references to arrays of corresponding Three.js * object references. Three.js objects are kept in arrays to account for * cases where more than one Three.js object corresponds to a single glTF * element. */ get gltfElementMap(): GLTFElementToThreeObjectMap { return this[$gltfElementMap]; } /** * A map of individual Three.js objects to corresponding elements in the * source glTF. */ get threeObjectMap(): ThreeObjectToGLTFElementHandleMap { return this[$threeObjectMap]; } constructor( threeGLTF: ThreeGLTF, gltf: GLTF, threeObjectMap: ThreeObjectToGLTFElementHandleMap, gltfElementMap: GLTFElementToThreeObjectMap) { this[$threeGLTF] = threeGLTF; this[$gltf] = gltf; this[$gltfElementMap] = gltfElementMap; this[$threeObjectMap] = threeObjectMap; } }