@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
278 lines (234 loc) • 9.72 kB
text/typescript
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;
}
}