threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
156 lines (134 loc) • 5.81 kB
text/typescript
/**
* Materials variants extension
* Modified from https://github.com/takahirox/three-gltf-extensions/blob/main/loaders/KHR_materials_variants/KHR_materials_variants.js
*
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants
*/
import {Material, Mesh, Object3D} from 'three'
import {GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js'
import {IObject3D} from '../../../core'
// export type OnUpdateType = ((arg0: Mesh, arg1: Material, arg2: any) => void) | null
/**
* KHR_materials_variants specification allows duplicated variant names
* but it makes handling the extension complex.
* We ensure tha names and make it easier.
* If you want to export the extension with the original names
* you are recommended to write GLTFExporter plugin to restore the names.
*
* @param variantNames {Array<string>}
* @return {Array<string>}
*/
const ensureUniqueNames = (variantNames: string[]): string[] => {
const uniqueNames = []
const knownNames = new Set()
for (const name of variantNames) {
let uniqueName = name
let suffix = 0
// @TODO: An easy solution.
// O(N^2) in the worst scenario where N is variantNames.length.
// Fix me if needed.
while (knownNames.has(uniqueName)) {
uniqueName = name + '.' + ++suffix
}
knownNames.add(uniqueName)
uniqueNames.push(uniqueName)
}
return uniqueNames
}
/**
* Convert mappings array to table object to make handling the extension easier.
*
* @param extensionDef {glTF.meshes[n].primitive.extensions.KHR_materials_variants}
* @param variantNames {Array<string>} Required to be unique names
* @return {Object}
*/
const mappingsArrayToTable = (extensionDef: any, variantNames: string[]): any => {
const table: any = {}
for (const mapping of extensionDef.mappings) {
for (const variant of mapping.variants) {
table[variantNames[variant]] = {
material: null,
gltfMaterialIndex: mapping.material,
}
}
}
return table
}
/**
* @param object {THREE.Object3D}
* @return {boolean}
*/
const compatibleObject = (object: Object3D) => {
return (object as Mesh).material !== undefined && // easier than (!object.isMesh && !object.isLine && !object.isPoints)
object.userData && // just in case
object.userData._variantMaterials
}
export const khrMaterialsVariantsGLTF = 'KHR_materials_variants'
export class GLTFMaterialsVariantsExtensionImport {
name = khrMaterialsVariantsGLTF
constructor(public parser: GLTFParser) {
}
// Note that the following properties will be overridden even if they are pre-defined
// - mesh.userData._variantMaterials
async afterRoot(gltf: any) {
const parser = this.parser
const json = parser.json
if (!json.extensions || !json.extensions[this.name]) return
const extensionDef = json.extensions[this.name]
const variantsDef = extensionDef.variants || []
const variants = ensureUniqueNames(variantsDef.map((v: any) => v.name))
// Save the _variantMaterials data under associated mesh.userData
for (const scene of gltf.scenes) {
// Save the variants data under associated mesh.userData
(scene as IObject3D).traverse(object => {
const association = parser.associations.get(object)
if (!association || association.meshes === undefined || (association as any).primitives === undefined) {
return
}
const meshDef = json.meshes[association.meshes]
const primitiveDef = meshDef.primitives[(association as any).primitives]
const extensionsDef = primitiveDef.extensions
if (!extensionsDef || !extensionsDef[this.name]) {
return
}
// object should be Mesh
object.userData._variantMaterials = mappingsArrayToTable(extensionsDef[this.name], variants)
})
}
// gltf.userData.variants = variants
/**
* @param object {THREE.Mesh}
* @return {Promise}
*/
const ensureLoadVariants = async(object: Mesh) => {
const currentMaterial = object.material as Material
const variantMaterials = object.userData._variantMaterials
const pending = []
for (const variantName in variantMaterials) {
const variantMaterial = variantMaterials[variantName]
if (variantMaterial.material) {
continue
}
const materialIndex = variantMaterial.gltfMaterialIndex
pending.push(parser.getDependency('material', materialIndex).then(material => {
object.material = material
parser.assignFinalMaterial(object)
variantMaterials[variantName].material = object.material
// delete variantMaterials[variantName].gltfMaterialIndex // todo;
}))
}
return Promise.all(pending).then(() => {
object.material = currentMaterial
})
}
await Promise.all(gltf.scenes.map(async(scene: Object3D) => {
const pending: Promise<any>[] = []
scene.traverse(o => compatibleObject(o) && pending.push(ensureLoadVariants(o as Mesh)))
if (!scene.userData.__importData) scene.userData.__importData = {}
scene.userData.__importData[khrMaterialsVariantsGLTF] = {
names: variants,
}
return Promise.all(pending)
}))
}
}