UNPKG

@google/model-viewer

Version:

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

278 lines (234 loc) 9.72 kB
import {Group, Material, Mesh, Object3D, Texture} from 'three'; import {GLTF as ThreeGLTF, GLTFReference} from 'three/examples/jsm/loaders/GLTFLoader.js'; import {GLTF, GLTFElement, VariantMappings} 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; const gltfElementMap: GLTFElementToThreeObjectMap = new Map(); const defaultMaterial = {name: 'Default'} as Material; const defaultReference = {type: 'materials', index: -1} as GLTFReference; // NOTE: IE11 does not have Map iterator methods associations.forEach((gltfElementReference, threeObject) => { // 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 (gltfElementReference == null) { if (defaultReference.index < 0) { if (gltf.materials == null) { gltf.materials = []; } defaultReference.index = gltf.materials.length; gltf.materials.push(defaultMaterial); } gltfElementReference = defaultReference; } const {type, index} = gltfElementReference; const elementArray = gltf[type] || []; const gltfElement = elementArray[index]; if (gltfElement == null) { // TODO: Maybe throw here... return; } 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) { const {type, index} = elementReference; const cloneElement = cloneGLTF[type]![index]; cloneThreeObjectMap.set(cloneObject, {type, index}); 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 isMesh = (object: unknown): object is Mesh => { return (object as Mesh).isMesh; }; const traverse = (a: ThreeSceneObject, b: ThreeSceneObject) => { callback(a, b); if ((a as Object3D).isObject3D) { if (isMesh(a)) { if (Array.isArray(a.material)) { for (let i = 0; i < a.material.length; ++i) { traverse( a.material[i], ((b as typeof a).material as Material[])[i]); } } else { traverse(a.material, (b as typeof a).material as Material); } } for (let i = 0; i < (a as Object3D).children.length; ++i) { traverse((a as Object3D).children[i], (b as Object3D).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; } loadVariant(variantIndex: number, onUpdate: () => void = () => {}): Set<number> { const updatedMaterials = new Set<number>(); this.threeGLTF.scene.traverse(async (object) => { const {gltfExtensions} = object.userData; if (!(object as Mesh).isMesh || gltfExtensions == null) { return; } const meshVariantData = gltfExtensions['KHR_materials_variants']; if (meshVariantData == null) { return; } let materialIndex = -1; for (const mapping of (meshVariantData.mappings as VariantMappings)) { if (mapping.variants.indexOf(variantIndex) >= 0) { materialIndex = mapping.material; break; } } if (materialIndex < 0) { return; } const material = await this.threeGLTF.parser.getDependency('material', materialIndex); updatedMaterials.add(materialIndex); (object as Mesh).material = material; this.threeGLTF.parser.assignFinalMaterial(object as Mesh); onUpdate(); const gltfElement = this.gltf.materials![materialIndex]; let threeObjects = this.gltfElementMap.get(gltfElement); if (threeObjects == null) { threeObjects = new Set(); this.gltfElementMap.set(gltfElement, threeObjects); } threeObjects.add((object as Mesh).material as Material); }); return updatedMaterials; } }