babylon.quarks 0.27.0 → 0.28.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 +425 -0
- package/README.md +3 -1
- 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.cts +4256 -831
- package/dist/types/index.d.ts +4256 -831
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +93 -78
- package/src/EffectPool.ts +91 -12
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +31 -3
- package/src/ParticleSystem.ts +92 -25
- package/src/QuarksLoader.ts +289 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +43 -15
- package/src/QuarksUtil.ts +64 -9
- package/src/SpriteBatch.ts +21 -6
- package/src/TrailBatch.ts +12 -6
- package/src/VFXBatch.ts +70 -528
- package/src/batchKey.ts +4 -2
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +21 -24
- package/src/envAtlas.ts +8 -2
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +39 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +110 -16
- package/src/shaderVariants.ts +24 -4
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +3 -1
- package/src/shaders/particle_frag.wgsl.ts +2 -0
- package/src/shaders/particle_physics_frag.glsl.ts +5 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
- package/src/shaders/trail_frag.glsl.ts +4 -1
- package/src/shaders/trail_frag.wgsl.ts +2 -0
- package/src/system/EmissionController.ts +2 -4
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/geometry.ts +22 -29
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +6 -1
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +32 -10
- package/src/system/types.ts +67 -28
- package/src/trailSettings.ts +9 -6
package/src/QuarksLoader.ts
CHANGED
|
@@ -8,13 +8,36 @@ import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
|
8
8
|
import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
9
9
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
10
10
|
import {Scene} from '@babylonjs/core/scene';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Behavior,
|
|
13
|
+
currentFormatPath,
|
|
14
|
+
EmitSubParticleSystem,
|
|
15
|
+
QuarksFormatError,
|
|
16
|
+
readFormat,
|
|
17
|
+
readFormatPart,
|
|
18
|
+
warnFormat,
|
|
19
|
+
} from 'quarks.core';
|
|
20
|
+
import {MeshSurfaceEmitter} from './MeshSurfaceEmitter';
|
|
12
21
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
13
22
|
import {ParticleSystem} from './ParticleSystem';
|
|
14
23
|
import {QuarksPrefab} from './QuarksPrefab';
|
|
24
|
+
import type {QuarksNodeJSON, QuarksTextureJSON} from './QuarksSerializer';
|
|
15
25
|
import {QUARKS_FORMAT_VERSION} from './QuarksSerializer';
|
|
26
|
+
import type {
|
|
27
|
+
BufferAttributeJSON,
|
|
28
|
+
BufferGeometryDataJSON,
|
|
29
|
+
EffectDocumentJSON,
|
|
30
|
+
EffectGeometryJSON,
|
|
31
|
+
EffectImageJSON,
|
|
32
|
+
GeometryArrays,
|
|
33
|
+
MaterialTextureRef,
|
|
34
|
+
QuarksMaterialJSON,
|
|
35
|
+
SphereGeometryJSON,
|
|
36
|
+
} from './effectDocumentJSON';
|
|
16
37
|
import {disposeEffect, retainEffectResources} from './effectResources';
|
|
17
38
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
39
|
+
import {fileNodeId, setQuarksNodeId} from './nodeId';
|
|
40
|
+
import type {ParticleSystemJSONParameters} from './system/types';
|
|
18
41
|
import {setTextureSourceUrl} from './textureSource';
|
|
19
42
|
|
|
20
43
|
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
@@ -35,6 +58,18 @@ export interface QuarksResourceResolver {
|
|
|
35
58
|
fetchJson?(resolvedUrl: string, options?: {signal?: AbortSignal}): Promise<unknown>;
|
|
36
59
|
}
|
|
37
60
|
|
|
61
|
+
/** Options for {@link QuarksLoader.resolveLinkedResources}. */
|
|
62
|
+
export interface QuarksResolveOptions {
|
|
63
|
+
/** Joined to relative urls. Default `''`. */
|
|
64
|
+
baseUrl?: string;
|
|
65
|
+
/** Resolves and fetches the linked files; default: join with `baseUrl` and `fetch`. */
|
|
66
|
+
resolver?: QuarksResourceResolver;
|
|
67
|
+
/** Shares the per-scene fetch cache when set. */
|
|
68
|
+
scene?: Scene;
|
|
69
|
+
/** Called for a geometry file that fails to load; default: a console warning. */
|
|
70
|
+
onResourceError?: QuarksLoaderOptions['onResourceError'];
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
/** Options for a {@link QuarksLoader}, shared by every load it does. */
|
|
39
74
|
export interface QuarksLoaderOptions {
|
|
40
75
|
/**
|
|
@@ -65,6 +100,22 @@ export interface QuarksLoaderOptions {
|
|
|
65
100
|
* keeps its link unresolved, and a texture draws nothing. Default: a console warning.
|
|
66
101
|
*/
|
|
67
102
|
onResourceError?: (kind: QuarksResourceKind, url: string, error: unknown) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Called for each part of an effect file that cannot be read as written (an unknown behavior,
|
|
105
|
+
* shape or curve type, a sub emitter or mesh surface shape whose target is not in the effect, a
|
|
106
|
+
* newer format), with what was used instead and the part's JSON path, such as
|
|
107
|
+
* `object.children[2].ps.behaviors[3]`. The effect still loads, with that part at its default.
|
|
108
|
+
* Default: a console warning that names the path.
|
|
109
|
+
*/
|
|
110
|
+
onFormatWarning?: (message: string, path: string) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Makes a load of a file with a part that cannot be read as written throw a
|
|
113
|
+
* {@link QuarksFormatError} instead of loading on with a warning: for checks that a set of
|
|
114
|
+
* assets loads cleanly, such as in CI. The whole effect is read first, so the error's message
|
|
115
|
+
* names the first such part and its `problems` list them all; what the load built is freed
|
|
116
|
+
* before it throws. `onFormatWarning` still hears each part. Default false.
|
|
117
|
+
*/
|
|
118
|
+
strict?: boolean;
|
|
68
119
|
}
|
|
69
120
|
|
|
70
121
|
/** Options for one {@link QuarksLoader.load} or {@link QuarksLoader.loadFromJSON}. */
|
|
@@ -109,17 +160,60 @@ export interface QuarksBatchLoadOptions extends QuarksLoadOptions {
|
|
|
109
160
|
continueOnError?: boolean;
|
|
110
161
|
}
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
/** An object with fields of unknown types, as a file's JSON may hold anywhere. */
|
|
164
|
+
type JSONRecord = {[key: string]: unknown};
|
|
165
|
+
|
|
166
|
+
function isRecord(value: unknown): value is JSONRecord {
|
|
167
|
+
return value !== null && typeof value === 'object';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether `value` has the root node an effect document needs. The other parts are checked as they
|
|
172
|
+
* are read, so a part of the wrong kind warns instead of failing the load.
|
|
173
|
+
*/
|
|
174
|
+
function isEffectDocument(value: unknown): value is EffectDocumentJSON {
|
|
175
|
+
return isRecord(value) && isRecord(value.object);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Whether `value` can be read as a node; its fields are checked as they are read. */
|
|
179
|
+
function isNodeJSON(value: unknown): value is QuarksNodeJSON {
|
|
180
|
+
return isRecord(value);
|
|
118
181
|
}
|
|
119
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The entries of a list in an effect document. A list that is not an array, or an entry that is
|
|
185
|
+
* not an object, warns and is left out; the fields of each entry are checked as they are read.
|
|
186
|
+
*/
|
|
187
|
+
function documentEntries<T>(list: unknown): T[] {
|
|
188
|
+
if (!Array.isArray(list)) {
|
|
189
|
+
warnFormat('This part is not a list; it is skipped.');
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const entries: T[] = [];
|
|
193
|
+
list.forEach((entry: unknown, index) => {
|
|
194
|
+
if (isRecord(entry)) {
|
|
195
|
+
entries.push(entry as T);
|
|
196
|
+
} else {
|
|
197
|
+
readFormatPart(`[${index}]`, () => warnFormat('This entry is not an object; it is skipped.'));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return entries;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `json[key]` when it is an array, else an empty one. */
|
|
204
|
+
function documentList(json: unknown, key: 'images' | 'geometries'): unknown[] {
|
|
205
|
+
const list = isRecord(json) ? json[key] : undefined;
|
|
206
|
+
return Array.isArray(list) ? list : [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The uuid of the mesh each loaded system's mesh surface shape names, until the effect links it. */
|
|
210
|
+
const meshSurfaceLinks = new WeakMap<ParticleSystem, string>();
|
|
211
|
+
/** The JSON path each loaded system was read from, for the warnings of linking it. */
|
|
212
|
+
const systemPaths = new WeakMap<ParticleSystem, string>();
|
|
213
|
+
|
|
120
214
|
/** Textures and geometries reused across loads, per scene. See QuarksLoaderOptions.shareResources. */
|
|
121
215
|
const sharedTextures = new WeakMap<Scene, Map<string, Texture>>();
|
|
122
|
-
const sharedGeometries = new WeakMap<Scene, Map<string,
|
|
216
|
+
const sharedGeometries = new WeakMap<Scene, Map<string, GeometryArrays>>();
|
|
123
217
|
/** In-flight / completed JSON fetches keyed by `json|resolvedUrl`, per scene. */
|
|
124
218
|
const sharedFetches = new WeakMap<Scene, Map<string, Promise<unknown>>>();
|
|
125
219
|
|
|
@@ -139,7 +233,7 @@ const resolvedImageUrls = new WeakMap<object, string>();
|
|
|
139
233
|
const sourceImageUrls = new WeakMap<object, string>();
|
|
140
234
|
|
|
141
235
|
/** Content key for a geometry: two independent 32-bit hashes over every array's bytes, plus lengths. */
|
|
142
|
-
function geometryKey(geometry:
|
|
236
|
+
function geometryKey(geometry: GeometryArrays): string {
|
|
143
237
|
let h1 = 0x811c9dc5;
|
|
144
238
|
let h2 = 0x9e3779b9;
|
|
145
239
|
const parts: Array<ArrayBufferView | undefined> = [
|
|
@@ -165,27 +259,18 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
165
259
|
return `${lengths}${(h1 >>> 0).toString(16)}${(h2 >>> 0).toString(16)}`;
|
|
166
260
|
}
|
|
167
261
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* cannot read (an unknown behavior, shape or curve type, a missing setting) only warns, and that
|
|
171
|
-
* part takes its default.
|
|
172
|
-
*/
|
|
173
|
-
export class QuarksFormatError extends Error {
|
|
174
|
-
constructor(message: string) {
|
|
175
|
-
super(message);
|
|
176
|
-
this.name = 'QuarksFormatError';
|
|
177
|
-
}
|
|
178
|
-
}
|
|
262
|
+
// Defined in quarks.core, whose readers throw it in a strict read; exported here as before.
|
|
263
|
+
export {QuarksFormatError};
|
|
179
264
|
|
|
180
265
|
/**
|
|
181
266
|
* A copy of an effect document that loading can rewrite (resolved image urls, inlined geometry
|
|
182
267
|
* files) without touching the caller's. Only the entries that change are copied.
|
|
183
268
|
*/
|
|
184
|
-
function detachDocument(json:
|
|
185
|
-
if (json
|
|
269
|
+
function detachDocument(json: unknown): unknown {
|
|
270
|
+
if (!isRecord(json)) {
|
|
186
271
|
return json;
|
|
187
272
|
}
|
|
188
|
-
const copy = {...json};
|
|
273
|
+
const copy: JSONRecord = {...json};
|
|
189
274
|
if (Array.isArray(json.images)) {
|
|
190
275
|
copy.images = json.images.map((image: unknown) => {
|
|
191
276
|
if (!image || typeof image !== 'object') {
|
|
@@ -250,8 +335,8 @@ interface LoadedMeta {
|
|
|
250
335
|
/** Image uuid to the url the file wrote, before any base url was joined. */
|
|
251
336
|
imageSources: {[uuid: string]: string};
|
|
252
337
|
textures: {[uuid: string]: Texture | null};
|
|
253
|
-
geometries: {[uuid: string]:
|
|
254
|
-
materials: {[uuid: string]:
|
|
338
|
+
geometries: {[uuid: string]: GeometryArrays};
|
|
339
|
+
materials: {[uuid: string]: QuarksMaterialJSON};
|
|
255
340
|
}
|
|
256
341
|
|
|
257
342
|
/**
|
|
@@ -292,18 +377,20 @@ export class QuarksLoader {
|
|
|
292
377
|
|
|
293
378
|
/**
|
|
294
379
|
* Resolves linked geometry / image urls (via {@link resolveResources}) then parses. Use when
|
|
295
|
-
* the effect JSON is already in hand (File, CDN payload, editor import). `json`
|
|
380
|
+
* the effect JSON is already in hand (File, CDN payload, editor import). `json` is an
|
|
381
|
+
* {@link EffectDocumentJSON} as parsed from its file; anything else throws
|
|
382
|
+
* {@link QuarksFormatError}, and a part of the wrong kind warns. `json` itself is
|
|
296
383
|
* left as it was, so the same document can be loaded again, into another scene or with
|
|
297
384
|
* another base url.
|
|
298
385
|
*/
|
|
299
|
-
async loadFromJSON(json:
|
|
386
|
+
async loadFromJSON(json: unknown, baseUrl?: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
300
387
|
const signal = options.signal;
|
|
301
388
|
checkLoad(this.scene, signal);
|
|
302
389
|
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
303
|
-
|
|
304
|
-
await untilStopped(this.resolveResources(
|
|
390
|
+
const document = detachDocument(json);
|
|
391
|
+
await untilStopped(this.resolveResources(document, resolvedBase), this.scene, signal);
|
|
305
392
|
// The urls are resolved now; joining them with the base again would load `fx/fx/...`.
|
|
306
|
-
const loaded = this.parseDocument(
|
|
393
|
+
const loaded = this.parseDocument(document, '');
|
|
307
394
|
await this.finishLoad(loaded, signal);
|
|
308
395
|
return loaded.root;
|
|
309
396
|
}
|
|
@@ -402,7 +489,7 @@ export class QuarksLoader {
|
|
|
402
489
|
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
403
490
|
options: QuarksBatchLoadOptions = {}
|
|
404
491
|
): Promise<(TransformNode | null)[]> {
|
|
405
|
-
const docs: Array<{json:
|
|
492
|
+
const docs: Array<{json: unknown; baseUrl: string} | null> = entries.map((entry) => {
|
|
406
493
|
// Copied, as loadFromJSON does, so the caller's documents stay as they were.
|
|
407
494
|
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
408
495
|
const typed = entry as QuarksJSONLoadEntry;
|
|
@@ -420,7 +507,7 @@ export class QuarksLoader {
|
|
|
420
507
|
* loading the same document again does not join its base url twice. Failures warn and leave
|
|
421
508
|
* the entry as is.
|
|
422
509
|
*/
|
|
423
|
-
async resolveResources(json:
|
|
510
|
+
async resolveResources(json: unknown, baseUrl: string = ''): Promise<void> {
|
|
424
511
|
await QuarksLoader.resolveLinkedResources(json, {
|
|
425
512
|
baseUrl,
|
|
426
513
|
resolver: this.options.resolver,
|
|
@@ -435,15 +522,7 @@ export class QuarksLoader {
|
|
|
435
522
|
* Like it, resolves each document once. A geometry file that fails to load goes to
|
|
436
523
|
* `onResourceError` (default: a console warning) and keeps its link.
|
|
437
524
|
*/
|
|
438
|
-
static async resolveLinkedResources(
|
|
439
|
-
json: any,
|
|
440
|
-
options: {
|
|
441
|
-
baseUrl?: string;
|
|
442
|
-
resolver?: QuarksResourceResolver;
|
|
443
|
-
scene?: Scene;
|
|
444
|
-
onResourceError?: QuarksLoaderOptions['onResourceError'];
|
|
445
|
-
} = {}
|
|
446
|
-
): Promise<void> {
|
|
525
|
+
static async resolveLinkedResources(json: unknown, options: QuarksResolveOptions = {}): Promise<void> {
|
|
447
526
|
const baseUrl = options.baseUrl ?? '';
|
|
448
527
|
const resolver = options.resolver;
|
|
449
528
|
const scene = options.scene;
|
|
@@ -457,10 +536,9 @@ export class QuarksLoader {
|
|
|
457
536
|
options.onResourceError
|
|
458
537
|
);
|
|
459
538
|
|
|
460
|
-
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
461
539
|
await Promise.all(
|
|
462
|
-
images.map(async (img) => {
|
|
463
|
-
if (typeof img
|
|
540
|
+
documentList(json, 'images').map(async (img) => {
|
|
541
|
+
if (!isRecord(img) || typeof img.url !== 'string' || !img.url) {
|
|
464
542
|
return;
|
|
465
543
|
}
|
|
466
544
|
// An entry this loader already rewrote keeps its url: resolving the same document
|
|
@@ -468,12 +546,13 @@ export class QuarksLoader {
|
|
|
468
546
|
if (resolvedImageUrls.get(img) === img.url) {
|
|
469
547
|
return;
|
|
470
548
|
}
|
|
471
|
-
const source
|
|
472
|
-
|
|
473
|
-
|
|
549
|
+
const source = img.url;
|
|
550
|
+
const url = await QuarksLoader.resolveWith(resolver, source, 'image', baseUrl);
|
|
551
|
+
img.url = url;
|
|
552
|
+
resolvedImageUrls.set(img, url);
|
|
474
553
|
// A resolver that inlined the file (`data:`, `blob:`) made the url the image itself;
|
|
475
554
|
// the file's path no longer names what the texture holds, so a save writes the url.
|
|
476
|
-
if (!/^(data|blob):/i.test(
|
|
555
|
+
if (!/^(data|blob):/i.test(url)) {
|
|
477
556
|
sourceImageUrls.set(img, source);
|
|
478
557
|
}
|
|
479
558
|
})
|
|
@@ -486,7 +565,10 @@ export class QuarksLoader {
|
|
|
486
565
|
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
487
566
|
* `loadGeometry` and no `QuarksLoader` instance.
|
|
488
567
|
*/
|
|
489
|
-
static async resolveExternalGeometries(
|
|
568
|
+
static async resolveExternalGeometries(
|
|
569
|
+
json: unknown,
|
|
570
|
+
loadGeometry: (url: string) => Promise<unknown>
|
|
571
|
+
): Promise<void> {
|
|
490
572
|
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
491
573
|
}
|
|
492
574
|
|
|
@@ -496,19 +578,41 @@ export class QuarksLoader {
|
|
|
496
578
|
* read warn and take their defaults; a document with no `object` throws
|
|
497
579
|
* {@link QuarksFormatError}. Free the effect with {@link disposeEffect}.
|
|
498
580
|
*/
|
|
499
|
-
parse(json:
|
|
581
|
+
parse(json: unknown, baseUrl: string = ''): TransformNode {
|
|
500
582
|
return this.parseDocument(json, baseUrl).root;
|
|
501
583
|
}
|
|
502
584
|
|
|
503
585
|
/** {@link parse}, and the textures the effect uses. */
|
|
504
|
-
private parseDocument(json:
|
|
505
|
-
|
|
586
|
+
private parseDocument(json: unknown, baseUrl: string): {root: TransformNode; textures: Texture[]} {
|
|
587
|
+
const {strict, onFormatWarning} = this.options;
|
|
588
|
+
const problems: string[] = [];
|
|
589
|
+
const onWarning = (message: string, path: string): void => {
|
|
590
|
+
const located = path ? `${message} (at ${path})` : message;
|
|
591
|
+
if (strict) problems.push(located);
|
|
592
|
+
if (onFormatWarning) onFormatWarning(message, path);
|
|
593
|
+
else if (!strict) console.warn(`[quarks] ${located}`);
|
|
594
|
+
};
|
|
595
|
+
const loaded = readFormat({onWarning}, () => this.readDocument(json, baseUrl));
|
|
596
|
+
if (problems.length > 0) {
|
|
597
|
+
// The whole effect is read first, so a strict load frees what it built the same way any
|
|
598
|
+
// other effect is freed, and reports how many parts failed.
|
|
599
|
+
QuarksLoader.disposeEffect(loaded.root);
|
|
600
|
+
const more = problems.length > 1 ? ` (and ${problems.length - 1} more)` : '';
|
|
601
|
+
throw new QuarksFormatError(`${problems[0]}${more}`, problems);
|
|
602
|
+
}
|
|
603
|
+
return loaded;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** {@link parseDocument} inside its format read. */
|
|
607
|
+
private readDocument(document: unknown, baseUrl: string): {root: TransformNode; textures: Texture[]} {
|
|
608
|
+
if (!isEffectDocument(document)) {
|
|
506
609
|
throw new QuarksFormatError('[QuarksLoader] Not an effect document: it has no "object" to load.');
|
|
507
610
|
}
|
|
611
|
+
const json = document;
|
|
508
612
|
const format = json.metadata?.quarksFormat;
|
|
509
613
|
if (typeof format === 'number' && format > QUARKS_FORMAT_VERSION) {
|
|
510
|
-
|
|
511
|
-
`
|
|
614
|
+
warnFormat(
|
|
615
|
+
`This effect was written in format ${format}, newer than the ${QUARKS_FORMAT_VERSION} ` +
|
|
512
616
|
'this version of babylon.quarks reads; settings it does not know are left out.'
|
|
513
617
|
);
|
|
514
618
|
}
|
|
@@ -521,27 +625,28 @@ export class QuarksLoader {
|
|
|
521
625
|
materials: {},
|
|
522
626
|
};
|
|
523
627
|
|
|
524
|
-
|
|
525
|
-
|
|
628
|
+
const {geometries, images, textures: textureList, materials} = json;
|
|
629
|
+
if (geometries) {
|
|
630
|
+
readFormatPart('geometries', () => this.parseGeometries(documentEntries(geometries), meta));
|
|
526
631
|
}
|
|
527
|
-
if (
|
|
528
|
-
this.parseImages(
|
|
632
|
+
if (images) {
|
|
633
|
+
readFormatPart('images', () => this.parseImages(documentEntries(images), baseUrl, meta));
|
|
529
634
|
}
|
|
530
|
-
if (
|
|
531
|
-
this.parseTextures(
|
|
635
|
+
if (textureList) {
|
|
636
|
+
readFormatPart('textures', () => this.parseTextures(documentEntries(textureList), meta));
|
|
532
637
|
}
|
|
533
|
-
if (
|
|
534
|
-
this.parseMaterials(
|
|
638
|
+
if (materials) {
|
|
639
|
+
readFormatPart('materials', () => this.parseMaterials(documentEntries(materials), meta));
|
|
535
640
|
}
|
|
536
641
|
|
|
537
|
-
const root = this.parseObject(json.object, meta);
|
|
642
|
+
const root = readFormatPart('object', () => this.parseObject(json.object, meta));
|
|
538
643
|
this.linkReferences(root);
|
|
539
644
|
const textures = [...new Set(Object.values(meta.textures).filter((t): t is Texture => t !== null))];
|
|
540
645
|
retainEffectResources(root, textures, meta.ownedMaterials);
|
|
541
646
|
return {root, textures};
|
|
542
647
|
}
|
|
543
648
|
|
|
544
|
-
private parseGeometries(geometries:
|
|
649
|
+
private parseGeometries(geometries: EffectGeometryJSON[], meta: LoadedMeta): void {
|
|
545
650
|
for (const geom of geometries) {
|
|
546
651
|
if (geom.type === 'PlaneGeometry') {
|
|
547
652
|
const w = geom.width || 1;
|
|
@@ -561,8 +666,8 @@ export class QuarksLoader {
|
|
|
561
666
|
meta.geometries[geom.uuid] = parsed;
|
|
562
667
|
} else if (geom.type === 'QuarksGeometry') {
|
|
563
668
|
if (typeof geom.url === 'string' && !geom.positions) {
|
|
564
|
-
|
|
565
|
-
`
|
|
669
|
+
warnFormat(
|
|
670
|
+
`Geometry "${geom.url}" is an external file; use load(), ` +
|
|
566
671
|
'loadFromJSON(), or resolveResources() before parse().'
|
|
567
672
|
);
|
|
568
673
|
}
|
|
@@ -578,7 +683,7 @@ export class QuarksLoader {
|
|
|
578
683
|
}
|
|
579
684
|
|
|
580
685
|
/** The scene's copy of an identical geometry when resources are shared, else `geometry` itself. */
|
|
581
|
-
private share(geometry:
|
|
686
|
+
private share(geometry: GeometryArrays): GeometryArrays {
|
|
582
687
|
if (this.options.shareResources === false) {
|
|
583
688
|
return geometry;
|
|
584
689
|
}
|
|
@@ -611,7 +716,7 @@ export class QuarksLoader {
|
|
|
611
716
|
return baseUrl + url;
|
|
612
717
|
}
|
|
613
718
|
|
|
614
|
-
private parseSphereGeometry(geom:
|
|
719
|
+
private parseSphereGeometry(geom: SphereGeometryJSON): GeometryArrays {
|
|
615
720
|
const radius = geom.radius ?? 0.5;
|
|
616
721
|
const widthSegments = Math.max(3, Math.floor(geom.widthSegments ?? 16));
|
|
617
722
|
const heightSegments = Math.max(2, Math.floor(geom.heightSegments ?? 8));
|
|
@@ -681,8 +786,8 @@ export class QuarksLoader {
|
|
|
681
786
|
};
|
|
682
787
|
}
|
|
683
788
|
|
|
684
|
-
private parseBufferGeometry(data:
|
|
685
|
-
let positions = new Float32Array(0);
|
|
789
|
+
private parseBufferGeometry(data: BufferGeometryDataJSON): GeometryArrays {
|
|
790
|
+
let positions: Float32Array = new Float32Array(0);
|
|
686
791
|
let indices: Uint32Array | Uint16Array = new Uint32Array(0);
|
|
687
792
|
let uvs: Float32Array | undefined;
|
|
688
793
|
let uvs2: Float32Array | undefined;
|
|
@@ -692,7 +797,7 @@ export class QuarksLoader {
|
|
|
692
797
|
|
|
693
798
|
if (data.interleavedBuffers && data.arrayBuffers) {
|
|
694
799
|
const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
|
|
695
|
-
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)
|
|
800
|
+
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)) {
|
|
696
801
|
const arrayBuffer = data.arrayBuffers[ibDef.buffer];
|
|
697
802
|
if (arrayBuffer) {
|
|
698
803
|
const floatArray = new Float32Array(
|
|
@@ -702,35 +807,17 @@ export class QuarksLoader {
|
|
|
702
807
|
}
|
|
703
808
|
}
|
|
704
809
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
if (uv2Attribute?.isInterleavedBufferAttribute) {
|
|
718
|
-
const buf = buffers[uv2Attribute.data];
|
|
719
|
-
if (buf)
|
|
720
|
-
uvs2 = this.extractInterleavedAttribute(
|
|
721
|
-
buf.data,
|
|
722
|
-
buf.stride,
|
|
723
|
-
uv2Attribute.offset,
|
|
724
|
-
uv2Attribute.itemSize
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
if (data.attributes.normal?.isInterleavedBufferAttribute) {
|
|
728
|
-
const attr = data.attributes.normal;
|
|
729
|
-
const buf = buffers[attr.data];
|
|
730
|
-
if (buf)
|
|
731
|
-
normals = this.extractInterleavedAttribute(buf.data, buf.stride, attr.offset, attr.itemSize);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
810
|
+
const readInterleaved = (attr: BufferAttributeJSON | undefined): Float32Array | undefined => {
|
|
811
|
+
const buf =
|
|
812
|
+
attr?.isInterleavedBufferAttribute && attr.data !== undefined ? buffers[attr.data] : undefined;
|
|
813
|
+
return buf
|
|
814
|
+
? this.extractInterleavedAttribute(buf.data, buf.stride, attr?.offset, attr?.itemSize)
|
|
815
|
+
: undefined;
|
|
816
|
+
};
|
|
817
|
+
positions = readInterleaved(data.attributes?.position) ?? positions;
|
|
818
|
+
uvs = readInterleaved(data.attributes?.uv);
|
|
819
|
+
uvs2 = readInterleaved(uv2Attribute);
|
|
820
|
+
normals = readInterleaved(data.attributes?.normal);
|
|
734
821
|
} else if (data.attributes) {
|
|
735
822
|
if (data.attributes.position?.array) {
|
|
736
823
|
positions = new Float32Array(data.attributes.position.array);
|
|
@@ -759,7 +846,12 @@ export class QuarksLoader {
|
|
|
759
846
|
return {positions, indices, uvs, uvs2, normals};
|
|
760
847
|
}
|
|
761
848
|
|
|
762
|
-
private extractInterleavedAttribute(
|
|
849
|
+
private extractInterleavedAttribute(
|
|
850
|
+
buffer: Float32Array,
|
|
851
|
+
stride: number,
|
|
852
|
+
offset: number = 0,
|
|
853
|
+
itemSize: number = 0
|
|
854
|
+
): Float32Array {
|
|
763
855
|
const count = Math.floor(buffer.length / stride);
|
|
764
856
|
const result = new Float32Array(count * itemSize);
|
|
765
857
|
for (let i = 0; i < count; i++) {
|
|
@@ -776,7 +868,7 @@ export class QuarksLoader {
|
|
|
776
868
|
for (let i = 0; i < intArray.length; i++) {
|
|
777
869
|
int32View[i] = intArray[i];
|
|
778
870
|
}
|
|
779
|
-
return new Float32Array(buffer)
|
|
871
|
+
return new Float32Array(buffer);
|
|
780
872
|
}
|
|
781
873
|
|
|
782
874
|
private async resolveResourceUrl(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
@@ -784,7 +876,7 @@ export class QuarksLoader {
|
|
|
784
876
|
}
|
|
785
877
|
|
|
786
878
|
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
787
|
-
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json:
|
|
879
|
+
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json: unknown; baseUrl: string}> {
|
|
788
880
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
789
881
|
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
790
882
|
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined, signal);
|
|
@@ -796,7 +888,7 @@ export class QuarksLoader {
|
|
|
796
888
|
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
797
889
|
*/
|
|
798
890
|
private async loadDocuments(
|
|
799
|
-
docs: Array<{json:
|
|
891
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
800
892
|
options: QuarksBatchLoadOptions
|
|
801
893
|
): Promise<(TransformNode | null)[]> {
|
|
802
894
|
const total = docs.length;
|
|
@@ -816,7 +908,7 @@ export class QuarksLoader {
|
|
|
816
908
|
}
|
|
817
909
|
|
|
818
910
|
private async loadResolvedDocuments(
|
|
819
|
-
docs: Array<{json:
|
|
911
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
820
912
|
options: QuarksBatchLoadOptions,
|
|
821
913
|
stopAll: AbortController
|
|
822
914
|
): Promise<(TransformNode | null)[]> {
|
|
@@ -976,31 +1068,32 @@ export class QuarksLoader {
|
|
|
976
1068
|
}
|
|
977
1069
|
|
|
978
1070
|
private static async inlineGeometryEntries(
|
|
979
|
-
json:
|
|
980
|
-
loadGeometry: (url: string) => Promise<
|
|
1071
|
+
json: unknown,
|
|
1072
|
+
loadGeometry: (url: string) => Promise<unknown>,
|
|
981
1073
|
onResourceError: QuarksLoaderOptions['onResourceError'] = QuarksLoader.warnResourceError
|
|
982
1074
|
): Promise<void> {
|
|
983
|
-
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
984
1075
|
await Promise.all(
|
|
985
|
-
geometries
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1076
|
+
documentList(json, 'geometries').map(async (geom) => {
|
|
1077
|
+
if (!isRecord(geom) || typeof geom.url !== 'string' || geom.positions) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const url = geom.url;
|
|
1081
|
+
try {
|
|
1082
|
+
const data = await loadGeometry(url);
|
|
1083
|
+
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
1084
|
+
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
1085
|
+
for (const key of ['positions', 'indices', 'uvs', 'uvs2', 'normals']) {
|
|
1086
|
+
const value = isRecord(data) ? data[key] : undefined;
|
|
1087
|
+
if (value === undefined) {
|
|
1088
|
+
continue;
|
|
998
1089
|
}
|
|
999
|
-
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
onResourceError('geometry', geom.url, err);
|
|
1090
|
+
geom[key] = Array.isArray(value) ? value.slice() : value;
|
|
1002
1091
|
}
|
|
1003
|
-
|
|
1092
|
+
delete geom.url;
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
onResourceError('geometry', url, err);
|
|
1095
|
+
}
|
|
1096
|
+
})
|
|
1004
1097
|
);
|
|
1005
1098
|
}
|
|
1006
1099
|
|
|
@@ -1012,17 +1105,18 @@ export class QuarksLoader {
|
|
|
1012
1105
|
(this.options.onResourceError ?? QuarksLoader.warnResourceError)(kind, url, error);
|
|
1013
1106
|
}
|
|
1014
1107
|
|
|
1015
|
-
private parseImages(images:
|
|
1016
|
-
for (const img of images) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1108
|
+
private parseImages(images: EffectImageJSON[], baseUrl: string, meta: LoadedMeta): void {
|
|
1109
|
+
for (const [index, img] of images.entries()) {
|
|
1110
|
+
const url = img.url;
|
|
1111
|
+
if (url) {
|
|
1112
|
+
readFormatPart(`[${index}]`, () => QuarksLoader.warnIfNotAnImageUrl(url));
|
|
1019
1113
|
}
|
|
1020
1114
|
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
1021
1115
|
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
1022
|
-
meta.images[img.uuid] =
|
|
1116
|
+
meta.images[img.uuid] = url ? QuarksLoader.joinUrl(baseUrl, url) : null;
|
|
1023
1117
|
// The url the file wrote, for serializeEffect to write back. An inlined image
|
|
1024
1118
|
// (`data:`, `blob:`) has no path to write; its texture writes its url.
|
|
1025
|
-
const source
|
|
1119
|
+
const source = sourceImageUrls.get(img) ?? url;
|
|
1026
1120
|
if (source && !/^(data|blob):/i.test(source)) {
|
|
1027
1121
|
meta.imageSources[img.uuid] = source;
|
|
1028
1122
|
}
|
|
@@ -1042,15 +1136,15 @@ export class QuarksLoader {
|
|
|
1042
1136
|
if (/^(data|blob):/i.test(url) || /\.(png|jpe?g|webp|gif|bmp|ktx2?|dds|env|basis)(\?|#|$)/i.test(url)) {
|
|
1043
1137
|
return;
|
|
1044
1138
|
}
|
|
1045
|
-
|
|
1046
|
-
`
|
|
1139
|
+
warnFormat(
|
|
1140
|
+
`Image "${url}" does not look like an image; its texture will fail to load. ` +
|
|
1047
1141
|
'This usually means the effect was exported without embedding that texture.'
|
|
1048
1142
|
);
|
|
1049
1143
|
}
|
|
1050
1144
|
|
|
1051
|
-
private parseTextures(textures:
|
|
1145
|
+
private parseTextures(textures: QuarksTextureJSON[], meta: LoadedMeta): void {
|
|
1052
1146
|
for (const texDef of textures) {
|
|
1053
|
-
const imageUrl = meta.images[texDef.image];
|
|
1147
|
+
const imageUrl = texDef.image !== undefined ? meta.images[texDef.image] : undefined;
|
|
1054
1148
|
if (imageUrl) {
|
|
1055
1149
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
1056
1150
|
const noMipmap = !!texDef.noMipmap;
|
|
@@ -1105,7 +1199,7 @@ export class QuarksLoader {
|
|
|
1105
1199
|
continue;
|
|
1106
1200
|
}
|
|
1107
1201
|
textureSettled.set(texture, settled);
|
|
1108
|
-
const source = meta.imageSources[texDef.image];
|
|
1202
|
+
const source = texDef.image !== undefined ? meta.imageSources[texDef.image] : undefined;
|
|
1109
1203
|
if (source !== undefined) {
|
|
1110
1204
|
setTextureSourceUrl(texture, source, imageUrl);
|
|
1111
1205
|
}
|
|
@@ -1154,9 +1248,12 @@ export class QuarksLoader {
|
|
|
1154
1248
|
}
|
|
1155
1249
|
}
|
|
1156
1250
|
|
|
1157
|
-
private parseMaterials(materials:
|
|
1251
|
+
private parseMaterials(materials: QuarksMaterialJSON[], meta: LoadedMeta): void {
|
|
1252
|
+
// A file names a texture by uuid; anything else in a slot is no texture.
|
|
1253
|
+
const texture = (ref: MaterialTextureRef | undefined): Texture | null =>
|
|
1254
|
+
typeof ref === 'string' ? meta.textures[ref] || null : null;
|
|
1158
1255
|
for (const matDef of materials) {
|
|
1159
|
-
const matInfo:
|
|
1256
|
+
const matInfo: QuarksMaterialJSON = {
|
|
1160
1257
|
uuid: matDef.uuid,
|
|
1161
1258
|
transparent: matDef.transparent ?? true,
|
|
1162
1259
|
blending: matDef.blending ?? 1,
|
|
@@ -1171,12 +1268,12 @@ export class QuarksLoader {
|
|
|
1171
1268
|
// emits) uses `texture`. Accept both so library-exported effects round-trip.
|
|
1172
1269
|
const textureRef = matDef.map ?? matDef.texture;
|
|
1173
1270
|
if (textureRef) {
|
|
1174
|
-
matInfo.texture =
|
|
1271
|
+
matInfo.texture = texture(textureRef);
|
|
1175
1272
|
}
|
|
1176
1273
|
|
|
1177
1274
|
const reflectionAtlasRef = matDef.reflectionAtlas;
|
|
1178
1275
|
if (reflectionAtlasRef) {
|
|
1179
|
-
matInfo.reflectionAtlas =
|
|
1276
|
+
matInfo.reflectionAtlas = texture(reflectionAtlasRef);
|
|
1180
1277
|
}
|
|
1181
1278
|
if (typeof matDef.reflectionLevel === 'number') {
|
|
1182
1279
|
matInfo.reflectionLevel = matDef.reflectionLevel;
|
|
@@ -1199,20 +1296,20 @@ export class QuarksLoader {
|
|
|
1199
1296
|
if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
|
|
1200
1297
|
matInfo.gradientMap = {
|
|
1201
1298
|
...matDef.gradientMap,
|
|
1202
|
-
ramp: matDef.gradientMap.ramp
|
|
1299
|
+
ramp: texture(matDef.gradientMap.ramp),
|
|
1203
1300
|
};
|
|
1204
1301
|
}
|
|
1205
1302
|
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
1206
1303
|
matInfo.erosion = {
|
|
1207
1304
|
...matDef.erosion,
|
|
1208
|
-
mask: matDef.erosion.mask
|
|
1305
|
+
mask: texture(matDef.erosion.mask),
|
|
1209
1306
|
};
|
|
1210
1307
|
}
|
|
1211
1308
|
// uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
|
|
1212
1309
|
if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
|
|
1213
1310
|
matInfo.uvDistortion = {
|
|
1214
1311
|
...matDef.uvDistortion,
|
|
1215
|
-
map: matDef.uvDistortion.map
|
|
1312
|
+
map: texture(matDef.uvDistortion.map),
|
|
1216
1313
|
};
|
|
1217
1314
|
}
|
|
1218
1315
|
|
|
@@ -1230,19 +1327,19 @@ export class QuarksLoader {
|
|
|
1230
1327
|
3: Constants.ALPHA_SUBTRACT,
|
|
1231
1328
|
4: Constants.ALPHA_MULTIPLY,
|
|
1232
1329
|
};
|
|
1233
|
-
matInfo.alphaMode = blendingToAlphaMode[matDef.blending] || Constants.ALPHA_COMBINE;
|
|
1330
|
+
matInfo.alphaMode = blendingToAlphaMode[matDef.blending ?? 1] || Constants.ALPHA_COMBINE;
|
|
1234
1331
|
}
|
|
1235
1332
|
|
|
1236
1333
|
meta.materials[matDef.uuid] = matInfo;
|
|
1237
1334
|
}
|
|
1238
1335
|
}
|
|
1239
1336
|
|
|
1240
|
-
private getGeometry(meta: LoadedMeta, geometryId: string | undefined):
|
|
1337
|
+
private getGeometry(meta: LoadedMeta, geometryId: string | undefined): GeometryArrays | undefined {
|
|
1241
1338
|
if (!geometryId) return undefined;
|
|
1242
1339
|
return meta.geometries[geometryId];
|
|
1243
1340
|
}
|
|
1244
1341
|
|
|
1245
|
-
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined):
|
|
1342
|
+
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined): QuarksMaterialJSON | undefined {
|
|
1246
1343
|
if (!materialId) return undefined;
|
|
1247
1344
|
const selectedId = Array.isArray(materialId) ? materialId[0] : materialId;
|
|
1248
1345
|
return meta.materials[selectedId];
|
|
@@ -1250,11 +1347,12 @@ export class QuarksLoader {
|
|
|
1250
1347
|
|
|
1251
1348
|
private createPlaceholderNode(type: string, name?: string): TransformNode {
|
|
1252
1349
|
const node = new TransformNode(name || type || 'node', this.scene);
|
|
1253
|
-
|
|
1350
|
+
// The three.js type the node stood for, which babylon.quarks has no node for.
|
|
1351
|
+
Object.assign(node, {quarksOriginalType: type});
|
|
1254
1352
|
return node;
|
|
1255
1353
|
}
|
|
1256
1354
|
|
|
1257
|
-
private createMeshNode(data:
|
|
1355
|
+
private createMeshNode(data: QuarksNodeJSON, meta: LoadedMeta): Mesh {
|
|
1258
1356
|
const mesh = new Mesh(data.name || data.type || 'mesh', this.scene);
|
|
1259
1357
|
const geometry = this.getGeometry(meta, data.geometry);
|
|
1260
1358
|
if (geometry && geometry.positions.length > 0 && geometry.indices.length > 0) {
|
|
@@ -1281,16 +1379,17 @@ export class QuarksLoader {
|
|
|
1281
1379
|
material.backFaceCulling = materialInfo.side !== 2;
|
|
1282
1380
|
material.alphaCutOff = materialInfo.alphaTest ?? 0;
|
|
1283
1381
|
material.disableDepthWrite = !(materialInfo.depthWrite ?? false);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
material.
|
|
1382
|
+
const texture = materialInfo.texture;
|
|
1383
|
+
if (texture && typeof texture !== 'string') {
|
|
1384
|
+
material.diffuseTexture = texture;
|
|
1385
|
+
material.opacityTexture = texture;
|
|
1287
1386
|
}
|
|
1288
1387
|
mesh.material = material;
|
|
1289
1388
|
}
|
|
1290
1389
|
return mesh;
|
|
1291
1390
|
}
|
|
1292
1391
|
|
|
1293
|
-
private parseObject(data:
|
|
1392
|
+
private parseObject(data: QuarksNodeJSON, meta: LoadedMeta): TransformNode {
|
|
1294
1393
|
let node: TransformNode;
|
|
1295
1394
|
switch (data.type) {
|
|
1296
1395
|
case 'QuarksPrefab':
|
|
@@ -1298,7 +1397,8 @@ export class QuarksLoader {
|
|
|
1298
1397
|
break;
|
|
1299
1398
|
case 'ParticleEmitter':
|
|
1300
1399
|
if (data.ps) {
|
|
1301
|
-
const
|
|
1400
|
+
const psJSON = data.ps;
|
|
1401
|
+
const ps = readFormatPart('ps', () => this.parseParticleSystem(psJSON, meta));
|
|
1302
1402
|
node = ps.emitter;
|
|
1303
1403
|
break;
|
|
1304
1404
|
}
|
|
@@ -1336,7 +1436,7 @@ export class QuarksLoader {
|
|
|
1336
1436
|
node = new TransformNode(data.name || data.type || 'node', this.scene);
|
|
1337
1437
|
}
|
|
1338
1438
|
|
|
1339
|
-
if (data.uuid) (node
|
|
1439
|
+
if (data.uuid) setQuarksNodeId(node, data.uuid);
|
|
1340
1440
|
if (data.name) node.name = data.name;
|
|
1341
1441
|
|
|
1342
1442
|
if (data.matrix) {
|
|
@@ -1367,19 +1467,31 @@ export class QuarksLoader {
|
|
|
1367
1467
|
if (data.isVisible === false && node instanceof Mesh) node.isVisible = false;
|
|
1368
1468
|
|
|
1369
1469
|
if (data.children) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1470
|
+
const children: unknown = data.children;
|
|
1471
|
+
if (!Array.isArray(children)) {
|
|
1472
|
+
readFormatPart('children', () => warnFormat('"children" is not a list; the node loads without them.'));
|
|
1473
|
+
} else {
|
|
1474
|
+
children.forEach((childData: unknown, index) => {
|
|
1475
|
+
const child = readFormatPart(`children[${index}]`, () =>
|
|
1476
|
+
isNodeJSON(childData)
|
|
1477
|
+
? this.parseObject(childData, meta)
|
|
1478
|
+
: warnFormat('A child is not an object; it is skipped.')
|
|
1479
|
+
);
|
|
1480
|
+
if (child) child.parent = node;
|
|
1481
|
+
});
|
|
1373
1482
|
}
|
|
1374
1483
|
}
|
|
1375
1484
|
|
|
1376
1485
|
return node;
|
|
1377
1486
|
}
|
|
1378
1487
|
|
|
1379
|
-
private parseParticleSystem(json:
|
|
1488
|
+
private parseParticleSystem(json: ParticleSystemJSONParameters, meta: LoadedMeta): ParticleSystem {
|
|
1380
1489
|
const dependencies: {[uuid: string]: Behavior} = {};
|
|
1381
|
-
const ps = ParticleSystem.fromJSON(json, meta
|
|
1382
|
-
(
|
|
1490
|
+
const ps = ParticleSystem.fromJSON(json, meta, dependencies, this.scene);
|
|
1491
|
+
if (json?.shape?.type === 'mesh_surface' && typeof json.shape.mesh === 'string') {
|
|
1492
|
+
meshSurfaceLinks.set(ps, json.shape.mesh);
|
|
1493
|
+
}
|
|
1494
|
+
systemPaths.set(ps, currentFormatPath());
|
|
1383
1495
|
return ps;
|
|
1384
1496
|
}
|
|
1385
1497
|
|
|
@@ -1387,8 +1499,9 @@ export class QuarksLoader {
|
|
|
1387
1499
|
const nodesMap: {[uuid: string]: TransformNode} = {};
|
|
1388
1500
|
|
|
1389
1501
|
const traverse = (node: TransformNode) => {
|
|
1390
|
-
|
|
1391
|
-
|
|
1502
|
+
const fileId = fileNodeId(node);
|
|
1503
|
+
if (fileId) {
|
|
1504
|
+
nodesMap[fileId] = node;
|
|
1392
1505
|
}
|
|
1393
1506
|
for (const child of node.getChildren()) {
|
|
1394
1507
|
if (child instanceof TransformNode) {
|
|
@@ -1404,14 +1517,19 @@ export class QuarksLoader {
|
|
|
1404
1517
|
}
|
|
1405
1518
|
if (node instanceof ParticleEmitter) {
|
|
1406
1519
|
const system = node.system as ParticleSystem;
|
|
1407
|
-
|
|
1408
|
-
|
|
1520
|
+
// Links are made once the whole effect is read; a warning names the system's path.
|
|
1521
|
+
const path = systemPaths.get(system);
|
|
1522
|
+
const warnAtSystem = (message: string) =>
|
|
1523
|
+
path ? readFormatPart(path, () => warnFormat(message)) : warnFormat(message);
|
|
1524
|
+
const meshSurfaceUUID = meshSurfaceLinks.get(system);
|
|
1525
|
+
const shape = system.emitterShape;
|
|
1526
|
+
if (meshSurfaceUUID && shape instanceof MeshSurfaceEmitter) {
|
|
1409
1527
|
const targetNode = nodesMap[meshSurfaceUUID];
|
|
1410
1528
|
if (targetNode instanceof Mesh) {
|
|
1411
|
-
|
|
1529
|
+
shape.mesh = targetNode;
|
|
1412
1530
|
} else {
|
|
1413
|
-
|
|
1414
|
-
`
|
|
1531
|
+
warnAtSystem(
|
|
1532
|
+
`Emitter "${node.name}" has a mesh surface shape on "${meshSurfaceUUID}", ` +
|
|
1415
1533
|
(targetNode ? 'which is not a mesh' : 'which is not in the effect') +
|
|
1416
1534
|
'; it emits nothing until a mesh is set.'
|
|
1417
1535
|
);
|
|
@@ -1420,13 +1538,13 @@ export class QuarksLoader {
|
|
|
1420
1538
|
for (let i = 0; i < system.behaviors.length; i++) {
|
|
1421
1539
|
if (system.behaviors[i] instanceof EmitSubParticleSystem) {
|
|
1422
1540
|
const subEmitter = system.behaviors[i] as EmitSubParticleSystem;
|
|
1423
|
-
const targetUUID = subEmitter.subParticleSystem
|
|
1541
|
+
const targetUUID = subEmitter.subParticleSystem;
|
|
1424
1542
|
if (typeof targetUUID !== 'string') {
|
|
1425
1543
|
continue;
|
|
1426
1544
|
}
|
|
1427
1545
|
const resolved = nodesMap[targetUUID];
|
|
1428
1546
|
if (resolved instanceof ParticleEmitter) {
|
|
1429
|
-
|
|
1547
|
+
subEmitter.subParticleSystem = resolved;
|
|
1430
1548
|
(resolved.system as ParticleSystem).onlyUsedByOther = true;
|
|
1431
1549
|
} else {
|
|
1432
1550
|
// An export that dropped the target emitter, or kept only
|
|
@@ -1434,8 +1552,8 @@ export class QuarksLoader {
|
|
|
1434
1552
|
// a string the behavior would treat as an emitter.
|
|
1435
1553
|
subEmitter.subParticleSystem = undefined;
|
|
1436
1554
|
if (targetUUID !== '') {
|
|
1437
|
-
|
|
1438
|
-
`
|
|
1555
|
+
warnAtSystem(
|
|
1556
|
+
`Emitter "${node.name}" has a sub emitter targeting "${targetUUID}", ` +
|
|
1439
1557
|
(resolved ? 'which is not a particle emitter' : 'which is not in the effect') +
|
|
1440
1558
|
'; that sub emitter is skipped.'
|
|
1441
1559
|
);
|