UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

143 lines (116 loc) 5.2 kB
import { Loader, LoadingManager, Material, Object3D, TextureLoader } from "three"; import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js"; import { ObjectUtils } from "../engine_create_objects.js"; import { MODULES } from "../engine_modules.js"; import { IContext } from "../engine_types.js"; // #region Utils export namespace MaterialX { /** * Utility function to load a MaterialX material from a URL. This can be used in your own code to load MaterialX materials outside of the glTF loading process. The URL should point to a MaterialX XML file. */ export async function loadFromUrl(urlOrXML: string, opts?: { url?: string, loadingManager?: LoadingManager, materialNameOrIndex?: number | string } ): Promise<import("three").Material | null> { if (!urlOrXML) throw new Error("URL or XML string is required to load a MaterialX material"); // Ensure the MaterialX module is loaded const module = await MODULES.MaterialX.load(); // Check if the input is an XML string or a URL // And fetch the XML content if it's a URL const isXmlString = urlOrXML.trimStart().startsWith("<"); const xml = isXmlString ? urlOrXML : await fetch(urlOrXML).then(r => r.text()).catch(console.error); if (!xml) { console.warn("Failed to load MaterialX file from url", urlOrXML); return null; } // For relative texture paths we might need to detect the base directory of the material file. // We can only do this if we have a URL (not an XML string) and if the URL is not a data URL. In that case we can use the URL to determine the base path for textures. // This can be used by the loader callback to resolve texture paths relative to the material file. let dir: string | undefined = undefined; if (opts?.url || !isXmlString) { const parts = (opts?.url || urlOrXML).split('/'); parts.pop(); dir = parts.join('/'); } const textureLoader = new TextureLoader(); return module.Experimental_API.createMaterialXMaterial(xml, opts?.materialNameOrIndex ?? 0, { getTexture: async url => { if (!url.startsWith("http") && !url.startsWith("data:") && !url.startsWith("blob:") && !url.startsWith("file:")) { if (dir) { url = dir + "/" + url; } } return textureLoader.loadAsync(url).catch(e => { console.warn(`Failed to load texture for MaterialX material ${url}`, e); }); } }, { cacheKey: urlOrXML, }) } } // #region Loader export class MaterialXLoader extends Loader<Object3D | null> { loadAsync(url: string, onProgress?: ((event: ProgressEvent<EventTarget>) => void) | undefined): Promise<Object3D> { return new Promise((resolve, reject) => { this.load(url, resolve, onProgress, reject); }); } load(url: string, onLoad: (data: Object3D) => void, onProgress?: ((event: ProgressEvent<EventTarget>) => void) | undefined, onError?: ((err: unknown) => void) | undefined): void { onProgress?.({ type: "progress", loaded: 0, total: 0 } as ProgressEvent); MaterialX.loadFromUrl(url, { }).then(mat => { if (mat) { onLoad(this.onLoaded(mat)); } else { onError?.(new Error("Failed to load MaterialX material from url: " + url)); } }); } private onLoaded(mat: Material): Object3D { const shaderball = ObjectUtils.createPrimitive("ShaderBall", { material: mat }); return shaderball; } } // #region GLTF Extension export class NEEDLE_materialx implements GLTFLoaderPlugin { get name(): string { return "materialx-loading-helper"; } constructor( private readonly context: IContext, private readonly loader: GLTFLoader, private readonly url: string, private readonly parser: GLTFParser, ) { } private mtlxLoader?: import("@needle-tools/materialx").MaterialXLoader; async beforeRoot() { const mtlxExtension = this.parser.json.extensions?.["NEEDLE_materials_mtlx"]; if (mtlxExtension) { const module = await MODULES.MaterialX.load(); try { this.mtlxLoader = new module.MaterialXLoader(this.parser, { cacheKey: `${this.url}:materialx`, parameters: { precision: this.context.renderer?.capabilities.precision as any, } }, { getFrame: () => this.context.time.frame, getTime: () => this.context.time.time, }) } catch (error) { console.error(error); } } } loadMaterial(index) { if (this.mtlxLoader) return this.mtlxLoader.loadMaterial(index); return null; } }