@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
215 lines • 10.1 kB
JavaScript
/* @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, Mesh } from 'three';
import { $clone, $prepare, $preparedGLTF, GLTFInstance } 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');
/**
* 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
*/
static [$prepare](source) {
const prepared = super[$prepare](source);
if (prepared[$correlatedSceneGraph] == null) {
prepared[$correlatedSceneGraph] = CorrelatedSceneGraph.from(prepared);
}
const { scene } = prepared;
const meshesToDuplicate = [];
scene.traverse((node) => {
// 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.isMesh) {
return;
}
node.castShadow = true;
const mesh = node;
let transparent = false;
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
materials.forEach(material => {
if (material.isMeshStandardMaterial) {
if (material.transparent && material.side === DoubleSide) {
transparent = true;
material.side = FrontSide;
}
Renderer.singleton.roughnessMipmapper.generateMipmaps(material);
}
});
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][$correlatedSceneGraph];
}
/**
* @override
*/
[$clone]() {
const clone = super[$clone]();
const sourceUUIDToClonedMaterial = new Map();
clone.scene.traverse((node) => {
// 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.isMesh) {
const mesh = node;
if (Array.isArray(mesh.material)) {
mesh.material = mesh.material.map((material) => this[$cloneAndPatchMaterial](material, sourceUUIDToClonedMaterial));
}
else if (mesh.material != null) {
mesh.material = this[$cloneAndPatchMaterial](mesh.material, 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, sourceUUIDToClonedMaterial) {
var _a, _b;
// 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();
// 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.isGLTFSpecularGlossinessMaterial) {
if (material.specularMap != null) {
clone.specularMap = (_a = material.specularMap) === null || _a === void 0 ? void 0 : _a.clone();
clone.specularMap.needsUpdate = true;
}
if (material.glossinessMap != null) {
clone.glossinessMap = (_b = material.glossinessMap) === null || _b === void 0 ? void 0 : _b.clone();
clone.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.isGLTFSpecularGlossinessMaterial ?
(shader) => {
oldOnBeforeCompile(shader, undefined);
shader.fragmentShader = shader.fragmentShader.replace('#include <alphatest_fragment>', alphaChunk);
} :
(shader) => {
shader.fragmentShader = shader.fragmentShader.replace('#include <alphatest_fragment>', alphaChunk);
oldOnBeforeCompile(shader, undefined);
};
// 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;
}
}
//# sourceMappingURL=ModelViewerGLTFInstance.js.map