UNPKG

@google/model-viewer

Version:

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

185 lines (158 loc) 6.25 kB
/* @license * Copyright 2021 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. */ /** * Materials variants extension * * Specification: * https://github.com/takahirox/three-gltf-extensions/tree/main/loaders/KHR_materials_variants */ /** * The code in this file is based on * https://github.com/takahirox/three-gltf-extensions/tree/main/exporters/KHR_materials_variants */ import {Material, Mesh, Object3D} from 'three'; import {GLTFExporterPlugin} from 'three/examples/jsm/Addons.js'; import {VariantData} from '../../features/scene-graph/model.js'; import {UserDataVariantMapping} from './VariantMaterialLoaderPlugin.js'; /** * @param object {THREE.Object3D} * @return {boolean} */ const compatibleObject = (object: Object3D) => { // @TODO: Need properer variantMaterials format validation? return (object as Mesh).material !== undefined && // easier than (!object.isMesh && !object.isLine && // !object.isPoints) object.userData && // just in case object.userData.variantMaterials && // Is this line costly? !!Array .from((object.userData.variantMaterials as Map<number, UserDataVariantMapping>) .values()) .filter(m => compatibleMaterial(m.material)); }; /** * @param material {THREE.Material} * @return {boolean} */ const compatibleMaterial = (material: Material|null) => { // @TODO: support multi materials? return material && material.isMaterial && !Array.isArray(material); }; export default class GLTFExporterMaterialsVariantsExtension implements GLTFExporterPlugin { writer: any; // @TODO: Replace with GLTFWriter when GLTFExporter plugin TS // declaration is ready name: string; variantNames: string[]; constructor(writer: any) { this.writer = writer; this.name = 'KHR_materials_variants'; this.variantNames = []; } beforeParse(objects: Object3D|Object3D[]) { // Find all variant names and store them to the table const variantNameSet = new Set<string>(); const addVariantNames = (o: Object3D) => { if (!compatibleObject(o)) { return; } const variantMaterials = o.userData.variantMaterials as Map<number, UserDataVariantMapping>; const variantDataMap = o.userData.variantData as Map<string, VariantData>; for (const [variantName, variantData] of variantDataMap) { const variantMaterial = variantMaterials.get(variantData.index); // Ignore unloaded variant materials if (variantMaterial && compatibleMaterial(variantMaterial.material)) { variantNameSet.add(variantName); } } }; if (Array.isArray(objects)) { for (const object of objects) { object.traverse(addVariantNames); } } else { objects.traverse(addVariantNames); } // We may want to sort? variantNameSet.forEach(name => this.variantNames.push(name)); } async writeMesh(mesh: Mesh, meshDef: any) { if (!compatibleObject(mesh)) { return; } const userData = mesh.userData; const variantMaterials = userData.variantMaterials as Map<number, UserDataVariantMapping>; const variantDataMap = userData.variantData as Map<string, VariantData>; const mappingTable = new Map<number, {material: number, variants: number[]}>(); // Removes gaps in the variant indices list (caused by deleting variants). const reIndexedVariants = new Map<number, number>(); const variants = Array.from(variantDataMap.values()).sort((a, b) => { return a.index - b.index; }); for (const [i, variantData] of variants.entries()) { reIndexedVariants.set(variantData.index, i); } for (const variantData of variantDataMap.values()) { const variantInstance = variantMaterials.get(variantData.index); if (!variantInstance || !compatibleMaterial(variantInstance.material)) { continue; } const materialIndex = await this.writer.processMaterialAsync(variantInstance.material); if (!mappingTable.has(materialIndex)) { mappingTable.set( materialIndex, {material: materialIndex, variants: []}); } mappingTable.get(materialIndex)!.variants.push( reIndexedVariants.get(variantData.index)!); } const mappingsDef = Array.from(mappingTable.values()) .map((m => {return m.variants.sort((a, b) => a - b) && m})) .sort((a, b) => a.material - b.material); if (mappingsDef.length === 0) { return; } const originalMaterialIndex = compatibleMaterial(userData.originalMaterial) ? await this.writer.processMaterialAsync(userData.originalMaterial) : -1; for (const primitiveDef of meshDef.primitives) { // Override primitiveDef.material with original material. if (originalMaterialIndex >= 0) { primitiveDef.material = originalMaterialIndex; } primitiveDef.extensions = primitiveDef.extensions || {}; primitiveDef.extensions[this.name] = {mappings: mappingsDef}; } } afterParse() { if (this.variantNames.length === 0) { return; } const root = this.writer.json; root.extensions = root.extensions || {}; const variantsDef = this.variantNames.map(n => { return {name: n}; }); root.extensions[this.name] = {variants: variantsDef}; this.writer.extensionsUsed[this.name] = true; } }