UNPKG

@gltf-transform/extensions

Version:

Adds extension support to @gltf-transform/core

207 lines (172 loc) 7.35 kB
import { Extension, type ReaderContext, type WriterContext } from '@gltf-transform/core'; import { KHR_MATERIALS_VARIANTS } from '../constants.js'; import { Mapping } from './mapping.js'; import { MappingList } from './mapping-list.js'; import { Variant } from './variant.js'; interface VariantsRootDef { variants: VariantDef[]; } interface VariantDef { name?: string; } interface VariantPrimDef { mappings: VariantMappingDef[]; } interface VariantMappingDef { material: number; variants: number[]; } /** * [`KHR_materials_variants`](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants/) * defines alternate {@link Material} states for any {@link Primitive} in the scene. * * ![Illustration](/media/extensions/khr-materials-variants.jpg) * * > _**Figure:** A sneaker, in three material variants. Source: Khronos Group._ * * Uses include product configurators, night/day states, healthy/damaged states, etc. The * `KHRMaterialsVariants` class provides three {@link ExtensionProperty} types: `Variant`, `Mapping`, * and `MappingList`. When attached to {@link Primitive} properties, these offer flexible ways of * defining the variants available to an application. Triggering a variant is out of scope of this * extension, but could be handled in the application with a UI dropdown, particular game states, * and so on. * * Mesh geometry cannot be changed by this extension, although another extension * (tentative: `KHR_mesh_variants`) is under consideration by the Khronos Group, for that purpose. * * Properties: * - {@link Variant} * - {@link Mapping} * - {@link MappingList} * * ### Example * * ```typescript * import { KHRMaterialsVariants } from '@gltf-transform/extensions'; * * // Create an Extension attached to the Document. * const variantExtension = document.createExtension(KHRMaterialsVariants); * * // Create some Variant states. * const healthyVariant = variantExtension.createVariant('Healthy'); * const damagedVariant = variantExtension.createVariant('Damaged'); * * // Create mappings from a Variant state to a Material. * const healthyMapping = variantExtension.createMapping() * .addVariant(healthyVariant) * .setMaterial(healthyMat); * const damagedMapping = variantExtension.createMapping() * .addVariant(damagedVariant) * .setMaterial(damagedMat); * * // Attach the mappings to a Primitive. * primitive.setExtension( * 'KHR_materials_variants', * variantExtension.createMappingList() * .addMapping(healthyMapping) * .addMapping(damagedMapping) * ); * ``` * * A few notes about this extension: * * 1. Viewers that don't recognized this extension will show the default material for each primitive * instead, so assign that material accordingly. This material can be — but doesn't have to be — * associated with one of the available variants. * 2. Mappings can list multiple Variants. In that case, the first Mapping containing an active * Variant will be chosen by the viewer. * 3. Variant names are how these states are identified, so choose informative names. * 4. When writing the file to an unpacked `.gltf`, instead of an embedded `.glb`, viewers will have * the option of downloading only textures associated with the default state, and lazy-loading * any textures for inactive Variants only when they are needed. */ export class KHRMaterialsVariants extends Extension { public readonly extensionName: typeof KHR_MATERIALS_VARIANTS = KHR_MATERIALS_VARIANTS; public static readonly EXTENSION_NAME: typeof KHR_MATERIALS_VARIANTS = KHR_MATERIALS_VARIANTS; /** Creates a new MappingList property. */ public createMappingList(): MappingList { return new MappingList(this.document.getGraph()); } /** Creates a new Variant property. */ public createVariant(name = ''): Variant { return new Variant(this.document.getGraph(), name); } /** Creates a new Mapping property. */ public createMapping(): Mapping { return new Mapping(this.document.getGraph()); } /** Lists all Variants on the current Document. */ public listVariants(): Variant[] { return Array.from(this.properties).filter((prop) => prop instanceof Variant) as Variant[]; } /** @hidden */ public read(context: ReaderContext): this { const jsonDoc = context.jsonDoc; if (!jsonDoc.json.extensions || !jsonDoc.json.extensions[KHR_MATERIALS_VARIANTS]) return this; // Read all top-level variant names. const variantsRootDef = jsonDoc.json.extensions[KHR_MATERIALS_VARIANTS] as VariantsRootDef; const variantDefs = variantsRootDef.variants || []; const variants = variantDefs.map((variantDef) => this.createVariant().setName(variantDef.name || '')); // For each mesh primitive, read its material/variant mappings. const meshDefs = jsonDoc.json.meshes || []; meshDefs.forEach((meshDef, meshIndex) => { const mesh = context.meshes[meshIndex]; const primDefs = meshDef.primitives || []; primDefs.forEach((primDef, primIndex) => { if (!primDef.extensions || !primDef.extensions[KHR_MATERIALS_VARIANTS]) { return; } const mappingList = this.createMappingList(); const variantPrimDef = primDef.extensions[KHR_MATERIALS_VARIANTS] as VariantPrimDef; for (const mappingDef of variantPrimDef.mappings) { const mapping = this.createMapping(); if (mappingDef.material !== undefined) { mapping.setMaterial(context.materials[mappingDef.material]); } for (const variantIndex of mappingDef.variants || []) { mapping.addVariant(variants[variantIndex]); } mappingList.addMapping(mapping); } mesh.listPrimitives()[primIndex].setExtension(KHR_MATERIALS_VARIANTS, mappingList); }); }); return this; } /** @hidden */ public write(context: WriterContext): this { const jsonDoc = context.jsonDoc; const variants = this.listVariants(); if (!variants.length) return this; // Write all top-level variant names. const variantDefs = []; const variantIndexMap = new Map<Variant, number>(); for (const variant of variants) { variantIndexMap.set(variant, variantDefs.length); variantDefs.push(context.createPropertyDef(variant)); } // For each mesh primitive, write its material/variant mappings. for (const mesh of this.document.getRoot().listMeshes()) { const meshIndex = context.meshIndexMap.get(mesh)!; mesh.listPrimitives().forEach((prim, primIndex) => { const mappingList = prim.getExtension<MappingList>(KHR_MATERIALS_VARIANTS); if (!mappingList) return; const primDef = context.jsonDoc.json.meshes![meshIndex].primitives[primIndex]; const mappingDefs = mappingList.listMappings().map((mapping) => { const mappingDef = context.createPropertyDef(mapping) as VariantMappingDef; const material = mapping.getMaterial(); if (material) { mappingDef.material = context.materialIndexMap.get(material)!; } mappingDef.variants = mapping.listVariants().map((variant) => variantIndexMap.get(variant)!); return mappingDef; }); primDef.extensions = primDef.extensions || {}; primDef.extensions[KHR_MATERIALS_VARIANTS] = { mappings: mappingDefs }; }); } jsonDoc.json.extensions = jsonDoc.json.extensions || {}; jsonDoc.json.extensions[KHR_MATERIALS_VARIANTS] = { variants: variantDefs }; return this; } }