Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ packages/core/docs/typedocs
test-results/
*.tsbuildinfo
.vite/
packages/core/.cache/usd/

.env.local
.DS_Store
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const internalNamingPlugin = {

export default tseslint.config(
{
ignores: ["**/dist/**", "**/node_modules/**", "test-results/**", "docs/**", "packages/core/docs/typedocs/**", "**/*.md"],
ignores: ["**/dist/**", "**/node_modules/**", "test-results/**", "docs/**", "packages/core/docs/typedocs/**", "packages/core/.cache/**", "**/*.md"],
},

js.configs.recommended,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"typedocs": "pnpm --filter @babylonjs/node-assets typedocs"
},
"devDependencies": {
"@babylonjs/core": "9.21.2",
"@babylonjs/ktx2decoder": "9.21.2",
"@babylonjs/core": "9.27.1",
"@babylonjs/ktx2decoder": "9.27.1",
"@babylonjs/node-assets": "workspace:*",
"@eslint/js": "^10.0.1",
"@gltf-transform/core": "4.5.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/core/build/codecBuildPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ import { fileURLToPath } from "node:url";

import type { Plugin, ResolvedConfig } from "vite";

import { prepareUsdRuntimeAsync, UsdRuntimeAssets, UsdRuntimeNotices } from "./prepareUsdRuntime";

export const MscTranscoderModuleId = "virtual:node-assets-msc-transcoder";
const ResolvedMscTranscoderModuleId = `\0${MscTranscoderModuleId}`;
const BasisEncoderWasmUrlModuleId = "virtual:node-assets-basis-encoder-wasm-url";
const UsdRuntimeUrlsModuleId = "virtual:node-assets-usd-runtime-urls";
const ResolvedUsdRuntimeUrlsModuleId = `\0${UsdRuntimeUrlsModuleId}`;
const DracoNodeRuntimeDetection = /"object"==typeof process&&"object"==typeof process\.versions&&"string"==typeof process\.versions\.node/g;

export function codecBuildPlugin(): Plugin {
let resolvePackage: ReturnType<ResolvedConfig["createResolver"]> | undefined;
let isBuild = false;
const usdCacheDirectory = fileURLToPath(new URL("../.cache/usd/5/", import.meta.url));
let usdFiles: ReturnType<typeof prepareUsdRuntimeAsync> | undefined;
const prepareUsdAsync = () => (usdFiles ??= prepareUsdRuntimeAsync(usdCacheDirectory));
return {
name: "node-assets-codecs",
enforce: "pre",
Expand All @@ -24,6 +31,9 @@ export function codecBuildPlugin(): Plugin {
if (id === MscTranscoderModuleId) {
return ResolvedMscTranscoderModuleId;
}
if (id === UsdRuntimeUrlsModuleId) {
return ResolvedUsdRuntimeUrlsModuleId;
}
if (id === BasisEncoderWasmUrlModuleId) {
// TODO: Remove this override when the encoder keeps its relative WASM URL valid after Vite pre-bundling.
const encoderEntry = await resolvePackage?.("babylonpress-ktx2-encoder", fileURLToPath(import.meta.url));
Expand All @@ -34,6 +44,17 @@ export function codecBuildPlugin(): Plugin {
}
},
async load(id) {
if (id === ResolvedUsdRuntimeUrlsModuleId) {
const files = await prepareUsdAsync();
const urls = UsdRuntimeAssets.map(({ name, option }, index) => {
const path = files.get(name);
if (!path) {
throw new Error(`Missing USD runtime asset "${name}".`);
}
return { import: `import asset${index} from ${JSON.stringify(`${path}?url&no-inline`)};`, option: `${option}: asset${index}` };
});
return [...urls.map((url) => url.import), `export default { ${urls.map((url) => url.option).join(", ")} };`].join("\n");
}
if (id !== ResolvedMscTranscoderModuleId) {
return;
}
Expand Down Expand Up @@ -62,5 +83,18 @@ export function codecBuildPlugin(): Plugin {
}
return { code: transformed, map: null };
},
async generateBundle() {
if (!usdFiles) {
return;
}
const files = await usdFiles;
for (const { name } of UsdRuntimeNotices) {
const path = files.get(name);
if (!path) {
throw new Error(`Missing USD runtime notice "${name}".`);
}
this.emitFile({ type: "asset", fileName: `licenses/usd/${name}`, source: await readFile(path) });
}
},
};
}
63 changes: 63 additions & 0 deletions packages/core/build/prepareUsdRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";

const BabylonRelease = "8b086dc2b313b61918b0dc3854d3a1d0aff4530e"; // 9.27.1
const ImporterRelease = "2baa7a087bad28498378dacf50d22f46c54eacc3"; // protocol 5
const BabylonRuntimeUrl = `https://raw.githubusercontent.com/BabylonJS/Babylon.js/${BabylonRelease}/packages/tools/babylonServer/public/babylonUsdImporter/5/`;
const ImporterUrl = `https://raw.githubusercontent.com/BabylonJS/babylon-usd-importer/${ImporterRelease}/`;

export const UsdRuntimeAssets = [
{ name: "babylon-usd-importer.worker.js", option: "workerUrl", sha256: "caaf962049c25a16ed15745508d1dd547580e36edf36f7a9ebc0f27759de2dc5" },
{ name: "babylon-usd-importer.js", option: "glueUrl", sha256: "ccd71ba966d27b1b08aa540915cb983ece00262b440f519d94c2505e2b88c120" },
{ name: "babylon-usd-importer.wasm", option: "wasmUrl", sha256: "72f0d5a1ce2476566b9455dc16f19738d6b4c03776e2e103027e0c7a096d98d2" },
{ name: "babylon-usd-importer.data", option: "dataUrl", sha256: "ededef6a1ecbc5aa112ffa0f4e2713a13bca316ae005b3d95b91fd8dad240225" },
] as const;

export const UsdRuntimeNotices = [
{ name: "usd-importer.license", source: `${ImporterUrl}LICENSE`, sha256: "5c9817c129b98e7bb966bca028c43c19107102ef8e03fe799bffb4354f4ef015" },
{ name: "openusd.license", source: `${BabylonRuntimeUrl}openusd.license`, sha256: "4d6e8e3a9bd0104e10c48e3bc6af2f0976448a70a377d20cef674740f96f4452" },
{ name: "tbb.license", source: `${BabylonRuntimeUrl}tbb.license`, sha256: "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" },
{ name: "zlib.license", source: `${BabylonRuntimeUrl}zlib.license`, sha256: "e32ff4e00d9d94930537635291da39e7e612703334bf6fde8c7f1686fe8a45a2" },
] as const;

export async function prepareUsdRuntimeAsync(cacheDirectory: string): Promise<Map<string, string>> {
const files = await Promise.all(
[...UsdRuntimeAssets.map((asset) => ({ ...asset, source: `${BabylonRuntimeUrl}${asset.name}` })), ...UsdRuntimeNotices].map(async ({ name, source, sha256 }) => {
const path = join(cacheDirectory, name);
let bytes: Uint8Array | undefined;
try {
bytes = await readFile(path);
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
throw error;
}
}
if (bytes === undefined) {
const response = await fetch(source, { signal: AbortSignal.timeout(60_000) });
if (!response.ok) {
throw new Error(`Unable to download USD runtime asset "${name}": ${response.status} ${response.statusText}`);
}
bytes = new Uint8Array(await response.arrayBuffer());
verifyChecksum(name, bytes, sha256);
await mkdir(cacheDirectory, { recursive: true });
const temporaryPath = `${path}.${randomUUID()}.tmp`;
try {
await writeFile(temporaryPath, bytes);
await rename(temporaryPath, path);
} finally {
await rm(temporaryPath, { force: true });
}
}
verifyChecksum(name, bytes, sha256);
return [name, path] as const;
})
);
return new Map(files);
}

function verifyChecksum(name: string, bytes: Uint8Array, expected: string): void {
if (createHash("sha256").update(bytes).digest("hex") !== expected) {
throw new Error(`USD runtime asset "${name}" does not match its pinned SHA-256 checksum.`);
}
}
5 changes: 5 additions & 0 deletions packages/core/docs/blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
- Output: `Document`
- Uses: Babylon STL loader
- Behavior: Uses the Babylon scene loader to load an STL using NullEngine, exports it as a GLB, then reimports the bytes as a `Document`.
- `UsdInputBlock`
- Input: `string` HTTP(S) or data URL, or a Node filesystem path/file URL, pointing to a USD, USDA, USDC, or USDZ file.
- Output: `Document`
- Uses: Babylon USD loader and the packaged OpenUSD importer runtime; requires Web Worker support.
- Behavior: Uses the Babylon scene loader to load USD using NullEngine, exports it as a GLB, then reimports the bytes as a `Document`.

# Transforms

Expand Down
8 changes: 4 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
"typedocs": "typedoc"
},
"dependencies": {
"@babylonjs/core": "^9.21.2",
"@babylonjs/ktx2decoder": "9.21.2",
"@babylonjs/loaders": "^9.21.2",
"@babylonjs/serializers": "^9.21.2",
"@babylonjs/core": "^9.27.1",
"@babylonjs/ktx2decoder": "9.27.1",
"@babylonjs/loaders": "^9.27.1",
"@babylonjs/serializers": "^9.27.1",
"@gltf-transform/core": "4.5.0",
"@gltf-transform/extensions": "4.5.0",
"@gltf-transform/functions": "4.5.0",
Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/blocks/usdInputBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ISceneLoaderPluginFactory } from "@babylonjs/core/Loading/sceneLoader.js";
import { USDFileLoaderMetadata } from "@babylonjs/loaders/USD/usdFileLoader.metadata.js";

import { GltfDocumentType } from "../connectionPoints/gltfDocument";
import { UrlType } from "../connectionPoints/url";
import { convertBabylonSceneToDocumentAsync } from "../helpers/convertBabylonSceneToDocument";
import { loadSceneWithPluginAsync } from "../helpers/loadSceneWithPlugin";
import { NullEngineResource } from "../resources/nullEngineResource";
import { PlatformIOResource } from "../resources/platformIOResource";
import { Block, type BlockOptions } from "./block";
import { defineBlock } from "./blockDefinition";

const UsdLoaderFactory = {
...USDFileLoaderMetadata,
createPlugin: async () => {
const [{ USDFileLoader, _RegisterUSDLoaderDependencies }, { default: runtimeUrls }] = await Promise.all([
import("@babylonjs/loaders/USD/usdFileLoader.pure.js"),
import("virtual:node-assets-usd-runtime-urls"),
]);
_RegisterUSDLoaderDependencies();
return new USDFileLoader(runtimeUrls);
},
} satisfies ISceneLoaderPluginFactory;

const UsdInputBlockDefinition = /* @__PURE__ */ defineBlock({
type: "input.usd",
input: UrlType,
output: GltfDocumentType,
resources: {
engine: NullEngineResource,
io: PlatformIOResource,
},
runAsync: async (url, _config, { engine, io }) =>
convertBabylonSceneToDocumentAsync(
await loadSceneWithPluginAsync(url, engine, UsdLoaderFactory, {
pluginExtension: ".usd",
}),
io
),
});

/** Loads a USD, USDA, USDC, or USDZ URL or Node filesystem path; requires Web Worker support. */
export class UsdInputBlock extends Block<typeof UsdInputBlockDefinition> {
public constructor(options?: BlockOptions<typeof UsdInputBlockDefinition>) {
super(UsdInputBlockDefinition, options);
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { GltfInputBlock } from "./blocks/gltfInputBlock";
export { GltfOutputBlock, type GltfOutputBlockOptions } from "./blocks/gltfOutputBlock";
export { ObjInputBlock } from "./blocks/objInputBlock";
export { StlInputBlock } from "./blocks/stlInputBlock";
export { UsdInputBlock } from "./blocks/usdInputBlock";
export { ValidateBlock, type ValidateBlockOptions } from "./blocks/validateBlock";
export { NodeAsset } from "./nodeAsset";
export { NodeAssetContext } from "./nodeAssetContext";
5 changes: 5 additions & 0 deletions packages/core/src/types/assets.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ declare module "virtual:node-assets-basis-encoder-wasm-url" {
const url: string;
export default url;
}

declare module "virtual:node-assets-usd-runtime-urls" {
const urls: { readonly workerUrl: string; readonly glueUrl: string; readonly wasmUrl: string; readonly dataUrl: string };
export default urls;
}
54 changes: 27 additions & 27 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading