@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
text/typescript
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;
}
}