UNPKG

wwobjloader2

Version:

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kaisalmen/WWOBJLoader/blob/dev/LICENSE) [![WWOBJLoader](https://github.com/kaisalmen/WWOBJLoader/actions/workflows/actions.yml/badge.svg)](https://github.com/kaisalm

406 lines 15.5 kB
import { FileLoader, Loader, LineSegments, Points, // Parser only Object3D, Mesh, BufferGeometry, BufferAttribute } from 'three'; import { MaterialStore, MaterialUtils } from 'wtd-three-ext'; import { OBJLoader2Parser } from './OBJLoader2Parser.js'; /** * Creates a new OBJLoader2. Use it to load OBJ data from files or to parse OBJ data from arraybuffer or text. * * @param {LoadingManager} [manager] The loadingManager for the loader to use. Default is {@link LoadingManager} * @constructor */ export class OBJLoader2 extends Loader { static OBJLOADER2_VERSION = '6.0.0'; parser = new OBJLoader2Parser(); baseObject3d = new Object3D(); materialStore = new MaterialStore(true); materialPerSmoothingGroup = false; useOAsMesh = false; useIndices = false; disregardNormals = false; modelName = 'noname'; callbacks; /** * * @param {LoadingManager} [manager] */ constructor(manager) { super(manager); this.callbacks = { onLoad: undefined, onError: undefined, onProgress: undefined, onMeshAlter: undefined }; } /** * Enable or disable logging in general (except warn and error), plus enable or disable debug logging. * * @param {boolean} enabled True or false. * @param {boolean} debug True or false. * * @return {OBJLoader2} */ setLogging(enabled, debug) { this.parser.setLogging(enabled, debug); return this; } /** * Tells whether a material shall be created per smoothing group. * * @param {boolean} materialPerSmoothingGroup=false * @return {OBJLoader2} */ setMaterialPerSmoothingGroup(materialPerSmoothingGroup) { this.materialPerSmoothingGroup = materialPerSmoothingGroup === true; return this; } /** * Usually 'o' is meta-information and does not result in creation of new meshes, but mesh creation on occurrence of "o" can be enforced. * * @param {boolean} useOAsMesh=false * @return {OBJLoader2} */ setUseOAsMesh(useOAsMesh) { this.useOAsMesh = useOAsMesh === true; return this; } /** * Instructs loaders to create indexed {@link BufferGeometry}. * * @param {boolean} useIndices=false * @return {OBJLoader2} */ setUseIndices(useIndices) { this.useIndices = useIndices === true; return this; } /** * Tells whether normals should be completely disregarded and regenerated. * * @param {boolean} disregardNormals=false * @return {OBJLoader2} */ setDisregardNormals(disregardNormals) { this.disregardNormals = disregardNormals === true; return this; } /** * Set the name of the model. * * @param {string} modelName * @return {OBJLoader2} */ setModelName(modelName) { if (modelName.length > 0) { this.modelName = modelName; } return this; } /** * Returns the name of the models * @return {String} */ getModelName() { return this.modelName; } /** * Set the node where the loaded objects will be attached directly. * * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to * @return {OBJLoader2} */ setBaseObject3d(baseObject3d) { this.baseObject3d = baseObject3d; return this; } /** * Clears materials object and sets the new ones. * * @param {Object} materials Object with named materials * @return {OBJLoader2} */ setMaterials(materials) { this.materialStore.addMaterialsFromObject(materials, false); return this; } /** * Register a function that is called when parsing was completed. * * @param {CallbackOnLoadType} onLoad * @return {OBJLoader2} */ setCallbackOnLoad(onLoad) { this.callbacks.onLoad = onLoad; return this; } /** * Register a function that is used to report overall processing progress. * * @param {CallbackOnProgressMessageType} onProgress * @return {OBJLoader2} */ setCallbackOnProgress(onProgress) { this.callbacks.onProgress = onProgress; return this; } /** * Register an error handler function that is called if errors occur. It can decide to just log or to throw an exception. * * @param {CallbackOnErrorMessageType} onError * @return {OBJLoader2} */ setCallbackOnError(onError) { this.callbacks.onError = onError; return this; } /** * Register a function that is called once a single mesh is available and it could be altered by the supplied function. * * @param {CallbackOnMeshAlterType} onMeshAlter * @return {OBJLoader2} */ setCallbackOnMeshAlter(onMeshAlter) { this.callbacks.onMeshAlter = onMeshAlter; return this; } /** * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer. * * @param {string} url A string containing the path/URL of the file to be loaded. * @param {FileLoaderOnLoadType} [onLoad] A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument. * @param {FileLoaderOnProgressType} [onProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes. * @param {FileLoaderOnErrorType} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument. * @param {OnMeshAlterType} [onMeshAlter] Called after every single mesh is made available by the parser */ load(url, onLoad, onProgress, onError, onMeshAlter) { if (!(onLoad instanceof Function)) { const badOnLoadError = new Error('onLoad is not a function! Aborting...'); this._onError(badOnLoadError); throw badOnLoadError; } else { this.setCallbackOnLoad(onLoad); } if (!onError || !(onError instanceof Function)) { onError = (errorEvent) => { if (Object.hasOwn(errorEvent, 'currentTarget')) { const errorMessage = 'Error occurred while downloading!\nurl: ' + errorEvent.currentTarget; this._onError(new Error(errorMessage)); } }; } if (url === undefined) { onError(new ErrorEvent('An invalid url was provided. Unable to continue!')); } let urlFull = ''; try { urlFull = new URL(url).href; } catch (error) { urlFull = new URL(url, window.location.href).href; } let filename = urlFull; const urlParts = urlFull.split('/'); if (urlParts.length > 2) { filename = urlParts[urlParts.length - 1]; const urlPartsPath = urlParts.slice(0, urlParts.length - 1).join('/') + '/'; if (urlPartsPath !== undefined) this.path = urlPartsPath; } if (!onProgress || !(onProgress instanceof Function)) { let numericalValueRef = 0; let numericalValue = 0; onProgress = (event) => { if (!event.lengthComputable) return; numericalValue = event.loaded / event.total; if (numericalValue > numericalValueRef) { numericalValueRef = numericalValue; const output = `Download of "${url}": ${(numericalValue * 100).toFixed(2)}%`; this._onProgress(output); } }; } if (onMeshAlter) { this.setCallbackOnMeshAlter(onMeshAlter); } const fileLoaderOnLoad = (content) => { this.parse(content); }; const fileLoader = new FileLoader(this.manager); fileLoader.setPath(this.path || this.resourcePath); fileLoader.setResponseType('arraybuffer'); fileLoader.load(filename, fileLoaderOnLoad, onProgress, onError); } /** * Overrides the implementation of THREE.Loader, so it supports onMeshAlter. * * @param {string} url A string containing the path/URL of the file to be loaded. * @param {FileLoaderOnProgressType} [onProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes. * @param {CallbackOnMeshAlterType} [onMeshAlter] Called after every single mesh is made available by the parser url * @returns Promise */ loadAsync(url, onProgress, onMeshAlter) { return new Promise((resolve, reject) => { this.load(url, resolve, onProgress, reject, onMeshAlter); }); } /** * Parses OBJ data synchronously from arraybuffer or string and returns the {@link Object3D}. * * @param {ArrayBuffer|string} content OBJ data as Uint8Array or String * @return {Object3D} */ parse(objToParse) { if (this.parser.isLoggingEnabled()) { console.info('Using OBJLoader2 version: ' + OBJLoader2.OBJLOADER2_VERSION); console.time('OBJLoader parse: ' + this.modelName); } if (objToParse instanceof ArrayBuffer) { if (this.parser.isLoggingEnabled()) { console.info('Parsing arrayBuffer...'); } this.configure(); this.parser.execute(objToParse); } else if (typeof (objToParse) === 'string') { if (this.parser.isLoggingEnabled()) { console.info('Parsing text...'); } this.configure(); this.parser.executeLegacy(objToParse); } else { this._onError(new Error('Provided objToParse was neither of type String nor Uint8Array! Aborting...')); } if (this.parser.isLoggingEnabled()) { console.timeEnd('OBJLoader parse: ' + this.modelName); } return this.baseObject3d; } configure() { this.parser.setBulkConfig({ materialPerSmoothingGroup: this.materialPerSmoothingGroup, useOAsMesh: this.useOAsMesh, useIndices: this.useIndices, disregardNormals: this.disregardNormals, modelName: this.modelName, materialNames: new Set(Array.from(this.materialStore.getMaterials().keys())) }); this.parser._onAssetAvailable = (preparedMesh) => { const mesh = OBJLoader2.buildThreeMesh(preparedMesh, this.materialStore.getMaterials(), this.parser.isDebugLoggingEnabled()); if (mesh) { this._onMeshAlter(mesh, preparedMesh.materialMetaInfo); this.baseObject3d.add(mesh); } }; this.parser._onLoad = () => { this._onLoad(); }; this.printCallbackConfig(); } printCallbackConfig() { if (this.parser.isLoggingEnabled()) { let printedConfig = 'OBJLoader2 callback configuration:'; if (this.callbacks.onProgress !== null) { printedConfig += `\n\tcallbacks.onProgress: ${this.callbacks.onProgress?.name ?? undefined}`; } if (this.callbacks.onError !== null) { printedConfig += `\n\tcallbacks.onError: ${this.callbacks.onError?.name ?? undefined}`; } if (this.callbacks.onMeshAlter !== null) { printedConfig += `\n\tcallbacks.onMeshAlter: ${this.callbacks.onMeshAlter?.name ?? undefined}`; } if (this.callbacks.onLoad !== null) { printedConfig += `\n\tcallbacks.onLoad: ${this.callbacks.onLoad?.name ?? undefined}`; } console.info(printedConfig); } } static buildThreeMesh({ meshName: meshName, vertexFA: vertexFA, normalFA: normalFA, uvFA: uvFA, colorFA: colorFA, indexUA: indexUA, createMultiMaterial: createMultiMaterial, geometryGroups: geometryGroups, multiMaterial: multiMaterial, materialMetaInfo: materialMetaInfo }, materials, debugLogging) { const geometry = new BufferGeometry(); geometry.setAttribute('position', new BufferAttribute(vertexFA, 3, false)); if (normalFA !== null) { geometry.setAttribute('normal', new BufferAttribute(normalFA, 3, false)); } if (uvFA !== null) { geometry.setAttribute('uv', new BufferAttribute(uvFA, 2, false)); } if (colorFA !== null) { geometry.setAttribute('color', new BufferAttribute(colorFA, 3, false)); } if (indexUA !== null) { geometry.setIndex(new BufferAttribute(indexUA, 1, false)); } if (geometryGroups.length > 0) { for (const geometryGroup of geometryGroups) { geometry.addGroup(geometryGroup.materialGroupOffset, geometryGroup.materialGroupLength, geometryGroup.materialIndex); } } // compute missing vertex normals only after indices have been added! if (normalFA === null) { geometry.computeVertexNormals(); } let material; if (materialMetaInfo.materialCloneInstructions.length > 0) { for (const materialCloneInstruction of materialMetaInfo.materialCloneInstructions) { material = MaterialUtils.cloneMaterial(materials, materialCloneInstruction, debugLogging); } } else { material = materials.get(materialMetaInfo.materialName); } const realMultiMaterials = []; if (createMultiMaterial) { for (let i = 0; i < multiMaterial.length; i++) { const currentMultiMaterial = materials.get(multiMaterial[i]); if (currentMultiMaterial) { realMultiMaterials[i] = currentMultiMaterial; } } } let mesh; const appliedMaterial = createMultiMaterial ? realMultiMaterials : material; if (materialMetaInfo.geometryType === 0) { mesh = new Mesh(geometry, appliedMaterial); } else if (materialMetaInfo.geometryType === 1) { mesh = new LineSegments(geometry, appliedMaterial); } else { mesh = new Points(geometry, appliedMaterial); } if (mesh) { mesh.name = meshName; } return mesh; } _onProgress(text) { if (this.callbacks.onProgress) { this.callbacks.onProgress(text); } else { this.parser._onProgress(text); } } _onError(error) { if (this.callbacks.onError) { this.callbacks.onError(error); } else { this.parser._onError(error.message); } } _onMeshAlter(mesh, _materialMetaInfo) { if (this.callbacks.onMeshAlter) { this.callbacks.onMeshAlter(mesh, this.baseObject3d); } } _onLoad() { if (this.callbacks.onLoad) { this.callbacks.onLoad(this.baseObject3d); } } } //# sourceMappingURL=OBJLoader2.js.map