@aura3d/assets 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +38 -5
  2. package/AGENTS.md +0 -46
  3. package/src/AdvancedAssetCorpus.ts +0 -454
  4. package/src/AssetBundleCacheFixtures.ts +0 -290
  5. package/src/AssetCache.ts +0 -76
  6. package/src/AssetCompatibility.ts +0 -224
  7. package/src/AssetCorpus.ts +0 -329
  8. package/src/AssetDependencyGraph.ts +0 -83
  9. package/src/AssetHandle.ts +0 -74
  10. package/src/AssetImportPreflight.ts +0 -179
  11. package/src/AssetInspection.ts +0 -706
  12. package/src/AssetLoader.ts +0 -36
  13. package/src/AssetManager.ts +0 -211
  14. package/src/AssetRegistry.ts +0 -44
  15. package/src/AssetRenderDefaults.ts +0 -36
  16. package/src/AudioLoader.ts +0 -41
  17. package/src/BlenderExportValidation.ts +0 -227
  18. package/src/CarConceptMaterialStability.ts +0 -1405
  19. package/src/EXRLoader.ts +0 -1
  20. package/src/ExternalParityGLTFCorpus.ts +0 -65
  21. package/src/GLTFAnimationRuntime.ts +0 -1416
  22. package/src/GLTFAutoFit.ts +0 -133
  23. package/src/GLTFCompressionDecoders.ts +0 -190
  24. package/src/GLTFExtensionSupport.ts +0 -138
  25. package/src/GLTFLoader.ts +0 -4060
  26. package/src/GLTFRenderResources.ts +0 -2270
  27. package/src/HDRLoader.ts +0 -1
  28. package/src/ImageLoader.ts +0 -52
  29. package/src/ImportPipeline.ts +0 -148
  30. package/src/KTX2BasisTextureTranscoder.ts +0 -225
  31. package/src/LoadContext.ts +0 -60
  32. package/src/MaterialLoader.ts +0 -166
  33. package/src/MeshOptimization.ts +0 -93
  34. package/src/OBJLoader.ts +0 -551
  35. package/src/SceneAnalysisFixtures.ts +0 -438
  36. package/src/SceneLoader.ts +0 -62
  37. package/src/ShaderLoader.ts +0 -30
  38. package/src/TextureLoader.ts +0 -30
  39. package/src/TexturePipeline.ts +0 -96
  40. package/src/TextureStreaming.ts +0 -20
  41. package/src/WorkerAssetJobs.ts +0 -56
  42. package/src/advanced-gallery/index.ts +0 -18
  43. package/src/asset-corpus/GLTFSceneLoader.ts +0 -9
  44. package/src/asset-corpus/ProductionAssetCorpus.ts +0 -230
  45. package/src/asset-corpus/ProductionAssetPipeline.ts +0 -5
  46. package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +0 -182
  47. package/src/asset-corpus/ProductionHDRTextureLoader.ts +0 -2
  48. package/src/asset-corpus/ProductionKTX2TextureLoader.ts +0 -2
  49. package/src/asset-corpus/ProductionTextureLoader.ts +0 -2
  50. package/src/asset-corpus/index.ts +0 -29
  51. package/src/browser-index.ts +0 -229
  52. package/src/createRenderableScene.ts +0 -128
  53. package/src/index.ts +0 -325
  54. package/src/loadRenderableAsset.ts +0 -67
  55. package/src/loaders/CubeTextureLoader.ts +0 -8
  56. package/src/loaders/EXRLoader.ts +0 -10
  57. package/src/loaders/HDRLoader.ts +0 -7
  58. package/src/loaders/KTX2Loader.ts +0 -7
  59. package/src/loaders/LoaderDiagnostics.ts +0 -36
  60. package/src/loaders/MTLLoader.ts +0 -20
  61. package/src/loaders/OBJLoader.ts +0 -24
  62. package/src/loaders/TextureLoader.ts +0 -14
  63. package/src/loaders/ThreeCompatGLTFLoader.ts +0 -19
  64. package/src/loaders/index.ts +0 -14
  65. package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +0 -51
  66. package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +0 -61
  67. package/src/threejs-example-parity/index.ts +0 -262
  68. package/tests/assets.test.ts +0 -393
  69. package/tsconfig.json +0 -7
package/src/HDRLoader.ts DELETED
@@ -1 +0,0 @@
1
- export { HDRLoaderThreeCompat as HDRLoader } from "./loaders/HDRLoader";
@@ -1,52 +0,0 @@
1
- import type { AssetLoadRequest, AssetLoader } from "./AssetLoader";
2
- import type { LoadContext } from "./LoadContext";
3
-
4
- export interface ImageAsset {
5
- readonly url: string;
6
- readonly width: number;
7
- readonly height: number;
8
- readonly source: ImageBitmap | HTMLImageElement;
9
- }
10
-
11
- export class ImageLoader implements AssetLoader<ImageAsset> {
12
- readonly type = "image";
13
-
14
- canLoad(request: AssetLoadRequest): boolean {
15
- return /\.(?:png|jpg|jpeg|webp|gif|avif)(?:\?.*)?$/i.test(request.url) || request.url.startsWith("data:image/");
16
- }
17
-
18
- async load(request: AssetLoadRequest, context: LoadContext): Promise<ImageAsset> {
19
- context.throwIfAborted(request.url);
20
-
21
- if (typeof createImageBitmap === "function" && typeof fetch === "function") {
22
- const response = await fetch(request.url, { signal: request.signal });
23
- if (!response.ok) {
24
- throw new Error(`Image request failed with ${response.status}`);
25
- }
26
-
27
- const bitmap = await createImageBitmap(await response.blob());
28
- return { url: request.url, width: bitmap.width, height: bitmap.height, source: bitmap };
29
- }
30
-
31
- const ImageCtor = globalThis.Image;
32
- if (!ImageCtor) {
33
- throw new Error("Image loading requires createImageBitmap or HTMLImageElement");
34
- }
35
-
36
- return new Promise<ImageAsset>((resolve, reject) => {
37
- const image = new ImageCtor();
38
- image.onload = () => resolve({ url: request.url, width: image.width, height: image.height, source: image });
39
- image.onerror = () => reject(new Error(`Image decode failed for ${request.url}`));
40
- request.signal?.addEventListener("abort", () => reject(new Error(`Image load aborted for ${request.url}`)), {
41
- once: true
42
- });
43
- image.src = request.url;
44
- });
45
- }
46
-
47
- dispose(asset: ImageAsset): void {
48
- if ("close" in asset.source && typeof asset.source.close === "function") {
49
- asset.source.close();
50
- }
51
- }
52
- }
@@ -1,148 +0,0 @@
1
- export interface ImportPipelineContext {
2
- readonly url: string;
3
- readonly signal?: AbortSignal;
4
- readonly onProgress?: (event: ImportPipelineProgressEvent) => void;
5
- }
6
-
7
- export interface ImportStage<TInput = unknown, TOutput = TInput> {
8
- readonly name: string;
9
- canRun?(input: TInput, context: ImportPipelineContext): Promise<boolean> | boolean;
10
- run(input: TInput, context: ImportPipelineContext): Promise<TOutput> | TOutput;
11
- rollback?(output: TOutput, context: ImportPipelineContext): Promise<void> | void;
12
- }
13
-
14
- export type ImportPipelineProgressStatus = "started" | "skipped" | "completed" | "rollback-started" | "rollback-completed";
15
-
16
- export interface ImportPipelineProgressEvent {
17
- readonly url: string;
18
- readonly stageName: string;
19
- readonly status: ImportPipelineProgressStatus;
20
- }
21
-
22
- export interface ImportPipelineErrorOptions {
23
- readonly url: string;
24
- readonly stageName?: string;
25
- readonly cause?: unknown;
26
- }
27
-
28
- export class ImportPipelineError extends Error {
29
- readonly url: string;
30
- readonly stageName?: string;
31
- override readonly cause?: unknown;
32
-
33
- constructor(message: string, options: ImportPipelineErrorOptions) {
34
- super(message);
35
- this.name = "ImportPipelineError";
36
- this.url = options.url;
37
- this.stageName = options.stageName;
38
- this.cause = options.cause;
39
- }
40
- }
41
-
42
- export class ImportPipeline {
43
- private readonly stages: ImportStage[] = [];
44
-
45
- addStage<TInput, TOutput>(stage: ImportStage<TInput, TOutput>): this {
46
- if (this.stages.some((existing) => existing.name === stage.name)) {
47
- throw new Error(`Import stage already exists: ${stage.name}`);
48
- }
49
- this.stages.push(stage as ImportStage);
50
- return this;
51
- }
52
-
53
- async run<TInput, TOutput = unknown>(input: TInput, context: ImportPipelineContext): Promise<TOutput> {
54
- const completed: Array<{ stage: ImportStage; output: unknown }> = [];
55
- let current: unknown = input;
56
-
57
- try {
58
- for (const stage of this.stages) {
59
- this.throwIfAborted(context, stage.name);
60
-
61
- if (stage.canRun && !(await this.withAbort(stage.canRun(current, context), context, stage.name))) {
62
- this.report(context, stage.name, "skipped");
63
- continue;
64
- }
65
-
66
- this.report(context, stage.name, "started");
67
- current = await this.withAbort(stage.run(current, context), context, stage.name);
68
- completed.push({ stage, output: current });
69
- this.throwIfAborted(context, stage.name);
70
- this.report(context, stage.name, "completed");
71
- }
72
- return current as TOutput;
73
- } catch (error) {
74
- for (const entry of completed.reverse()) {
75
- if (!entry.stage.rollback) {
76
- continue;
77
- }
78
- this.report(context, entry.stage.name, "rollback-started");
79
- try {
80
- await this.withAbort(entry.stage.rollback(entry.output, context), context, entry.stage.name);
81
- this.report(context, entry.stage.name, "rollback-completed");
82
- } catch (rollbackError) {
83
- throw new ImportPipelineError(`Import pipeline rollback failed at stage ${entry.stage.name} for ${context.url}`, {
84
- url: context.url,
85
- stageName: entry.stage.name,
86
- cause: rollbackError
87
- });
88
- }
89
- }
90
- throw this.toPipelineError(error, context);
91
- }
92
- }
93
-
94
- private async withAbort<T>(operation: Promise<T> | T, context: ImportPipelineContext, stageName: string): Promise<T> {
95
- if (!context.signal) {
96
- return operation;
97
- }
98
- this.throwIfAborted(context, stageName);
99
-
100
- return new Promise<T>((resolve, reject) => {
101
- const abort = () => reject(this.createAbortError(context, stageName));
102
- context.signal?.addEventListener("abort", abort, { once: true });
103
- Promise.resolve(operation).then(
104
- (value) => {
105
- context.signal?.removeEventListener("abort", abort);
106
- resolve(value);
107
- },
108
- (error) => {
109
- context.signal?.removeEventListener("abort", abort);
110
- reject(error);
111
- }
112
- );
113
- });
114
- }
115
-
116
- private throwIfAborted(context: ImportPipelineContext, stageName?: string): void {
117
- if (context.signal?.aborted) {
118
- throw this.createAbortError(context, stageName);
119
- }
120
- }
121
-
122
- private createAbortError(context: ImportPipelineContext, stageName?: string): ImportPipelineError {
123
- return new ImportPipelineError(
124
- stageName ? `Import pipeline aborted at stage ${stageName} for ${context.url}` : `Import pipeline aborted for ${context.url}`,
125
- { url: context.url, stageName, cause: context.signal?.reason }
126
- );
127
- }
128
-
129
- private toPipelineError(error: unknown, context: ImportPipelineContext): Error {
130
- if (error instanceof ImportPipelineError) {
131
- return error;
132
- }
133
- if (error instanceof Error) {
134
- return new ImportPipelineError(`Import pipeline failed for ${context.url}: ${error.message}`, {
135
- url: context.url,
136
- cause: error
137
- });
138
- }
139
- return new ImportPipelineError(`Import pipeline failed for ${context.url}: ${String(error)}`, {
140
- url: context.url,
141
- cause: error
142
- });
143
- }
144
-
145
- private report(context: ImportPipelineContext, stageName: string, status: ImportPipelineProgressStatus): void {
146
- context.onProgress?.({ url: context.url, stageName, status });
147
- }
148
- }
@@ -1,225 +0,0 @@
1
- import type { TextureCompressedFormat, TextureMipLevelDescriptor } from "@aura3d/rendering";
2
- import type { DecodedGLTFImage } from "./GLTFRenderResources";
3
-
4
- export type KTX2BasisTargetFormat = "etc2-rgba8unorm" | "bc3-rgba-unorm" | "astc-4x4-rgba-unorm" | "rgba8";
5
-
6
- export interface KTX2BasisTextureTranscoderOptions {
7
- readonly targetFormat?: KTX2BasisTargetFormat;
8
- readonly includeFallback?: boolean;
9
- readonly loaderOptions?: Record<string, unknown>;
10
- }
11
-
12
- interface LoadersGLTextureLevel {
13
- readonly width: number;
14
- readonly height: number;
15
- readonly data: Uint8Array;
16
- readonly compressed?: boolean;
17
- readonly textureFormat?: string;
18
- }
19
-
20
- type LoadersGLTextureResult = readonly (readonly LoadersGLTextureLevel[])[];
21
-
22
- const DEFAULT_BROWSER_CDN = "https://unpkg.com/@loaders.gl";
23
- let nodeFileHooksReady = false;
24
-
25
- export async function transcodeKTX2BasisTexture(
26
- bytes: ArrayBuffer | ArrayBufferView,
27
- options: KTX2BasisTextureTranscoderOptions = {}
28
- ): Promise<DecodedGLTFImage> {
29
- const source = toArrayBuffer(bytes);
30
- const targetFormat = options.targetFormat ?? "etc2-rgba8unorm";
31
- const compressedLevels = await parseTextureLevels(source, targetFormat, options.loaderOptions);
32
- const baseLevel = compressedLevels[0];
33
- if (!baseLevel) {
34
- throw new Error("KTX2/Basis transcode produced no texture levels");
35
- }
36
-
37
- if (targetFormat === "rgba8") {
38
- return {
39
- width: baseLevel.width,
40
- height: baseLevel.height,
41
- colorSpace: "linear",
42
- format: "rgba8",
43
- data: baseLevel.data
44
- };
45
- }
46
-
47
- const fallbackLevels = options.includeFallback === false
48
- ? []
49
- : validRGBA8Levels(await parseTextureLevels(source, "rgba8", options.loaderOptions));
50
- const format = mapTargetFormat(targetFormat);
51
-
52
- return {
53
- width: baseLevel.width,
54
- height: baseLevel.height,
55
- colorSpace: "linear",
56
- format,
57
- mipLevels: compressedLevels,
58
- fallbackMipLevels: fallbackLevels
59
- };
60
- }
61
-
62
- function validRGBA8Levels(levels: readonly TextureMipLevelDescriptor[]): readonly TextureMipLevelDescriptor[] {
63
- const validLevels: TextureMipLevelDescriptor[] = [];
64
- for (const level of levels) {
65
- if (level.data.byteLength !== level.width * level.height * 4) break;
66
- validLevels.push(level);
67
- }
68
- return validLevels;
69
- }
70
-
71
- async function parseTextureLevels(
72
- source: ArrayBuffer,
73
- targetFormat: KTX2BasisTargetFormat,
74
- loaderOptions: Record<string, unknown> | undefined
75
- ): Promise<readonly TextureMipLevelDescriptor[]> {
76
- await installNodeLoadersGLFileHooks();
77
- const { parse, BasisLoader } = await loadLoadersGLModules();
78
- const parsed = await parse(source.slice(0), BasisLoader, {
79
- // worker:false is required: worker mode resolves transcoder module URLs to dev-only /node_modules paths and the Node file hooks on globalThis.loaders only exist on the main thread.
80
- worker: false,
81
- CDN: defaultLoadersGLCdn(),
82
- modules: defaultLoadersGLModules(),
83
- ...loaderOptions,
84
- ...(loaderOptions?.modules && typeof loaderOptions.modules === "object"
85
- ? { modules: { ...defaultLoadersGLModules(), ...(loaderOptions.modules as Record<string, unknown>) } }
86
- : {}),
87
- basis: {
88
- ...basisOptions(loaderOptions),
89
- containerFormat: "ktx2",
90
- module: "encoder",
91
- format: loadersGLBasisFormat(targetFormat)
92
- }
93
- }) as LoadersGLTextureResult;
94
- const levels = parsed[0];
95
- if (!levels || levels.length === 0) {
96
- throw new Error("KTX2/Basis transcode returned no texture levels");
97
- }
98
- return levels.map((level) => ({
99
- width: level.width,
100
- height: level.height,
101
- data: new Uint8Array(level.data)
102
- }));
103
- }
104
-
105
- function defaultLoadersGLCdn(): string {
106
- if (typeof window !== "undefined" && window.location?.origin) {
107
- return `${window.location.origin}/node_modules/@loaders.gl`;
108
- }
109
- return DEFAULT_BROWSER_CDN;
110
- }
111
-
112
- function defaultLoadersGLModules(): Record<string, string> | undefined {
113
- if (typeof window === "undefined" || !window.location?.origin) return undefined;
114
- const baseUrl = `${window.location.origin}/node_modules/@loaders.gl/textures/dist/libs`;
115
- return {
116
- "basis_encoder.js": `${baseUrl}/basis_encoder.js`,
117
- "basis_encoder.wasm": `${baseUrl}/basis_encoder.wasm`,
118
- "basis_transcoder.js": `${baseUrl}/basis_transcoder.js`,
119
- "basis_transcoder.wasm": `${baseUrl}/basis_transcoder.wasm`
120
- };
121
- }
122
-
123
- async function loadLoadersGLModules(): Promise<{ readonly parse: typeof import("@loaders.gl/core").parse; readonly BasisLoader: typeof import("@loaders.gl/textures").BasisLoader }> {
124
- try {
125
- const [core, textures] = await Promise.all([
126
- import("@loaders.gl/core"),
127
- import("@loaders.gl/textures")
128
- ]);
129
- return { parse: core.parse, BasisLoader: textures.BasisLoader };
130
- } catch (error) {
131
- throw new Error(`KTX2/Basis transcoder dependency resolution failed: ${error instanceof Error ? error.message : String(error)}`);
132
- }
133
- }
134
-
135
- function basisOptions(loaderOptions: Record<string, unknown> | undefined): Record<string, unknown> {
136
- const basis = loaderOptions?.basis;
137
- return basis && typeof basis === "object" ? basis as Record<string, unknown> : {};
138
- }
139
-
140
- function loadersGLBasisFormat(format: KTX2BasisTargetFormat): string {
141
- switch (format) {
142
- case "etc2-rgba8unorm":
143
- return "etc2";
144
- case "bc3-rgba-unorm":
145
- return "bc3";
146
- case "astc-4x4-rgba-unorm":
147
- return "astc-4x4";
148
- case "rgba8":
149
- return "rgba32";
150
- }
151
- }
152
-
153
- function mapTargetFormat(format: KTX2BasisTargetFormat): TextureCompressedFormat {
154
- switch (format) {
155
- case "etc2-rgba8unorm":
156
- case "bc3-rgba-unorm":
157
- case "astc-4x4-rgba-unorm":
158
- return format;
159
- case "rgba8":
160
- throw new Error("rgba8 is not a compressed texture target");
161
- }
162
- }
163
-
164
- function toArrayBuffer(bytes: ArrayBuffer | ArrayBufferView): ArrayBuffer {
165
- if (bytes instanceof ArrayBuffer) return bytes.slice(0);
166
- const copy = new Uint8Array(bytes.byteLength);
167
- copy.set(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength));
168
- return copy.buffer;
169
- }
170
-
171
- async function installNodeLoadersGLFileHooks(): Promise<void> {
172
- if (nodeFileHooksReady || typeof process === "undefined" || !process.versions?.node) return;
173
- const existing = (globalThis as typeof globalThis & {
174
- loaders?: {
175
- requireFromFile?: (path: string) => unknown;
176
- readFileAsArrayBuffer?: (path: string) => Promise<ArrayBuffer>;
177
- };
178
- }).loaders ?? {};
179
- if (existing.requireFromFile && existing.readFileAsArrayBuffer) {
180
- nodeFileHooksReady = true;
181
- return;
182
- }
183
-
184
- const [{ readFile }, { dirname, join }, { createRequire }, vm] = await Promise.all([
185
- importNodeModule<typeof import("node:fs/promises")>("node:fs/promises"),
186
- importNodeModule<typeof import("node:path")>("node:path"),
187
- importNodeModule<typeof import("node:module")>("node:module"),
188
- importNodeModule<typeof import("node:vm")>("node:vm")
189
- ]);
190
- const require = createRequire(import.meta.url);
191
- const texturesRoot = dirname(dirname(require.resolve("@loaders.gl/textures")));
192
- const moduleCache = new Map<string, unknown>();
193
- const resolveLibraryPath = (libraryPath: string): string => {
194
- if (libraryPath.startsWith("modules/textures/")) {
195
- return join(texturesRoot, libraryPath.slice("modules/textures/".length));
196
- }
197
- return libraryPath;
198
- };
199
-
200
- (globalThis as typeof globalThis & { loaders?: typeof existing }).loaders = {
201
- ...existing,
202
- async requireFromFile(libraryPath: string): Promise<unknown> {
203
- const resolved = resolveLibraryPath(libraryPath);
204
- if (moduleCache.has(resolved)) return moduleCache.get(resolved);
205
- const code = await readFile(resolved, "utf8");
206
- const module = { exports: {} as unknown };
207
- const wrapper = vm.runInNewContext(
208
- `(function(exports,module,require,__dirname,__filename){${code}\n})`,
209
- { Buffer, WebAssembly, clearTimeout, console, process, setTimeout, TextDecoder, TextEncoder }
210
- ) as (exports: unknown, module: { exports: unknown }, require: NodeJS.Require, dirname: string, filename: string) => void;
211
- wrapper(module.exports, module, require, dirname(resolved), resolved);
212
- moduleCache.set(resolved, module.exports);
213
- return module.exports;
214
- },
215
- async readFileAsArrayBuffer(libraryPath: string): Promise<ArrayBuffer> {
216
- const buffer = await readFile(resolveLibraryPath(libraryPath));
217
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
218
- }
219
- };
220
- nodeFileHooksReady = true;
221
- }
222
-
223
- async function importNodeModule<T>(specifier: string): Promise<T> {
224
- return import(/* @vite-ignore */ specifier) as Promise<T>;
225
- }
@@ -1,60 +0,0 @@
1
- import { AssetLoadError } from "./AssetLoader";
2
- import type { AssetHandle } from "./AssetHandle";
3
- import type { AssetManager } from "./AssetManager";
4
-
5
- export interface LoadContextOptions {
6
- readonly baseUrl?: string;
7
- readonly signal?: AbortSignal;
8
- readonly dependencyChain?: readonly string[];
9
- readonly manager?: AssetManager;
10
- }
11
-
12
- export class LoadContext {
13
- readonly baseUrl: string;
14
- readonly signal?: AbortSignal;
15
- readonly dependencyChain: readonly string[];
16
-
17
- private readonly manager?: AssetManager;
18
-
19
- constructor(options: LoadContextOptions = {}) {
20
- this.baseUrl = options.baseUrl ?? "";
21
- this.signal = options.signal;
22
- this.dependencyChain = options.dependencyChain ?? [];
23
- this.manager = options.manager;
24
- }
25
-
26
- resolve(url: string): string {
27
- if (/^(?:[a-z]+:)?\/\//i.test(url) || url.startsWith("data:") || url.startsWith("blob:")) {
28
- return url;
29
- }
30
-
31
- if (this.baseUrl.length === 0) {
32
- return url;
33
- }
34
-
35
- return new URL(url, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`).toString();
36
- }
37
-
38
- throwIfAborted(url: string): void {
39
- if (this.signal?.aborted) {
40
- throw new AssetLoadError("Asset load aborted", url, this.dependencyChain, this.signal.reason);
41
- }
42
- }
43
-
44
- child(url: string): LoadContext {
45
- return new LoadContext({
46
- baseUrl: this.baseUrl,
47
- signal: this.signal,
48
- manager: this.manager,
49
- dependencyChain: [...this.dependencyChain, url]
50
- });
51
- }
52
-
53
- async loadDependency<T = unknown>(url: string, type?: string): Promise<AssetHandle<T>> {
54
- if (!this.manager) {
55
- throw new AssetLoadError("Cannot load dependency without an AssetManager", url, this.dependencyChain);
56
- }
57
-
58
- return this.manager.load<T>(url, { type, signal: this.signal, dependencyChain: this.dependencyChain });
59
- }
60
- }
@@ -1,166 +0,0 @@
1
- import type { AssetLoadRequest, AssetLoader } from "./AssetLoader";
2
- import type { LoadContext } from "./LoadContext";
3
- import {
4
- Material,
5
- PBRMaterial,
6
- UnlitMaterial,
7
- type PBRMaterialOptions,
8
- type RenderState,
9
- type UnlitMaterialOptions
10
- } from "@aura3d/rendering";
11
-
12
- export interface MaterialDescriptorAsset {
13
- readonly url: string;
14
- readonly name?: string;
15
- readonly model: "unlit" | "pbr";
16
- readonly properties: Readonly<Record<string, unknown>>;
17
- createMaterial(): Material;
18
- }
19
-
20
- export class MaterialLoader implements AssetLoader<MaterialDescriptorAsset> {
21
- readonly type = "material";
22
-
23
- canLoad(request: AssetLoadRequest): boolean {
24
- return /\.(?:mat|material)\.json(?:\?.*)?$/i.test(request.url);
25
- }
26
-
27
- async load(request: AssetLoadRequest, context: LoadContext): Promise<MaterialDescriptorAsset> {
28
- context.throwIfAborted(request.url);
29
-
30
- if (typeof fetch !== "function") {
31
- throw new Error("MaterialLoader requires fetch");
32
- }
33
-
34
- const response = await fetch(request.url, { signal: request.signal });
35
- if (!response.ok) {
36
- throw new Error(`Material request failed with ${response.status}`);
37
- }
38
-
39
- const parsed = (await response.json()) as Partial<MaterialDescriptorAsset>;
40
- if (parsed.model !== "unlit" && parsed.model !== "pbr") {
41
- throw new Error(`Unsupported material model for ${request.url}`);
42
- }
43
-
44
- const descriptor = {
45
- url: request.url,
46
- name: parsed.name,
47
- model: parsed.model,
48
- properties: parseProperties(parsed.properties, request.url)
49
- };
50
- return {
51
- ...descriptor,
52
- createMaterial: () => createMaterialFromDescriptor(descriptor)
53
- };
54
- }
55
- }
56
-
57
- export function createMaterialFromDescriptor(descriptor: Pick<MaterialDescriptorAsset, "name" | "model" | "properties">): Material {
58
- if (descriptor.model === "unlit") {
59
- return new UnlitMaterial(toUnlitOptions(descriptor));
60
- }
61
- return new PBRMaterial(toPBRMaterialOptions(descriptor));
62
- }
63
-
64
- function toUnlitOptions(descriptor: Pick<MaterialDescriptorAsset, "name" | "properties">): UnlitMaterialOptions {
65
- const color = readVec4(descriptor.properties, ["color", "baseColor", "baseColorFactor"]);
66
- return {
67
- name: descriptor.name,
68
- color: color ?? undefined,
69
- renderState: readRenderState(descriptor.properties)
70
- };
71
- }
72
-
73
- function toPBRMaterialOptions(descriptor: Pick<MaterialDescriptorAsset, "name" | "properties">): PBRMaterialOptions {
74
- const properties = descriptor.properties;
75
- return {
76
- name: descriptor.name,
77
- baseColor: readVec4(properties, ["baseColor", "baseColorFactor"]) ?? undefined,
78
- metallic: readNumber(properties, ["metallic", "metallicFactor"]),
79
- roughness: readNumber(properties, ["roughness", "roughnessFactor"]),
80
- emissiveColor: readVec3(properties, ["emissiveColor", "emissiveFactor"]) ?? undefined,
81
- emissiveStrength: readNumber(properties, ["emissiveStrength"]),
82
- clearcoatFactor: readNumber(properties, ["clearcoatFactor"]),
83
- clearcoatRoughnessFactor: readNumber(properties, ["clearcoatRoughnessFactor"]),
84
- transmissionFactor: readNumber(properties, ["transmissionFactor"]),
85
- diffuseTransmissionFactor: readNumber(properties, ["diffuseTransmissionFactor"]),
86
- diffuseTransmissionColorFactor: readVec3(properties, ["diffuseTransmissionColorFactor"]) ?? undefined,
87
- volumeThicknessFactor: readNumber(properties, ["volumeThicknessFactor"]),
88
- volumeAttenuationDistance: readNumber(properties, ["volumeAttenuationDistance"]),
89
- volumeAttenuationColor: readVec3(properties, ["volumeAttenuationColor"]) ?? undefined,
90
- ior: readNumber(properties, ["ior"]),
91
- specularFactor: readNumber(properties, ["specularFactor"]),
92
- specularColorFactor: readVec3(properties, ["specularColorFactor"]) ?? undefined,
93
- sheenColorFactor: readVec3(properties, ["sheenColorFactor"]) ?? undefined,
94
- sheenRoughnessFactor: readNumber(properties, ["sheenRoughnessFactor"]),
95
- anisotropyStrength: readNumber(properties, ["anisotropyStrength"]),
96
- anisotropyRotation: readNumber(properties, ["anisotropyRotation"]),
97
- iridescenceFactor: readNumber(properties, ["iridescenceFactor"]),
98
- iridescenceIor: readNumber(properties, ["iridescenceIor"]),
99
- iridescenceThicknessMinimum: readNumber(properties, ["iridescenceThicknessMinimum"]),
100
- iridescenceThicknessMaximum: readNumber(properties, ["iridescenceThicknessMaximum"]),
101
- dispersion: readNumber(properties, ["dispersion"]),
102
- renderState: readRenderState(properties)
103
- };
104
- }
105
-
106
- function parseProperties(value: unknown, url: string): Readonly<Record<string, unknown>> {
107
- if (value === undefined) return {};
108
- if (!isRecord(value) || Array.isArray(value)) {
109
- throw new Error(`Material properties for ${url} must be an object`);
110
- }
111
- return value;
112
- }
113
-
114
- function readNumber(properties: Readonly<Record<string, unknown>>, keys: readonly string[]): number | undefined {
115
- const value = readFirst(properties, keys);
116
- if (value === undefined) return undefined;
117
- if (typeof value !== "number") {
118
- throw new Error(`Material property ${keys[0]} must be a number`);
119
- }
120
- return value;
121
- }
122
-
123
- function readVec3(properties: Readonly<Record<string, unknown>>, keys: readonly string[]): readonly [number, number, number] | undefined {
124
- const value = readFirst(properties, keys);
125
- if (value === undefined) return undefined;
126
- if (!Array.isArray(value) || value.length !== 3) {
127
- throw new Error(`Material property ${keys[0]} must be a vec3`);
128
- }
129
- return [readFinite(value[0], keys[0]), readFinite(value[1], keys[0]), readFinite(value[2], keys[0])];
130
- }
131
-
132
- function readVec4(properties: Readonly<Record<string, unknown>>, keys: readonly string[]): readonly [number, number, number, number] | undefined {
133
- const value = readFirst(properties, keys);
134
- if (value === undefined) return undefined;
135
- if (!Array.isArray(value) || value.length !== 4) {
136
- throw new Error(`Material property ${keys[0]} must be a vec4`);
137
- }
138
- return [readFinite(value[0], keys[0]), readFinite(value[1], keys[0]), readFinite(value[2], keys[0]), readFinite(value[3], keys[0])];
139
- }
140
-
141
- function readRenderState(properties: Readonly<Record<string, unknown>>): Partial<RenderState> | undefined {
142
- const value = properties.renderState;
143
- if (value === undefined) return undefined;
144
- if (!isRecord(value) || Array.isArray(value)) {
145
- throw new Error("Material property renderState must be an object");
146
- }
147
- return value as Partial<RenderState>;
148
- }
149
-
150
- function readFirst(properties: Readonly<Record<string, unknown>>, keys: readonly string[]): unknown {
151
- for (const key of keys) {
152
- if (Object.hasOwn(properties, key)) return properties[key];
153
- }
154
- return undefined;
155
- }
156
-
157
- function readFinite(value: unknown, label: string): number {
158
- if (typeof value !== "number" || !Number.isFinite(value)) {
159
- throw new Error(`Material property ${label} must contain finite numbers`);
160
- }
161
- return value;
162
- }
163
-
164
- function isRecord(value: unknown): value is Record<string, unknown> {
165
- return typeof value === "object" && value !== null;
166
- }