babylon.quarks 0.26.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 +1194 -8
- package/README.md +13 -9
- 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 +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/src/QuarksLoader.ts
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
2
|
+
import type {ITextureCreationOptions} from '@babylonjs/core/Materials/Textures/texture';
|
|
2
3
|
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
4
|
+
import type {Material} from '@babylonjs/core/Materials/material';
|
|
3
5
|
import {StandardMaterial} from '@babylonjs/core/Materials/standardMaterial';
|
|
4
6
|
import {Matrix, Quaternion, Vector3} from '@babylonjs/core/Maths/math.vector';
|
|
5
7
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
6
8
|
import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
7
9
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
8
10
|
import {Scene} from '@babylonjs/core/scene';
|
|
9
|
-
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';
|
|
10
21
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
11
22
|
import {ParticleSystem} from './ParticleSystem';
|
|
12
23
|
import {QuarksPrefab} from './QuarksPrefab';
|
|
24
|
+
import type {QuarksNodeJSON, QuarksTextureJSON} from './QuarksSerializer';
|
|
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';
|
|
37
|
+
import {disposeEffect, retainEffectResources} from './effectResources';
|
|
13
38
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
39
|
+
import {fileNodeId, setQuarksNodeId} from './nodeId';
|
|
40
|
+
import type {ParticleSystemJSONParameters} from './system/types';
|
|
41
|
+
import {setTextureSourceUrl} from './textureSource';
|
|
14
42
|
|
|
15
43
|
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
16
44
|
export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
@@ -22,11 +50,32 @@ export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
|
22
50
|
export interface QuarksResourceResolver {
|
|
23
51
|
/** Resolve a relative url to an absolute / blob / data url. Default: {@link QuarksLoader.joinUrl}. */
|
|
24
52
|
resolve?(url: string, kind: QuarksResourceKind, baseUrl: string): string | Promise<string>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Load a JSON asset (effect and geometry files). Default: `fetch(resolvedUrl).json()`.
|
|
55
|
+
* `options.signal` is passed for an effect document, whose fetch belongs to one load; geometry
|
|
56
|
+
* fetches are shared between loads and get none.
|
|
57
|
+
*/
|
|
58
|
+
fetchJson?(resolvedUrl: string, options?: {signal?: AbortSignal}): Promise<unknown>;
|
|
59
|
+
}
|
|
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'];
|
|
27
71
|
}
|
|
28
72
|
|
|
73
|
+
/** Options for a {@link QuarksLoader}, shared by every load it does. */
|
|
29
74
|
export interface QuarksLoaderOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Prefix joined to relative geometry and image urls (see {@link QuarksLoader.joinUrl}).
|
|
77
|
+
* Unset, {@link QuarksLoader.load} uses the effect file's own folder and the other loads `''`.
|
|
78
|
+
*/
|
|
30
79
|
baseUrl?: string;
|
|
31
80
|
/**
|
|
32
81
|
* Reuse one Texture per scene for every effect that references the same image with the same
|
|
@@ -39,10 +88,50 @@ export interface QuarksLoaderOptions {
|
|
|
39
88
|
shareResources?: boolean;
|
|
40
89
|
/** Custom url / fetch backend (File picker, CDN, IndexedDB). */
|
|
41
90
|
resolver?: QuarksResourceResolver;
|
|
91
|
+
/**
|
|
92
|
+
* Makes `load`, `loadFromJSON`, `loadAll` and `loadFromJSONAll` resolve only once every texture
|
|
93
|
+
* the effect uses has loaded or failed, so it does not draw its first frames without them.
|
|
94
|
+
* A texture a scene with `useDelayedTextureLoading` would load only once drawn is loaded at
|
|
95
|
+
* once. `parse` never waits. Default false: they resolve when the nodes are built.
|
|
96
|
+
*/
|
|
97
|
+
waitForTextures?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Called for a geometry file or image that fails to load. The effect still loads: a geometry
|
|
100
|
+
* keeps its link unresolved, and a texture draws nothing. Default: a console warning.
|
|
101
|
+
*/
|
|
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;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Options for one {@link QuarksLoader.load} or {@link QuarksLoader.loadFromJSON}. */
|
|
122
|
+
export interface QuarksLoadOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Cancels the load, e.g. on a level change. The promise rejects with the signal's reason as
|
|
125
|
+
* soon as it is aborted, and no node of the effect is left in the scene. The effect document's
|
|
126
|
+
* own fetch is cancelled; a geometry fetch shared with other loads runs on, and its result is
|
|
127
|
+
* dropped. A scene disposed during the load rejects it the same way.
|
|
128
|
+
*/
|
|
129
|
+
signal?: AbortSignal;
|
|
42
130
|
}
|
|
43
131
|
|
|
44
132
|
/** One in-memory effect for {@link QuarksLoader.loadFromJSONAll}. */
|
|
45
133
|
export interface QuarksJSONLoadEntry {
|
|
134
|
+
/** The effect document, as {@link serializeEffect} or the editor writes it. Left unchanged. */
|
|
46
135
|
json: unknown;
|
|
47
136
|
/** Overrides loader `baseUrl` for relative texture / geometry links in this effect. */
|
|
48
137
|
baseUrl?: string;
|
|
@@ -53,7 +142,7 @@ export interface QuarksJSONLoadEntry {
|
|
|
53
142
|
* Batch loads share the per-scene fetch cache and `shareResources`, so N effects that point at
|
|
54
143
|
* the same `assets/geometries/*.json` or texture only pay for that asset once.
|
|
55
144
|
*/
|
|
56
|
-
export interface QuarksBatchLoadOptions {
|
|
145
|
+
export interface QuarksBatchLoadOptions extends QuarksLoadOptions {
|
|
57
146
|
/**
|
|
58
147
|
* Max concurrent effect-document fetches (phase 1). Linked geometry/image resolve still runs
|
|
59
148
|
* across the whole pack in parallel so shared urls coalesce. Default: all at once.
|
|
@@ -71,17 +160,60 @@ export interface QuarksBatchLoadOptions {
|
|
|
71
160
|
continueOnError?: boolean;
|
|
72
161
|
}
|
|
73
162
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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);
|
|
181
|
+
}
|
|
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 : [];
|
|
80
207
|
}
|
|
81
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
|
+
|
|
82
214
|
/** Textures and geometries reused across loads, per scene. See QuarksLoaderOptions.shareResources. */
|
|
83
215
|
const sharedTextures = new WeakMap<Scene, Map<string, Texture>>();
|
|
84
|
-
const sharedGeometries = new WeakMap<Scene, Map<string,
|
|
216
|
+
const sharedGeometries = new WeakMap<Scene, Map<string, GeometryArrays>>();
|
|
85
217
|
/** In-flight / completed JSON fetches keyed by `json|resolvedUrl`, per scene. */
|
|
86
218
|
const sharedFetches = new WeakMap<Scene, Map<string, Promise<unknown>>>();
|
|
87
219
|
|
|
@@ -94,8 +226,14 @@ function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<st
|
|
|
94
226
|
return map;
|
|
95
227
|
}
|
|
96
228
|
|
|
229
|
+
/** Image entries {@link QuarksLoader.resolveLinkedResources} rewrote, with the url it wrote. */
|
|
230
|
+
const resolvedImageUrls = new WeakMap<object, string>();
|
|
231
|
+
|
|
232
|
+
/** Image entries {@link QuarksLoader.resolveLinkedResources} rewrote, with the url the file had. */
|
|
233
|
+
const sourceImageUrls = new WeakMap<object, string>();
|
|
234
|
+
|
|
97
235
|
/** Content key for a geometry: two independent 32-bit hashes over every array's bytes, plus lengths. */
|
|
98
|
-
function geometryKey(geometry:
|
|
236
|
+
function geometryKey(geometry: GeometryArrays): string {
|
|
99
237
|
let h1 = 0x811c9dc5;
|
|
100
238
|
let h2 = 0x9e3779b9;
|
|
101
239
|
const parts: Array<ArrayBufferView | undefined> = [
|
|
@@ -121,12 +259,100 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
121
259
|
return `${lengths}${(h1 >>> 0).toString(16)}${(h2 >>> 0).toString(16)}`;
|
|
122
260
|
}
|
|
123
261
|
|
|
262
|
+
// Defined in quarks.core, whose readers throw it in a strict read; exported here as before.
|
|
263
|
+
export {QuarksFormatError};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A copy of an effect document that loading can rewrite (resolved image urls, inlined geometry
|
|
267
|
+
* files) without touching the caller's. Only the entries that change are copied.
|
|
268
|
+
*/
|
|
269
|
+
function detachDocument(json: unknown): unknown {
|
|
270
|
+
if (!isRecord(json)) {
|
|
271
|
+
return json;
|
|
272
|
+
}
|
|
273
|
+
const copy: JSONRecord = {...json};
|
|
274
|
+
if (Array.isArray(json.images)) {
|
|
275
|
+
copy.images = json.images.map((image: unknown) => {
|
|
276
|
+
if (!image || typeof image !== 'object') {
|
|
277
|
+
return image;
|
|
278
|
+
}
|
|
279
|
+
const imageCopy: {url?: unknown} = {...(image as object)};
|
|
280
|
+
// An entry resolveResources() already rewrote stays resolved in its copy, so loading the
|
|
281
|
+
// document it prepared, with the same base, does not join the base a second time.
|
|
282
|
+
if (resolvedImageUrls.get(image) === imageCopy.url && typeof imageCopy.url === 'string') {
|
|
283
|
+
resolvedImageUrls.set(imageCopy, imageCopy.url);
|
|
284
|
+
const source = sourceImageUrls.get(image);
|
|
285
|
+
if (source !== undefined) {
|
|
286
|
+
sourceImageUrls.set(imageCopy, source);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return imageCopy;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(json.geometries)) {
|
|
293
|
+
copy.geometries = json.geometries.map((geometry: unknown) =>
|
|
294
|
+
geometry && typeof geometry === 'object' ? {...(geometry as object)} : geometry
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return copy;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Settles when a texture the loader made has loaded or failed. */
|
|
301
|
+
const textureSettled = new WeakMap<Texture, Promise<void>>();
|
|
302
|
+
|
|
303
|
+
const abortReason = (signal: AbortSignal) => signal.reason ?? new Error('[QuarksLoader] The load was aborted.');
|
|
304
|
+
const sceneDisposed = () => new Error('[QuarksLoader] The scene was disposed while the effect loaded.');
|
|
305
|
+
|
|
306
|
+
/** Throws the reason a load stops early: its signal was aborted, or its scene disposed. */
|
|
307
|
+
function checkLoad(scene: Scene, signal: AbortSignal | undefined): void {
|
|
308
|
+
if (signal?.aborted) {
|
|
309
|
+
throw abortReason(signal);
|
|
310
|
+
}
|
|
311
|
+
if (scene.isDisposed) {
|
|
312
|
+
throw sceneDisposed();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Follows `promise`, or rejects as {@link checkLoad} would as soon as the load is stopped. */
|
|
317
|
+
function untilStopped<T>(promise: Promise<T>, scene: Scene, signal: AbortSignal | undefined): Promise<T> {
|
|
318
|
+
checkLoad(scene, signal);
|
|
319
|
+
return new Promise<T>((resolve, reject) => {
|
|
320
|
+
const aborted = () => reject(abortReason(signal!));
|
|
321
|
+
signal?.addEventListener('abort', aborted, {once: true});
|
|
322
|
+
const disposed = scene.onDisposeObservable.addOnce(() => reject(sceneDisposed()));
|
|
323
|
+
promise.then(resolve, reject).finally(() => {
|
|
324
|
+
signal?.removeEventListener('abort', aborted);
|
|
325
|
+
scene.onDisposeObservable.remove(disposed);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
124
330
|
interface LoadedMeta {
|
|
331
|
+
/** The materials this load made, which the effect owns. */
|
|
332
|
+
ownedMaterials: Material[];
|
|
333
|
+
/** Image uuid to the url its textures load. */
|
|
334
|
+
images: {[uuid: string]: string | null};
|
|
335
|
+
/** Image uuid to the url the file wrote, before any base url was joined. */
|
|
336
|
+
imageSources: {[uuid: string]: string};
|
|
125
337
|
textures: {[uuid: string]: Texture | null};
|
|
126
|
-
geometries: {[uuid: string]:
|
|
127
|
-
materials: {[uuid: string]:
|
|
338
|
+
geometries: {[uuid: string]: GeometryArrays};
|
|
339
|
+
materials: {[uuid: string]: QuarksMaterialJSON};
|
|
128
340
|
}
|
|
129
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Loads effects saved as JSON into a scene: fetches the document and the geometry and image files
|
|
344
|
+
* it links, builds the nodes, particle systems, textures and materials, and returns the root.
|
|
345
|
+
* Add the systems to a {@link BatchedRenderer} with {@link QuarksUtil.addToBatchRenderer} to draw
|
|
346
|
+
* them, and free the effect with {@link QuarksLoader.disposeEffect}.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* const loader = new QuarksLoader(scene, {waitForTextures: true});
|
|
351
|
+
* const effect = await loader.load('effects/fireball.json');
|
|
352
|
+
* QuarksUtil.addToBatchRenderer(effect, renderer);
|
|
353
|
+
* QuarksUtil.play(effect);
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
130
356
|
export class QuarksLoader {
|
|
131
357
|
private scene: Scene;
|
|
132
358
|
private options: QuarksLoaderOptions;
|
|
@@ -136,20 +362,79 @@ export class QuarksLoader {
|
|
|
136
362
|
this.options = options;
|
|
137
363
|
}
|
|
138
364
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Fetches an effect JSON from `url`, resolves linked sources, and parses it. Free the effect
|
|
367
|
+
* with {@link disposeEffect}.
|
|
368
|
+
*/
|
|
369
|
+
async load(url: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
370
|
+
const {json, baseUrl} = await untilStopped(
|
|
371
|
+
this.fetchEffectDocument(url, options.signal),
|
|
372
|
+
this.scene,
|
|
373
|
+
options.signal
|
|
374
|
+
);
|
|
375
|
+
return this.loadFromJSON(json, baseUrl, options);
|
|
143
376
|
}
|
|
144
377
|
|
|
145
378
|
/**
|
|
146
|
-
* Resolves linked geometry / image urls
|
|
147
|
-
*
|
|
379
|
+
* Resolves linked geometry / image urls (via {@link resolveResources}) then parses. Use when
|
|
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
|
|
383
|
+
* left as it was, so the same document can be loaded again, into another scene or with
|
|
384
|
+
* another base url.
|
|
148
385
|
*/
|
|
149
|
-
async loadFromJSON(json:
|
|
386
|
+
async loadFromJSON(json: unknown, baseUrl?: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
387
|
+
const signal = options.signal;
|
|
388
|
+
checkLoad(this.scene, signal);
|
|
150
389
|
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
151
|
-
|
|
152
|
-
|
|
390
|
+
const document = detachDocument(json);
|
|
391
|
+
await untilStopped(this.resolveResources(document, resolvedBase), this.scene, signal);
|
|
392
|
+
// The urls are resolved now; joining them with the base again would load `fx/fx/...`.
|
|
393
|
+
const loaded = this.parseDocument(document, '');
|
|
394
|
+
await this.finishLoad(loaded, signal);
|
|
395
|
+
return loaded.root;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Disposes an effect a loader built: its nodes and systems, the materials made for it, and
|
|
400
|
+
* each texture it uses once no other loaded effect uses it (textures are shared per scene,
|
|
401
|
+
* see {@link QuarksLoaderOptions.shareResources}). Copies made with
|
|
402
|
+
* `QuarksUtil.instantiate` hold the textures too, so disposing the source effect leaves them
|
|
403
|
+
* drawing. The same happens when the root is disposed any other way (`root.dispose()`, its
|
|
404
|
+
* parent's dispose, the scene's). The materials and textures are freed even when other code
|
|
405
|
+
* was handed them; give that code its own. For a root the loader did not build, this is
|
|
406
|
+
* `root.dispose()`.
|
|
407
|
+
*/
|
|
408
|
+
static disposeEffect(root: TransformNode): void {
|
|
409
|
+
disposeEffect(root);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Waits for the effect's textures when the loader is asked to, and frees the effect when the
|
|
414
|
+
* load is stopped meanwhile.
|
|
415
|
+
*/
|
|
416
|
+
private async finishLoad(loaded: {root: TransformNode; textures: Texture[]}, signal?: AbortSignal): Promise<void> {
|
|
417
|
+
try {
|
|
418
|
+
if (this.options.waitForTextures) {
|
|
419
|
+
for (const texture of loaded.textures) {
|
|
420
|
+
// A delayed texture would load only once drawn, which it is not until the load resolves.
|
|
421
|
+
if (texture.delayLoadState === Constants.DELAYLOADSTATE_NOTLOADED) {
|
|
422
|
+
texture.delayLoad();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await untilStopped(
|
|
426
|
+
Promise.all(loaded.textures.map((texture) => textureSettled.get(texture))),
|
|
427
|
+
this.scene,
|
|
428
|
+
signal
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
checkLoad(this.scene, signal);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (!this.scene.isDisposed) {
|
|
434
|
+
disposeEffect(loaded.root);
|
|
435
|
+
}
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
153
438
|
}
|
|
154
439
|
|
|
155
440
|
/**
|
|
@@ -170,17 +455,21 @@ export class QuarksLoader {
|
|
|
170
455
|
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
171
456
|
): Promise<(TransformNode | null)[]>;
|
|
172
457
|
async loadAll(urls: string[], options: QuarksBatchLoadOptions = {}): Promise<(TransformNode | null)[]> {
|
|
173
|
-
|
|
458
|
+
checkLoad(this.scene, options.signal);
|
|
459
|
+
const fetching = QuarksLoader.mapPool(urls, options.concurrency, async (url, index) => {
|
|
460
|
+
checkLoad(this.scene, options.signal);
|
|
174
461
|
try {
|
|
175
|
-
return await this.fetchEffectDocument(url);
|
|
462
|
+
return await this.fetchEffectDocument(url, options.signal);
|
|
176
463
|
} catch (err) {
|
|
177
|
-
|
|
464
|
+
// A stopped load is never one effect's failure.
|
|
465
|
+
if (!options.continueOnError || options.signal?.aborted || this.scene.isDisposed) {
|
|
178
466
|
throw err;
|
|
179
467
|
}
|
|
180
468
|
console.warn(`[QuarksLoader] Failed to fetch effect "${url}" (${index + 1}/${urls.length}):`, err);
|
|
181
469
|
return null;
|
|
182
470
|
}
|
|
183
471
|
});
|
|
472
|
+
const docs = await untilStopped(fetching, this.scene, options.signal);
|
|
184
473
|
return this.loadDocuments(docs, options);
|
|
185
474
|
}
|
|
186
475
|
|
|
@@ -200,53 +489,72 @@ export class QuarksLoader {
|
|
|
200
489
|
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
201
490
|
options: QuarksBatchLoadOptions = {}
|
|
202
491
|
): Promise<(TransformNode | null)[]> {
|
|
203
|
-
const docs: Array<{json:
|
|
492
|
+
const docs: Array<{json: unknown; baseUrl: string} | null> = entries.map((entry) => {
|
|
493
|
+
// Copied, as loadFromJSON does, so the caller's documents stay as they were.
|
|
204
494
|
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
205
495
|
const typed = entry as QuarksJSONLoadEntry;
|
|
206
|
-
return {json: typed.json
|
|
496
|
+
return {json: detachDocument(typed.json), baseUrl: typed.baseUrl ?? this.options.baseUrl ?? ''};
|
|
207
497
|
}
|
|
208
|
-
return {json: entry
|
|
498
|
+
return {json: detachDocument(entry), baseUrl: this.options.baseUrl ?? ''};
|
|
209
499
|
});
|
|
210
500
|
return this.loadDocuments(docs, options);
|
|
211
501
|
}
|
|
212
502
|
|
|
213
503
|
/**
|
|
214
504
|
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
215
|
-
* {@link QuarksResourceResolver}
|
|
216
|
-
* and
|
|
505
|
+
* {@link QuarksResourceResolver}, in place on `json`. Each document is resolved once:
|
|
506
|
+
* geometries already inlined and image urls this method already rewrote are left alone, so
|
|
507
|
+
* loading the same document again does not join its base url twice. Failures warn and leave
|
|
508
|
+
* the entry as is.
|
|
217
509
|
*/
|
|
218
|
-
async resolveResources(json:
|
|
510
|
+
async resolveResources(json: unknown, baseUrl: string = ''): Promise<void> {
|
|
219
511
|
await QuarksLoader.resolveLinkedResources(json, {
|
|
220
512
|
baseUrl,
|
|
221
513
|
resolver: this.options.resolver,
|
|
222
514
|
scene: this.scene,
|
|
515
|
+
onResourceError: this.options.onResourceError,
|
|
223
516
|
});
|
|
224
517
|
}
|
|
225
518
|
|
|
226
519
|
/**
|
|
227
520
|
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
228
521
|
* editor import that only prepares JSON). Pass `scene` to share the per-scene fetch cache.
|
|
522
|
+
* Like it, resolves each document once. A geometry file that fails to load goes to
|
|
523
|
+
* `onResourceError` (default: a console warning) and keeps its link.
|
|
229
524
|
*/
|
|
230
|
-
static async resolveLinkedResources(
|
|
231
|
-
json: any,
|
|
232
|
-
options: {baseUrl?: string; resolver?: QuarksResourceResolver; scene?: Scene} = {}
|
|
233
|
-
): Promise<void> {
|
|
525
|
+
static async resolveLinkedResources(json: unknown, options: QuarksResolveOptions = {}): Promise<void> {
|
|
234
526
|
const baseUrl = options.baseUrl ?? '';
|
|
235
527
|
const resolver = options.resolver;
|
|
236
528
|
const scene = options.scene;
|
|
237
529
|
|
|
238
|
-
await QuarksLoader.inlineGeometryEntries(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
530
|
+
await QuarksLoader.inlineGeometryEntries(
|
|
531
|
+
json,
|
|
532
|
+
async (geometryUrl) => {
|
|
533
|
+
const resolved = await QuarksLoader.resolveWith(resolver, geometryUrl, 'geometry', baseUrl);
|
|
534
|
+
return QuarksLoader.fetchJsonWith(resolver, resolved, scene);
|
|
535
|
+
},
|
|
536
|
+
options.onResourceError
|
|
537
|
+
);
|
|
242
538
|
|
|
243
|
-
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
244
539
|
await Promise.all(
|
|
245
|
-
images.map(async (img) => {
|
|
246
|
-
if (typeof img
|
|
540
|
+
documentList(json, 'images').map(async (img) => {
|
|
541
|
+
if (!isRecord(img) || typeof img.url !== 'string' || !img.url) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// An entry this loader already rewrote keeps its url: resolving the same document
|
|
545
|
+
// again (a second load, or a fetch cached for several loads) would join it twice.
|
|
546
|
+
if (resolvedImageUrls.get(img) === img.url) {
|
|
247
547
|
return;
|
|
248
548
|
}
|
|
249
|
-
|
|
549
|
+
const source = img.url;
|
|
550
|
+
const url = await QuarksLoader.resolveWith(resolver, source, 'image', baseUrl);
|
|
551
|
+
img.url = url;
|
|
552
|
+
resolvedImageUrls.set(img, url);
|
|
553
|
+
// A resolver that inlined the file (`data:`, `blob:`) made the url the image itself;
|
|
554
|
+
// the file's path no longer names what the texture holds, so a save writes the url.
|
|
555
|
+
if (!/^(data|blob):/i.test(url)) {
|
|
556
|
+
sourceImageUrls.set(img, source);
|
|
557
|
+
}
|
|
250
558
|
})
|
|
251
559
|
);
|
|
252
560
|
}
|
|
@@ -257,36 +565,88 @@ export class QuarksLoader {
|
|
|
257
565
|
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
258
566
|
* `loadGeometry` and no `QuarksLoader` instance.
|
|
259
567
|
*/
|
|
260
|
-
static async resolveExternalGeometries(
|
|
568
|
+
static async resolveExternalGeometries(
|
|
569
|
+
json: unknown,
|
|
570
|
+
loadGeometry: (url: string) => Promise<unknown>
|
|
571
|
+
): Promise<void> {
|
|
261
572
|
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
262
573
|
}
|
|
263
574
|
|
|
264
|
-
|
|
575
|
+
/**
|
|
576
|
+
* Builds the node tree of an effect document whose linked files are already inline (see
|
|
577
|
+
* {@link loadFromJSON}). `baseUrl` is joined to relative image urls. Parts the loader cannot
|
|
578
|
+
* read warn and take their defaults; a document with no `object` throws
|
|
579
|
+
* {@link QuarksFormatError}. Free the effect with {@link disposeEffect}.
|
|
580
|
+
*/
|
|
581
|
+
parse(json: unknown, baseUrl: string = ''): TransformNode {
|
|
582
|
+
return this.parseDocument(json, baseUrl).root;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** {@link parse}, and the textures the effect uses. */
|
|
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)) {
|
|
609
|
+
throw new QuarksFormatError('[QuarksLoader] Not an effect document: it has no "object" to load.');
|
|
610
|
+
}
|
|
611
|
+
const json = document;
|
|
612
|
+
const format = json.metadata?.quarksFormat;
|
|
613
|
+
if (typeof format === 'number' && format > QUARKS_FORMAT_VERSION) {
|
|
614
|
+
warnFormat(
|
|
615
|
+
`This effect was written in format ${format}, newer than the ${QUARKS_FORMAT_VERSION} ` +
|
|
616
|
+
'this version of babylon.quarks reads; settings it does not know are left out.'
|
|
617
|
+
);
|
|
618
|
+
}
|
|
265
619
|
const meta: LoadedMeta = {
|
|
620
|
+
ownedMaterials: [],
|
|
621
|
+
images: {},
|
|
622
|
+
imageSources: {},
|
|
266
623
|
textures: {},
|
|
267
624
|
geometries: {},
|
|
268
625
|
materials: {},
|
|
269
626
|
};
|
|
270
627
|
|
|
271
|
-
|
|
272
|
-
|
|
628
|
+
const {geometries, images, textures: textureList, materials} = json;
|
|
629
|
+
if (geometries) {
|
|
630
|
+
readFormatPart('geometries', () => this.parseGeometries(documentEntries(geometries), meta));
|
|
273
631
|
}
|
|
274
|
-
if (
|
|
275
|
-
this.parseImages(
|
|
632
|
+
if (images) {
|
|
633
|
+
readFormatPart('images', () => this.parseImages(documentEntries(images), baseUrl, meta));
|
|
276
634
|
}
|
|
277
|
-
if (
|
|
278
|
-
this.parseTextures(
|
|
635
|
+
if (textureList) {
|
|
636
|
+
readFormatPart('textures', () => this.parseTextures(documentEntries(textureList), meta));
|
|
279
637
|
}
|
|
280
|
-
if (
|
|
281
|
-
this.parseMaterials(
|
|
638
|
+
if (materials) {
|
|
639
|
+
readFormatPart('materials', () => this.parseMaterials(documentEntries(materials), meta));
|
|
282
640
|
}
|
|
283
641
|
|
|
284
|
-
const root = this.parseObject(json.object, meta);
|
|
642
|
+
const root = readFormatPart('object', () => this.parseObject(json.object, meta));
|
|
285
643
|
this.linkReferences(root);
|
|
286
|
-
|
|
644
|
+
const textures = [...new Set(Object.values(meta.textures).filter((t): t is Texture => t !== null))];
|
|
645
|
+
retainEffectResources(root, textures, meta.ownedMaterials);
|
|
646
|
+
return {root, textures};
|
|
287
647
|
}
|
|
288
648
|
|
|
289
|
-
private parseGeometries(geometries:
|
|
649
|
+
private parseGeometries(geometries: EffectGeometryJSON[], meta: LoadedMeta): void {
|
|
290
650
|
for (const geom of geometries) {
|
|
291
651
|
if (geom.type === 'PlaneGeometry') {
|
|
292
652
|
const w = geom.width || 1;
|
|
@@ -306,8 +666,8 @@ export class QuarksLoader {
|
|
|
306
666
|
meta.geometries[geom.uuid] = parsed;
|
|
307
667
|
} else if (geom.type === 'QuarksGeometry') {
|
|
308
668
|
if (typeof geom.url === 'string' && !geom.positions) {
|
|
309
|
-
|
|
310
|
-
`
|
|
669
|
+
warnFormat(
|
|
670
|
+
`Geometry "${geom.url}" is an external file; use load(), ` +
|
|
311
671
|
'loadFromJSON(), or resolveResources() before parse().'
|
|
312
672
|
);
|
|
313
673
|
}
|
|
@@ -323,7 +683,7 @@ export class QuarksLoader {
|
|
|
323
683
|
}
|
|
324
684
|
|
|
325
685
|
/** The scene's copy of an identical geometry when resources are shared, else `geometry` itself. */
|
|
326
|
-
private share(geometry:
|
|
686
|
+
private share(geometry: GeometryArrays): GeometryArrays {
|
|
327
687
|
if (this.options.shareResources === false) {
|
|
328
688
|
return geometry;
|
|
329
689
|
}
|
|
@@ -356,7 +716,7 @@ export class QuarksLoader {
|
|
|
356
716
|
return baseUrl + url;
|
|
357
717
|
}
|
|
358
718
|
|
|
359
|
-
private parseSphereGeometry(geom:
|
|
719
|
+
private parseSphereGeometry(geom: SphereGeometryJSON): GeometryArrays {
|
|
360
720
|
const radius = geom.radius ?? 0.5;
|
|
361
721
|
const widthSegments = Math.max(3, Math.floor(geom.widthSegments ?? 16));
|
|
362
722
|
const heightSegments = Math.max(2, Math.floor(geom.heightSegments ?? 8));
|
|
@@ -426,8 +786,8 @@ export class QuarksLoader {
|
|
|
426
786
|
};
|
|
427
787
|
}
|
|
428
788
|
|
|
429
|
-
private parseBufferGeometry(data:
|
|
430
|
-
let positions = new Float32Array(0);
|
|
789
|
+
private parseBufferGeometry(data: BufferGeometryDataJSON): GeometryArrays {
|
|
790
|
+
let positions: Float32Array = new Float32Array(0);
|
|
431
791
|
let indices: Uint32Array | Uint16Array = new Uint32Array(0);
|
|
432
792
|
let uvs: Float32Array | undefined;
|
|
433
793
|
let uvs2: Float32Array | undefined;
|
|
@@ -437,7 +797,7 @@ export class QuarksLoader {
|
|
|
437
797
|
|
|
438
798
|
if (data.interleavedBuffers && data.arrayBuffers) {
|
|
439
799
|
const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
|
|
440
|
-
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)
|
|
800
|
+
for (const [uuid, ibDef] of Object.entries(data.interleavedBuffers)) {
|
|
441
801
|
const arrayBuffer = data.arrayBuffers[ibDef.buffer];
|
|
442
802
|
if (arrayBuffer) {
|
|
443
803
|
const floatArray = new Float32Array(
|
|
@@ -447,35 +807,17 @@ export class QuarksLoader {
|
|
|
447
807
|
}
|
|
448
808
|
}
|
|
449
809
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
if (uv2Attribute?.isInterleavedBufferAttribute) {
|
|
463
|
-
const buf = buffers[uv2Attribute.data];
|
|
464
|
-
if (buf)
|
|
465
|
-
uvs2 = this.extractInterleavedAttribute(
|
|
466
|
-
buf.data,
|
|
467
|
-
buf.stride,
|
|
468
|
-
uv2Attribute.offset,
|
|
469
|
-
uv2Attribute.itemSize
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
if (data.attributes.normal?.isInterleavedBufferAttribute) {
|
|
473
|
-
const attr = data.attributes.normal;
|
|
474
|
-
const buf = buffers[attr.data];
|
|
475
|
-
if (buf)
|
|
476
|
-
normals = this.extractInterleavedAttribute(buf.data, buf.stride, attr.offset, attr.itemSize);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
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);
|
|
479
821
|
} else if (data.attributes) {
|
|
480
822
|
if (data.attributes.position?.array) {
|
|
481
823
|
positions = new Float32Array(data.attributes.position.array);
|
|
@@ -504,7 +846,12 @@ export class QuarksLoader {
|
|
|
504
846
|
return {positions, indices, uvs, uvs2, normals};
|
|
505
847
|
}
|
|
506
848
|
|
|
507
|
-
private extractInterleavedAttribute(
|
|
849
|
+
private extractInterleavedAttribute(
|
|
850
|
+
buffer: Float32Array,
|
|
851
|
+
stride: number,
|
|
852
|
+
offset: number = 0,
|
|
853
|
+
itemSize: number = 0
|
|
854
|
+
): Float32Array {
|
|
508
855
|
const count = Math.floor(buffer.length / stride);
|
|
509
856
|
const result = new Float32Array(count * itemSize);
|
|
510
857
|
for (let i = 0; i < count; i++) {
|
|
@@ -521,7 +868,7 @@ export class QuarksLoader {
|
|
|
521
868
|
for (let i = 0; i < intArray.length; i++) {
|
|
522
869
|
int32View[i] = intArray[i];
|
|
523
870
|
}
|
|
524
|
-
return new Float32Array(buffer)
|
|
871
|
+
return new Float32Array(buffer);
|
|
525
872
|
}
|
|
526
873
|
|
|
527
874
|
private async resolveResourceUrl(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
@@ -529,10 +876,10 @@ export class QuarksLoader {
|
|
|
529
876
|
}
|
|
530
877
|
|
|
531
878
|
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
532
|
-
private async fetchEffectDocument(url: string): Promise<{json:
|
|
879
|
+
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json: unknown; baseUrl: string}> {
|
|
533
880
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
534
881
|
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
535
|
-
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined);
|
|
882
|
+
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined, signal);
|
|
536
883
|
return {json, baseUrl};
|
|
537
884
|
}
|
|
538
885
|
|
|
@@ -541,19 +888,37 @@ export class QuarksLoader {
|
|
|
541
888
|
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
542
889
|
*/
|
|
543
890
|
private async loadDocuments(
|
|
544
|
-
docs: Array<{json:
|
|
891
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
545
892
|
options: QuarksBatchLoadOptions
|
|
546
893
|
): Promise<(TransformNode | null)[]> {
|
|
547
894
|
const total = docs.length;
|
|
548
895
|
if (total === 0) {
|
|
549
896
|
return [];
|
|
550
897
|
}
|
|
898
|
+
checkLoad(this.scene, options.signal);
|
|
899
|
+
// Stops the whole batch on abort, and the effects still waiting for textures once one fails.
|
|
900
|
+
const stopAll = new AbortController();
|
|
901
|
+
const abort = () => stopAll.abort(abortReason(options.signal!));
|
|
902
|
+
options.signal?.addEventListener('abort', abort, {once: true});
|
|
903
|
+
try {
|
|
904
|
+
return await this.loadResolvedDocuments(docs, options, stopAll);
|
|
905
|
+
} finally {
|
|
906
|
+
options.signal?.removeEventListener('abort', abort);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
551
909
|
|
|
552
|
-
|
|
910
|
+
private async loadResolvedDocuments(
|
|
911
|
+
docs: Array<{json: unknown; baseUrl: string} | null>,
|
|
912
|
+
options: QuarksBatchLoadOptions,
|
|
913
|
+
stopAll: AbortController
|
|
914
|
+
): Promise<(TransformNode | null)[]> {
|
|
915
|
+
const total = docs.length;
|
|
916
|
+
const signal = stopAll.signal;
|
|
917
|
+
const results: (TransformNode | null)[] = new Array(total).fill(null);
|
|
553
918
|
const settled = new Array<boolean>(total).fill(false);
|
|
554
919
|
let done = 0;
|
|
555
920
|
const settle = (index: number, value: TransformNode | null) => {
|
|
556
|
-
if (settled[index]) {
|
|
921
|
+
if (settled[index] || signal.aborted) {
|
|
557
922
|
return;
|
|
558
923
|
}
|
|
559
924
|
settled[index] = true;
|
|
@@ -562,42 +927,67 @@ export class QuarksLoader {
|
|
|
562
927
|
options.onProgress?.(done, total);
|
|
563
928
|
};
|
|
564
929
|
|
|
565
|
-
await
|
|
566
|
-
|
|
930
|
+
await untilStopped(
|
|
931
|
+
Promise.all(
|
|
932
|
+
docs.map(async (doc, i) => {
|
|
933
|
+
if (!doc) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
await this.resolveResources(doc.json, doc.baseUrl);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
if (!options.continueOnError) {
|
|
940
|
+
throw err;
|
|
941
|
+
}
|
|
942
|
+
console.warn(`[QuarksLoader] Failed to resolve resources for effect ${i + 1}/${total}:`, err);
|
|
943
|
+
docs[i] = null;
|
|
944
|
+
settle(i, null);
|
|
945
|
+
}
|
|
946
|
+
})
|
|
947
|
+
),
|
|
948
|
+
this.scene,
|
|
949
|
+
signal
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const loaded: Array<{root: TransformNode; textures: Texture[]}> = [];
|
|
953
|
+
const finishing: Promise<void>[] = [];
|
|
954
|
+
try {
|
|
955
|
+
for (let i = 0; i < total; i++) {
|
|
956
|
+
if (settled[i]) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const doc = docs[i];
|
|
567
960
|
if (!doc) {
|
|
568
|
-
|
|
961
|
+
settle(i, null);
|
|
962
|
+
continue;
|
|
569
963
|
}
|
|
964
|
+
let effect: {root: TransformNode; textures: Texture[]};
|
|
570
965
|
try {
|
|
571
|
-
|
|
966
|
+
// Resolved above, so the base must not be joined a second time.
|
|
967
|
+
effect = this.parseDocument(doc.json, '');
|
|
572
968
|
} catch (err) {
|
|
573
969
|
if (!options.continueOnError) {
|
|
574
970
|
throw err;
|
|
575
971
|
}
|
|
576
|
-
console.warn(`[QuarksLoader] Failed to
|
|
577
|
-
docs[i] = null;
|
|
972
|
+
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
578
973
|
settle(i, null);
|
|
974
|
+
continue;
|
|
579
975
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
for (let i = 0; i < total; i++) {
|
|
584
|
-
if (settled[i]) {
|
|
585
|
-
continue;
|
|
976
|
+
loaded.push(effect);
|
|
977
|
+
// An effect counts as loaded once its textures have too, when the loader waits for them.
|
|
978
|
+
finishing.push(this.finishLoad(effect, signal).then(() => settle(i, effect.root)));
|
|
586
979
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
980
|
+
await Promise.all(finishing);
|
|
981
|
+
} catch (err) {
|
|
982
|
+
// One failure, or the load stopped: stop the rest and free every effect already built.
|
|
983
|
+
if (!signal.aborted) {
|
|
984
|
+
stopAll.abort(err);
|
|
591
985
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (!options.continueOnError) {
|
|
596
|
-
throw err;
|
|
597
|
-
}
|
|
598
|
-
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
599
|
-
settle(i, null);
|
|
986
|
+
void Promise.allSettled(finishing);
|
|
987
|
+
if (!this.scene.isDisposed) {
|
|
988
|
+
loaded.forEach((effect) => disposeEffect(effect.root));
|
|
600
989
|
}
|
|
990
|
+
throw err;
|
|
601
991
|
}
|
|
602
992
|
return results;
|
|
603
993
|
}
|
|
@@ -643,13 +1033,15 @@ export class QuarksLoader {
|
|
|
643
1033
|
private static fetchJsonWith(
|
|
644
1034
|
resolver: QuarksResourceResolver | undefined,
|
|
645
1035
|
resolvedUrl: string,
|
|
646
|
-
scene: Scene | undefined
|
|
1036
|
+
scene: Scene | undefined,
|
|
1037
|
+
signal?: AbortSignal
|
|
647
1038
|
): Promise<unknown> {
|
|
648
1039
|
const load = async () => {
|
|
649
1040
|
if (resolver?.fetchJson) {
|
|
650
|
-
return resolver.fetchJson(resolvedUrl);
|
|
1041
|
+
return signal ? resolver.fetchJson(resolvedUrl, {signal}) : resolver.fetchJson(resolvedUrl);
|
|
651
1042
|
}
|
|
652
|
-
|
|
1043
|
+
// Only an uncached fetch (an effect document) can be cancelled: a cached one is shared.
|
|
1044
|
+
const response = await (signal ? fetch(resolvedUrl, {signal}) : fetch(resolvedUrl));
|
|
653
1045
|
if (!response.ok) {
|
|
654
1046
|
throw new Error(`HTTP ${response.status}`);
|
|
655
1047
|
}
|
|
@@ -675,40 +1067,59 @@ export class QuarksLoader {
|
|
|
675
1067
|
return pending;
|
|
676
1068
|
}
|
|
677
1069
|
|
|
678
|
-
private static async inlineGeometryEntries(
|
|
679
|
-
|
|
1070
|
+
private static async inlineGeometryEntries(
|
|
1071
|
+
json: unknown,
|
|
1072
|
+
loadGeometry: (url: string) => Promise<unknown>,
|
|
1073
|
+
onResourceError: QuarksLoaderOptions['onResourceError'] = QuarksLoader.warnResourceError
|
|
1074
|
+
): Promise<void> {
|
|
680
1075
|
await Promise.all(
|
|
681
|
-
geometries
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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;
|
|
694
1089
|
}
|
|
695
|
-
|
|
696
|
-
} catch (err) {
|
|
697
|
-
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
1090
|
+
geom[key] = Array.isArray(value) ? value.slice() : value;
|
|
698
1091
|
}
|
|
699
|
-
|
|
1092
|
+
delete geom.url;
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
onResourceError('geometry', url, err);
|
|
1095
|
+
}
|
|
1096
|
+
})
|
|
700
1097
|
);
|
|
701
1098
|
}
|
|
702
1099
|
|
|
703
|
-
private
|
|
704
|
-
(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1100
|
+
private static warnResourceError(kind: QuarksResourceKind, url: string, error: unknown): void {
|
|
1101
|
+
console.warn(`[QuarksLoader] Could not load ${kind} "${url}":`, error);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private reportResourceError(kind: QuarksResourceKind, url: string, error: unknown): void {
|
|
1105
|
+
(this.options.onResourceError ?? QuarksLoader.warnResourceError)(kind, url, error);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
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));
|
|
708
1113
|
}
|
|
709
1114
|
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
710
1115
|
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
711
|
-
|
|
1116
|
+
meta.images[img.uuid] = url ? QuarksLoader.joinUrl(baseUrl, url) : null;
|
|
1117
|
+
// The url the file wrote, for serializeEffect to write back. An inlined image
|
|
1118
|
+
// (`data:`, `blob:`) has no path to write; its texture writes its url.
|
|
1119
|
+
const source = sourceImageUrls.get(img) ?? url;
|
|
1120
|
+
if (source && !/^(data|blob):/i.test(source)) {
|
|
1121
|
+
meta.imageSources[img.uuid] = source;
|
|
1122
|
+
}
|
|
712
1123
|
}
|
|
713
1124
|
}
|
|
714
1125
|
|
|
@@ -725,16 +1136,15 @@ export class QuarksLoader {
|
|
|
725
1136
|
if (/^(data|blob):/i.test(url) || /\.(png|jpe?g|webp|gif|bmp|ktx2?|dds|env|basis)(\?|#|$)/i.test(url)) {
|
|
726
1137
|
return;
|
|
727
1138
|
}
|
|
728
|
-
|
|
729
|
-
`
|
|
1139
|
+
warnFormat(
|
|
1140
|
+
`Image "${url}" does not look like an image; its texture will fail to load. ` +
|
|
730
1141
|
'This usually means the effect was exported without embedding that texture.'
|
|
731
1142
|
);
|
|
732
1143
|
}
|
|
733
1144
|
|
|
734
|
-
private parseTextures(textures:
|
|
735
|
-
const images = (this as any)._images || {};
|
|
1145
|
+
private parseTextures(textures: QuarksTextureJSON[], meta: LoadedMeta): void {
|
|
736
1146
|
for (const texDef of textures) {
|
|
737
|
-
const imageUrl = images[texDef.image];
|
|
1147
|
+
const imageUrl = texDef.image !== undefined ? meta.images[texDef.image] : undefined;
|
|
738
1148
|
if (imageUrl) {
|
|
739
1149
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
740
1150
|
const noMipmap = !!texDef.noMipmap;
|
|
@@ -760,11 +1170,41 @@ export class QuarksLoader {
|
|
|
760
1170
|
meta.textures[texDef.uuid] = cached;
|
|
761
1171
|
continue;
|
|
762
1172
|
}
|
|
763
|
-
|
|
1173
|
+
let settle!: () => void;
|
|
1174
|
+
const settled = new Promise<void>((resolve) => (settle = resolve));
|
|
1175
|
+
let failed = false;
|
|
1176
|
+
// Babylon calls onError from inside the constructor, before `texture` is assigned, and
|
|
1177
|
+
// then throws, when the engine cannot make the texture at all.
|
|
1178
|
+
let texture: Texture | undefined = undefined;
|
|
1179
|
+
const options: ITextureCreationOptions = {
|
|
764
1180
|
noMipmap,
|
|
765
1181
|
invertY,
|
|
766
1182
|
samplingMode: typeof texDef.samplingMode === 'number' ? texDef.samplingMode : undefined,
|
|
767
|
-
|
|
1183
|
+
onLoad: () => settle(),
|
|
1184
|
+
onError: (message, exception) => {
|
|
1185
|
+
failed = true;
|
|
1186
|
+
// A later load tries the image again rather than reuse the failed texture.
|
|
1187
|
+
if (cache !== undefined && cache.get(key) === texture) {
|
|
1188
|
+
cache.delete(key);
|
|
1189
|
+
}
|
|
1190
|
+
this.reportResourceError('image', imageUrl, exception ?? message);
|
|
1191
|
+
settle();
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
try {
|
|
1195
|
+
texture = new Texture(imageUrl, this.scene, options);
|
|
1196
|
+
} catch {
|
|
1197
|
+
// Reported through onError: the effect draws without this texture.
|
|
1198
|
+
meta.textures[texDef.uuid] = null;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
textureSettled.set(texture, settled);
|
|
1202
|
+
const source = texDef.image !== undefined ? meta.imageSources[texDef.image] : undefined;
|
|
1203
|
+
if (source !== undefined) {
|
|
1204
|
+
setTextureSourceUrl(texture, source, imageUrl);
|
|
1205
|
+
}
|
|
1206
|
+
// A texture disposed before it loads never will.
|
|
1207
|
+
texture.onDisposeObservable.addOnce(() => settle());
|
|
768
1208
|
if (Array.isArray(texDef.wrap)) {
|
|
769
1209
|
texture.wrapU = this.mapWrapMode(texDef.wrap[0]);
|
|
770
1210
|
texture.wrapV = this.mapWrapMode(texDef.wrap[1] ?? texDef.wrap[0]);
|
|
@@ -780,7 +1220,7 @@ export class QuarksLoader {
|
|
|
780
1220
|
if (typeof texDef.rotation === 'number') {
|
|
781
1221
|
texture.wAng = texDef.rotation;
|
|
782
1222
|
}
|
|
783
|
-
if (cache !== undefined) {
|
|
1223
|
+
if (cache !== undefined && !failed) {
|
|
784
1224
|
cache.set(key, texture);
|
|
785
1225
|
// Something outside the library may dispose it; never hand out a dead texture.
|
|
786
1226
|
texture.onDisposeObservable.addOnce(() => {
|
|
@@ -808,9 +1248,12 @@ export class QuarksLoader {
|
|
|
808
1248
|
}
|
|
809
1249
|
}
|
|
810
1250
|
|
|
811
|
-
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;
|
|
812
1255
|
for (const matDef of materials) {
|
|
813
|
-
const matInfo:
|
|
1256
|
+
const matInfo: QuarksMaterialJSON = {
|
|
814
1257
|
uuid: matDef.uuid,
|
|
815
1258
|
transparent: matDef.transparent ?? true,
|
|
816
1259
|
blending: matDef.blending ?? 1,
|
|
@@ -825,12 +1268,12 @@ export class QuarksLoader {
|
|
|
825
1268
|
// emits) uses `texture`. Accept both so library-exported effects round-trip.
|
|
826
1269
|
const textureRef = matDef.map ?? matDef.texture;
|
|
827
1270
|
if (textureRef) {
|
|
828
|
-
matInfo.texture =
|
|
1271
|
+
matInfo.texture = texture(textureRef);
|
|
829
1272
|
}
|
|
830
1273
|
|
|
831
1274
|
const reflectionAtlasRef = matDef.reflectionAtlas;
|
|
832
1275
|
if (reflectionAtlasRef) {
|
|
833
|
-
matInfo.reflectionAtlas =
|
|
1276
|
+
matInfo.reflectionAtlas = texture(reflectionAtlasRef);
|
|
834
1277
|
}
|
|
835
1278
|
if (typeof matDef.reflectionLevel === 'number') {
|
|
836
1279
|
matInfo.reflectionLevel = matDef.reflectionLevel;
|
|
@@ -853,24 +1296,24 @@ export class QuarksLoader {
|
|
|
853
1296
|
if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
|
|
854
1297
|
matInfo.gradientMap = {
|
|
855
1298
|
...matDef.gradientMap,
|
|
856
|
-
ramp: matDef.gradientMap.ramp
|
|
1299
|
+
ramp: texture(matDef.gradientMap.ramp),
|
|
857
1300
|
};
|
|
858
1301
|
}
|
|
859
1302
|
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
860
1303
|
matInfo.erosion = {
|
|
861
1304
|
...matDef.erosion,
|
|
862
|
-
mask: matDef.erosion.mask
|
|
1305
|
+
mask: texture(matDef.erosion.mask),
|
|
863
1306
|
};
|
|
864
1307
|
}
|
|
865
1308
|
// uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
|
|
866
1309
|
if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
|
|
867
1310
|
matInfo.uvDistortion = {
|
|
868
1311
|
...matDef.uvDistortion,
|
|
869
|
-
map: matDef.uvDistortion.map
|
|
1312
|
+
map: texture(matDef.uvDistortion.map),
|
|
870
1313
|
};
|
|
871
1314
|
}
|
|
872
1315
|
|
|
873
|
-
// QuarksMaterial (what
|
|
1316
|
+
// QuarksMaterial (what `materialMeta` in system/serialization.ts writes) writes `alphaMode`
|
|
874
1317
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
|
875
1318
|
// etc.) instead use `blending` with three.js's own 1..4 numbering, which does NOT
|
|
876
1319
|
// line up with Babylon's constants (e.g. three.js Additive=2 vs Babylon ALPHA_ADD=1)
|
|
@@ -884,19 +1327,19 @@ export class QuarksLoader {
|
|
|
884
1327
|
3: Constants.ALPHA_SUBTRACT,
|
|
885
1328
|
4: Constants.ALPHA_MULTIPLY,
|
|
886
1329
|
};
|
|
887
|
-
matInfo.alphaMode = blendingToAlphaMode[matDef.blending] || Constants.ALPHA_COMBINE;
|
|
1330
|
+
matInfo.alphaMode = blendingToAlphaMode[matDef.blending ?? 1] || Constants.ALPHA_COMBINE;
|
|
888
1331
|
}
|
|
889
1332
|
|
|
890
1333
|
meta.materials[matDef.uuid] = matInfo;
|
|
891
1334
|
}
|
|
892
1335
|
}
|
|
893
1336
|
|
|
894
|
-
private getGeometry(meta: LoadedMeta, geometryId: string | undefined):
|
|
1337
|
+
private getGeometry(meta: LoadedMeta, geometryId: string | undefined): GeometryArrays | undefined {
|
|
895
1338
|
if (!geometryId) return undefined;
|
|
896
1339
|
return meta.geometries[geometryId];
|
|
897
1340
|
}
|
|
898
1341
|
|
|
899
|
-
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined):
|
|
1342
|
+
private getMaterial(meta: LoadedMeta, materialId: string | string[] | undefined): QuarksMaterialJSON | undefined {
|
|
900
1343
|
if (!materialId) return undefined;
|
|
901
1344
|
const selectedId = Array.isArray(materialId) ? materialId[0] : materialId;
|
|
902
1345
|
return meta.materials[selectedId];
|
|
@@ -904,11 +1347,12 @@ export class QuarksLoader {
|
|
|
904
1347
|
|
|
905
1348
|
private createPlaceholderNode(type: string, name?: string): TransformNode {
|
|
906
1349
|
const node = new TransformNode(name || type || 'node', this.scene);
|
|
907
|
-
|
|
1350
|
+
// The three.js type the node stood for, which babylon.quarks has no node for.
|
|
1351
|
+
Object.assign(node, {quarksOriginalType: type});
|
|
908
1352
|
return node;
|
|
909
1353
|
}
|
|
910
1354
|
|
|
911
|
-
private createMeshNode(data:
|
|
1355
|
+
private createMeshNode(data: QuarksNodeJSON, meta: LoadedMeta): Mesh {
|
|
912
1356
|
const mesh = new Mesh(data.name || data.type || 'mesh', this.scene);
|
|
913
1357
|
const geometry = this.getGeometry(meta, data.geometry);
|
|
914
1358
|
if (geometry && geometry.positions.length > 0 && geometry.indices.length > 0) {
|
|
@@ -930,20 +1374,22 @@ export class QuarksLoader {
|
|
|
930
1374
|
const materialInfo = this.getMaterial(meta, data.material);
|
|
931
1375
|
if (materialInfo) {
|
|
932
1376
|
const material = new StandardMaterial(`quarks_loader_material_${materialInfo.uuid}`, this.scene);
|
|
1377
|
+
meta.ownedMaterials.push(material);
|
|
933
1378
|
material.alphaMode = materialInfo.alphaMode ?? Constants.ALPHA_COMBINE;
|
|
934
1379
|
material.backFaceCulling = materialInfo.side !== 2;
|
|
935
1380
|
material.alphaCutOff = materialInfo.alphaTest ?? 0;
|
|
936
1381
|
material.disableDepthWrite = !(materialInfo.depthWrite ?? false);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
material.
|
|
1382
|
+
const texture = materialInfo.texture;
|
|
1383
|
+
if (texture && typeof texture !== 'string') {
|
|
1384
|
+
material.diffuseTexture = texture;
|
|
1385
|
+
material.opacityTexture = texture;
|
|
940
1386
|
}
|
|
941
1387
|
mesh.material = material;
|
|
942
1388
|
}
|
|
943
1389
|
return mesh;
|
|
944
1390
|
}
|
|
945
1391
|
|
|
946
|
-
private parseObject(data:
|
|
1392
|
+
private parseObject(data: QuarksNodeJSON, meta: LoadedMeta): TransformNode {
|
|
947
1393
|
let node: TransformNode;
|
|
948
1394
|
switch (data.type) {
|
|
949
1395
|
case 'QuarksPrefab':
|
|
@@ -951,7 +1397,8 @@ export class QuarksLoader {
|
|
|
951
1397
|
break;
|
|
952
1398
|
case 'ParticleEmitter':
|
|
953
1399
|
if (data.ps) {
|
|
954
|
-
const
|
|
1400
|
+
const psJSON = data.ps;
|
|
1401
|
+
const ps = readFormatPart('ps', () => this.parseParticleSystem(psJSON, meta));
|
|
955
1402
|
node = ps.emitter;
|
|
956
1403
|
break;
|
|
957
1404
|
}
|
|
@@ -989,7 +1436,7 @@ export class QuarksLoader {
|
|
|
989
1436
|
node = new TransformNode(data.name || data.type || 'node', this.scene);
|
|
990
1437
|
}
|
|
991
1438
|
|
|
992
|
-
if (data.uuid) (node
|
|
1439
|
+
if (data.uuid) setQuarksNodeId(node, data.uuid);
|
|
993
1440
|
if (data.name) node.name = data.name;
|
|
994
1441
|
|
|
995
1442
|
if (data.matrix) {
|
|
@@ -1017,21 +1464,34 @@ export class QuarksLoader {
|
|
|
1017
1464
|
}
|
|
1018
1465
|
|
|
1019
1466
|
if (data.visible !== undefined) node.setEnabled(data.visible);
|
|
1467
|
+
if (data.isVisible === false && node instanceof Mesh) node.isVisible = false;
|
|
1020
1468
|
|
|
1021
1469
|
if (data.children) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
+
});
|
|
1025
1482
|
}
|
|
1026
1483
|
}
|
|
1027
1484
|
|
|
1028
1485
|
return node;
|
|
1029
1486
|
}
|
|
1030
1487
|
|
|
1031
|
-
private parseParticleSystem(json:
|
|
1488
|
+
private parseParticleSystem(json: ParticleSystemJSONParameters, meta: LoadedMeta): ParticleSystem {
|
|
1032
1489
|
const dependencies: {[uuid: string]: Behavior} = {};
|
|
1033
|
-
const ps = ParticleSystem.fromJSON(json, meta
|
|
1034
|
-
(
|
|
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());
|
|
1035
1495
|
return ps;
|
|
1036
1496
|
}
|
|
1037
1497
|
|
|
@@ -1039,8 +1499,9 @@ export class QuarksLoader {
|
|
|
1039
1499
|
const nodesMap: {[uuid: string]: TransformNode} = {};
|
|
1040
1500
|
|
|
1041
1501
|
const traverse = (node: TransformNode) => {
|
|
1042
|
-
|
|
1043
|
-
|
|
1502
|
+
const fileId = fileNodeId(node);
|
|
1503
|
+
if (fileId) {
|
|
1504
|
+
nodesMap[fileId] = node;
|
|
1044
1505
|
}
|
|
1045
1506
|
for (const child of node.getChildren()) {
|
|
1046
1507
|
if (child instanceof TransformNode) {
|
|
@@ -1056,23 +1517,34 @@ export class QuarksLoader {
|
|
|
1056
1517
|
}
|
|
1057
1518
|
if (node instanceof ParticleEmitter) {
|
|
1058
1519
|
const system = node.system as ParticleSystem;
|
|
1059
|
-
|
|
1060
|
-
|
|
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) {
|
|
1061
1527
|
const targetNode = nodesMap[meshSurfaceUUID];
|
|
1062
1528
|
if (targetNode instanceof Mesh) {
|
|
1063
|
-
|
|
1529
|
+
shape.mesh = targetNode;
|
|
1530
|
+
} else {
|
|
1531
|
+
warnAtSystem(
|
|
1532
|
+
`Emitter "${node.name}" has a mesh surface shape on "${meshSurfaceUUID}", ` +
|
|
1533
|
+
(targetNode ? 'which is not a mesh' : 'which is not in the effect') +
|
|
1534
|
+
'; it emits nothing until a mesh is set.'
|
|
1535
|
+
);
|
|
1064
1536
|
}
|
|
1065
1537
|
}
|
|
1066
1538
|
for (let i = 0; i < system.behaviors.length; i++) {
|
|
1067
1539
|
if (system.behaviors[i] instanceof EmitSubParticleSystem) {
|
|
1068
1540
|
const subEmitter = system.behaviors[i] as EmitSubParticleSystem;
|
|
1069
|
-
const targetUUID = subEmitter.subParticleSystem
|
|
1541
|
+
const targetUUID = subEmitter.subParticleSystem;
|
|
1070
1542
|
if (typeof targetUUID !== 'string') {
|
|
1071
1543
|
continue;
|
|
1072
1544
|
}
|
|
1073
1545
|
const resolved = nodesMap[targetUUID];
|
|
1074
1546
|
if (resolved instanceof ParticleEmitter) {
|
|
1075
|
-
|
|
1547
|
+
subEmitter.subParticleSystem = resolved;
|
|
1076
1548
|
(resolved.system as ParticleSystem).onlyUsedByOther = true;
|
|
1077
1549
|
} else {
|
|
1078
1550
|
// An export that dropped the target emitter, or kept only
|
|
@@ -1080,8 +1552,8 @@ export class QuarksLoader {
|
|
|
1080
1552
|
// a string the behavior would treat as an emitter.
|
|
1081
1553
|
subEmitter.subParticleSystem = undefined;
|
|
1082
1554
|
if (targetUUID !== '') {
|
|
1083
|
-
|
|
1084
|
-
`
|
|
1555
|
+
warnAtSystem(
|
|
1556
|
+
`Emitter "${node.name}" has a sub emitter targeting "${targetUUID}", ` +
|
|
1085
1557
|
(resolved ? 'which is not a particle emitter' : 'which is not in the effect') +
|
|
1086
1558
|
'; that sub emitter is skipped.'
|
|
1087
1559
|
);
|