UNPKG

@gltf-transform/extensions

Version:

Adds extension support to @gltf-transform/core

1 lines 416 kB
{"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/ext-mesh-gpu-instancing/instanced-mesh.ts","../src/ext-mesh-gpu-instancing/mesh-gpu-instancing.ts","../src/ext-meshopt-compression/constants.ts","../src/ext-meshopt-compression/decoder.ts","../src/ext-meshopt-compression/encoder.ts","../src/ext-meshopt-compression/meshopt-compression.ts","../src/ext-texture-avif/texture-avif.ts","../src/ext-texture-webp/texture-webp.ts","../src/khr-draco-mesh-compression/decoder.ts","../src/khr-draco-mesh-compression/encoder.ts","../src/khr-draco-mesh-compression/draco-mesh-compression.ts","../src/khr-lights-punctual/light.ts","../src/khr-lights-punctual/lights-punctual.ts","../src/khr-materials-anisotropy/anisotropy.ts","../src/khr-materials-anisotropy/materials-anisotropy.ts","../src/khr-materials-clearcoat/clearcoat.ts","../src/khr-materials-clearcoat/materials-clearcoat.ts","../src/khr-materials-diffuse-transmission/diffuse-transmission.ts","../src/khr-materials-diffuse-transmission/materials-diffuse-transmission.ts","../src/khr-materials-dispersion/dispersion.ts","../src/khr-materials-dispersion/materials-dispersion.ts","../src/khr-materials-emissive-strength/emissive-strength.ts","../src/khr-materials-emissive-strength/materials-emissive-strength.ts","../src/khr-materials-ior/ior.ts","../src/khr-materials-ior/materials-ior.ts","../src/khr-materials-iridescence/iridescence.ts","../src/khr-materials-iridescence/materials-iridescence.ts","../src/khr-materials-pbr-specular-glossiness/pbr-specular-glossiness.ts","../src/khr-materials-pbr-specular-glossiness/materials-pbr-specular-glossiness.ts","../src/khr-materials-sheen/sheen.ts","../src/khr-materials-sheen/materials-sheen.ts","../src/khr-materials-specular/specular.ts","../src/khr-materials-specular/materials-specular.ts","../src/khr-materials-transmission/transmission.ts","../src/khr-materials-transmission/materials-transmission.ts","../src/khr-materials-unlit/unlit.ts","../src/khr-materials-unlit/materials-unlit.ts","../src/khr-materials-variants/mapping.ts","../src/khr-materials-variants/mapping-list.ts","../src/khr-materials-variants/variant.ts","../src/khr-materials-variants/materials-variants.ts","../src/khr-materials-volume/volume.ts","../src/khr-materials-volume/materials-volume.ts","../src/khr-mesh-quantization/mesh-quantization.ts","../src/khr-texture-basisu/texture-basisu.ts","../src/khr-texture-transform/transform.ts","../src/khr-texture-transform/texture-transform.ts","../src/khr-xmp-json-ld/packet.ts","../src/khr-xmp-json-ld/xmp.ts","../src/index.ts"],"sourcesContent":["export const EXT_MESH_GPU_INSTANCING = 'EXT_mesh_gpu_instancing';\nexport const EXT_MESHOPT_COMPRESSION = 'EXT_meshopt_compression';\nexport const EXT_TEXTURE_WEBP = 'EXT_texture_webp';\nexport const EXT_TEXTURE_AVIF = 'EXT_texture_avif';\nexport const KHR_DRACO_MESH_COMPRESSION = 'KHR_draco_mesh_compression';\nexport const KHR_LIGHTS_PUNCTUAL = 'KHR_lights_punctual';\nexport const KHR_MATERIALS_ANISOTROPY = 'KHR_materials_anisotropy';\nexport const KHR_MATERIALS_CLEARCOAT = 'KHR_materials_clearcoat';\nexport const KHR_MATERIALS_DIFFUSE_TRANSMISSION = 'KHR_materials_diffuse_transmission';\nexport const KHR_MATERIALS_DISPERSION = 'KHR_materials_dispersion';\nexport const KHR_MATERIALS_EMISSIVE_STRENGTH = 'KHR_materials_emissive_strength';\nexport const KHR_MATERIALS_IOR = 'KHR_materials_ior';\nexport const KHR_MATERIALS_IRIDESCENCE = 'KHR_materials_iridescence';\nexport const KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS = 'KHR_materials_pbrSpecularGlossiness';\nexport const KHR_MATERIALS_SHEEN = 'KHR_materials_sheen';\nexport const KHR_MATERIALS_SPECULAR = 'KHR_materials_specular';\nexport const KHR_MATERIALS_TRANSMISSION = 'KHR_materials_transmission';\nexport const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit';\nexport const KHR_MATERIALS_VOLUME = 'KHR_materials_volume';\nexport const KHR_MATERIALS_VARIANTS = 'KHR_materials_variants';\nexport const KHR_MESH_QUANTIZATION = 'KHR_mesh_quantization';\nexport const KHR_TEXTURE_BASISU = 'KHR_texture_basisu';\nexport const KHR_TEXTURE_TRANSFORM = 'KHR_texture_transform';\nexport const KHR_XMP_JSON_LD = 'KHR_xmp_json_ld';\n","import {\n\ttype Accessor,\n\tExtensionProperty,\n\ttype IProperty,\n\ttype Nullable,\n\tPropertyType,\n\tRefMap,\n} from '@gltf-transform/core';\nimport { EXT_MESH_GPU_INSTANCING } from '../constants.js';\n\ninterface IInstancedMesh extends IProperty {\n\tattributes: RefMap<Accessor>;\n}\n\n// See BufferViewUsage in `writer-context.ts`.\nexport const INSTANCE_ATTRIBUTE = 'INSTANCE_ATTRIBUTE';\n\n/**\n * Defines GPU instances of a {@link Mesh} under one {@link Node}. See {@link EXTMeshGPUInstancing}.\n */\nexport class InstancedMesh extends ExtensionProperty<IInstancedMesh> {\n\tpublic static EXTENSION_NAME: typeof EXT_MESH_GPU_INSTANCING = EXT_MESH_GPU_INSTANCING;\n\tpublic declare extensionName: typeof EXT_MESH_GPU_INSTANCING;\n\tpublic declare propertyType: 'InstancedMesh';\n\tpublic declare parentTypes: [PropertyType.NODE];\n\n\tprotected init(): void {\n\t\tthis.extensionName = EXT_MESH_GPU_INSTANCING;\n\t\tthis.propertyType = 'InstancedMesh';\n\t\tthis.parentTypes = [PropertyType.NODE];\n\t}\n\n\tprotected getDefaults(): Nullable<IInstancedMesh> {\n\t\treturn Object.assign(super.getDefaults() as IProperty, { attributes: new RefMap<Accessor>() });\n\t}\n\n\t/** Returns an instance attribute as an {@link Accessor}. */\n\tpublic getAttribute(semantic: string): Accessor | null {\n\t\treturn this.getRefMap('attributes', semantic);\n\t}\n\n\t/**\n\t * Sets an instance attribute to an {@link Accessor}. All attributes must have the same\n\t * instance count.\n\t */\n\tpublic setAttribute(semantic: string, accessor: Accessor | null): this {\n\t\treturn this.setRefMap('attributes', semantic, accessor, { usage: INSTANCE_ATTRIBUTE });\n\t}\n\n\t/**\n\t * Lists all instance attributes {@link Accessor}s associated with the InstancedMesh. Order\n\t * will be consistent with the order returned by {@link .listSemantics}().\n\t */\n\tpublic listAttributes(): Accessor[] {\n\t\treturn this.listRefMapValues('attributes');\n\t}\n\n\t/**\n\t * Lists all instance attribute semantics associated with the primitive. Order will be\n\t * consistent with the order returned by {@link .listAttributes}().\n\t */\n\tpublic listSemantics(): string[] {\n\t\treturn this.listRefMapKeys('attributes');\n\t}\n}\n","import { Extension, PropertyType, type ReaderContext, type WriterContext } from '@gltf-transform/core';\nimport { EXT_MESH_GPU_INSTANCING } from '../constants.js';\nimport { INSTANCE_ATTRIBUTE, InstancedMesh } from './instanced-mesh.js';\n\ninterface InstancedMeshDef {\n\tattributes: {\n\t\t[name: string]: number;\n\t};\n}\n\n/**\n * [`EXT_mesh_gpu_instancing`](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing/)\n * prepares mesh data for efficient GPU instancing.\n *\n * GPU instancing allows engines to render many copies of a single mesh at once using a small number\n * of draw calls. Instancing is particularly useful for things like trees, grass, road signs, etc.\n * Keep in mind that predefined batches, as used in this extension, may prevent frustum culling\n * within a batch. Dividing batches into collocated cells may be preferable to using a single large\n * batch.\n *\n * > _**NOTICE:** While this extension stores mesh data optimized for GPU instancing, it\n * > is important to note that (1) GPU instancing and other optimizations are possible — and\n * > encouraged — even without this extension, and (2) other common meanings of the term\n * > \"instancing\" exist, distinct from this extension. See\n * > [Appendix: Motivation and Purpose](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing#appendix-motivation-and-purpose)\n * > of the `EXT_mesh_gpu_instancing` specification._\n *\n * Properties:\n * - {@link InstancedMesh}\n *\n * ### Example\n *\n * The `EXTMeshGPUInstancing` class provides a single {@link ExtensionProperty} type, `InstancedMesh`,\n * which may be attached to any {@link Node} instance. For example:\n *\n * ```typescript\n * import { EXTMeshGPUInstancing } from '@gltf-transform/extensions';\n *\n * // Create standard mesh, node, and scene hierarchy.\n * // ...\n *\n * // Assign positions for each instance.\n * const batchPositions = doc.createAccessor('instance_positions')\n * \t.setArray(new Float32Array([\n * \t\t0, 0, 0,\n * \t\t1, 0, 0,\n * \t\t2, 0, 0,\n * \t]))\n * \t.setType(Accessor.Type.VEC3)\n * \t.setBuffer(buffer);\n *\n * // Assign IDs for each instance.\n * const batchIDs = doc.createAccessor('instance_ids')\n * \t.setArray(new Uint8Array([0, 1, 2]))\n * \t.setType(Accessor.Type.SCALAR)\n * \t.setBuffer(buffer);\n *\n * // Create an Extension attached to the Document.\n * const batchExtension = document.createExtension(EXTMeshGPUInstancing)\n * \t.setRequired(true);\n * const batch = batchExtension.createInstancedMesh()\n * \t.setAttribute('TRANSLATION', batchPositions)\n * \t.setAttribute('_ID', batchIDs);\n *\n * node\n * \t.setMesh(mesh)\n * \t.setExtension('EXT_mesh_gpu_instancing', batch);\n * ```\n *\n * Standard instance attributes are `TRANSLATION`, `ROTATION`, and `SCALE`, and support the accessor\n * types allowed by the extension specification. Custom instance attributes are allowed, and should\n * be prefixed with an underscore (`_*`).\n */\nexport class EXTMeshGPUInstancing extends Extension {\n\tpublic readonly extensionName: typeof EXT_MESH_GPU_INSTANCING = EXT_MESH_GPU_INSTANCING;\n\t/** @hidden */\n\tpublic readonly provideTypes: PropertyType[] = [PropertyType.NODE];\n\t/** @hidden */\n\tpublic readonly prewriteTypes: PropertyType[] = [PropertyType.ACCESSOR];\n\tpublic static readonly EXTENSION_NAME: typeof EXT_MESH_GPU_INSTANCING = EXT_MESH_GPU_INSTANCING;\n\n\t/** Creates a new InstancedMesh property for use on a {@link Node}. */\n\tpublic createInstancedMesh(): InstancedMesh {\n\t\treturn new InstancedMesh(this.document.getGraph());\n\t}\n\n\t/** @hidden */\n\tpublic read(context: ReaderContext): this {\n\t\tconst jsonDoc = context.jsonDoc;\n\n\t\tconst nodeDefs = jsonDoc.json.nodes || [];\n\t\tnodeDefs.forEach((nodeDef, nodeIndex) => {\n\t\t\tif (!nodeDef.extensions || !nodeDef.extensions[EXT_MESH_GPU_INSTANCING]) return;\n\n\t\t\tconst instancedMeshDef = nodeDef.extensions[EXT_MESH_GPU_INSTANCING] as InstancedMeshDef;\n\t\t\tconst instancedMesh = this.createInstancedMesh();\n\n\t\t\tfor (const semantic in instancedMeshDef.attributes) {\n\t\t\t\tinstancedMesh.setAttribute(semantic, context.accessors[instancedMeshDef.attributes[semantic]]);\n\t\t\t}\n\n\t\t\tcontext.nodes[nodeIndex].setExtension(EXT_MESH_GPU_INSTANCING, instancedMesh);\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/** @hidden */\n\tpublic prewrite(context: WriterContext): this {\n\t\t// Set usage for instance attribute accessors, so they are stored in separate buffer\n\t\t// views grouped by parent reference.\n\t\tcontext.accessorUsageGroupedByParent.add(INSTANCE_ATTRIBUTE);\n\t\tfor (const prop of this.properties) {\n\t\t\tfor (const attribute of (prop as InstancedMesh).listAttributes()) {\n\t\t\t\tcontext.addAccessorToUsageGroup(attribute, INSTANCE_ATTRIBUTE);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** @hidden */\n\tpublic write(context: WriterContext): this {\n\t\tconst jsonDoc = context.jsonDoc;\n\n\t\tthis.document\n\t\t\t.getRoot()\n\t\t\t.listNodes()\n\t\t\t.forEach((node) => {\n\t\t\t\tconst instancedMesh = node.getExtension<InstancedMesh>(EXT_MESH_GPU_INSTANCING);\n\t\t\t\tif (instancedMesh) {\n\t\t\t\t\tconst nodeIndex = context.nodeIndexMap.get(node)!;\n\t\t\t\t\tconst nodeDef = jsonDoc.json.nodes![nodeIndex];\n\n\t\t\t\t\tconst instancedMeshDef = { attributes: {} } as InstancedMeshDef;\n\n\t\t\t\t\tinstancedMesh.listSemantics().forEach((semantic) => {\n\t\t\t\t\t\tconst attribute = instancedMesh.getAttribute(semantic)!;\n\t\t\t\t\t\tinstancedMeshDef.attributes[semantic] = context.accessorIndexMap.get(attribute)!;\n\t\t\t\t\t});\n\n\t\t\t\t\tnodeDef.extensions = nodeDef.extensions || {};\n\t\t\t\t\tnodeDef.extensions[EXT_MESH_GPU_INSTANCING] = instancedMeshDef;\n\t\t\t\t}\n\t\t\t});\n\n\t\treturn this;\n\t}\n}\n","import type { GLTF, TypedArray } from '@gltf-transform/core';\n\nexport enum EncoderMethod {\n\tQUANTIZE = 'quantize',\n\tFILTER = 'filter',\n}\n\nexport interface MeshoptBufferExtension {\n\tfallback?: boolean;\n}\n\nexport enum MeshoptMode {\n\tATTRIBUTES = 'ATTRIBUTES',\n\tTRIANGLES = 'TRIANGLES',\n\tINDICES = 'INDICES',\n}\n\nexport enum MeshoptFilter {\n\t/** No filter — quantize only. */\n\tNONE = 'NONE',\n\t/** Four 8- or 16-bit normalized values. */\n\tOCTAHEDRAL = 'OCTAHEDRAL',\n\t/** Four 16-bit normalized values. */\n\tQUATERNION = 'QUATERNION',\n\t/** K single-precision floating point values. */\n\tEXPONENTIAL = 'EXPONENTIAL',\n}\n\nexport interface MeshoptBufferViewExtension {\n\tbuffer: number;\n\tbyteOffset: number;\n\tbyteLength: number;\n\tbyteStride: number;\n\tcount: number;\n\tmode: MeshoptMode;\n\tfilter?: MeshoptFilter;\n}\n\n/**\n * When using filters, the accessor definition written to the file will not necessarily have the\n * same properties as the input accessor. For example, octahedral encoding requires int8 or int16\n * output, so float32 input must be ignored.\n */\nexport interface PreparedAccessor {\n\tarray: TypedArray;\n\tbyteStride: number;\n\tnormalized: boolean;\n\tcomponentType: GLTF.AccessorComponentType;\n\tmin?: number[];\n\tmax?: number[];\n}\n","import type { GLTF } from '@gltf-transform/core';\nimport { EXT_MESHOPT_COMPRESSION } from '../constants.js';\nimport type { MeshoptBufferExtension } from './constants.js';\n\n/**\n * Returns true for a fallback buffer, else false.\n *\n * - All references to the fallback buffer must come from bufferViews that\n * have a EXT_meshopt_compression extension specified.\n * - No references to the fallback buffer may come from\n * EXT_meshopt_compression extension JSON.\n */\nexport function isFallbackBuffer(bufferDef: GLTF.IBuffer): boolean {\n\tif (!bufferDef.extensions || !bufferDef.extensions[EXT_MESHOPT_COMPRESSION]) return false;\n\tconst fallbackDef = bufferDef.extensions[EXT_MESHOPT_COMPRESSION] as MeshoptBufferExtension;\n\treturn !!fallbackDef.fallback;\n}\n","import {\n\tAccessor,\n\tAnimationChannel,\n\tAnimationSampler,\n\tBufferUtils,\n\ttype Document,\n\ttype GLTF,\n\tMathUtils,\n\tPrimitive,\n\tPropertyType,\n\tRoot,\n\ttype TypedArray,\n\ttype TypedArrayConstructor,\n\tWriterContext,\n} from '@gltf-transform/core';\nimport type { MeshoptEncoder } from 'meshoptimizer';\nimport { MeshoptFilter, MeshoptMode, type PreparedAccessor } from './constants.js';\n\nconst { BYTE, SHORT, FLOAT } = Accessor.ComponentType;\nconst { encodeNormalizedInt, decodeNormalizedInt } = MathUtils;\n\n/** Pre-processes array with required filters or padding. */\nexport function prepareAccessor(\n\taccessor: Accessor,\n\tencoder: typeof MeshoptEncoder,\n\tmode: MeshoptMode,\n\tfilterOptions: { filter: MeshoptFilter; bits?: number },\n): PreparedAccessor {\n\tconst { filter, bits } = filterOptions as { filter: MeshoptFilter; bits: number };\n\tconst result: PreparedAccessor = {\n\t\tarray: accessor.getArray()!,\n\t\tbyteStride: accessor.getElementSize() * accessor.getComponentSize(),\n\t\tcomponentType: accessor.getComponentType(),\n\t\tnormalized: accessor.getNormalized(),\n\t};\n\n\tif (mode !== MeshoptMode.ATTRIBUTES) return result;\n\n\tif (filter !== MeshoptFilter.NONE) {\n\t\tlet array = accessor.getNormalized() ? decodeNormalizedIntArray(accessor) : new Float32Array(result.array);\n\n\t\tswitch (filter) {\n\t\t\tcase MeshoptFilter.EXPONENTIAL: // → K single-precision floating point values.\n\t\t\t\tresult.byteStride = accessor.getElementSize() * 4;\n\t\t\t\tresult.componentType = FLOAT;\n\t\t\t\tresult.normalized = false;\n\t\t\t\tresult.array = encoder.encodeFilterExp(array, accessor.getCount(), result.byteStride, bits);\n\t\t\t\tbreak;\n\n\t\t\tcase MeshoptFilter.OCTAHEDRAL: // → four 8- or 16-bit normalized values.\n\t\t\t\tresult.byteStride = bits > 8 ? 8 : 4;\n\t\t\t\tresult.componentType = bits > 8 ? SHORT : BYTE;\n\t\t\t\tresult.normalized = true;\n\t\t\t\tarray = accessor.getElementSize() === 3 ? padNormals(array) : array;\n\t\t\t\tresult.array = encoder.encodeFilterOct(array, accessor.getCount(), result.byteStride, bits);\n\t\t\t\tbreak;\n\n\t\t\tcase MeshoptFilter.QUATERNION: // → four 16-bit normalized values.\n\t\t\t\tresult.byteStride = 8;\n\t\t\t\tresult.componentType = SHORT;\n\t\t\t\tresult.normalized = true;\n\t\t\t\tresult.array = encoder.encodeFilterQuat(array, accessor.getCount(), result.byteStride, bits);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error('Invalid filter.');\n\t\t}\n\n\t\tresult.min = accessor.getMin([]);\n\t\tresult.max = accessor.getMax([]);\n\t\tif (accessor.getNormalized()) {\n\t\t\tresult.min = result.min.map((v) => decodeNormalizedInt(v, accessor.getComponentType()));\n\t\t\tresult.max = result.max.map((v) => decodeNormalizedInt(v, accessor.getComponentType()));\n\t\t}\n\t\tif (result.normalized) {\n\t\t\tresult.min = result.min.map((v) => encodeNormalizedInt(v, result.componentType));\n\t\t\tresult.max = result.max.map((v) => encodeNormalizedInt(v, result.componentType));\n\t\t}\n\t} else if (result.byteStride % 4) {\n\t\tresult.array = padArrayElements(result.array, accessor.getElementSize());\n\t\tresult.byteStride = result.array.byteLength / accessor.getCount();\n\t}\n\n\treturn result;\n}\n\nfunction decodeNormalizedIntArray(attribute: Accessor): Float32Array {\n\tconst componentType = attribute.getComponentType();\n\tconst srcArray = attribute.getArray()!;\n\tconst dstArray = new Float32Array(srcArray.length);\n\tfor (let i = 0; i < srcArray.length; i++) {\n\t\tdstArray[i] = decodeNormalizedInt(srcArray[i], componentType);\n\t}\n\treturn dstArray;\n}\n\n/** Pads array to 4 byte alignment, required for Meshopt ATTRIBUTE buffer views. */\nexport function padArrayElements<T extends TypedArray>(srcArray: T, elementSize: number): T {\n\tconst byteStride = BufferUtils.padNumber(srcArray.BYTES_PER_ELEMENT * elementSize);\n\tconst elementStride = byteStride / srcArray.BYTES_PER_ELEMENT;\n\tconst elementCount = srcArray.length / elementSize;\n\n\tconst dstArray = new (srcArray.constructor as TypedArrayConstructor)(elementCount * elementStride) as T;\n\n\tfor (let i = 0; i * elementSize < srcArray.length; i++) {\n\t\tfor (let j = 0; j < elementSize; j++) {\n\t\t\tdstArray[i * elementStride + j] = srcArray[i * elementSize + j];\n\t\t}\n\t}\n\n\treturn dstArray;\n}\n\n/** Pad normals with a .w component for octahedral encoding. */\nfunction padNormals(srcArray: Float32Array): Float32Array {\n\tconst dstArray = new Float32Array((srcArray.length * 4) / 3);\n\tfor (let i = 0, il = srcArray.length / 3; i < il; i++) {\n\t\tdstArray[i * 4] = srcArray[i * 3];\n\t\tdstArray[i * 4 + 1] = srcArray[i * 3 + 1];\n\t\tdstArray[i * 4 + 2] = srcArray[i * 3 + 2];\n\t}\n\treturn dstArray;\n}\n\nexport function getMeshoptMode(accessor: Accessor, usage: string): MeshoptMode {\n\tif (usage === WriterContext.BufferViewUsage.ELEMENT_ARRAY_BUFFER) {\n\t\tconst isTriangles = accessor.listParents().some((parent) => {\n\t\t\treturn parent instanceof Primitive && parent.getMode() === Primitive.Mode.TRIANGLES;\n\t\t});\n\t\treturn isTriangles ? MeshoptMode.TRIANGLES : MeshoptMode.INDICES;\n\t}\n\n\treturn MeshoptMode.ATTRIBUTES;\n}\n\nexport function getMeshoptFilter(accessor: Accessor, doc: Document): { filter: MeshoptFilter; bits?: number } {\n\tconst refs = doc\n\t\t.getGraph()\n\t\t.listParentEdges(accessor)\n\t\t.filter((edge) => !(edge.getParent() instanceof Root));\n\n\tfor (const ref of refs) {\n\t\tconst refName = ref.getName();\n\t\tconst refKey = (ref.getAttributes().key || '') as string;\n\t\tconst isDelta = ref.getParent().propertyType === PropertyType.PRIMITIVE_TARGET;\n\n\t\t// Indices.\n\t\tif (refName === 'indices') return { filter: MeshoptFilter.NONE };\n\n\t\t// Attributes.\n\t\t//\n\t\t// NOTES:\n\t\t// - Vertex attributes should be filtered IFF they are _not_ quantized in\n\t\t// 'packages/cli/src/transforms/meshopt.ts'.\n\t\t// - POSITION and TEXCOORD_0 could use exponential filtering, but this produces broken\n\t\t// output in some cases (e.g. Matilda.glb), for unknown reasons. gltfpack uses manual\n\t\t// quantization for these attributes.\n\t\t// - NORMAL and TANGENT attributes use Octahedral filters, but deltas in morphs do not.\n\t\t// - When specifying bit depth for vertex attributes, check the defaults in `quantize.ts`\n\t\t//\t and overrides in `meshopt.ts`. Don't store deltas at higher precision than base.\n\t\tif (refName === 'attributes') {\n\t\t\tif (refKey === 'POSITION') return { filter: MeshoptFilter.NONE };\n\t\t\tif (refKey === 'TEXCOORD_0') return { filter: MeshoptFilter.NONE };\n\t\t\tif (refKey.startsWith('JOINTS_')) return { filter: MeshoptFilter.NONE };\n\t\t\tif (refKey.startsWith('WEIGHTS_')) return { filter: MeshoptFilter.NONE };\n\t\t\tif (refKey === 'NORMAL' || refKey === 'TANGENT') {\n\t\t\t\treturn isDelta ? { filter: MeshoptFilter.NONE } : { filter: MeshoptFilter.OCTAHEDRAL, bits: 8 };\n\t\t\t}\n\t\t}\n\n\t\t// Animation.\n\t\tif (refName === 'output') {\n\t\t\tconst targetPath = getTargetPath(accessor);\n\t\t\tif (targetPath === 'rotation') return { filter: MeshoptFilter.QUATERNION, bits: 16 };\n\t\t\tif (targetPath === 'translation') return { filter: MeshoptFilter.EXPONENTIAL, bits: 12 };\n\t\t\tif (targetPath === 'scale') return { filter: MeshoptFilter.EXPONENTIAL, bits: 12 };\n\t\t\treturn { filter: MeshoptFilter.NONE };\n\t\t}\n\n\t\t// See: https://github.com/donmccurdy/glTF-Transform/issues/489\n\t\tif (refName === 'input') return { filter: MeshoptFilter.NONE };\n\n\t\tif (refName === 'inverseBindMatrices') return { filter: MeshoptFilter.NONE };\n\t}\n\n\treturn { filter: MeshoptFilter.NONE };\n}\n\nexport function getTargetPath(accessor: Accessor): GLTF.AnimationChannelTargetPath | null {\n\tfor (const sampler of accessor.listParents()) {\n\t\tif (!(sampler instanceof AnimationSampler)) continue;\n\t\tfor (const channel of sampler.listParents()) {\n\t\t\tif (!(channel instanceof AnimationChannel)) continue;\n\t\t\treturn channel.getTargetPath();\n\t\t}\n\t}\n\treturn null;\n}\n","import {\n\tAccessor,\n\ttype Buffer,\n\tBufferUtils,\n\tExtension,\n\tGLB_BUFFER,\n\ttype GLTF,\n\ttype Property,\n\tPropertyType,\n\ttype ReaderContext,\n\tWriterContext,\n} from '@gltf-transform/core';\nimport type { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';\nimport { EXT_MESHOPT_COMPRESSION } from '../constants.js';\nimport { EncoderMethod, type MeshoptBufferViewExtension, MeshoptFilter } from './constants.js';\nimport { isFallbackBuffer } from './decoder.js';\nimport { getMeshoptFilter, getMeshoptMode, getTargetPath, prepareAccessor } from './encoder.js';\n\ninterface EncoderOptions {\n\tmethod?: EncoderMethod;\n}\n\nconst DEFAULT_ENCODER_OPTIONS: Required<EncoderOptions> = {\n\tmethod: EncoderMethod.QUANTIZE,\n};\n\ntype MeshoptBufferView = { extensions: { [EXT_MESHOPT_COMPRESSION]: MeshoptBufferViewExtension } };\ntype EncodedBufferView = GLTF.IBufferView & MeshoptBufferView;\n\n/**\n * [`EXT_meshopt_compression`](https://github.com/KhronosGroup/gltf/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/)\n * provides compression and fast decoding for geometry, morph targets, and animations.\n *\n * Meshopt compression (based on the [meshoptimizer](https://github.com/zeux/meshoptimizer)\n * library) offers a lightweight decoder with very fast runtime decompression, and is\n * appropriate for models of any size. Meshopt can reduce the transmission sizes of geometry,\n * morph targets, animation, and other numeric data stored in buffer views. When textures are\n * large, other complementary compression methods should be used as well.\n *\n * For the full benefits of meshopt compression, **apply gzip, brotli, or another lossless\n * compression method** to the resulting .glb, .gltf, or .bin files. Meshopt specifically\n * pre-optimizes assets for this purpose — without this secondary compression, the size\n * reduction is considerably less.\n *\n * Be aware that decompression happens before uploading to the GPU. While Meshopt decoding is\n * considerably faster than Draco decoding, neither compression method will improve runtime\n * performance directly. To improve framerate, you'll need to simplify the geometry by reducing\n * vertex count or draw calls — not just compress it. Finally, be aware that Meshopt compression is\n * lossy: repeatedly compressing and decompressing a model in a pipeline will lose precision, so\n * compression should generally be the last stage of an art workflow, and uncompressed original\n * files should be kept.\n *\n * The meshoptimizer library ([github](https://github.com/zeux/meshoptimizer/tree/master/js),\n * [npm](https://www.npmjs.com/package/meshoptimizer)) is a required dependency for reading or\n * writing files, and must be provided by the application. Compression may alternatively be applied\n * with the [gltfpack](https://github.com/zeux/meshoptimizer/tree/master/gltf) tool.\n *\n * ### Example — Read\n *\n * To read glTF files using Meshopt compression, ensure that the extension\n * and a decoder are registered. Geometry and other data are decompressed\n * while reading the file.\n *\n * ```typescript\n * import { NodeIO } from '@gltf-transform/core';\n * import { EXTMeshoptCompression } from '@gltf-transform/extensions';\n * import { MeshoptDecoder } from 'meshoptimizer';\n *\n * await MeshoptDecoder.ready;\n *\n * const io = new NodeIO()\n * \t.registerExtensions([EXTMeshoptCompression])\n * \t.registerDependencies({ 'meshopt.decoder': MeshoptDecoder });\n *\n * // Read and decode.\n * const document = await io.read('compressed.glb');\n * ```\n *\n * ### Example — Write\n *\n * The simplest way to apply Meshopt compression is with the {@link meshopt}\n * transform. The extension and an encoder must be registered.\n *\n * ```typescript\n * import { NodeIO } from '@gltf-transform/core';\n * import { EXTMeshoptCompression } from '@gltf-transform/extensions';\n * import { meshopt } from '@gltf-transform/functions';\n * import { MeshoptEncoder } from 'meshoptimizer';\n *\n * await MeshoptEncoder.ready;\n *\n * const io = new NodeIO()\n * \t.registerExtensions([EXTMeshoptCompression])\n * \t.registerDependencies({ 'meshopt.encoder': MeshoptEncoder });\n *\n * await document.transform(\n * meshopt({encoder: MeshoptEncoder, level: 'medium'})\n * );\n *\n * await io.write('compressed-medium.glb', document);\n * ```\n *\n * ### Example — Advanced\n *\n * Internally, the {@link meshopt} transform reorders and quantizes vertex data\n * to preparate for compression. If you prefer different pre-processing, the\n * EXTMeshoptCompression extension can be added to the document manually:\n *\n * ```typescript\n * import { reorder, quantize } from '@gltf-transform/functions';\n * import { EXTMeshoptCompression } from '@gltf-transform/extensions';\n * import { MeshoptEncoder } from 'meshoptimizer';\n *\n * await document.transform(\n * \treorder({encoder: MeshoptEncoder}),\n * \tquantize()\n * );\n *\n * document.createExtension(EXTMeshoptCompression)\n * \t.setRequired(true)\n * \t.setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.QUANTIZE });\n * ```\n *\n * In either case, compression is deferred until generating output with an I/O\n * class.\n */\nexport class EXTMeshoptCompression extends Extension {\n\tpublic readonly extensionName: typeof EXT_MESHOPT_COMPRESSION = EXT_MESHOPT_COMPRESSION;\n\t/** @hidden */\n\tpublic readonly prereadTypes: PropertyType[] = [PropertyType.BUFFER, PropertyType.PRIMITIVE];\n\t/** @hidden */\n\tpublic readonly prewriteTypes: PropertyType[] = [PropertyType.BUFFER, PropertyType.ACCESSOR];\n\t/** @hidden */\n\tpublic readonly readDependencies: string[] = ['meshopt.decoder'];\n\t/** @hidden */\n\tpublic readonly writeDependencies: string[] = ['meshopt.encoder'];\n\n\tpublic static readonly EXTENSION_NAME: typeof EXT_MESHOPT_COMPRESSION = EXT_MESHOPT_COMPRESSION;\n\tpublic static readonly EncoderMethod: typeof EncoderMethod = EncoderMethod;\n\n\tprivate _decoder: typeof MeshoptDecoder | null = null;\n\tprivate _decoderFallbackBufferMap = new Map<Buffer, Buffer>();\n\tprivate _encoder: typeof MeshoptEncoder | null = null;\n\tprivate _encoderOptions: Required<EncoderOptions> = DEFAULT_ENCODER_OPTIONS;\n\tprivate _encoderFallbackBuffer: Buffer | null = null;\n\tprivate _encoderBufferViews: { [key: string]: EncodedBufferView } = {};\n\tprivate _encoderBufferViewData: { [key: string]: Uint8Array[] } = {};\n\tprivate _encoderBufferViewAccessors: { [key: string]: GLTF.IAccessor[] } = {};\n\n\t/** @hidden */\n\tpublic install(key: string, dependency: unknown): this {\n\t\tif (key === 'meshopt.decoder') {\n\t\t\tthis._decoder = dependency as typeof MeshoptDecoder;\n\t\t}\n\t\tif (key === 'meshopt.encoder') {\n\t\t\tthis._encoder = dependency as typeof MeshoptEncoder;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Configures Meshopt options for quality/compression tuning. The two methods rely on different\n\t * pre-processing before compression, and should be compared on the basis of (a) quality/loss\n\t * and (b) final asset size after _also_ applying a lossless compression such as gzip or brotli.\n\t *\n\t * - QUANTIZE: Default. Pre-process with {@link quantize quantize()} (lossy to specified\n\t * \tprecision) before applying lossless Meshopt compression. Offers a considerable compression\n\t * \tratio with or without further supercompression. Equivalent to `gltfpack -c`.\n\t * - FILTER: Pre-process with lossy filters to improve compression, before applying lossless\n\t *\tMeshopt compression. While output may initially be larger than with the QUANTIZE method,\n\t *\tthis method will benefit more from supercompression (e.g. gzip or brotli). Equivalent to\n\t * \t`gltfpack -cc`.\n\t *\n\t * Output with the FILTER method will generally be smaller after supercompression (e.g. gzip or\n\t * brotli) is applied, but may be larger than QUANTIZE output without it. Decoding is very fast\n\t * with both methods.\n\t *\n\t * Example:\n\t *\n\t * ```ts\n\t * import { EXTMeshoptCompression } from '@gltf-transform/extensions';\n\t *\n\t * doc.createExtension(EXTMeshoptCompression)\n\t * \t.setRequired(true)\n\t * \t.setEncoderOptions({\n\t * \t\tmethod: EXTMeshoptCompression.EncoderMethod.QUANTIZE\n\t * \t});\n\t * ```\n\t */\n\tpublic setEncoderOptions(options: EncoderOptions): this {\n\t\tthis._encoderOptions = { ...DEFAULT_ENCODER_OPTIONS, ...options };\n\t\treturn this;\n\t}\n\n\t/**********************************************************************************************\n\t * Decoding.\n\t */\n\n\t/** @internal Checks preconditions, decodes buffer views, and creates decoded primitives. */\n\tpublic preread(context: ReaderContext, propertyType: PropertyType): this {\n\t\tif (!this._decoder) {\n\t\t\tif (!this.isRequired()) return this;\n\t\t\tthrow new Error(`[${EXT_MESHOPT_COMPRESSION}] Please install extension dependency, \"meshopt.decoder\".`);\n\t\t}\n\t\tif (!this._decoder.supported) {\n\t\t\tif (!this.isRequired()) return this;\n\t\t\tthrow new Error(`[${EXT_MESHOPT_COMPRESSION}]: Missing WASM support.`);\n\t\t}\n\n\t\tif (propertyType === PropertyType.BUFFER) {\n\t\t\tthis._prereadBuffers(context);\n\t\t} else if (propertyType === PropertyType.PRIMITIVE) {\n\t\t\tthis._prereadPrimitives(context);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/** @internal Decode buffer views. */\n\tprivate _prereadBuffers(context: ReaderContext): void {\n\t\tconst jsonDoc = context.jsonDoc;\n\n\t\tconst viewDefs = jsonDoc.json.bufferViews || [];\n\t\tviewDefs.forEach((viewDef, index) => {\n\t\t\tif (!viewDef.extensions || !viewDef.extensions[EXT_MESHOPT_COMPRESSION]) return;\n\n\t\t\tconst meshoptDef = viewDef.extensions[EXT_MESHOPT_COMPRESSION] as MeshoptBufferViewExtension;\n\t\t\tconst byteOffset = meshoptDef.byteOffset || 0;\n\t\t\tconst byteLength = meshoptDef.byteLength || 0;\n\t\t\tconst count = meshoptDef.count;\n\t\t\tconst stride = meshoptDef.byteStride;\n\t\t\tconst result = new Uint8Array(count * stride);\n\n\t\t\tconst bufferDef = jsonDoc.json.buffers![meshoptDef.buffer];\n\t\t\t// TODO(cleanup): Should be encapsulated in writer-context.ts.\n\t\t\tconst resource = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER];\n\t\t\tconst source = BufferUtils.toView(resource, byteOffset, byteLength);\n\n\t\t\tthis._decoder!.decodeGltfBuffer(result, count, stride, source, meshoptDef.mode, meshoptDef.filter);\n\n\t\t\tcontext.bufferViews[index] = result;\n\t\t});\n\t}\n\n\t/**\n\t * Mark fallback buffers and replacements.\n\t *\n\t * Note: Alignment with primitives is arbitrary; this just needs to happen\n\t * after Buffers have been parsed.\n\t * @internal\n\t */\n\tprivate _prereadPrimitives(context: ReaderContext): void {\n\t\tconst jsonDoc = context.jsonDoc;\n\t\tconst viewDefs = jsonDoc.json.bufferViews || [];\n\n\t\t//\n\t\tviewDefs.forEach((viewDef) => {\n\t\t\tif (!viewDef.extensions || !viewDef.extensions[EXT_MESHOPT_COMPRESSION]) return;\n\n\t\t\tconst meshoptDef = viewDef.extensions[EXT_MESHOPT_COMPRESSION] as MeshoptBufferViewExtension;\n\n\t\t\tconst buffer = context.buffers[meshoptDef.buffer];\n\t\t\tconst fallbackBuffer = context.buffers[viewDef.buffer];\n\t\t\tconst fallbackBufferDef = jsonDoc.json.buffers![viewDef.buffer];\n\t\t\tif (isFallbackBuffer(fallbackBufferDef)) {\n\t\t\t\tthis._decoderFallbackBufferMap.set(fallbackBuffer, buffer);\n\t\t\t}\n\t\t});\n\t}\n\n\t/** @hidden Removes Fallback buffers, if extension is required. */\n\tpublic read(_context: ReaderContext): this {\n\t\tif (!this.isRequired()) return this;\n\n\t\t// Replace fallback buffers.\n\t\tfor (const [fallbackBuffer, buffer] of this._decoderFallbackBufferMap) {\n\t\t\tfor (const parent of fallbackBuffer.listParents()) {\n\t\t\t\tif (parent instanceof Accessor) {\n\t\t\t\t\tparent.swap(fallbackBuffer, buffer);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfallbackBuffer.dispose();\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**********************************************************************************************\n\t * Encoding.\n\t */\n\n\t/** @internal Claims accessors that can be compressed and writes compressed buffer views. */\n\tpublic prewrite(context: WriterContext, propertyType: PropertyType): this {\n\t\tif (propertyType === PropertyType.ACCESSOR) {\n\t\t\tthis._prewriteAccessors(context);\n\t\t} else if (propertyType === PropertyType.BUFFER) {\n\t\t\tthis._prewriteBuffers(context);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** @internal Claims accessors that can be compressed. */\n\tprivate _prewriteAccessors(context: WriterContext): void {\n\t\tconst json = context.jsonDoc.json;\n\t\tconst encoder = this._encoder!;\n\t\tconst options = this._encoderOptions;\n\t\tconst graph = this.document.getGraph();\n\n\t\tconst fallbackBuffer = this.document.createBuffer(); // Disposed on write.\n\t\tconst fallbackBufferIndex = this.document.getRoot().listBuffers().indexOf(fallbackBuffer);\n\n\t\tlet nextID = 1;\n\t\tconst parentToID = new Map<Property, number>();\n\t\tconst getParentID = (property: Property): number => {\n\t\t\tfor (const parent of graph.listParents(property)) {\n\t\t\t\tif (parent.propertyType === PropertyType.ROOT) continue;\n\t\t\t\tlet id = parentToID.get(property);\n\t\t\t\tif (id === undefined) parentToID.set(property, (id = nextID++));\n\t\t\t\treturn id;\n\t\t\t}\n\t\t\treturn -1;\n\t\t};\n\n\t\tthis._encoderFallbackBuffer = fallbackBuffer;\n\t\tthis._encoderBufferViews = {};\n\t\tthis._encoderBufferViewData = {};\n\t\tthis._encoderBufferViewAccessors = {};\n\n\t\tfor (const accessor of this.document.getRoot().listAccessors()) {\n\t\t\t// See: https://github.com/donmccurdy/glTF-Transform/pull/323#issuecomment-898791251\n\t\t\t// Example: https://skfb.ly/6qAD8\n\t\t\tif (getTargetPath(accessor) === 'weights') continue;\n\n\t\t\t// See: https://github.com/donmccurdy/glTF-Transform/issues/289\n\t\t\tif (accessor.getSparse()) continue;\n\n\t\t\tconst usage = context.getAccessorUsage(accessor);\n\t\t\tconst parentID = context.accessorUsageGroupedByParent.has(usage) ? getParentID(accessor) : null;\n\t\t\tconst mode = getMeshoptMode(accessor, usage);\n\t\t\tconst filter =\n\t\t\t\toptions.method === EncoderMethod.FILTER\n\t\t\t\t\t? getMeshoptFilter(accessor, this.document)\n\t\t\t\t\t: { filter: MeshoptFilter.NONE };\n\t\t\tconst preparedAccessor = prepareAccessor(accessor, encoder, mode, filter);\n\t\t\tconst { array, byteStride } = preparedAccessor;\n\n\t\t\tconst buffer = accessor.getBuffer();\n\t\t\tif (!buffer) throw new Error(`${EXT_MESHOPT_COMPRESSION}: Missing buffer for accessor.`);\n\t\t\tconst bufferIndex = this.document.getRoot().listBuffers().indexOf(buffer);\n\n\t\t\t// Buffer view grouping key.\n\t\t\tconst key = [usage, parentID, mode, filter.filter, byteStride, bufferIndex].join(':');\n\n\t\t\tlet bufferView = this._encoderBufferViews[key];\n\t\t\tlet bufferViewData = this._encoderBufferViewData[key];\n\t\t\tlet bufferViewAccessors = this._encoderBufferViewAccessors[key];\n\n\t\t\t// Write new buffer view, if needed.\n\t\t\tif (!bufferView || !bufferViewData) {\n\t\t\t\tbufferViewAccessors = this._encoderBufferViewAccessors[key] = [];\n\t\t\t\tbufferViewData = this._encoderBufferViewData[key] = [];\n\t\t\t\tbufferView = this._encoderBufferViews[key] = {\n\t\t\t\t\tbuffer: fallbackBufferIndex,\n\t\t\t\t\ttarget: WriterContext.USAGE_TO_TARGET[usage],\n\t\t\t\t\tbyteOffset: 0,\n\t\t\t\t\tbyteLength: 0,\n\t\t\t\t\tbyteStride: usage === WriterContext.BufferViewUsage.ARRAY_BUFFER ? byteStride : undefined,\n\t\t\t\t\textensions: {\n\t\t\t\t\t\t[EXT_MESHOPT_COMPRESSION]: {\n\t\t\t\t\t\t\tbuffer: bufferIndex,\n\t\t\t\t\t\t\tbyteOffset: 0,\n\t\t\t\t\t\t\tbyteLength: 0,\n\t\t\t\t\t\t\tmode: mode,\n\t\t\t\t\t\t\tfilter: filter.filter !== MeshoptFilter.NONE ? filter.filter : undefined,\n\t\t\t\t\t\t\tbyteStride: byteStride,\n\t\t\t\t\t\t\tcount: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Write accessor.\n\t\t\tconst accessorDef = context.createAccessorDef(accessor);\n\t\t\taccessorDef.componentType = preparedAccessor.componentType;\n\t\t\taccessorDef.normalized = preparedAccessor.normalized;\n\t\t\taccessorDef.byteOffset = bufferView.byteLength;\n\t\t\tif (accessorDef.min && preparedAccessor.min) accessorDef.min = preparedAccessor.min;\n\t\t\tif (accessorDef.max && preparedAccessor.max) accessorDef.max = preparedAccessor.max;\n\t\t\tcontext.accessorIndexMap.set(accessor, json.accessors!.length);\n\t\t\tjson.accessors!.push(accessorDef);\n\t\t\tbufferViewAccessors.push(accessorDef);\n\n\t\t\t// Update buffer view.\n\t\t\tbufferViewData.push(new Uint8Array(array.buffer, array.byteOffset, array.byteLength));\n\t\t\tbufferView.byteLength += array.byteLength;\n\t\t\tbufferView.extensions.EXT_meshopt_compression.count += accessor.getCount();\n\t\t}\n\t}\n\n\t/** @internal Writes compressed buffer views. */\n\tprivate _prewriteBuffers(context: WriterContext): void {\n\t\tconst encoder = this._encoder!;\n\n\t\tfor (const key in this._encoderBufferViews) {\n\t\t\tconst bufferView = this._encoderBufferViews[key];\n\t\t\tconst bufferViewData = this._encoderBufferViewData[key];\n\t\t\tconst buffer = this.document.getRoot().listBuffers()[bufferView.extensions[EXT_MESHOPT_COMPRESSION].buffer];\n\t\t\tconst otherBufferViews = context.otherBufferViews.get(buffer) || [];\n\n\t\t\tconst { count, byteStride, mode } = bufferView.extensions[EXT_MESHOPT_COMPRESSION];\n\t\t\tconst srcArray = BufferUtils.concat(bufferViewData);\n\t\t\tconst dstArray = encoder.encodeGltfBuffer(srcArray, count, byteStride, mode);\n\t\t\tconst compressedData = BufferUtils.pad(dstArray);\n\n\t\t\tbufferView.extensions[EXT_MESHOPT_COMPRESSION].byteLength = dstArray.byteLength;\n\n\t\t\tbufferViewData.length = 0;\n\t\t\tbufferViewData.push(compressedData);\n\t\t\totherBufferViews.push(compressedData);\n\t\t\tcontext.otherBufferViews.set(buffer, otherBufferViews);\n\t\t}\n\t}\n\n\t/** @hidden Puts encoded data into glTF output. */\n\tpublic write(context: WriterContext): this {\n\t\tlet fallbackBufferByteOffset = 0;\n\n\t\t// Write final encoded buffer view properties.\n\t\tfor (const key in this._encoderBufferViews) {\n\t\t\tconst bufferView = this._encoderBufferViews[key];\n\t\t\tconst bufferViewData = this._encoderBufferViewData[key][0];\n\t\t\tconst bufferViewIndex = context.otherBufferViewsIndexMap.get(bufferViewData)!;\n\n\t\t\tconst bufferViewAccessors = this._encoderBufferViewAccessors[key];\n\t\t\tfor (const accessorDef of bufferViewAccessors) {\n\t\t\t\taccessorDef.bufferView = bufferViewIndex;\n\t\t\t}\n\n\t\t\tconst finalBufferViewDef = context.jsonDoc.json.bufferViews![bufferViewIndex];\n\t\t\tconst compressedByteOffset = finalBufferViewDef.byteOffset || 0;\n\n\t\t\tObject.assign(finalBufferViewDef, bufferView);\n\t\t\tfinalBufferViewDef.byteOffset = fallbackBufferByteOffset;\n\t\t\tconst bufferViewExtensionDef = finalBufferViewDef.extensions![\n\t\t\t\tEXT_MESHOPT_COMPRESSION\n\t\t\t] as MeshoptBufferViewExtension;\n\t\t\tbufferViewExtensionDef.byteOffset = compressedByteOffset;\n\n\t\t\tfallbackBufferByteOffset += BufferUtils.padNumber(bufferView.byteLength);\n\t\t}\n\n\t\t// Write final fallback buffer.\n\t\tconst fallbackBuffer = this._encoderFallbackBuffer!;\n\t\tconst fallbackBufferIndex = context.bufferIndexMap.get(fallbackBuffer)!;\n\t\tconst fallbackBufferDef = context.jsonDoc.json.buffers![fallbackBufferIndex];\n\t\tfallbackBufferDef.byteLength = fallbackBufferByteOffset;\n\t\tfallbackBufferDef.extensions = { [EXT_MESHOPT_COMPRESSION]: { fallback: true } };\n\t\tfallbackBuffer.dispose();\n\n\t\treturn this;\n\t}\n}\n","import {\n\tBufferUtils,\n\tExtension,\n\tImageUtils,\n\ttype ImageUtilsFormat,\n\tPropertyType,\n\ttype ReaderContext,\n\ttype vec2,\n\ttype WriterContext,\n} from '@gltf-transform/core';\nimport { EXT_TEXTURE_AVIF } from '../constants.js';\n\nclass AVIFImageUtils implements ImageUtilsFormat {\n\tmatch(array: Uint8Array): boolean {\n\t\treturn array.length >= 12 && BufferUtils.decodeText(array.slice(4, 12)) === 'ftypavif';\n\t}\n\t/**\n\t * Probes size of AVIF or HEIC image. Assumes a single static image, without\n\t * orientation or other metadata that would affect dimensions.\n\t */\n\tgetSize(array: Uint8Array): vec2 | null {\n\t\tif (!this.match(array)) return null;\n\n\t\t// References:\n\t\t// - https://stackoverflow.com/questions/66222773/how-to-get-image-dimensions-from-an-avif-file\n\t\t// - https://github.com/nodeca/probe-image-size/blob/master/lib/parse_sync/avif.js\n\n\t\tconst view = new DataView(array.buffer, array.byteOffset, array.byteLength);\n\n\t\tlet box = unbox(view, 0);\n\t\tif (!box) return null;\n\n\t\tlet offset = box.end;\n\t\twhile ((box = unbox(view, offset))) {\n\t\t\tif (box.type === 'meta') {\n\t\t\t\toffset = box.start + 4; // version + flags\n\t\t\t} else if (box.type === 'iprp' || box.type === 'ipco') {\n\t\t\t\toffset = box.start;\n\t\t\t} else if (box.type === 'ispe') {\n\t\t\t\treturn [view.getUint32(box.start + 4), view.getUint32(box.start + 8)];\n\t\t\t} else if (box.type === 'mdat') {\n\t\t\t\tbreak; // mdat should be last, unlikely to find metadata past here.\n\t\t\t} else {\n\t\t\t\toffset = box.end;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\tgetChannels(_buffer: Uint8Array): number {\n\t\treturn 4;\n\t}\n}\n\n/**\n * [`EXT_texture_avif`](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_avif/)\n * enables AVIF images for any material texture.\n *\n * AVIF offers greatly reduced transmission size, but\n * [requires browser support](https://caniuse.com/avif). Like PNG and JPEG, an AVIF image is\n * *fully decompressed* when uploaded to the GPU, which increases upload time and GPU memory cost.\n * For seamless uploads and minimal GPU memory cost, it is necessary to use a GPU texture format\n * like Basis Universal, with the `KHR_texture_basisu` extension.\n *\n * Defining no {@link ExtensionProperty} types, this {@link Extension} is simply attached to the\n * {@link Document}, and affects the entire Document by allowing use of the `image/avif` MIME type\n * and passing AVIF image data to the {@link Texture.setImage} method. Without the Extension, the\n * same MIME types and image data would yield an invalid glTF document, under the stricter core glTF\n * specification.\n *\n * Properties:\n * - N/A\n *\n * ### Example\n *\n * ```typescript\n * import { TextureAVIF } from '@gltf-transform/extensions';\n *\n * // Create an Extension attached to the Document.\n * const avifExtension = document.createExtension(TextureAVIF)\n * \t.setRequired(true);\n * document.createTexture('MyAVIFTexture')\n * \t.setMimeType('image/avif')\n * \t.setImage(fs.readFileSync('my-texture.avif'));\n * ```\n *\n * AVIF conversion is not done automatically when adding the extension as shown above — you must\n * convert the image data first, then pass the `.avif` payload to {@link Texture.setImage}.\n *\n * When the `EXT_texture_avif` extension is added to a file by glTF-Transform, the extension should\n * always be required. This tool does not support writing assets that \"fall back\" to optional PNG or\n * JPEG image data.\n */\nexport class EXTTextureAVIF extends Extension {\n\tpublic readonly extensionName: typeof EXT_TEXTURE_AVIF = EXT_TEXTURE_AVIF;\n\t/** @hidden */\n\tpublic readonly prereadTypes: PropertyType[] = [PropertyType.TEXTURE];\n\tpublic static readonly EXTENSION_NAME: typeof EXT_TEXTURE_AVIF = EXT_TEXTURE_AVIF;\n\n\t/** @hidden */\n\tpublic static register(): void {\n\t\tImageUtils.registerFormat('image/avif', new AVIFImageUtils());\n\t}\n\n\t/** @hidden */\n\tpublic preread(context: ReaderContext): this {\n\t\tconst textureDefs = context.jsonDoc.json.textures || [];\n\t\ttextureDefs.forEach((textureDef) => {\n\t\t\tif (textureDef.extensions && textureDef.extensions[EXT_TEXTURE_AVIF]) {\n\t\t\t\ttextureDef.source = (textureDef.extensions[EXT_TEXTURE_AVIF] as { source: number }).source;\n\t\t\t}\n\t\t});\n\t\treturn this;\n\t}\n\n\t/** @hidden */\n\tpublic read(_context: ReaderContext): this {\n\t\treturn this;\n\t}\n\n\t/** @hidden */\n\tpublic write(context: WriterContext): this {\n\t\tconst jsonDoc = context.jsonDoc;\n\n\t\tthis.document\n\t\t\t.getRoot()\n\t\t\t.listTextures()\n\t\t\t.forEach((texture) => {\n\t\t\t\tif (texture.getMimeType() === 'image/avif') {\n\t\t\t\t\tconst imageIndex = context.imageIndexMap.get(texture);\n\t\t\t\t\tconst textureDefs = jsonDoc.json.textures || [];\n\t\t\t\t\ttextureDefs.forEach((textureDef) => {\n\t\t\t\t\t\tif (textureDef.source === imageIndex) {\n\t\t\t\t\t\t\ttextureDef.extensions = textureDef.extensions || {};\n\t\t\t\t\t\t\ttextureDef.extensions[EXT_TEXTURE_AVIF] = { source: textureDef.source };\n\t\t\t\t\t\t\tdelete textureDef.source;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\treturn this;\n\t}\n}\n\ninterface IBox {\n\ttype: string;\n\tstart: number;\n\tend: number;\n}\n\nfunction unbox(data: DataView, offset: number): IBox | null {\n\tif (data.byteLength < 4 + offset) return null;\n\n\t// size includes first 4 bytes (length)\n\tconst size = data.getUint32(offset);\n\tif (data.byteLength < size + offset || size < 8) return null;\n\n\treturn {\n\t\ttype: BufferUtils.decodeText(new Uint8Array(data.buffer, data.byteOffset + offset + 4, 4)),\n\t\tstart: offset + 8,\n\t\tend: offset + size,\n\t};\n}\n","import {\n\tBufferUtils,\n\tExtension,\n\tImageUtils,\n\ttype ImageUtilsFormat,\n\tPropertyType,\n\ttype ReaderContext,\n\ttype vec2,\n\ttype WriterContext,\n} from '@gltf-transform/core';\nimport { EXT_TEXTURE_WEBP } from '../constants.js';\n\nclass WEBPImageUtils implements ImageUtilsFormat {\n\tmatch(array: Uint8Array): boolean {\n\t\treturn array.length >= 12 && array[8] === 87 && array[9] === 69 && array[10] === 66 && array[11] === 80;\n\t}\n\tgetSize(array: Uint8Array): vec2 | null {\n\t\t// Reference: http://tools.ietf.org/html/rfc6386\n\t\tconst RIFF = BufferUtils.decodeText(array.slice(0, 4));\n\t\tconst WEBP = BufferUtils.decodeText(array.slice(8, 12));\n\t\tif (RIFF !== 'RIFF' || WEBP !== 'WEBP') return null;\n\n\t\tconst view = new DataView(array.buffer, array.byteOffset);\n\n\t\t// Reference: https://wiki.tcl-lang.org/page/Reading+WEBP+image+dimensions\n\t\tlet offset = 12;\n\t\twhile (offset < view.byteLength) {\n\t\t\tconst chunkId = BufferUtils.decodeText(\n\t\t\t\tnew Uint8Array([\n\t\t\t\t\tview.getUint8(offset),\n\t\t\t\t\tview.getUint8(offset + 1),\n\t\t\t\t\tview.getUint8(offset + 2),\n\t\t\t\t\tview.getUint8(offset + 3),\n\t\t\t\t]),\n\t\t\t);\n\t\t\tconst chunkByteLength = view.getUint32(offset + 4, true);\n\t\t\tif (chunkId === 'VP8 ') {\n\t\t\t\tconst width = view.getInt16(offset + 14, true) & 0x3fff;\n\t\t\t\tconst height = view.getInt16(offset + 16, true) & 0x3fff;\n\t\t\t\treturn [width, height];\n\t\t\t} else if (chunkId === 'VP8L') {\n\t\t\t\tconst b0 = view.getUint8(offset + 9);\n\t\t\t\tconst b1 = view.getUint8(offset + 10);\n\t\t\t\tconst b2 = view.getUint8(offset + 11);\n\t\t\t\tconst b3 = view.getUint8(offset + 12);\n\t\t\t\tconst width = 1 + (((b1 & 0x3f) << 8) | b0);\n\t\t\t\tconst height = 1 + (((b3 & 0xf) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6));\n\t\t\t\treturn [width, height];\n\t\t\t}\n\t\t\toffset += 8 + chunkByteLength + (chunkByteLength % 2);\n\t\t}\n\n\t\treturn null;\n\t}\n\tgetChannels(_buffer: Uint8Array): number {\n\t\treturn 4;\n\t}\n}\n\n/**\n * [`EXT_texture_webp`](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp/)\n * enables WebP images for any material texture.\n *\n * WebP offers greatly reduced transmission size, but\n * [requires browser support](https://caniuse.com/webp). Like PNG and JPEG, a