@gltf-transform/extensions
Version:
Adds extension support to @gltf-transform/core
207 lines (172 loc) • 7.35 kB
text/typescript
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.
*
* 
*
* > _**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;
}
}