UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

367 lines (357 loc) 14.2 kB
import { m as macro } from '../../../macros2.js'; import { SEMANTIC_ATTRIBUTE_MAP, MODES, ALPHA_MODE, BYTES, COMPONENTS, DEFAULT_SAMPLER, GL_SAMPLER } from './Constants.js'; import { getAccessorArrayTypeAndLength, resolveUrl, getGLEnumFromSamplerParameter } from './Utils.js'; /* eslint-disable guard-for-in */ const { vtkDebugMacro, vtkWarningMacro } = macro; class GLTFParser { constructor(glTF, options = {}) { const { json, baseUri = '' } = glTF; this.glTF = glTF; this.options = options; this.baseUri = baseUri; this.json = json; this.extensions = json.extensions || {}; this.extensionsUsed = json.extensionsUsed || []; } async parse() { const buffers = this.json.buffers || []; this.buffers = new Array(buffers.length).fill(null); const images = this.json.images || []; this.images = new Array(images.length).fill({}); await this.loadBuffers(); await this.loadImages(); this.resolveTree(); return this.glTF.json; } resolveTree() { this.json.scenes = this.json.scenes?.map((scene, idx) => this.resolveScene(scene, idx)); this.json.cameras = this.json.cameras?.map((camera, idx) => this.resolveCamera(camera, idx)); this.json.bufferViews = this.json.bufferViews?.map((bufView, idx) => this.resolveBufferView(bufView, idx)); this.json.images = this.json.images?.map((image, idx) => this.resolveImage(image, idx)); this.json.samplers = this.json.samplers?.map((sampler, idx) => this.resolveSampler(sampler, idx)); this.json.textures = this.json.textures?.map((texture, idx) => this.resolveTexture(texture, idx)); this.json.accessors = this.json.accessors?.map((accessor, idx) => this.resolveAccessor(accessor, idx)); this.json.materials = this.json.materials?.map((material, idx) => this.resolveMaterial(material, idx)); this.json.meshes = this.json.meshes?.map((mesh, idx) => this.resolveMesh(mesh, idx)); this.json.nodes = this.json.nodes?.map((node, idx) => this.resolveNode(node, idx)); this.json.skins = this.json.skins?.map((skin, idx) => this.resolveSkin(skin, idx)); this.json.animations = this.json.animations?.map((animation, idx) => this.resolveAnimation(animation, idx)); } get(array, index) { // check if already resolved if (typeof index === 'object') { return index; } const object = this.json[array] && this.json[array][index]; if (!object) { vtkWarningMacro(`glTF file error: Could not find ${array}[${index}]`); } return object; } resolveScene(scene, index) { scene.id = scene.id || `scene-${index}`; scene.nodes = (scene.nodes || []).map(node => this.get('nodes', node)); return scene; } resolveNode(node, index) { node.id = node.id || `node-${index}`; if (node.children) { node.children = node.children.map(child => this.get('nodes', child)); } if (node.mesh !== undefined) { node.mesh = this.get('meshes', node.mesh); } else if (node.meshes !== undefined && node.meshes.length) { node.mesh = node.meshes.reduce((accum, meshIndex) => { const mesh = this.get('meshes', meshIndex); accum.id = mesh.id; accum.primitives = accum.primitives.concat(mesh.primitives); return accum; }, { primitives: [] }); } if (node.camera !== undefined) { node.camera = this.get('cameras', node.camera); } if (node.skin !== undefined) { node.skin = this.get('skins', node.skin); } // Fill punctual lights objects if (node.extensions?.KHR_lights_punctual) { node.extensions.KHR_lights_punctual.light = this.extensions?.KHR_lights_punctual.lights[node.extensions.KHR_lights_punctual.light]; } return node; } resolveSkin(skin, index) { skin.id = skin.id || `skin-${index}`; skin.inverseBindMatrices = this.get('accessors', skin.inverseBindMatrices); return skin; } resolveMesh(mesh, index) { mesh.id = mesh.id || `mesh-${index}`; if (mesh.primitives) { mesh.primitives = mesh.primitives.map((primitive, idx) => { const attributes = primitive.attributes; primitive.name = `primitive-${idx}`; primitive.attributes = {}; for (const attribute in attributes) { const attr = SEMANTIC_ATTRIBUTE_MAP[attribute]; primitive.attributes[attr] = this.get('accessors', attributes[attribute]); } if (primitive.indices !== undefined) { primitive.indices = this.get('accessors', primitive.indices); } if (primitive.material !== undefined) { primitive.material = this.get('materials', primitive.material); } if (primitive.mode === undefined) { primitive.mode = MODES.GL_TRIANGLES; // Default one } if (primitive.extensions?.KHR_draco_mesh_compression) { vtkDebugMacro('Using Draco mesh compression'); const bufferView = this.get('bufferViews', primitive.extensions.KHR_draco_mesh_compression.bufferView); primitive.extensions.KHR_draco_mesh_compression.bufferView = bufferView.data; } return primitive; }); } return mesh; } resolveMaterial(material, index) { material.id = material.id || `material-${index}`; if (material.alphaMode === undefined) material.alphaMode = ALPHA_MODE.OPAQUE; if (material.doubleSided === undefined) material.doubleSided = false; if (material.alphaCutoff === undefined) material.alphaCutoff = 0.5; if (material.normalTexture) { material.normalTexture = { ...material.normalTexture }; material.normalTexture.texture = this.get('textures', material.normalTexture.index); } if (material.occlusionTexture) { material.occlusionTexture = { ...material.occlusionTexture }; material.occlusionTexture.texture = this.get('textures', material.occlusionTexture.index); } if (material.emissiveTexture) { material.emissiveTexture = { ...material.emissiveTexture }; material.emissiveTexture.texture = this.get('textures', material.emissiveTexture.index); } if (!material.emissiveFactor) { material.emissiveFactor = material.emissiveTexture ? 1 : 0; } else material.emissiveFactor = material.emissiveFactor[0]; if (material.pbrMetallicRoughness) { material.pbrMetallicRoughness = { ...material.pbrMetallicRoughness }; const mr = material.pbrMetallicRoughness; if (mr.baseColorTexture) { mr.baseColorTexture = { ...mr.baseColorTexture }; mr.baseColorTexture.texture = this.get('textures', mr.baseColorTexture.index); } if (mr.metallicRoughnessTexture) { mr.metallicRoughnessTexture = { ...mr.metallicRoughnessTexture }; mr.metallicRoughnessTexture.texture = this.get('textures', mr.metallicRoughnessTexture.index); } } else { material.pbrMetallicRoughness = { baseColorFactor: [1, 1, 1, 1], metallicFactor: 1.0, roughnessFactor: 1.0 }; } return material; } /** * Take values of particular accessor from interleaved buffer various parts of * the buffer */ getValueFromInterleavedBuffer(buffer, byteOffset, byteStride, bytesPerElement, count) { const result = new Uint8Array(count * bytesPerElement); for (let i = 0; i < count; i++) { const elementOffset = byteOffset + i * byteStride; result.set(new Uint8Array(buffer.arrayBuffer.slice(elementOffset, elementOffset + bytesPerElement)), i * bytesPerElement); } return result.buffer; } resolveAccessor(accessor, index) { accessor.id = accessor.id || `accessor-${index}`; if (accessor.bufferView !== undefined) { // Draco encoded meshes don't have bufferView accessor.bufferView = this.get('bufferViews', accessor.bufferView); } // Look up enums accessor.bytesPerComponent = BYTES[accessor.componentType]; accessor.components = COMPONENTS[accessor.type]; accessor.bytesPerElement = accessor.bytesPerComponent * accessor.components; // Create TypedArray for the accessor // Note: The canonical way to instantiate is to ignore this array and create // WebGLBuffer's using the bufferViews. if (accessor.bufferView) { const buffer = accessor.bufferView.buffer; const { ArrayType } = getAccessorArrayTypeAndLength(accessor, accessor.bufferView); const baseByteOffset = (accessor.bufferView.byteOffset || 0) + buffer.byteOffset; const byteOffset = baseByteOffset + (accessor.byteOffset || 0); let arrayBufferView; if (accessor.bufferView.byteStride) { // Only extract if stride is not equal to element size if (accessor.bufferView.byteStride === accessor.bytesPerElement) { arrayBufferView = new ArrayType(buffer.arrayBuffer, byteOffset, accessor.count * accessor.components); } else { // Interleaved buffer, extract only needed bytes const interleavedBuffer = this.getValueFromInterleavedBuffer(buffer, byteOffset, accessor.bufferView.byteStride, accessor.bytesPerElement, accessor.count); arrayBufferView = new ArrayType(interleavedBuffer); } } else { arrayBufferView = new ArrayType(buffer.arrayBuffer, byteOffset, accessor.count * accessor.components); } accessor.value = arrayBufferView; } return accessor; } resolveTexture(texture, index) { texture.id = texture.id || `texture-${index}`; texture.sampler = 'sampler' in texture ? this.get('samplers', texture.sampler) : DEFAULT_SAMPLER; texture.source = this.get('images', texture.source); // Handle texture extensions sources if (texture.extensions !== undefined) { const extensionsNames = Object.keys(texture.extensions); extensionsNames.forEach(extensionName => { const extension = texture.extensions[extensionName]; switch (extensionName) { case 'KHR_texture_basisu': case 'EXT_texture_webp': case 'EXT_texture_avif': texture.source = this.get('images', extension.source); break; default: vtkWarningMacro(`Unhandled extension: ${extensionName}`); } }); } return texture; } resolveSampler(sampler, index) { sampler.id = sampler.id || `sampler-${index}`; if (!Object.hasOwn(sampler, 'wrapS')) sampler.wrapS = GL_SAMPLER.REPEAT; if (!Object.hasOwn(sampler, 'wrapT')) sampler.wrapT = GL_SAMPLER.REPEAT; if (!Object.hasOwn(sampler, 'minFilter')) sampler.minFilter = GL_SAMPLER.LINEAR_MIPMAP_LINEAR; if (!Object.hasOwn(sampler, 'magFilter')) sampler.magFilter = GL_SAMPLER.NEAREST; // Map textual parameters to GL parameter values sampler.parameters = {}; for (const key in sampler) { const glEnum = getGLEnumFromSamplerParameter(key); if (glEnum !== undefined) { sampler.parameters[glEnum] = sampler[key]; } } return sampler; } resolveImage(image, index) { image.id = image.id || `image-${index}`; if (image.bufferView !== undefined) { image.bufferView = this.get('bufferViews', image.bufferView); } return image; } resolveBufferView(bufferView, index) { bufferView.id = bufferView.id || `bufferView-${index}`; const bufferIndex = bufferView.buffer; bufferView.buffer = this.buffers[bufferIndex]; const arrayBuffer = this.buffers[bufferIndex].arrayBuffer; let byteOffset = this.buffers[bufferIndex].byteOffset || 0; if ('byteOffset' in bufferView) { byteOffset += bufferView.byteOffset; } bufferView.data = new Uint8Array(arrayBuffer, byteOffset, bufferView.byteLength); return bufferView; } resolveCamera(camera, index) { camera.id = camera.id || `camera-${index}`; return camera; } resolveAnimation(animation, index) { animation.id = animation.id || `animation-${index}`; animation.samplers.map(sampler => { sampler.input = this.get('accessors', sampler.input).value; sampler.output = this.get('accessors', sampler.output).value; return sampler; }); return animation; } loadBuffers() { const promises = this.json.buffers.map((buffer, idx) => this.loadBuffer(buffer, idx).then(() => { delete buffer.uri; })); return Promise.all(promises); } async loadBuffer(buffer, index) { let arrayBuffer = buffer; if (buffer.uri) { vtkDebugMacro('Loading uri', buffer.uri); const uri = resolveUrl(buffer.uri, this.options.baseUri); const response = await fetch(uri); arrayBuffer = await response.arrayBuffer(); } else if (this.glTF.glbBuffers) { arrayBuffer = this.glTF.glbBuffers[index]; } this.buffers[index] = { arrayBuffer, byteOffset: 0, byteLength: arrayBuffer.byteLength }; } loadImages() { const images = this.json.images || []; const promises = []; return new Promise((resolve, reject) => { for (let i = 0; i < images.length; ++i) { promises.push(Promise.resolve(this.loadImage(images[i], i).then(() => { vtkDebugMacro('Texture loaded ', images[i]); }))); } Promise.all(promises).then(() => resolve(this.images)); }); } async loadImage(image, index) { let arrayBuffer; let buffer; if (image.uri) { vtkDebugMacro('Loading texture', image.uri); const uri = resolveUrl(image.uri, this.options.baseUri); const response = await fetch(uri); arrayBuffer = await response.arrayBuffer(); image.uri = uri; image.bufferView = { data: arrayBuffer }; } else if (image.bufferView) { const bufferView = this.get('bufferViews', image.bufferView); buffer = this.get('buffers', bufferView.buffer); // GLB buffer if (this.glTF.glbBuffers) { buffer = this.glTF.glbBuffers[bufferView.buffer]; arrayBuffer = buffer.slice(bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength); } image.bufferView = { data: arrayBuffer }; } } } export { GLTFParser as default };