Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/blocks/fbxInputBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const FbxInputBlockDefinition = /* @__PURE__ */ defineBlock({
resources: {
engine: NullEngineResource,
},
runAsync: (url, _config, { engine }) => loadSingleFileSceneWithPluginAsync(url, engine, ".fbx", () => import("@babylonjs/loaders/FBX/index.js")),
runAsync: (url, _config, { engine }) => loadSingleFileSceneWithPluginAsync(url, engine, () => import("@babylonjs/loaders/FBX/index.js"), { pluginExtension: ".fbx" }),
});

/** Loads an FBX URL into a Babylon.js scene. */
Expand Down
45 changes: 20 additions & 25 deletions src/blocks/gltfInputBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Block, type BlockOptions } from "../block/block";
import { defineBlock } from "../block/blockDefinition";
import { BabylonSceneType, UrlType } from "../block/connectionPointType";
import { NullEngineResource } from "../resources/nullEngineResource";
import { fetchAsDataUriAsync, fetchOrThrowAsync, isHttpUrl, loadSceneWithPluginAsync } from "../helpers/loadSceneWithPlugin";
import { fetchOrThrowAsync, isHttpUrl, loadSingleFileSceneWithPluginAsync, responseToDataUriAsync } from "../helpers/loadSceneWithPlugin";

const GltfInputBlockDefinition = /* @__PURE__ */ defineBlock({
type: "input.gltf",
Expand All @@ -11,32 +11,27 @@ const GltfInputBlockDefinition = /* @__PURE__ */ defineBlock({
resources: {
engine: NullEngineResource,
},
runAsync: async (url, _config, { engine }) => {
if (!isHttpUrl(url)) {
return loadSceneWithPluginAsync(url, engine, () => import("@babylonjs/loaders/glTF/index.js"));
}
runAsync: (url, _config, { engine }) =>
loadSingleFileSceneWithPluginAsync(url, engine, () => import("@babylonjs/loaders/glTF/index.js"), {
prepareSceneLoadAsync: async (response, resolvedUrl, signal) => {
const format = await readGltfResponseAsync(response, resolvedUrl);
return {
source: format.source,
pluginExtension: format.extension,
pluginOptions: {
gltf: {
preprocessUrlAsync: async (dependencyUrl) => {
if (!isHttpUrl(dependencyUrl)) {
return dependencyUrl;
}

const abortController = new AbortController();
try {
const response = await fetchOrThrowAsync(url, abortController.signal);
const resolvedUrl = response.url || url;
const format = await readGltfResponseAsync(response, resolvedUrl);

return await loadSceneWithPluginAsync(format.source, engine, () => import("@babylonjs/loaders/glTF/index.js"), {
rootUrl: new URL(".", resolvedUrl).href,
pluginExtension: format.extension,
name: new URL(resolvedUrl).pathname.split("/").pop() ?? "",
pluginOptions: {
gltf: {
preprocessUrlAsync: (dependencyUrl) =>
isHttpUrl(dependencyUrl) ? fetchAsDataUriAsync(dependencyUrl, abortController.signal) : Promise.resolve(dependencyUrl),
return responseToDataUriAsync(await fetchOrThrowAsync(dependencyUrl, signal));
},
},
},
},
});
} finally {
abortController.abort();
}
},
};
},
}),
});

/** Loads a glTF or GLB URL into a Babylon.js scene. */
Expand Down
84 changes: 35 additions & 49 deletions src/blocks/objInputBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Scene as BabylonScene } from "@babylonjs/core/scene.js";
import { Block, type BlockOptions } from "../block/block";
import { defineBlock } from "../block/blockDefinition";
import { BabylonSceneType, UrlType } from "../block/connectionPointType";
import { createDataUri, fetchOrThrowAsync, isHttpUrl, loadSceneWithPluginAsync } from "../helpers/loadSceneWithPlugin";
import { createDataUri, fetchOrThrowAsync, loadSingleFileSceneWithPluginAsync } from "../helpers/loadSceneWithPlugin";
import { NullEngineResource } from "../resources/nullEngineResource";

const MaximumConcurrentTextureFetches = 8;
Expand All @@ -18,43 +18,40 @@ const ObjInputBlockDefinition = /* @__PURE__ */ defineBlock({
},
runAsync: async (url, _config, { engine }) => {
const pluginPromise = import("@babylonjs/loaders/OBJ/index.js");
if (!isHttpUrl(url)) {
return loadSceneWithPluginAsync(url, engine, () => pluginPromise, createObjLoadOptions());
}

const abortController = new AbortController();
try {
const objResponse = await fetchOrThrowAsync(url, abortController.signal);
const resolvedObjUrl = objResponse.url || url;
const obj = await objResponse.text();
const references = analyzeObjReferences(obj);

let source = obj;
let textureAssets = new Map<string, TextureAsset>();
let materialTokens = new Map<string, string>();
if (references.mtl?.value) {
const mtlUrl = resolveDependencyUrl(references.mtl.value, resolvedObjUrl, "MTL");
const mtlResponse = await fetchOrThrowAsync(mtlUrl, abortController.signal);
const resolvedMtlUrl = mtlResponse.url || mtlUrl;
const mtl = await mtlResponse.text();
const rewritten = await rewriteMtlAsync(mtl, resolvedMtlUrl, abortController.signal);
textureAssets = rewritten.textureAssets;
materialTokens = rewritten.materialTokens;
source = rewriteObjReferences(obj, references, createTextDataUri(rewritten.source), materialTokens, resolvedObjUrl);
} else if (references.mtl) {
throw new Error(`Invalid OBJ file "${resolvedObjUrl}": mtllib has no material library path.`);
}

const scene = await loadSceneWithPluginAsync(createDirectTextSource(source), engine, () => pluginPromise, {
...createObjLoadOptions(),
name: new URL(resolvedObjUrl).pathname.split("/").pop() ?? "",
});
await attachTextureDataAsync(scene, textureAssets);
restoreMaterialNames(scene, materialTokens);
return scene;
} finally {
abortController.abort();
}
let textureAssets = new Map<string, TextureAsset>();
let materialTokens = new Map<string, string>();
const scene = await loadSingleFileSceneWithPluginAsync(url, engine, () => pluginPromise, {
includeRootUrl: false,
pluginExtension: ".obj",
pluginOptions: {
obj: {
materialLoadingFailsSilently: false,
},
},
prepareSceneLoadAsync: async (objResponse, resolvedObjUrl, signal) => {
const obj = await objResponse.text();
const references = analyzeObjReferences(obj);

let source = obj;
if (references.mtl?.value) {
const mtlUrl = resolveDependencyUrl(references.mtl.value, resolvedObjUrl, "MTL");
const mtlResponse = await fetchOrThrowAsync(mtlUrl, signal);
const resolvedMtlUrl = mtlResponse.url || mtlUrl;
const mtl = await mtlResponse.text();
const rewritten = await rewriteMtlAsync(mtl, resolvedMtlUrl, signal);
textureAssets = rewritten.textureAssets;
materialTokens = rewritten.materialTokens;
source = rewriteObjReferences(obj, references, createTextDataUri(rewritten.source), materialTokens, resolvedObjUrl);
} else if (references.mtl) {
throw new Error(`Invalid OBJ file "${resolvedObjUrl}": mtllib has no material library path.`);
}

return { source: createDirectTextSource(source) };
},
});
await attachTextureDataAsync(scene, textureAssets);
restoreMaterialNames(scene, materialTokens);
return scene;
},
});

Expand All @@ -65,17 +62,6 @@ export class ObjInputBlock extends Block<typeof ObjInputBlockDefinition> {
}
}

function createObjLoadOptions() {
return {
pluginExtension: ".obj",
pluginOptions: {
obj: {
materialLoadingFailsSilently: false,
},
},
} as const;
}

interface ObjLineReference {
readonly end: number;
readonly indentation: string;
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/stlInputBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const StlInputBlockDefinition = /* @__PURE__ */ defineBlock({
resources: {
engine: NullEngineResource,
},
runAsync: (url, _config, { engine }) => loadSingleFileSceneWithPluginAsync(url, engine, ".stl", () => import("@babylonjs/loaders/STL/index.js")),
runAsync: (url, _config, { engine }) => loadSingleFileSceneWithPluginAsync(url, engine, () => import("@babylonjs/loaders/STL/index.js"), { pluginExtension: ".stl" }),
});

/** Loads an STL URL into a Babylon.js scene. */
Expand Down
75 changes: 56 additions & 19 deletions src/helpers/loadSceneWithPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,66 @@ import type { Scene } from "@babylonjs/core/scene.js";

type SceneSource = string | ArrayBufferView;

interface SceneLoadPreparation {
readonly source: SceneSource;
readonly pluginExtension?: string;
readonly pluginOptions?: LoadOptions["pluginOptions"];
}

type PrepareSceneLoadAsync = (response: Response, resolvedUrl: string, signal: AbortSignal) => Promise<SceneLoadPreparation>;

interface SingleFileSceneLoadOptions {
readonly includeRootUrl?: boolean;
readonly pluginExtension?: string;
readonly pluginOptions?: LoadOptions["pluginOptions"];
readonly prepareSceneLoadAsync?: PrepareSceneLoadAsync;
}

export async function loadSceneWithPluginAsync(source: SceneSource, engine: AbstractEngine, loadPluginAsync: () => Promise<unknown>, options?: LoadOptions): Promise<Scene> {
const [{ LoadSceneAsync }] = await Promise.all([import("@babylonjs/core/Loading/sceneLoader.js"), loadPluginAsync()]);
return LoadSceneAsync(source, engine, options);
}

export async function loadSingleFileSceneWithPluginAsync(url: string, engine: AbstractEngine, pluginExtension: string, loadPluginAsync: () => Promise<unknown>): Promise<Scene> {
export async function loadSingleFileSceneWithPluginAsync(
url: string,
engine: AbstractEngine,
loadPluginAsync: () => Promise<unknown>,
options: SingleFileSceneLoadOptions = {}
): Promise<Scene> {
if (!isHttpUrl(url)) {
return loadSceneWithPluginAsync(url, engine, loadPluginAsync, { pluginExtension });
if (options.pluginExtension === undefined && options.pluginOptions === undefined) {
return loadSceneWithPluginAsync(url, engine, loadPluginAsync);
}

return loadSceneWithPluginAsync(url, engine, loadPluginAsync, {
...(options.pluginExtension === undefined ? {} : { pluginExtension: options.pluginExtension }),
...(options.pluginOptions === undefined ? {} : { pluginOptions: options.pluginOptions }),
});
}

const abortController = new AbortController();
try {
const fetched = await fetchAsDataUriWithUrlAsync(url, abortController.signal);
return await loadSceneWithPluginAsync(fetched.dataUri, engine, loadPluginAsync, {
rootUrl: new URL(".", fetched.url).href,
pluginExtension,
name: new URL(fetched.url).pathname.split("/").pop() ?? "",
});
const response = await fetchOrThrowAsync(url, abortController.signal);
const resolvedUrl = response.url || url;
const preparation =
options.prepareSceneLoadAsync === undefined
? { source: await responseToDataUriAsync(response) }
: await options.prepareSceneLoadAsync(response, resolvedUrl, abortController.signal);
const loadOptions: LoadOptions = {
name: new URL(resolvedUrl).pathname.split("/").pop() ?? "",
};
if (options.includeRootUrl !== false) {
loadOptions.rootUrl = new URL(".", resolvedUrl).href;
}
const resolvedPluginExtension = preparation.pluginExtension ?? options.pluginExtension;
if (resolvedPluginExtension !== undefined) {
loadOptions.pluginExtension = resolvedPluginExtension;
}
const resolvedPluginOptions = preparation.pluginOptions ?? options.pluginOptions;
if (resolvedPluginOptions !== undefined) {
loadOptions.pluginOptions = resolvedPluginOptions;
}
return await loadSceneWithPluginAsync(preparation.source, engine, loadPluginAsync, loadOptions);
} finally {
abortController.abort();
}
Expand All @@ -40,17 +82,6 @@ export async function fetchOrThrowAsync(url: string, signal: AbortSignal): Promi
return response;
}

export async function fetchAsDataUriAsync(url: string, signal: AbortSignal): Promise<string> {
return (await fetchAsDataUriWithUrlAsync(url, signal)).dataUri;
}

export async function fetchAsDataUriWithUrlAsync(url: string, signal: AbortSignal): Promise<{ readonly dataUri: string; readonly url: string }> {
const response = await fetchOrThrowAsync(url, signal);
const contentType = response.headers.get("content-type")?.split(";", 1)[0] || "application/octet-stream";
const dataUri = createDataUri(new Uint8Array(await response.arrayBuffer()), contentType);
return { dataUri, url: response.url || url };
}

export function createDataUri(data: Uint8Array, contentType: string): string {
return `data:${contentType};base64,${toBase64(data)}`;
}
Expand All @@ -67,3 +98,9 @@ export function toBase64(data: Uint8Array): string {
}
return btoa(binary);
}

export async function responseToDataUriAsync(response: Response): Promise<string> {
const contentType = response.headers.get("content-type")?.split(";", 1)[0] || "application/octet-stream";
const data = new Uint8Array(await response.arrayBuffer());
return createDataUri(data, contentType);
}
Loading