babylon.quarks 0.20.0 → 0.23.0
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.
- package/CHANGELOG.md +105 -0
- package/README.md +2 -0
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.ts +523 -43
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +59 -2
- package/src/ParticleSystem.ts +388 -59
- package/src/QuarksLoader.ts +381 -56
- package/src/SpriteBatch.ts +388 -111
- package/src/TrailBatch.ts +79 -63
- package/src/VFXBatch.ts +101 -0
- package/src/index.ts +9 -3
- package/src/shaders/particle_physics_frag.glsl.ts +26 -1
- package/src/shaders/particle_physics_frag.wgsl.ts +26 -1
package/src/QuarksLoader.ts
CHANGED
|
@@ -12,6 +12,20 @@ import {ParticleSystem} from './ParticleSystem';
|
|
|
12
12
|
import {QuarksPrefab} from './QuarksPrefab';
|
|
13
13
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
14
14
|
|
|
15
|
+
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
16
|
+
export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hooks for resolving relative source links (textures, geometry files) and loading their JSON.
|
|
20
|
+
* Omit either method to keep the default (`baseUrl` join / `fetch`).
|
|
21
|
+
*/
|
|
22
|
+
export interface QuarksResourceResolver {
|
|
23
|
+
/** Resolve a relative url to an absolute / blob / data url. Default: {@link QuarksLoader.joinUrl}. */
|
|
24
|
+
resolve?(url: string, kind: QuarksResourceKind, baseUrl: string): string | Promise<string>;
|
|
25
|
+
/** Load a JSON asset (geometry files). Default: `fetch(resolvedUrl).json()`. */
|
|
26
|
+
fetchJson?(resolvedUrl: string): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export interface QuarksLoaderOptions {
|
|
16
30
|
baseUrl?: string;
|
|
17
31
|
/**
|
|
@@ -23,6 +37,38 @@ export interface QuarksLoaderOptions {
|
|
|
23
37
|
* change one effect's texture settings without touching the others.
|
|
24
38
|
*/
|
|
25
39
|
shareResources?: boolean;
|
|
40
|
+
/** Custom url / fetch backend (File picker, CDN, IndexedDB). */
|
|
41
|
+
resolver?: QuarksResourceResolver;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One in-memory effect for {@link QuarksLoader.loadFromJSONAll}. */
|
|
45
|
+
export interface QuarksJSONLoadEntry {
|
|
46
|
+
json: unknown;
|
|
47
|
+
/** Overrides loader `baseUrl` for relative texture / geometry links in this effect. */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for {@link QuarksLoader.loadAll} / {@link QuarksLoader.loadFromJSONAll}.
|
|
53
|
+
* Batch loads share the per-scene fetch cache and `shareResources`, so N effects that point at
|
|
54
|
+
* the same `assets/geometries/*.json` or texture only pay for that asset once.
|
|
55
|
+
*/
|
|
56
|
+
export interface QuarksBatchLoadOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Max concurrent effect-document fetches (phase 1). Linked geometry/image resolve still runs
|
|
59
|
+
* across the whole pack in parallel so shared urls coalesce. Default: all at once.
|
|
60
|
+
*/
|
|
61
|
+
concurrency?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Called once per effect when it has fully settled (parsed successfully, or failed under
|
|
64
|
+
* `continueOnError`). `done` is how many of `total` have settled so far.
|
|
65
|
+
*/
|
|
66
|
+
onProgress?: (done: number, total: number) => void;
|
|
67
|
+
/**
|
|
68
|
+
* When true, a failed effect becomes `null` in the result array and the rest keep loading.
|
|
69
|
+
* When false (default), the first failure rejects the whole batch.
|
|
70
|
+
*/
|
|
71
|
+
continueOnError?: boolean;
|
|
26
72
|
}
|
|
27
73
|
|
|
28
74
|
interface ParsedGeometry {
|
|
@@ -35,6 +81,8 @@ interface ParsedGeometry {
|
|
|
35
81
|
/** Textures and geometries reused across loads, per scene. See QuarksLoaderOptions.shareResources. */
|
|
36
82
|
const sharedTextures = new WeakMap<Scene, Map<string, Texture>>();
|
|
37
83
|
const sharedGeometries = new WeakMap<Scene, Map<string, ParsedGeometry>>();
|
|
84
|
+
/** In-flight / completed JSON fetches keyed by `json|resolvedUrl`, per scene. */
|
|
85
|
+
const sharedFetches = new WeakMap<Scene, Map<string, Promise<unknown>>>();
|
|
38
86
|
|
|
39
87
|
function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<string, T> {
|
|
40
88
|
let map = maps.get(scene);
|
|
@@ -86,49 +134,131 @@ export class QuarksLoader {
|
|
|
86
134
|
this.options = options;
|
|
87
135
|
}
|
|
88
136
|
|
|
137
|
+
/** Fetches an effect JSON from `url`, resolves linked sources, and parses it. */
|
|
89
138
|
async load(url: string): Promise<TransformNode> {
|
|
90
|
-
const baseUrl = this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
139
|
+
const {json, baseUrl} = await this.fetchEffectDocument(url);
|
|
140
|
+
return this.loadFromJSON(json, baseUrl);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves linked geometry / image urls on `json` (via {@link resolveResources}) then parses.
|
|
145
|
+
* Use when the effect JSON is already in hand (File, CDN payload, editor import).
|
|
146
|
+
*/
|
|
147
|
+
async loadFromJSON(json: any, baseUrl?: string): Promise<TransformNode> {
|
|
148
|
+
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
149
|
+
await this.resolveResources(json, resolvedBase);
|
|
150
|
+
return this.parse(json, resolvedBase);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Loads many effects in parallel for battle / level preload. Phase 1 fetches every effect
|
|
155
|
+
* JSON (optionally concurrency-limited); phase 2 resolves all linked geometries/images across
|
|
156
|
+
* the pack together so shared asset urls hit the fetch cache once; phase 3 parses.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const roots = await loader.loadAll([
|
|
161
|
+
* 'fx/hit.json', 'fx/explosion.json', 'fx/heal.json',
|
|
162
|
+
* ], { concurrency: 6, continueOnError: true });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
loadAll(urls: string[], options?: QuarksBatchLoadOptions & {continueOnError?: false}): Promise<TransformNode[]>;
|
|
166
|
+
loadAll(
|
|
167
|
+
urls: string[],
|
|
168
|
+
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
169
|
+
): Promise<(TransformNode | null)[]>;
|
|
170
|
+
async loadAll(urls: string[], options: QuarksBatchLoadOptions = {}): Promise<(TransformNode | null)[]> {
|
|
171
|
+
const docs = await QuarksLoader.mapPool(urls, options.concurrency, async (url, index) => {
|
|
172
|
+
try {
|
|
173
|
+
return await this.fetchEffectDocument(url);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!options.continueOnError) {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
console.warn(`[QuarksLoader] Failed to fetch effect "${url}" (${index + 1}/${urls.length}):`, err);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
99
181
|
});
|
|
100
|
-
return this.
|
|
182
|
+
return this.loadDocuments(docs, options);
|
|
101
183
|
}
|
|
102
184
|
|
|
103
185
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* `loadGeometry`, and the `positions / indices / uvs / normals` it returns replace the url in
|
|
107
|
-
* place, so `parse()` (which is synchronous) sees an ordinary inline geometry. `load()` does
|
|
108
|
-
* this with `fetch`; call it yourself before `parse()` when the files come from elsewhere.
|
|
109
|
-
* A geometry that fails to load is left as is and warned about.
|
|
186
|
+
* Same two-phase batch as {@link loadAll}, but effect JSON is already in memory (asset pack,
|
|
187
|
+
* IndexedDB, editor). Entries may be bare JSON or `{ json, baseUrl }`.
|
|
110
188
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
189
|
+
loadFromJSONAll(
|
|
190
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
191
|
+
options?: QuarksBatchLoadOptions & {continueOnError?: false}
|
|
192
|
+
): Promise<TransformNode[]>;
|
|
193
|
+
loadFromJSONAll(
|
|
194
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
195
|
+
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
196
|
+
): Promise<(TransformNode | null)[]>;
|
|
197
|
+
async loadFromJSONAll(
|
|
198
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
199
|
+
options: QuarksBatchLoadOptions = {}
|
|
200
|
+
): Promise<(TransformNode | null)[]> {
|
|
201
|
+
const docs: Array<{json: any; baseUrl: string} | null> = entries.map((entry) => {
|
|
202
|
+
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
203
|
+
const typed = entry as QuarksJSONLoadEntry;
|
|
204
|
+
return {json: typed.json as any, baseUrl: typed.baseUrl ?? this.options.baseUrl ?? ''};
|
|
205
|
+
}
|
|
206
|
+
return {json: entry as any, baseUrl: this.options.baseUrl ?? ''};
|
|
207
|
+
});
|
|
208
|
+
return this.loadDocuments(docs, options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
213
|
+
* {@link QuarksResourceResolver}. Idempotent for already-inlined geometries. Failures warn
|
|
214
|
+
* and leave the entry as is.
|
|
215
|
+
*/
|
|
216
|
+
async resolveResources(json: any, baseUrl: string = ''): Promise<void> {
|
|
217
|
+
await QuarksLoader.resolveLinkedResources(json, {
|
|
218
|
+
baseUrl,
|
|
219
|
+
resolver: this.options.resolver,
|
|
220
|
+
scene: this.scene,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
226
|
+
* editor import that only prepares JSON). Pass `scene` to share the per-scene fetch cache.
|
|
227
|
+
*/
|
|
228
|
+
static async resolveLinkedResources(
|
|
229
|
+
json: any,
|
|
230
|
+
options: {baseUrl?: string; resolver?: QuarksResourceResolver; scene?: Scene} = {}
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const baseUrl = options.baseUrl ?? '';
|
|
233
|
+
const resolver = options.resolver;
|
|
234
|
+
const scene = options.scene;
|
|
235
|
+
|
|
236
|
+
await QuarksLoader.inlineGeometryEntries(json, async (geometryUrl) => {
|
|
237
|
+
const resolved = await QuarksLoader.resolveWith(resolver, geometryUrl, 'geometry', baseUrl);
|
|
238
|
+
return QuarksLoader.fetchJsonWith(resolver, resolved, scene);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
113
242
|
await Promise.all(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (data?.[key] !== undefined) {
|
|
121
|
-
geom[key] = data[key];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
delete geom.url;
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
127
|
-
}
|
|
128
|
-
})
|
|
243
|
+
images.map(async (img) => {
|
|
244
|
+
if (typeof img?.url !== 'string' || !img.url) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
img.url = await QuarksLoader.resolveWith(resolver, img.url, 'image', baseUrl);
|
|
248
|
+
})
|
|
129
249
|
);
|
|
130
250
|
}
|
|
131
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Inlines geometries that an export wrote to their own file (`{uuid, type: 'QuarksGeometry',
|
|
254
|
+
* url}`). Prefer {@link load}, {@link loadFromJSON}, or {@link resolveResources} — those use
|
|
255
|
+
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
256
|
+
* `loadGeometry` and no `QuarksLoader` instance.
|
|
257
|
+
*/
|
|
258
|
+
static async resolveExternalGeometries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void> {
|
|
259
|
+
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
260
|
+
}
|
|
261
|
+
|
|
132
262
|
parse(json: any, baseUrl: string = ''): TransformNode {
|
|
133
263
|
const meta: LoadedMeta = {
|
|
134
264
|
textures: {},
|
|
@@ -175,8 +305,8 @@ export class QuarksLoader {
|
|
|
175
305
|
} else if (geom.type === 'QuarksGeometry') {
|
|
176
306
|
if (typeof geom.url === 'string' && !geom.positions) {
|
|
177
307
|
console.warn(
|
|
178
|
-
`[QuarksLoader] Geometry "${geom.url}" is an external file; use load()
|
|
179
|
-
'
|
|
308
|
+
`[QuarksLoader] Geometry "${geom.url}" is an external file; use load(), ` +
|
|
309
|
+
'loadFromJSON(), or resolveResources() before parse().'
|
|
180
310
|
);
|
|
181
311
|
}
|
|
182
312
|
meta.geometries[geom.uuid] = this.share({
|
|
@@ -205,13 +335,22 @@ export class QuarksLoader {
|
|
|
205
335
|
}
|
|
206
336
|
|
|
207
337
|
/**
|
|
208
|
-
* Forgets the textures and
|
|
209
|
-
* {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
210
|
-
* load creates new ones. Textures are not disposed — they may still be in use.
|
|
338
|
+
* Forgets the textures, geometries, and in-flight JSON fetches shared between loads into
|
|
339
|
+
* `scene` (see {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
340
|
+
* theirs; the next load creates new ones. Textures are not disposed — they may still be in use.
|
|
211
341
|
*/
|
|
212
342
|
static clearSharedResources(scene: Scene): void {
|
|
213
343
|
sharedTextures.delete(scene);
|
|
214
344
|
sharedGeometries.delete(scene);
|
|
345
|
+
sharedFetches.delete(scene);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Joins `baseUrl` with a relative `url`; leaves data/blob/absolute/root-absolute urls alone. */
|
|
349
|
+
static joinUrl(baseUrl: string, url: string): string {
|
|
350
|
+
if (/^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/')) {
|
|
351
|
+
return url;
|
|
352
|
+
}
|
|
353
|
+
return baseUrl + url;
|
|
215
354
|
}
|
|
216
355
|
|
|
217
356
|
private parseSphereGeometry(geom: any): ParsedGeometry {
|
|
@@ -366,13 +505,191 @@ export class QuarksLoader {
|
|
|
366
505
|
return new Float32Array(buffer) as any;
|
|
367
506
|
}
|
|
368
507
|
|
|
508
|
+
private async resolveResourceUrl(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
509
|
+
return QuarksLoader.resolveWith(this.options.resolver, url, kind, baseUrl);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
513
|
+
private async fetchEffectDocument(url: string): Promise<{json: any; baseUrl: string}> {
|
|
514
|
+
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
515
|
+
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
516
|
+
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined);
|
|
517
|
+
return {json, baseUrl};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Resolve every document's linked resources in parallel (shared urls coalesce via the
|
|
522
|
+
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
523
|
+
*/
|
|
524
|
+
private async loadDocuments(
|
|
525
|
+
docs: Array<{json: any; baseUrl: string} | null>,
|
|
526
|
+
options: QuarksBatchLoadOptions
|
|
527
|
+
): Promise<(TransformNode | null)[]> {
|
|
528
|
+
const total = docs.length;
|
|
529
|
+
if (total === 0) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const results: (TransformNode | null)[] = new Array(total);
|
|
534
|
+
const settled = new Array<boolean>(total).fill(false);
|
|
535
|
+
let done = 0;
|
|
536
|
+
const settle = (index: number, value: TransformNode | null) => {
|
|
537
|
+
if (settled[index]) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
settled[index] = true;
|
|
541
|
+
results[index] = value;
|
|
542
|
+
done++;
|
|
543
|
+
options.onProgress?.(done, total);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
await Promise.all(
|
|
547
|
+
docs.map(async (doc, i) => {
|
|
548
|
+
if (!doc) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await this.resolveResources(doc.json, doc.baseUrl);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
if (!options.continueOnError) {
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
console.warn(`[QuarksLoader] Failed to resolve resources for effect ${i + 1}/${total}:`, err);
|
|
558
|
+
docs[i] = null;
|
|
559
|
+
settle(i, null);
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < total; i++) {
|
|
565
|
+
if (settled[i]) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const doc = docs[i];
|
|
569
|
+
if (!doc) {
|
|
570
|
+
settle(i, null);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
settle(i, this.parse(doc.json, doc.baseUrl));
|
|
575
|
+
} catch (err) {
|
|
576
|
+
if (!options.continueOnError) {
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
580
|
+
settle(i, null);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return results;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Runs `fn` over `items` with at most `concurrency` in flight (or all if unset/≤0). */
|
|
587
|
+
private static async mapPool<T, R>(
|
|
588
|
+
items: T[],
|
|
589
|
+
concurrency: number | undefined,
|
|
590
|
+
fn: (item: T, index: number) => Promise<R>
|
|
591
|
+
): Promise<R[]> {
|
|
592
|
+
if (items.length === 0) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
const limit = concurrency !== undefined && concurrency > 0 ? concurrency : items.length;
|
|
596
|
+
const results = new Array<R>(items.length);
|
|
597
|
+
let next = 0;
|
|
598
|
+
|
|
599
|
+
const worker = async () => {
|
|
600
|
+
while (next < items.length) {
|
|
601
|
+
const index = next++;
|
|
602
|
+
results[index] = await fn(items[index], index);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const workers = Math.min(limit, items.length);
|
|
607
|
+
await Promise.all(Array.from({length: workers}, () => worker()));
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private static async resolveWith(
|
|
612
|
+
resolver: QuarksResourceResolver | undefined,
|
|
613
|
+
url: string,
|
|
614
|
+
kind: QuarksResourceKind,
|
|
615
|
+
baseUrl: string
|
|
616
|
+
): Promise<string> {
|
|
617
|
+
if (resolver?.resolve) {
|
|
618
|
+
return resolver.resolve(url, kind, baseUrl);
|
|
619
|
+
}
|
|
620
|
+
return QuarksLoader.joinUrl(baseUrl, url);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** One in-flight fetch per scene + resolved url when `scene` is set; otherwise uncached. */
|
|
624
|
+
private static fetchJsonWith(
|
|
625
|
+
resolver: QuarksResourceResolver | undefined,
|
|
626
|
+
resolvedUrl: string,
|
|
627
|
+
scene: Scene | undefined
|
|
628
|
+
): Promise<unknown> {
|
|
629
|
+
const load = async () => {
|
|
630
|
+
if (resolver?.fetchJson) {
|
|
631
|
+
return resolver.fetchJson(resolvedUrl);
|
|
632
|
+
}
|
|
633
|
+
const response = await fetch(resolvedUrl);
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw new Error(`HTTP ${response.status}`);
|
|
636
|
+
}
|
|
637
|
+
return response.json();
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
if (!scene) {
|
|
641
|
+
return load();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const cache = sceneMap(sharedFetches, scene);
|
|
645
|
+
const key = `json|${resolvedUrl}`;
|
|
646
|
+
let pending = cache.get(key);
|
|
647
|
+
if (pending === undefined) {
|
|
648
|
+
pending = load().catch((err) => {
|
|
649
|
+
if (cache.get(key) === pending) {
|
|
650
|
+
cache.delete(key);
|
|
651
|
+
}
|
|
652
|
+
throw err;
|
|
653
|
+
});
|
|
654
|
+
cache.set(key, pending);
|
|
655
|
+
}
|
|
656
|
+
return pending;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private static async inlineGeometryEntries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void> {
|
|
660
|
+
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
661
|
+
await Promise.all(
|
|
662
|
+
geometries
|
|
663
|
+
.filter((geom) => typeof geom?.url === 'string' && !geom.positions)
|
|
664
|
+
.map(async (geom) => {
|
|
665
|
+
try {
|
|
666
|
+
const data = await loadGeometry(geom.url);
|
|
667
|
+
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
668
|
+
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
669
|
+
for (const key of ['positions', 'indices', 'uvs', 'normals']) {
|
|
670
|
+
const value = data?.[key];
|
|
671
|
+
if (value === undefined) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
geom[key] = Array.isArray(value) ? value.slice() : value;
|
|
675
|
+
}
|
|
676
|
+
delete geom.url;
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
679
|
+
}
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
369
684
|
private parseImages(images: any[], baseUrl: string, _meta: LoadedMeta): void {
|
|
370
685
|
(this as any)._images = {};
|
|
371
686
|
for (const img of images) {
|
|
372
687
|
if (img.url) {
|
|
373
688
|
QuarksLoader.warnIfNotAnImageUrl(img.url);
|
|
374
689
|
}
|
|
375
|
-
|
|
690
|
+
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
691
|
+
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
692
|
+
(this as any)._images[img.uuid] = img.url ? QuarksLoader.joinUrl(baseUrl, img.url) : null;
|
|
376
693
|
}
|
|
377
694
|
}
|
|
378
695
|
|
|
@@ -395,19 +712,6 @@ export class QuarksLoader {
|
|
|
395
712
|
);
|
|
396
713
|
}
|
|
397
714
|
|
|
398
|
-
/**
|
|
399
|
-
* `img.url` is already absolute for data/blob URIs and full http(s) URLs — common for
|
|
400
|
-
* "portable" exports that embed textures as base64 (see ensurePortableTextureUrl in
|
|
401
|
-
* babylon.quarks-editor). Prepending baseUrl to those turns e.g.
|
|
402
|
-
* "data:image/png;base64,AAAA" into an invalid path like "/some/dir/data:image/png;base64,AAAA".
|
|
403
|
-
*/
|
|
404
|
-
private static resolveUrl(baseUrl: string, url: string): string {
|
|
405
|
-
if (/^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/')) {
|
|
406
|
-
return url;
|
|
407
|
-
}
|
|
408
|
-
return baseUrl + url;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
715
|
private parseTextures(textures: any[], meta: LoadedMeta): void {
|
|
412
716
|
const images = (this as any)._images || {};
|
|
413
717
|
for (const texDef of textures) {
|
|
@@ -512,6 +816,14 @@ export class QuarksLoader {
|
|
|
512
816
|
if (typeof matDef.reflectionLevel === 'number') {
|
|
513
817
|
matInfo.reflectionLevel = matDef.reflectionLevel;
|
|
514
818
|
}
|
|
819
|
+
// Mesh particle lighting (QuarksMaterial): 'unlit' | 'fixed' | 'scene'.
|
|
820
|
+
if (matDef.lighting === 'unlit' || matDef.lighting === 'fixed' || matDef.lighting === 'scene') {
|
|
821
|
+
matInfo.lighting = matDef.lighting;
|
|
822
|
+
}
|
|
823
|
+
// Mesh particle rim (QuarksMaterial): {color, power, strength, mode}.
|
|
824
|
+
if (matDef.rim && typeof matDef.rim === 'object') {
|
|
825
|
+
matInfo.rim = matDef.rim;
|
|
826
|
+
}
|
|
515
827
|
|
|
516
828
|
// QuarksMaterial (what ParticleSystem.ensureMaterialMeta emits) writes `alphaMode`
|
|
517
829
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
|
@@ -707,11 +1019,24 @@ export class QuarksLoader {
|
|
|
707
1019
|
if (system.behaviors[i] instanceof EmitSubParticleSystem) {
|
|
708
1020
|
const subEmitter = system.behaviors[i] as EmitSubParticleSystem;
|
|
709
1021
|
const targetUUID = subEmitter.subParticleSystem as any;
|
|
710
|
-
if (typeof targetUUID
|
|
711
|
-
|
|
1022
|
+
if (typeof targetUUID !== 'string') {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
const resolved = nodesMap[targetUUID];
|
|
1026
|
+
if (resolved instanceof ParticleEmitter) {
|
|
712
1027
|
(subEmitter as any).subParticleSystem = resolved;
|
|
713
|
-
|
|
714
|
-
|
|
1028
|
+
(resolved.system as ParticleSystem).onlyUsedByOther = true;
|
|
1029
|
+
} else {
|
|
1030
|
+
// An export that dropped the target emitter, or kept only
|
|
1031
|
+
// some emitters of a prefab: emit nothing rather than leave
|
|
1032
|
+
// a string the behavior would treat as an emitter.
|
|
1033
|
+
subEmitter.subParticleSystem = undefined;
|
|
1034
|
+
if (targetUUID !== '') {
|
|
1035
|
+
console.warn(
|
|
1036
|
+
`[QuarksLoader] Emitter "${node.name}" has a sub emitter targeting "${targetUUID}", ` +
|
|
1037
|
+
(resolved ? 'which is not a particle emitter' : 'which is not in the effect') +
|
|
1038
|
+
'; that sub emitter is skipped.'
|
|
1039
|
+
);
|
|
715
1040
|
}
|
|
716
1041
|
}
|
|
717
1042
|
}
|