/* eslint-disable camelcase */ import * as KHR_binary_glTF from '../extensions/KHR_binary_gltf'; // Binary format changes (mainly implemented by GLBLoader) // https://github.com/KhronosGroup/glTF/tree/master/extensions/1.0/Khronos/KHR_binary_glTF // JSON format changes: // https://github.com/khronosgroup/gltf/issues/605 // - [x] Top-level JSON objects are arrays now // - [ ] Removed indirection from animation: sampler now refers directly to accessors, #712 // - [ ] material.parameter.value and technique.parameter.value must be an array, #690 // - [ ] Node can have only one mesh #821 // - [ ] Added reqs on JSON encoding // - [ ] Added reqs on binary data alignment #802 (comment) // Additions: // - [ ] Added accessor.normalized, #691, #706 // - [ ] Added glExtensionsUsed property and 5125 (UNSIGNED_INT) accessor.componentType value, #619 // - [ ] Added extensionsRequired property, #720, #721 // - [ ] Added "STEP" as valid animation.sampler.interpolation value, #712 // Removals: // - [x] Removed buffer.type, #786, #629 // - [ ] Removed revision number from profile.version, #709 // - [ ] Removed technique.functions.scissor and removed 3089 (SCISSOR_TEST) as a valid value for technique.states.enable, #681 // - [ ] Techniques, programs, and shaders were moved out to KHR_technique_webgl extension. // Other edits: // - [x] asset is now required, #642 // - [ ] buffer.byteLength and bufferView.byteLength are now required, #560. // - [ ] accessor.min and accessor.max are now required, #593, and clarified that the JSON value and binary data must be the same, #628. // - [ ] Clarified animation.sampler and animation.channel restrictions, #712 // - [ ] skin.inverseBindMatrices is now optional, #461. // - [ ] Attribute parameters can't have a value defined in the technique or parameter, #563 (comment). // - [ ] Only TEXCOORD and COLOR attribute semantics can be written in the form [semantic]_[set_index], #563 (comment). // - [ ] TEXCOORD and COLOR attribute semantics must be written in the form [semantic]_[set_index], e.g., just TEXCOORD should be TEXCOORD_0, and just COLOR should be COLOR_0, #649 // - [ ] camera.perspective.aspectRatio and camera.perspective.yfov must now be > 0, not >= 0, #563 (comment). // - [ ] Application-specific parameter semantics must start with an underscore, e.g., _TEMPERATURE and _SIMULATION_TIME, #563 (comment). // - [ ] Properties in technique.parameters must be defined in technique.uniforms or technique.attributes, // #563 (comment). // - [ ] technique.parameter.count can only be defined when the semantic is JOINTMATRIX or an application-specific semantic is used. It can never be defined for attribute parameters; only uniforms, d2f6945 // - [ ] technique.parameter.semantic is required when the parameter is an attribute, 28e113d // - [ ] Mesh-only models are allowed, e.g., without materials, #642 // - [ ] Skeleton hierarchies (nodes containing jointName) must be separated from non-skeleton hierarchies., #647 // - [ ] technique.states.functions.blendColor and technique.states.functions.depthRange parameters now must match WebGL function min/max, #707 const GLTF_ARRAYS = { accessors: 'accessor', animations: 'animation', buffers: 'buffer', bufferViews: 'bufferView', images: 'image', materials: 'material', meshes: 'mesh', nodes: 'node', samplers: 'sampler', scenes: 'scene', skins: 'skin', textures: 'texture' }; const GLTF_KEYS = { accessor: 'accessors', animations: 'animation', buffer: 'buffers', bufferView: 'bufferViews', image: 'images', material: 'materials', mesh: 'meshes', node: 'nodes', sampler: 'samplers', scene: 'scenes', skin: 'skins', texture: 'textures' }; /** * Converts (normalizes) glTF v1 to v2 */ class GLTFV1Normalizer { idToIndexMap = { animations: {}, accessors: {}, buffers: {}, bufferViews: {}, images: {}, materials: {}, meshes: {}, nodes: {}, samplers: {}, scenes: {}, skins: {}, textures: {} }; json; // constructor() {} /** * Convert (normalize) glTF < 2.0 to glTF 2.0 * @param gltf - object with json and binChunks * @param options * @param options normalize Whether to actually normalize */ normalize(gltf, options) { this.json = gltf.json; const json = gltf.json; // Check version switch (json.asset && json.asset.version) { // We are converting to v2 format. Return if there is nothing to do case '2.0': return; // This class is written to convert 1.0 case undefined: case '1.0': break; default: // eslint-disable-next-line no-undef, no-console console.warn(`glTF: Unknown version ${json.asset.version}`); return; } if (!options.normalize) { // We are still missing a few conversion tricks, remove once addressed throw new Error('glTF v1 is not supported.'); } // eslint-disable-next-line no-undef, no-console console.warn('Converting glTF v1 to glTF v2 format. This is experimental and may fail.'); this._addAsset(json); // In glTF2 top-level fields are Arrays not Object maps this._convertTopLevelObjectsToArrays(json); // Extract bufferView indices for images // (this extension needs to be invoked early in the normalization process) // TODO can this be handled by standard extension processing instead of called explicitly? KHR_binary_glTF.preprocess(gltf); // Convert object references from ids to indices this._convertObjectIdsToArrayIndices(json); this._updateObjects(json); this._updateMaterial(json); } // asset is now required, #642 https://github.com/KhronosGroup/glTF/issues/639 _addAsset(json) { json.asset = json.asset || {}; // We are normalizing to glTF v2, so change version to "2.0" json.asset.version = '2.0'; json.asset.generator = json.asset.generator || 'Normalized to glTF 2.0 by loaders.gl'; } _convertTopLevelObjectsToArrays(json) { // TODO check that all arrays are covered for (const arrayName in GLTF_ARRAYS) { this._convertTopLevelObjectToArray(json, arrayName); } } /** Convert one top level object to array */ _convertTopLevelObjectToArray(json, mapName) { const objectMap = json[mapName]; if (!objectMap || Array.isArray(objectMap)) { return; } // Rewrite the top-level field as an array json[mapName] = []; // Copy the map key into object.id for (const id in objectMap) { const object = objectMap[id]; object.id = object.id || id; // Mutates the loaded object const index = json[mapName].length; json[mapName].push(object); this.idToIndexMap[mapName][id] = index; } } /** Go through all objects in all top-level arrays and replace ids with indices */ _convertObjectIdsToArrayIndices(json) { for (const arrayName in GLTF_ARRAYS) { this._convertIdsToIndices(json, arrayName); } if ('scene' in json) { json.scene = this._convertIdToIndex(json.scene, 'scene'); } // Convert any index references that are not using array names // texture.source (image) for (const texture of json.textures) { this._convertTextureIds(texture); } for (const mesh of json.meshes) { this._convertMeshIds(mesh); } for (const node of json.nodes) { this._convertNodeIds(node); } for (const node of json.scenes) { this._convertSceneIds(node); } } _convertTextureIds(texture) { if (texture.source) { texture.source = this._convertIdToIndex(texture.source, 'image'); } } _convertMeshIds(mesh) { for (const primitive of mesh.primitives) { const {attributes, indices, material} = primitive; for (const attributeName in attributes) { attributes[attributeName] = this._convertIdToIndex(attributes[attributeName], 'accessor'); } if (indices) { primitive.indices = this._convertIdToIndex(indices, 'accessor'); } if (material) { primitive.material = this._convertIdToIndex(material, 'material'); } } } _convertNodeIds(node) { if (node.children) { node.children = node.children.map((child) => this._convertIdToIndex(child, 'node')); } if (node.meshes) { node.meshes = node.meshes.map((mesh) => this._convertIdToIndex(mesh, 'mesh')); } } _convertSceneIds(scene) { if (scene.nodes) { scene.nodes = scene.nodes.map((node) => this._convertIdToIndex(node, 'node')); } } /** Go through all objects in a top-level array and replace ids with indices */ _convertIdsToIndices(json, topLevelArrayName) { if (!json[topLevelArrayName]) { console.warn(`gltf v1: json doesn't contain attribute ${topLevelArrayName}`); // eslint-disable-line no-console, no-undef json[topLevelArrayName] = []; } for (const object of json[topLevelArrayName]) { for (const key in object) { const id = object[key]; const index = this._convertIdToIndex(id, key); object[key] = index; } } } _convertIdToIndex(id, key) { const arrayName = GLTF_KEYS[key]; if (arrayName in this.idToIndexMap) { const index = this.idToIndexMap[arrayName][id]; if (!Number.isFinite(index)) { throw new Error(`gltf v1: failed to resolve ${key} with id ${id}`); } return index; } return id; } /** * * @param {*} json */ _updateObjects(json) { for (const buffer of this.json.buffers) { // - [x] Removed buffer.type, #786, #629 delete buffer.type; } } /** * Update material (set pbrMetallicRoughness) * @param {*} json */ _updateMaterial(json) { for (const material of json.materials) { material.pbrMetallicRoughness = { baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }; const textureId = material.values?.tex || material.values?.texture2d_0 || material.values?.diffuseTex; const textureIndex = json.textures.findIndex((texture) => texture.id === textureId); if (textureIndex !== -1) { material.pbrMetallicRoughness.baseColorTexture = {index: textureIndex}; } } } } export function normalizeGLTFV1(gltf, options = {}) { return new GLTFV1Normalizer().normalize(gltf, options); }