babylon.quarks 0.27.0 → 0.29.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 +585 -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 +4811 -974
- package/dist/types/index.d.ts +4811 -974
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +106 -79
- package/src/EffectPool.ts +96 -16
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +119 -26
- package/src/ParticleSystem.ts +305 -55
- package/src/QuarksLoader.ts +298 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +44 -16
- package/src/QuarksUtil.ts +88 -11
- package/src/SpriteBatch.ts +24 -6
- package/src/TrailBatch.ts +20 -7
- package/src/VFXBatch.ts +182 -528
- package/src/batchKey.ts +6 -2
- package/src/batchSettings.ts +548 -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 +41 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +213 -16
- package/src/shaderVariants.ts +124 -7
- package/src/shaders/chunks/blend.ts +39 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +5 -1
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +7 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/trail_frag.glsl.ts +6 -1
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/system/EmissionController.ts +23 -20
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/geometry.ts +22 -29
- package/src/system/integrate.ts +61 -4
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +34 -2
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +57 -17
- package/src/system/types.ts +150 -42
- 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,10 @@ 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};
|
|
340
|
+
/** The file's `quarksFormat`; 0 for a file written before the field existed. */
|
|
341
|
+
format: number;
|
|
255
342
|
}
|
|
256
343
|
|
|
257
344
|
/**
|
|
@@ -292,18 +379,20 @@ export class QuarksLoader {
|
|
|
292
379
|
|
|
293
380
|
/**
|
|
294
381
|
* 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`
|
|
382
|
+
* the effect JSON is already in hand (File, CDN payload, editor import). `json` is an
|
|
383
|
+
* {@link EffectDocumentJSON} as parsed from its file; anything else throws
|
|
384
|
+
* {@link QuarksFormatError}, and a part of the wrong kind warns. `json` itself is
|
|
296
385
|
* left as it was, so the same document can be loaded again, into another scene or with
|
|
297
386
|
* another base url.
|
|
298
387
|
*/
|
|
299
|
-
async loadFromJSON(json:
|
|
388
|
+
async loadFromJSON(json: unknown, baseUrl?: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
300
389
|
const signal = options.signal;
|
|
301
390
|
checkLoad(this.scene, signal);
|
|
302
391
|
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
303
|
-
|
|
304
|
-
await untilStopped(this.resolveResources(
|
|
392
|
+
const document = detachDocument(json);
|
|
393
|
+
await untilStopped(this.resolveResources(document, resolvedBase), this.scene, signal);
|
|
305
394
|
// The urls are resolved now; joining them with the base again would load `fx/fx/...`.
|
|
306
|
-
const loaded = this.parseDocument(
|
|
395
|
+
const loaded = this.parseDocument(document, '');
|
|
307
396
|
await this.finishLoad(loaded, signal);
|
|
308
397
|
return loaded.root;
|
|
309
398
|
}
|
|
@@ -402,7 +491,7 @@ export class QuarksLoader {
|
|
|
402
491
|
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
403
492
|
options: QuarksBatchLoadOptions = {}
|
|
404
493
|
): Promise<(TransformNode | null)[]> {
|
|
405
|
-
const docs: Array<{json:
|
|
494
|
+
const docs: Array<{json: unknown; baseUrl: string} | null> = entries.map((entry) => {
|
|
406
495
|
// Copied, as loadFromJSON does, so the caller's documents stay as they were.
|
|
407
496
|
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
408
497
|
const typed = entry as QuarksJSONLoadEntry;
|
|
@@ -420,7 +509,7 @@ export class QuarksLoader {
|
|
|
420
509
|
* loading the same document again does not join its base url twice. Failures warn and leave
|
|
421
510
|
* the entry as is.
|
|
422
511
|
*/
|
|
423
|
-
async resolveResources(json:
|
|
512
|
+
async resolveResources(json: unknown, baseUrl: string = ''): Promise<void> {
|
|
424
513
|
await QuarksLoader.resolveLinkedResources(json, {
|
|
425
514
|
baseUrl,
|
|
426
515
|
resolver: this.options.resolver,
|
|
@@ -435,15 +524,7 @@ export class QuarksLoader {
|
|
|
435
524
|
* Like it, resolves each document once. A geometry file that fails to load goes to
|
|
436
525
|
* `onResourceError` (default: a console warning) and keeps its link.
|
|
437
526
|
*/
|
|
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> {
|
|
527
|
+
static async resolveLinkedResources(json: unknown, options: QuarksResolveOptions = {}): Promise<void> {
|
|
447
528
|
const baseUrl = options.baseUrl ?? '';
|
|
448
529
|
const resolver = options.resolver;
|
|
449
530
|
const scene = options.scene;
|
|
@@ -457,10 +538,9 @@ export class QuarksLoader {
|
|
|
457
538
|
options.onResourceError
|
|
458
539
|
);
|
|
459
540
|
|
|
460
|
-
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
461
541
|
await Promise.all(
|
|
462
|
-
images.map(async (img) => {
|
|
463
|
-
if (typeof img
|
|
542
|
+
documentList(json, 'images').map(async (img) => {
|
|
543
|
+
if (!isRecord(img) || typeof img.url !== 'string' || !img.url) {
|
|
464
544
|
return;
|
|
465
545
|
}
|
|
466
546
|
// An entry this loader already rewrote keeps its url: resolving the same document
|
|
@@ -468,12 +548,13 @@ export class QuarksLoader {
|
|
|
468
548
|
if (resolvedImageUrls.get(img) === img.url) {
|
|
469
549
|
return;
|
|
470
550
|
}
|
|
471
|
-
const source
|
|
472
|
-
|
|
473
|
-
|
|
551
|
+
const source = img.url;
|
|
552
|
+
const url = await QuarksLoader.resolveWith(resolver, source, 'image', baseUrl);
|
|
553
|
+
img.url = url;
|
|
554
|
+
resolvedImageUrls.set(img, url);
|
|
474
555
|
// A resolver that inlined the file (`data:`, `blob:`) made the url the image itself;
|
|
475
556
|
// the file's path no longer names what the texture holds, so a save writes the url.
|
|
476
|
-
if (!/^(data|blob):/i.test(
|
|
557
|
+
if (!/^(data|blob):/i.test(url)) {
|
|
477
558
|
sourceImageUrls.set(img, source);
|
|
478
559
|
}
|
|
479
560
|
})
|
|
@@ -486,7 +567,10 @@ export class QuarksLoader {
|
|
|
486
567
|
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
487
568
|
* `loadGeometry` and no `QuarksLoader` instance.
|
|
488
569
|
*/
|
|
489
|
-
static async resolveExternalGeometries(
|
|
570
|
+
static async resolveExternalGeometries(
|
|
571
|
+
json: unknown,
|
|
572
|
+
loadGeometry: (url: string) => Promise<unknown>
|
|
573
|
+
): Promise<void> {
|
|
490
574
|
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
491
575
|
}
|
|
492
576
|
|
|
@@ -496,19 +580,41 @@ export class QuarksLoader {
|
|
|
496
580
|
* read warn and take their defaults; a document with no `object` throws
|
|
497
581
|
* {@link QuarksFormatError}. Free the effect with {@link disposeEffect}.
|
|
498
582
|
*/
|
|
499
|
-
parse(json:
|
|
583
|
+
parse(json: unknown, baseUrl: string = ''): TransformNode {
|
|
500
584
|
return this.parseDocument(json, baseUrl).root;
|
|
501
585
|
}
|
|
502
586
|
|
|
503
587
|
/** {@link parse}, and the textures the effect uses. */
|
|
504
|
-
private parseDocument(json:
|
|
505
|
-
|
|
588
|
+
private parseDocument(json: unknown, baseUrl: string): {root: TransformNode; textures: Texture[]} {
|
|
589
|
+
const {strict, onFormatWarning} = this.options;
|
|
590
|
+
const problems: string[] = [];
|
|
591
|
+
const onWarning = (message: string, path: string): void => {
|
|
592
|
+
const located = path ? `${message} (at ${path})` : message;
|
|
593
|
+
if (strict) problems.push(located);
|
|
594
|
+
if (onFormatWarning) onFormatWarning(message, path);
|
|
595
|
+
else if (!strict) console.warn(`[quarks] ${located}`);
|
|
596
|
+
};
|
|
597
|
+
const loaded = readFormat({onWarning}, () => this.readDocument(json, baseUrl));
|
|
598
|
+
if (problems.length > 0) {
|
|
599
|
+
// The whole effect is read first, so a strict load frees what it built the same way any
|
|
600
|
+
// other effect is freed, and reports how many parts failed.
|
|
601
|
+
QuarksLoader.disposeEffect(loaded.root);
|
|
602
|
+
const more = problems.length > 1 ? ` (and ${problems.length - 1} more)` : '';
|
|
603
|
+
throw new QuarksFormatError(`${problems[0]}${more}`, problems);
|
|
604
|
+
}
|
|
605
|
+
return loaded;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** {@link parseDocument} inside its format read. */
|
|
609
|
+
private readDocument(document: unknown, baseUrl: string): {root: TransformNode; textures: Texture[]} {
|
|
610
|
+
if (!isEffectDocument(document)) {
|
|
506
611
|
throw new QuarksFormatError('[QuarksLoader] Not an effect document: it has no "object" to load.');
|
|
507
612
|
}
|
|
613
|
+
const json = document;
|
|
508
614
|
const format = json.metadata?.quarksFormat;
|
|
509
615
|
if (typeof format === 'number' && format > QUARKS_FORMAT_VERSION) {
|
|
510
|
-
|
|
511
|
-
`
|
|
616
|
+
warnFormat(
|
|
617
|
+
`This effect was written in format ${format}, newer than the ${QUARKS_FORMAT_VERSION} ` +
|
|
512
618
|
'this version of babylon.quarks reads; settings it does not know are left out.'
|
|
513
619
|
);
|
|
514
620
|
}
|
|
@@ -519,29 +625,31 @@ export class QuarksLoader {
|
|
|
519
625
|
textures: {},
|
|
520
626
|
geometries: {},
|
|
521
627
|
materials: {},
|
|
628
|
+
format: typeof format === 'number' ? format : 0,
|
|
522
629
|
};
|
|
523
630
|
|
|
524
|
-
|
|
525
|
-
|
|
631
|
+
const {geometries, images, textures: textureList, materials} = json;
|
|
632
|
+
if (geometries) {
|
|
633
|
+
readFormatPart('geometries', () => this.parseGeometries(documentEntries(geometries), meta));
|
|
526
634
|
}
|
|
527
|
-
if (
|
|
528
|
-
this.parseImages(
|
|
635
|
+
if (images) {
|
|
636
|
+
readFormatPart('images', () => this.parseImages(documentEntries(images), baseUrl, meta));
|
|
529
637
|
}
|
|
530
|
-
if (
|
|
531
|
-
this.parseTextures(
|
|
638
|
+
if (textureList) {
|
|
639
|
+
readFormatPart('textures', () => this.parseTextures(documentEntries(textureList), meta));
|
|
532
640
|
}
|
|
533
|
-
if (
|
|
534
|
-
this.parseMaterials(
|
|
641
|
+
if (materials) {
|
|
642
|
+
readFormatPart('materials', () => this.parseMaterials(documentEntries(materials), meta));
|
|
535
643
|
}
|
|
536
644
|
|
|
537
|
-
const root = this.parseObject(json.object, meta);
|
|
645
|
+
const root = readFormatPart('object', () => this.parseObject(json.object, meta));
|
|
538
646
|
this.linkReferences(root);
|
|
539
647
|
const textures = [...new Set(Object.values(meta.textures).filter((t): t is Texture => t !== null))];
|
|
540
648
|
retainEffectResources(root, textures, meta.ownedMaterials);
|
|
541
649
|
return {root, textures};
|
|
542
650
|
}
|
|
543
651
|
|
|
544
|
-
private parseGeometries(geometries:
|
|
652
|
+
private parseGeometries(geometries: EffectGeometryJSON[], meta: LoadedMeta): void {
|
|
545
653
|
for (const geom of geometries) {
|
|
546
654
|
if (geom.type === 'PlaneGeometry') {
|
|
547
655
|
const w = geom.width || 1;
|
|
@@ -561,8 +669,8 @@ export class QuarksLoader {
|
|
|
561
669
|
meta.geometries[geom.uuid] = parsed;
|
|
562
670
|
} else if (geom.type === 'QuarksGeometry') {
|
|
563
671
|
if (typeof geom.url === 'string' && !geom.positions) {
|
|
564
|
-
|
|
565
|
-
`
|
|
672
|
+
warnFormat(
|
|
673
|
+
`Geometry "${geom.url}" is an external file; use load(), ` +
|
|
566
674
|
'loadFromJSON(), or resolveResources() before parse().'
|
|
567
675
|
);
|
|
568
676
|
}
|
|
@@ -578,7 +686,7 @@ export class QuarksLoader {
|
|
|
578
686
|
}
|
|
579
687
|
|
|
580
688
|
/** The scene's copy of an identical geometry when resources are shared, else `geometry` itself. */
|
|
581
|
-
private share(geometry:
|
|
689
|
+
private share(geometry: GeometryArrays): GeometryArrays {
|
|
582
690
|
if (this.options.shareResources === false) {
|
|
583
691
|
return geometry;
|
|
584
692
|
}
|
|
@@ -611,7 +719,7 @@ export class QuarksLoader {
|
|
|
611
719
|
return baseUrl + url;
|
|
612
720
|
}
|
|
613
721
|
|
|
614
|
-
private parseSphereGeometry(geom:
|
|
722
|
+
private parseSphereGeometry(geom: SphereGeometryJSON): GeometryArrays {
|
|
615
723
|
const radius = geom.radius ?? 0.5;
|
|
616
724
|
const widthSegments = Math.max(3, Math.floor(geom.widthSegments ?? 16));
|
|
617
725
|
const heightSegments = Math.max(2, Math.floor(geom.heightSegments ?? 8));
|
|
@@ -681,8 +789,8 @@ export class QuarksLoader {
|
|
|
681
789
|
};
|
|
682
790
|
}
|
|
683
791
|
|
|
684
|
-
private parseBufferGeometry(data:
|
|
685
|
-
let positions = new Float32Array(0);
|
|
792
|
+
private parseBufferGeometry(data: BufferGeometryDataJSON): GeometryArrays {
|
|
793
|
+
let positions: Float32Array = new Float32Array(0);
|
|
686
794
|
let indices: Uint32Array | Uint16Array = new Uint32Array(0);
|
|
687
795
|
let uvs: Float32Array | undefined;
|
|
688
796
|
let uvs2: Float32Array | undefined;
|
|
@@ -692,7 +800,7 @@ export class QuarksLoader {
|
|
|
692
800
|
|
|
693
801
|
if (data.interleavedBuffers && data.arrayBuffers) {
|
|
694
802
|
const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
|
|
695
|
-
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)
|
|
803
|
+
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)) {
|
|
696
804
|
const arrayBuffer = data.arrayBuffers[ibDef.buffer];
|
|
697
805
|
if (arrayBuffer) {
|
|
698
806
|
const floatArray = new Float32Array(
|
|
@@ -702,35 +810,17 @@ export class QuarksLoader {
|
|
|
702
810
|
}
|
|
703
811
|
}
|
|
704
812
|
|
|
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
|
-
}
|
|
813
|
+
const readInterleaved = (attr: BufferAttributeJSON | undefined): Float32Array | undefined => {
|
|
814
|
+
const buf =
|
|
815
|
+
attr?.isInterleavedBufferAttribute && attr.data !== undefined ? buffers[attr.data] : undefined;
|
|
816
|
+
return buf
|
|
817
|
+
? this.extractInterleavedAttribute(buf.data, buf.stride, attr?.offset, attr?.itemSize)
|
|
818
|
+
: undefined;
|
|
819
|
+
};
|
|
820
|
+
positions = readInterleaved(data.attributes?.position) ?? positions;
|
|
821
|
+
uvs = readInterleaved(data.attributes?.uv);
|
|
822
|
+
uvs2 = readInterleaved(uv2Attribute);
|
|
823
|
+
normals = readInterleaved(data.attributes?.normal);
|
|
734
824
|
} else if (data.attributes) {
|
|
735
825
|
if (data.attributes.position?.array) {
|
|
736
826
|
positions = new Float32Array(data.attributes.position.array);
|
|
@@ -759,7 +849,12 @@ export class QuarksLoader {
|
|
|
759
849
|
return {positions, indices, uvs, uvs2, normals};
|
|
760
850
|
}
|
|
761
851
|
|
|
762
|
-
private extractInterleavedAttribute(
|
|
852
|
+
private extractInterleavedAttribute(
|
|
853
|
+
buffer: Float32Array,
|
|
854
|
+
stride: number,
|
|
855
|
+
offset: number = 0,
|
|
856
|
+
itemSize: number = 0
|
|
857
|
+
): Float32Array {
|
|
763
858
|
const count = Math.floor(buffer.length / stride);
|
|
764
859
|
const result = new Float32Array(count * itemSize);
|
|
765
860
|
for (let i = 0; i < count; i++) {
|
|
@@ -776,7 +871,7 @@ export class QuarksLoader {
|
|
|
776
871
|
for (let i = 0; i < intArray.length; i++) {
|
|
777
872
|
int32View[i] = intArray[i];
|
|
778
873
|
}
|
|
779
|
-
return new Float32Array(buffer)
|
|
874
|
+
return new Float32Array(buffer);
|
|
780
875
|
}
|
|
781
876
|
|
|
782
877
|
private async resolveResourceUrl(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
@@ -784,7 +879,7 @@ export class QuarksLoader {
|
|
|
784
879
|
}
|
|
785
880
|
|
|
786
881
|
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
787
|
-
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json:
|
|
882
|
+
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json: unknown; baseUrl: string}> {
|
|
788
883
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
789
884
|
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
790
885
|
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined, signal);
|
|
@@ -796,7 +891,7 @@ export class QuarksLoader {
|
|
|
796
891
|
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
797
892
|
*/
|
|
798
893
|
private async loadDocuments(
|
|
799
|
-
docs: Array<{json:
|
|
894
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
800
895
|
options: QuarksBatchLoadOptions
|
|
801
896
|
): Promise<(TransformNode | null)[]> {
|
|
802
897
|
const total = docs.length;
|
|
@@ -816,7 +911,7 @@ export class QuarksLoader {
|
|
|
816
911
|
}
|
|
817
912
|
|
|
818
913
|
private async loadResolvedDocuments(
|
|
819
|
-
docs: Array<{json:
|
|
914
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
820
915
|
options: QuarksBatchLoadOptions,
|
|
821
916
|
stopAll: AbortController
|
|
822
917
|
): Promise<(TransformNode | null)[]> {
|
|
@@ -976,31 +1071,32 @@ export class QuarksLoader {
|
|
|
976
1071
|
}
|
|
977
1072
|
|
|
978
1073
|
private static async inlineGeometryEntries(
|
|
979
|
-
json:
|
|
980
|
-
loadGeometry: (url: string) => Promise<
|
|
1074
|
+
json: unknown,
|
|
1075
|
+
loadGeometry: (url: string) => Promise<unknown>,
|
|
981
1076
|
onResourceError: QuarksLoaderOptions['onResourceError'] = QuarksLoader.warnResourceError
|
|
982
1077
|
): Promise<void> {
|
|
983
|
-
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
984
1078
|
await Promise.all(
|
|
985
|
-
geometries
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1079
|
+
documentList(json, 'geometries').map(async (geom) => {
|
|
1080
|
+
if (!isRecord(geom) || typeof geom.url !== 'string' || geom.positions) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const url = geom.url;
|
|
1084
|
+
try {
|
|
1085
|
+
const data = await loadGeometry(url);
|
|
1086
|
+
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
1087
|
+
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
1088
|
+
for (const key of ['positions', 'indices', 'uvs', 'uvs2', 'normals']) {
|
|
1089
|
+
const value = isRecord(data) ? data[key] : undefined;
|
|
1090
|
+
if (value === undefined) {
|
|
1091
|
+
continue;
|
|
998
1092
|
}
|
|
999
|
-
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
onResourceError('geometry', geom.url, err);
|
|
1093
|
+
geom[key] = Array.isArray(value) ? value.slice() : value;
|
|
1002
1094
|
}
|
|
1003
|
-
|
|
1095
|
+
delete geom.url;
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
onResourceError('geometry', url, err);
|
|
1098
|
+
}
|
|
1099
|
+
})
|
|
1004
1100
|
);
|
|
1005
1101
|
}
|
|
1006
1102
|
|
|
@@ -1012,17 +1108,18 @@ export class QuarksLoader {
|
|
|
1012
1108
|
(this.options.onResourceError ?? QuarksLoader.warnResourceError)(kind, url, error);
|
|
1013
1109
|
}
|
|
1014
1110
|
|
|
1015
|
-
private parseImages(images:
|
|
1016
|
-
for (const img of images) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1111
|
+
private parseImages(images: EffectImageJSON[], baseUrl: string, meta: LoadedMeta): void {
|
|
1112
|
+
for (const [index, img] of images.entries()) {
|
|
1113
|
+
const url = img.url;
|
|
1114
|
+
if (url) {
|
|
1115
|
+
readFormatPart(`[${index}]`, () => QuarksLoader.warnIfNotAnImageUrl(url));
|
|
1019
1116
|
}
|
|
1020
1117
|
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
1021
1118
|
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
1022
|
-
meta.images[img.uuid] =
|
|
1119
|
+
meta.images[img.uuid] = url ? QuarksLoader.joinUrl(baseUrl, url) : null;
|
|
1023
1120
|
// The url the file wrote, for serializeEffect to write back. An inlined image
|
|
1024
1121
|
// (`data:`, `blob:`) has no path to write; its texture writes its url.
|
|
1025
|
-
const source
|
|
1122
|
+
const source = sourceImageUrls.get(img) ?? url;
|
|
1026
1123
|
if (source && !/^(data|blob):/i.test(source)) {
|
|
1027
1124
|
meta.imageSources[img.uuid] = source;
|
|
1028
1125
|
}
|
|
@@ -1042,15 +1139,15 @@ export class QuarksLoader {
|
|
|
1042
1139
|
if (/^(data|blob):/i.test(url) || /\.(png|jpe?g|webp|gif|bmp|ktx2?|dds|env|basis)(\?|#|$)/i.test(url)) {
|
|
1043
1140
|
return;
|
|
1044
1141
|
}
|
|
1045
|
-
|
|
1046
|
-
`
|
|
1142
|
+
warnFormat(
|
|
1143
|
+
`Image "${url}" does not look like an image; its texture will fail to load. ` +
|
|
1047
1144
|
'This usually means the effect was exported without embedding that texture.'
|
|
1048
1145
|
);
|
|
1049
1146
|
}
|
|
1050
1147
|
|
|
1051
|
-
private parseTextures(textures:
|
|
1148
|
+
private parseTextures(textures: QuarksTextureJSON[], meta: LoadedMeta): void {
|
|
1052
1149
|
for (const texDef of textures) {
|
|
1053
|
-
const imageUrl = meta.images[texDef.image];
|
|
1150
|
+
const imageUrl = texDef.image !== undefined ? meta.images[texDef.image] : undefined;
|
|
1054
1151
|
if (imageUrl) {
|
|
1055
1152
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
1056
1153
|
const noMipmap = !!texDef.noMipmap;
|
|
@@ -1105,7 +1202,7 @@ export class QuarksLoader {
|
|
|
1105
1202
|
continue;
|
|
1106
1203
|
}
|
|
1107
1204
|
textureSettled.set(texture, settled);
|
|
1108
|
-
const source = meta.imageSources[texDef.image];
|
|
1205
|
+
const source = texDef.image !== undefined ? meta.imageSources[texDef.image] : undefined;
|
|
1109
1206
|
if (source !== undefined) {
|
|
1110
1207
|
setTextureSourceUrl(texture, source, imageUrl);
|
|
1111
1208
|
}
|
|
@@ -1154,9 +1251,12 @@ export class QuarksLoader {
|
|
|
1154
1251
|
}
|
|
1155
1252
|
}
|
|
1156
1253
|
|
|
1157
|
-
private parseMaterials(materials:
|
|
1254
|
+
private parseMaterials(materials: QuarksMaterialJSON[], meta: LoadedMeta): void {
|
|
1255
|
+
// A file names a texture by uuid; anything else in a slot is no texture.
|
|
1256
|
+
const texture = (ref: MaterialTextureRef | undefined): Texture | null =>
|
|
1257
|
+
typeof ref === 'string' ? meta.textures[ref] || null : null;
|
|
1158
1258
|
for (const matDef of materials) {
|
|
1159
|
-
const matInfo:
|
|
1259
|
+
const matInfo: QuarksMaterialJSON = {
|
|
1160
1260
|
uuid: matDef.uuid,
|
|
1161
1261
|
transparent: matDef.transparent ?? true,
|
|
1162
1262
|
blending: matDef.blending ?? 1,
|
|
@@ -1171,12 +1271,12 @@ export class QuarksLoader {
|
|
|
1171
1271
|
// emits) uses `texture`. Accept both so library-exported effects round-trip.
|
|
1172
1272
|
const textureRef = matDef.map ?? matDef.texture;
|
|
1173
1273
|
if (textureRef) {
|
|
1174
|
-
matInfo.texture =
|
|
1274
|
+
matInfo.texture = texture(textureRef);
|
|
1175
1275
|
}
|
|
1176
1276
|
|
|
1177
1277
|
const reflectionAtlasRef = matDef.reflectionAtlas;
|
|
1178
1278
|
if (reflectionAtlasRef) {
|
|
1179
|
-
matInfo.reflectionAtlas =
|
|
1279
|
+
matInfo.reflectionAtlas = texture(reflectionAtlasRef);
|
|
1180
1280
|
}
|
|
1181
1281
|
if (typeof matDef.reflectionLevel === 'number') {
|
|
1182
1282
|
matInfo.reflectionLevel = matDef.reflectionLevel;
|
|
@@ -1199,20 +1299,20 @@ export class QuarksLoader {
|
|
|
1199
1299
|
if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
|
|
1200
1300
|
matInfo.gradientMap = {
|
|
1201
1301
|
...matDef.gradientMap,
|
|
1202
|
-
ramp: matDef.gradientMap.ramp
|
|
1302
|
+
ramp: texture(matDef.gradientMap.ramp),
|
|
1203
1303
|
};
|
|
1204
1304
|
}
|
|
1205
1305
|
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
1206
1306
|
matInfo.erosion = {
|
|
1207
1307
|
...matDef.erosion,
|
|
1208
|
-
mask: matDef.erosion.mask
|
|
1308
|
+
mask: texture(matDef.erosion.mask),
|
|
1209
1309
|
};
|
|
1210
1310
|
}
|
|
1211
1311
|
// uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
|
|
1212
1312
|
if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
|
|
1213
1313
|
matInfo.uvDistortion = {
|
|
1214
1314
|
...matDef.uvDistortion,
|
|
1215
|
-
map: matDef.uvDistortion.map
|
|
1315
|
+
map: texture(matDef.uvDistortion.map),
|
|
1216
1316
|
};
|
|
1217
1317
|
}
|
|
1218
1318
|
|
|
@@ -1230,19 +1330,19 @@ export class QuarksLoader {
|
|
|
1230
1330
|
3: Constants.ALPHA_SUBTRACT,
|
|
1231
1331
|
4: Constants.ALPHA_MULTIPLY,
|
|
1232
1332
|
};
|
|
1233
|
-
matInfo.alphaMode = blendingToAlphaMode[matDef.blending] || Constants.ALPHA_COMBINE;
|
|
1333
|
+
matInfo.alphaMode = blendingToAlphaMode[matDef.blending ?? 1] || Constants.ALPHA_COMBINE;
|
|
1234
1334
|
}
|
|
1235
1335
|
|
|
1236
1336
|
meta.materials[matDef.uuid] = matInfo;
|
|
1237
1337
|
}
|
|
1238
1338
|
}
|
|
1239
1339
|
|
|
1240
|
-
private getGeometry(meta: LoadedMeta, geometryId: string | undefined):
|
|
1340
|
+
private getGeometry(meta: LoadedMeta, geometryId: string | undefined): GeometryArrays | undefined {
|
|
1241
1341
|
if (!geometryId) return undefined;
|
|
1242
1342
|
return meta.geometries[geometryId];
|
|
1243
1343
|
}
|
|
1244
1344
|
|
|
1245
|
-
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined):
|
|
1345
|
+
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined): QuarksMaterialJSON | undefined {
|
|
1246
1346
|
if (!materialId) return undefined;
|
|
1247
1347
|
const selectedId = Array.isArray(materialId) ? materialId[0] : materialId;
|
|
1248
1348
|
return meta.materials[selectedId];
|
|
@@ -1250,11 +1350,12 @@ export class QuarksLoader {
|
|
|
1250
1350
|
|
|
1251
1351
|
private createPlaceholderNode(type: string, name?: string): TransformNode {
|
|
1252
1352
|
const node = new TransformNode(name || type || 'node', this.scene);
|
|
1253
|
-
|
|
1353
|
+
// The three.js type the node stood for, which babylon.quarks has no node for.
|
|
1354
|
+
Object.assign(node, {quarksOriginalType: type});
|
|
1254
1355
|
return node;
|
|
1255
1356
|
}
|
|
1256
1357
|
|
|
1257
|
-
private createMeshNode(data:
|
|
1358
|
+
private createMeshNode(data: QuarksNodeJSON, meta: LoadedMeta): Mesh {
|
|
1258
1359
|
const mesh = new Mesh(data.name || data.type || 'mesh', this.scene);
|
|
1259
1360
|
const geometry = this.getGeometry(meta, data.geometry);
|
|
1260
1361
|
if (geometry && geometry.positions.length > 0 && geometry.indices.length > 0) {
|
|
@@ -1281,16 +1382,17 @@ export class QuarksLoader {
|
|
|
1281
1382
|
material.backFaceCulling = materialInfo.side !== 2;
|
|
1282
1383
|
material.alphaCutOff = materialInfo.alphaTest ?? 0;
|
|
1283
1384
|
material.disableDepthWrite = !(materialInfo.depthWrite ?? false);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
material.
|
|
1385
|
+
const texture = materialInfo.texture;
|
|
1386
|
+
if (texture && typeof texture !== 'string') {
|
|
1387
|
+
material.diffuseTexture = texture;
|
|
1388
|
+
material.opacityTexture = texture;
|
|
1287
1389
|
}
|
|
1288
1390
|
mesh.material = material;
|
|
1289
1391
|
}
|
|
1290
1392
|
return mesh;
|
|
1291
1393
|
}
|
|
1292
1394
|
|
|
1293
|
-
private parseObject(data:
|
|
1395
|
+
private parseObject(data: QuarksNodeJSON, meta: LoadedMeta): TransformNode {
|
|
1294
1396
|
let node: TransformNode;
|
|
1295
1397
|
switch (data.type) {
|
|
1296
1398
|
case 'QuarksPrefab':
|
|
@@ -1298,7 +1400,8 @@ export class QuarksLoader {
|
|
|
1298
1400
|
break;
|
|
1299
1401
|
case 'ParticleEmitter':
|
|
1300
1402
|
if (data.ps) {
|
|
1301
|
-
const
|
|
1403
|
+
const psJSON = data.ps;
|
|
1404
|
+
const ps = readFormatPart('ps', () => this.parseParticleSystem(psJSON, meta));
|
|
1302
1405
|
node = ps.emitter;
|
|
1303
1406
|
break;
|
|
1304
1407
|
}
|
|
@@ -1336,7 +1439,7 @@ export class QuarksLoader {
|
|
|
1336
1439
|
node = new TransformNode(data.name || data.type || 'node', this.scene);
|
|
1337
1440
|
}
|
|
1338
1441
|
|
|
1339
|
-
if (data.uuid) (node
|
|
1442
|
+
if (data.uuid) setQuarksNodeId(node, data.uuid);
|
|
1340
1443
|
if (data.name) node.name = data.name;
|
|
1341
1444
|
|
|
1342
1445
|
if (data.matrix) {
|
|
@@ -1367,19 +1470,37 @@ export class QuarksLoader {
|
|
|
1367
1470
|
if (data.isVisible === false && node instanceof Mesh) node.isVisible = false;
|
|
1368
1471
|
|
|
1369
1472
|
if (data.children) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1473
|
+
const children: unknown = data.children;
|
|
1474
|
+
if (!Array.isArray(children)) {
|
|
1475
|
+
readFormatPart('children', () => warnFormat('"children" is not a list; the node loads without them.'));
|
|
1476
|
+
} else {
|
|
1477
|
+
children.forEach((childData: unknown, index) => {
|
|
1478
|
+
const child = readFormatPart(`children[${index}]`, () =>
|
|
1479
|
+
isNodeJSON(childData)
|
|
1480
|
+
? this.parseObject(childData, meta)
|
|
1481
|
+
: warnFormat('A child is not an object; it is skipped.')
|
|
1482
|
+
);
|
|
1483
|
+
if (child) child.parent = node;
|
|
1484
|
+
});
|
|
1373
1485
|
}
|
|
1374
1486
|
}
|
|
1375
1487
|
|
|
1376
1488
|
return node;
|
|
1377
1489
|
}
|
|
1378
1490
|
|
|
1379
|
-
private parseParticleSystem(json:
|
|
1491
|
+
private parseParticleSystem(json: ParticleSystemJSONParameters, meta: LoadedMeta): ParticleSystem {
|
|
1492
|
+
if (meta.format < 2 && json !== null && typeof json === 'object' && json.scalingMode !== 'hierarchy') {
|
|
1493
|
+
// Before format 2 the emitter's whole world scale reached every system's shape and
|
|
1494
|
+
// particle size, and a local-space system's motion too: 'hierarchy' is the mode that
|
|
1495
|
+
// keeps such an effect's look. 'local' became the default with Unity's meaning.
|
|
1496
|
+
json = {...json, scalingMode: 'hierarchy'};
|
|
1497
|
+
}
|
|
1380
1498
|
const dependencies: {[uuid: string]: Behavior} = {};
|
|
1381
|
-
const ps = ParticleSystem.fromJSON(json, meta
|
|
1382
|
-
(
|
|
1499
|
+
const ps = ParticleSystem.fromJSON(json, meta, dependencies, this.scene);
|
|
1500
|
+
if (json?.shape?.type === 'mesh_surface' && typeof json.shape.mesh === 'string') {
|
|
1501
|
+
meshSurfaceLinks.set(ps, json.shape.mesh);
|
|
1502
|
+
}
|
|
1503
|
+
systemPaths.set(ps, currentFormatPath());
|
|
1383
1504
|
return ps;
|
|
1384
1505
|
}
|
|
1385
1506
|
|
|
@@ -1387,8 +1508,9 @@ export class QuarksLoader {
|
|
|
1387
1508
|
const nodesMap: {[uuid: string]: TransformNode} = {};
|
|
1388
1509
|
|
|
1389
1510
|
const traverse = (node: TransformNode) => {
|
|
1390
|
-
|
|
1391
|
-
|
|
1511
|
+
const fileId = fileNodeId(node);
|
|
1512
|
+
if (fileId) {
|
|
1513
|
+
nodesMap[fileId] = node;
|
|
1392
1514
|
}
|
|
1393
1515
|
for (const child of node.getChildren()) {
|
|
1394
1516
|
if (child instanceof TransformNode) {
|
|
@@ -1404,14 +1526,19 @@ export class QuarksLoader {
|
|
|
1404
1526
|
}
|
|
1405
1527
|
if (node instanceof ParticleEmitter) {
|
|
1406
1528
|
const system = node.system as ParticleSystem;
|
|
1407
|
-
|
|
1408
|
-
|
|
1529
|
+
// Links are made once the whole effect is read; a warning names the system's path.
|
|
1530
|
+
const path = systemPaths.get(system);
|
|
1531
|
+
const warnAtSystem = (message: string) =>
|
|
1532
|
+
path ? readFormatPart(path, () => warnFormat(message)) : warnFormat(message);
|
|
1533
|
+
const meshSurfaceUUID = meshSurfaceLinks.get(system);
|
|
1534
|
+
const shape = system.emitterShape;
|
|
1535
|
+
if (meshSurfaceUUID && shape instanceof MeshSurfaceEmitter) {
|
|
1409
1536
|
const targetNode = nodesMap[meshSurfaceUUID];
|
|
1410
1537
|
if (targetNode instanceof Mesh) {
|
|
1411
|
-
|
|
1538
|
+
shape.mesh = targetNode;
|
|
1412
1539
|
} else {
|
|
1413
|
-
|
|
1414
|
-
`
|
|
1540
|
+
warnAtSystem(
|
|
1541
|
+
`Emitter "${node.name}" has a mesh surface shape on "${meshSurfaceUUID}", ` +
|
|
1415
1542
|
(targetNode ? 'which is not a mesh' : 'which is not in the effect') +
|
|
1416
1543
|
'; it emits nothing until a mesh is set.'
|
|
1417
1544
|
);
|
|
@@ -1420,13 +1547,13 @@ export class QuarksLoader {
|
|
|
1420
1547
|
for (let i = 0; i < system.behaviors.length; i++) {
|
|
1421
1548
|
if (system.behaviors[i] instanceof EmitSubParticleSystem) {
|
|
1422
1549
|
const subEmitter = system.behaviors[i] as EmitSubParticleSystem;
|
|
1423
|
-
const targetUUID = subEmitter.subParticleSystem
|
|
1550
|
+
const targetUUID = subEmitter.subParticleSystem;
|
|
1424
1551
|
if (typeof targetUUID !== 'string') {
|
|
1425
1552
|
continue;
|
|
1426
1553
|
}
|
|
1427
1554
|
const resolved = nodesMap[targetUUID];
|
|
1428
1555
|
if (resolved instanceof ParticleEmitter) {
|
|
1429
|
-
|
|
1556
|
+
subEmitter.subParticleSystem = resolved;
|
|
1430
1557
|
(resolved.system as ParticleSystem).onlyUsedByOther = true;
|
|
1431
1558
|
} else {
|
|
1432
1559
|
// An export that dropped the target emitter, or kept only
|
|
@@ -1434,8 +1561,8 @@ export class QuarksLoader {
|
|
|
1434
1561
|
// a string the behavior would treat as an emitter.
|
|
1435
1562
|
subEmitter.subParticleSystem = undefined;
|
|
1436
1563
|
if (targetUUID !== '') {
|
|
1437
|
-
|
|
1438
|
-
`
|
|
1564
|
+
warnAtSystem(
|
|
1565
|
+
`Emitter "${node.name}" has a sub emitter targeting "${targetUUID}", ` +
|
|
1439
1566
|
(resolved ? 'which is not a particle emitter' : 'which is not in the effect') +
|
|
1440
1567
|
'; that sub emitter is skipped.'
|
|
1441
1568
|
);
|