@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
252 lines (225 loc) • 9.83 kB
text/typescript
/* @license
* Copyright 2020 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {BackSide, DoubleSide, FrontSide, Material, Mesh, MeshStandardMaterial, Object3D, Shader} from 'three';
import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js';
import {$clone, $prepare, $preparedGLTF, GLTFInstance, PreparedGLTF} from '../GLTFInstance.js';
import {Renderer} from '../Renderer.js';
import {alphaChunk} from '../shader-chunk/alphatest_fragment.glsl.js';
import {CorrelatedSceneGraph} from './correlated-scene-graph.js';
const $cloneAndPatchMaterial = Symbol('cloneAndPatchMaterial');
const $correlatedSceneGraph = Symbol('correlatedSceneGraph');
interface PreparedModelViewerGLTF extends PreparedGLTF {
[$correlatedSceneGraph]?: CorrelatedSceneGraph;
}
/**
* This specialization of GLTFInstance collects all of the processing needed
* to prepare a model and to clone it making special considerations for
* <model-viewer> use cases.
*/
export class ModelViewerGLTFInstance extends GLTFInstance {
/**
* @override
*/
protected static[$prepare](source: GLTF) {
const prepared = super[$prepare](source) as PreparedModelViewerGLTF;
if (prepared[$correlatedSceneGraph] == null) {
prepared[$correlatedSceneGraph] = CorrelatedSceneGraph.from(prepared);
}
const {scene} = prepared;
const meshesToDuplicate: Mesh[] = [];
scene.traverse((node: Object3D) => {
// Set a high renderOrder while we're here to ensure the model
// always renders on top of the skysphere
node.renderOrder = 1000;
// Three.js seems to cull some animated models incorrectly. Since we
// expect to view our whole scene anyway, we turn off the frustum
// culling optimization here.
node.frustumCulled = false;
// Animations for objects without names target their UUID instead. When
// objects are cloned, they get new UUIDs which the animation can't
// find. To fix this, we assign their UUID as their name.
if (!node.name) {
node.name = node.uuid;
}
if (!(node as Mesh).isMesh) {
return;
}
node.castShadow = true;
const mesh = node as Mesh;
let transparent = false;
const materials =
Array.isArray(mesh.material) ? mesh.material : [mesh.material];
materials.forEach(material => {
if ((material as any).isMeshStandardMaterial) {
if (material.transparent && material.side === DoubleSide) {
transparent = true;
material.side = FrontSide;
}
Renderer.singleton.roughnessMipmapper.generateMipmaps(
material as MeshStandardMaterial);
}
});
if (transparent) {
meshesToDuplicate.push(mesh);
}
});
// We duplicate transparent, double-sided meshes and render the back face
// before the front face. This creates perfect triangle sorting for all
// convex meshes. Sorting artifacts can still appear when you can see
// through more than two layers of a given mesh, but this can usually be
// mitigated by the author splitting the mesh into mostly convex regions.
// The performance cost is not too great as the same shader is reused and
// the same number of fragments are processed; only the vertex shader is run
// twice. @see https://threejs.org/examples/webgl_materials_physical_transparency.html
for (const mesh of meshesToDuplicate) {
const materials =
Array.isArray(mesh.material) ? mesh.material : [mesh.material];
const duplicateMaterials = materials.map((material) => {
const backMaterial = material.clone();
backMaterial.side = BackSide;
return backMaterial;
});
const duplicateMaterial = Array.isArray(mesh.material) ?
duplicateMaterials :
duplicateMaterials[0];
const meshBack = new Mesh(mesh.geometry, duplicateMaterial);
meshBack.renderOrder = -1;
mesh.add(meshBack);
}
return prepared;
}
get correlatedSceneGraph() {
return (
this[$preparedGLTF] as PreparedModelViewerGLTF)[$correlatedSceneGraph]!;
}
/**
* @override
*/
[$clone](): PreparedGLTF {
const clone: PreparedModelViewerGLTF = super[$clone]();
const sourceUUIDToClonedMaterial = new Map<string, Material>();
clone.scene.traverse((node: any) => {
// Materials aren't cloned when cloning meshes; geometry
// and materials are copied by reference. This is necessary
// for the same model to be used twice with different
// environment maps.
if ((node as Mesh).isMesh) {
const mesh = node as Mesh;
if (Array.isArray(mesh.material)) {
mesh.material = mesh.material.map(
(material) => this[$cloneAndPatchMaterial](
material as MeshStandardMaterial,
sourceUUIDToClonedMaterial));
} else if (mesh.material != null) {
mesh.material = this[$cloneAndPatchMaterial](
mesh.material as MeshStandardMaterial,
sourceUUIDToClonedMaterial);
}
}
});
// Cross-correlate the scene graph by relying on information in the
// current scene graph; without this step, relationships between the
// Three.js object graph and the glTF scene graph will be lost.
clone[$correlatedSceneGraph] =
CorrelatedSceneGraph.from(clone, this.correlatedSceneGraph);
return clone;
}
/**
* Creates a clone of the given material, and applies a patch to the
* shader program.
*/
[$cloneAndPatchMaterial](
material: MeshStandardMaterial,
sourceUUIDToClonedMaterial: Map<string, Material>) {
// If we already cloned this material (determined by tracking the UUID of
// source materials that have been cloned), then return that previously
// cloned instance:
if (sourceUUIDToClonedMaterial.has(material.uuid)) {
return sourceUUIDToClonedMaterial.get(material.uuid)!;
}
const clone = material.clone() as MeshStandardMaterial;
// Clone the textures manually since material cloning is shallow. The
// underlying images are still shared.
if (material.map != null) {
clone.map = material.map.clone();
clone.map.needsUpdate = true;
}
if (material.normalMap != null) {
clone.normalMap = material.normalMap.clone();
clone.normalMap.needsUpdate = true;
}
if (material.emissiveMap != null) {
clone.emissiveMap = material.emissiveMap.clone();
clone.emissiveMap.needsUpdate = true;
}
if ((material as any).isGLTFSpecularGlossinessMaterial) {
if ((material as any).specularMap != null) {
(clone as any).specularMap = (material as any).specularMap?.clone();
(clone as any).specularMap.needsUpdate = true;
}
if ((material as any).glossinessMap != null) {
(clone as any).glossinessMap = (material as any).glossinessMap?.clone();
(clone as any).glossinessMap.needsUpdate = true;
}
} else {
// ao, roughness and metalness sometimes share a texture.
if (material.metalnessMap === material.aoMap) {
clone.metalnessMap = clone.aoMap;
} else if (material.metalnessMap != null) {
clone.metalnessMap = material.metalnessMap.clone();
clone.metalnessMap.needsUpdate = true;
}
if (material.roughnessMap === material.aoMap) {
clone.roughnessMap = clone.aoMap;
} else if (material.roughnessMap === material.metalnessMap) {
clone.roughnessMap = clone.metalnessMap;
} else if (material.roughnessMap != null) {
clone.roughnessMap = material.roughnessMap.clone();
clone.roughnessMap.needsUpdate = true;
}
}
// This allows us to patch three's materials, on top of patches already
// made, for instance GLTFLoader patches SpecularGlossiness materials.
// Unfortunately, three's program cache differentiates SpecGloss materials
// via onBeforeCompile.toString(), so these two functions do the same
// thing but look different in order to force a proper recompile.
const oldOnBeforeCompile = material.onBeforeCompile;
clone.onBeforeCompile = (material as any).isGLTFSpecularGlossinessMaterial ?
(shader: Shader) => {
oldOnBeforeCompile(shader, undefined as any);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <alphatest_fragment>', alphaChunk);
} :
(shader: Shader) => {
shader.fragmentShader = shader.fragmentShader.replace(
'#include <alphatest_fragment>', alphaChunk);
oldOnBeforeCompile(shader, undefined as any);
};
// This makes shadows better for non-manifold meshes
clone.shadowSide = FrontSide;
// This improves transparent rendering and can be removed whenever
// https://github.com/mrdoob/three.js/pull/18235 finally lands.
if (clone.transparent) {
clone.depthWrite = false;
}
// This little hack ignores alpha for opaque materials, in order to comply
// with the glTF spec.
if (!clone.alphaTest && !clone.transparent) {
clone.alphaTest = -0.5;
}
sourceUUIDToClonedMaterial.set(material.uuid, clone);
return clone;
}
}