@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
322 lines • 13.4 kB
JavaScript
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { showBalloonMessage } from "./debug/index.js";
import { getLoader, registerLoader } from "./engine_gltf.js";
import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components.js";
// import * as object from "./engine_gltf_builtin_components.js";
import * as loaders from "./engine_loaders.js";
import { registerPrewarmObject } from "./engine_mainloop_utils.js";
import { postprocessFBXMaterials } from "./engine_three_utils.js";
import * as utils from "./engine_utils.js";
import { tryDetermineFileTypeFromURL } from "./engine_utils_format.js";
import { invokeAfterImportPluginHooks, registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
/** @internal */
export class NeedleLoader {
createBuiltinComponents(context, gltfId, gltf, seed, extension) {
return createBuiltinComponents(context, gltfId, gltf, seed, extension);
}
writeBuiltinComponentData(comp, context) {
return writeBuiltinComponentData(comp, context);
}
parseSync(context, data, path, seed) {
return parseSync(context, data, path, seed);
}
loadSync(context, url, sourceId, seed, prog) {
return loadSync(context, url, sourceId, seed, prog);
}
}
registerLoader(NeedleLoader); // Register the loader
const printGltf = utils.getParam("printGltf") || utils.getParam("printgltf");
const downloadGltf = utils.getParam("downloadgltf");
const debugFileTypes = utils.getParam("debugfileformat");
// const loader = new GLTFLoader();
// registerExtensions(loader);
export var GltfLoadEventType;
(function (GltfLoadEventType) {
GltfLoadEventType[GltfLoadEventType["BeforeLoad"] = 0] = "BeforeLoad";
GltfLoadEventType[GltfLoadEventType["AfterLoaded"] = 1] = "AfterLoaded";
GltfLoadEventType[GltfLoadEventType["FinishedSetup"] = 10] = "FinishedSetup";
})(GltfLoadEventType || (GltfLoadEventType = {}));
export class GltfLoadEvent {
context;
loader;
path;
gltf;
constructor(context, path, loader, gltf) {
this.context = context;
this.path = path;
this.loader = loader;
this.gltf = gltf;
}
}
const eventListeners = {};
export function addGltfLoadEventListener(type, listener) {
eventListeners[type] = eventListeners[type] || [];
eventListeners[type].push(listener);
}
export function removeGltfLoadEventListener(type, listener) {
if (eventListeners[type]) {
const index = eventListeners[type].indexOf(listener);
if (index >= 0) {
eventListeners[type].splice(index, 1);
}
}
}
function invokeEvents(type, event) {
if (eventListeners[type]) {
for (const listener of eventListeners[type]) {
listener(event);
}
}
}
async function handleLoadedGltf(context, gltfId, gltf, seed, componentsExtension) {
if (printGltf)
console.warn("glTF", gltfId, gltf);
// Remove query parameters from gltfId
if (gltfId.includes("?")) {
gltfId = gltfId.split("?")[0];
}
await getLoader().createBuiltinComponents(context, gltfId, gltf, seed, componentsExtension);
}
export async function createLoader(url, context) {
const type = await tryDetermineFileTypeFromURL(url) || "unknown";
if (debugFileTypes)
console.debug("Determined file type: " + type + " for url", url);
switch (type) {
case "unknown":
{
console.warn("Unknown file type. Assuming glTF:", url);
const loader = new GLTFLoader();
await registerExtensions(loader, context, url);
return loader;
}
case "fbx":
return new FBXLoader();
case "obj":
return new OBJLoader();
case "usd":
case "usda":
case "usdz":
console.warn(type.toUpperCase() + " files are not supported.");
return null;
// return new USDZLoader();
default:
console.warn("Unknown file type:", type);
case "gltf":
case "glb":
case "vrm":
{
const loader = new GLTFLoader();
await registerExtensions(loader, context, url);
return loader;
}
}
}
/** Load a gltf file from a url. This is the core method used by Needle Engine to load gltf files. All known extensions are registered here.
* @param context The current context
* @param data The gltf data as string or ArrayBuffer
* @param path The path to the gltf file
* @param seed The seed for generating unique ids
* @returns The loaded gltf object
*/
export async function parseSync(context, data, path, seed) {
if (typeof path !== "string") {
console.warn("Parse gltf binary without path, this might lead to errors in resolving extensions. Please provide the source path of the gltf/glb file", path, typeof path);
path = "";
}
if (printGltf)
console.log("Parse glTF", path);
const loader = await createLoader(path, context);
if (!loader) {
return undefined;
}
// Handle OBJ Loader
if (loader instanceof OBJLoader) {
if (typeof data !== "string") {
data = new TextDecoder().decode(data);
}
const res = loader.parse(data);
return {
animations: res.animations,
scene: res,
scenes: [res]
};
}
// Handle any other loader that is not a GLTFLoader
const isNotGLTF = !(loader instanceof GLTFLoader);
if (isNotGLTF) {
const res = loader.parse(data, path);
postprocessLoadedFile(loader, res);
return {
animations: res.animations,
scene: res,
scenes: [res]
};
}
const componentsExtension = registerComponentExtension(loader);
return new Promise((resolve, reject) => {
try {
// GltfLoader expects a base path for resolving referenced assets
// https://threejs.org/docs/#examples/en/loaders/GLTFLoader.parse
// so we make sure that "path" is never a file path
let gltfLoaderPath = path.split("?")[0].trimEnd();
// This assumes that the path is a FILE path and not already a directory
// (it does not end with "/") – see https://linear.app/needle/issue/NE-6075
{
// strip file from path
const parts = gltfLoaderPath.split("/");
// check if the last part is a /, otherwise remove it
if (parts.length > 0 && parts[parts.length - 1] !== "")
parts.pop();
gltfLoaderPath = parts.join("/");
if (!gltfLoaderPath.endsWith("/"))
gltfLoaderPath += "/";
}
loader.resourcePath = gltfLoaderPath;
loaders.addDracoAndKTX2Loaders(loader, context);
invokeEvents(GltfLoadEventType.BeforeLoad, new GltfLoadEvent(context, path, loader));
const camera = context.mainCamera;
loader.parse(data, "", async (res) => {
invokeAfterImportPluginHooks(path, res, context);
invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, path, loader, res));
await handleLoadedGltf(context, path, res, seed, componentsExtension);
await compileAsync(res.scene, context, camera);
invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, path, loader, res));
resolve(res);
if (downloadGltf) {
_downloadGltf(data);
}
}, err => {
console.error("Loading asset at \"" + path + "\" failed\n", err);
resolve(undefined);
});
}
catch (err) {
console.error(err);
reject(err);
}
});
}
/**
* Load a gltf file from a url. This is the core method used by Needle Engine to load gltf files. All known extensions are registered here.
* @param context The current context
* @param url The url to the gltf file
* @param sourceId The source id of the gltf file - this is usually the url
* @param seed The seed for generating unique ids
* @param prog A progress callback
* @returns The loaded gltf object
*/
export async function loadSync(context, url, sourceId, seed, prog) {
// better to create new loaders every time
// (maybe we can cache them...)
// but due to the async nature and potentially triggering multiple loads at the same time
// we need to make sure the extensions dont override each other
// creating new loaders should not be expensive as well
checkIfUserAttemptedToLoadALocalFile(url);
const loader = await createLoader(url, context);
if (!loader) {
return undefined;
}
// Handle any loader that is not a GLTFLoader
if (!(loader instanceof GLTFLoader)) {
const res = await loader.loadAsync(url, prog);
postprocessLoadedFile(loader, res);
return {
animations: res.animations,
scene: res,
scenes: [res]
};
}
const componentsExtension = registerComponentExtension(loader);
return new Promise((resolve, reject) => {
try {
loaders.addDracoAndKTX2Loaders(loader, context);
invokeEvents(GltfLoadEventType.BeforeLoad, new GltfLoadEvent(context, url, loader));
const camera = context.mainCamera;
loader.load(url, async (res) => {
invokeAfterImportPluginHooks(url, res, context);
invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, url, loader, res));
await handleLoadedGltf(context, sourceId, res, seed, componentsExtension);
await compileAsync(res.scene, context, camera);
invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, url, loader, res));
resolve(res);
if (downloadGltf) {
_downloadGltf(url);
}
}, evt => {
prog?.call(loader, evt);
}, err => {
console.error("Loading asset at \"" + url + "\" failed\n", err);
resolve(undefined);
});
}
catch (err) {
console.error(err);
reject(err);
}
});
}
async function compileAsync(scene, context, camera) {
if (!camera)
camera = context.mainCamera;
try {
if (camera) {
await context.renderer.compileAsync(scene, camera, context.scene)
.catch(err => {
console.warn(err.message);
});
}
else
registerPrewarmObject(scene, context);
}
catch (err) {
console.warn(err?.message || err);
}
}
function checkIfUserAttemptedToLoadALocalFile(url) {
const fullurl = new URL(url, window.location.href).href;
if (fullurl.startsWith("file://")) {
const msg = "Hi - it looks like you are trying to load a local file which will not work. You need to use a webserver to serve your files.\nPlease refer to the documentation on <a href=\"https://fwd.needle.tools/needle-engine/docs/local-server\">https://docs.needle.tools</a> or ask for help in our <a href=\"https://discord.needle.tools\">discord community</a>";
showBalloonMessage(msg);
console.warn(msg);
}
}
function _downloadGltf(data) {
if (typeof data === "string") {
const a = document.createElement("a");
a.href = data;
a.download = data.split("/").pop();
a.click();
}
else {
const blob = new Blob([data], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "download.glb";
a.click();
}
}
function postprocessLoadedFile(loader, result) {
if (result?.isObject3D) {
const obj = result;
if (loader instanceof FBXLoader || loader instanceof OBJLoader) {
obj.traverse((child) => {
const mesh = child;
// See https://github.com/needle-tools/three.js/blob/b8df3843ff123ac9dc0ed0d3ccc5b568f840c804/examples/webgl_loader_multiple.html#L377
if (mesh?.isMesh) {
postprocessFBXMaterials(mesh, mesh.material);
}
});
}
// else if (loader instanceof OBJLoader) {
// obj.traverse(_child => {
// // TODO: Needs testing
// // if (!(child instanceof Mesh)) return;
// // child.material = new MeshStandardMaterial();
// });
// }
}
}
//# sourceMappingURL=engine_scenetools.js.map