babylon.quarks 0.25.0 → 0.27.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 +854 -8
- package/README.md +10 -8
- 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 +6683 -0
- package/dist/types/index.d.ts +2210 -613
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +215 -106
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +1012 -1962
- package/src/QuarksLoader.ts +463 -75
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +149 -238
- package/src/TrailBatch.ts +586 -286
- package/src/VFXBatch.ts +517 -153
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +51 -35
- package/src/nodeId.ts +13 -0
- package/src/shaderProgram.ts +325 -0
- package/src/shaderVariants.ts +225 -0
- package/src/shaders/chunks/fog.ts +4 -2
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +84 -0
- package/src/shaders/chunks/surface.ts +132 -42
- package/src/shaders/local_particle_physics_vert.glsl.ts +28 -27
- package/src/shaders/local_particle_physics_vert.wgsl.ts +30 -28
- package/src/shaders/particle_frag.glsl.ts +6 -22
- package/src/shaders/particle_frag.wgsl.ts +6 -23
- package/src/shaders/particle_physics_frag.glsl.ts +22 -29
- package/src/shaders/particle_physics_frag.wgsl.ts +22 -30
- package/src/shaders/particle_vert.glsl.ts +11 -26
- package/src/shaders/particle_vert.wgsl.ts +11 -25
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +21 -39
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +22 -40
- package/src/shaders/trail_frag.glsl.ts +14 -1
- package/src/shaders/trail_frag.wgsl.ts +14 -0
- package/src/shaders/trail_vert.glsl.ts +53 -36
- package/src/shaders/trail_vert.wgsl.ts +54 -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 +360 -0
- package/src/system/EventDispatcher.ts +44 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +111 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +116 -0
- package/src/system/normalize.ts +95 -0
- package/src/system/serialization.ts +401 -0
- package/src/system/settingsSchema.ts +317 -0
- package/src/system/types.ts +410 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +51 -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,5 +1,7 @@
|
|
|
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';
|
|
@@ -10,7 +12,10 @@ import {Behavior, EmitSubParticleSystem} from 'quarks.core';
|
|
|
10
12
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
11
13
|
import {ParticleSystem} from './ParticleSystem';
|
|
12
14
|
import {QuarksPrefab} from './QuarksPrefab';
|
|
15
|
+
import {QUARKS_FORMAT_VERSION} from './QuarksSerializer';
|
|
16
|
+
import {disposeEffect, retainEffectResources} from './effectResources';
|
|
13
17
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
18
|
+
import {setTextureSourceUrl} from './textureSource';
|
|
14
19
|
|
|
15
20
|
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
16
21
|
export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
@@ -22,11 +27,20 @@ export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
|
22
27
|
export interface QuarksResourceResolver {
|
|
23
28
|
/** Resolve a relative url to an absolute / blob / data url. Default: {@link QuarksLoader.joinUrl}. */
|
|
24
29
|
resolve?(url: string, kind: QuarksResourceKind, baseUrl: string): string | Promise<string>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Load a JSON asset (effect and geometry files). Default: `fetch(resolvedUrl).json()`.
|
|
32
|
+
* `options.signal` is passed for an effect document, whose fetch belongs to one load; geometry
|
|
33
|
+
* fetches are shared between loads and get none.
|
|
34
|
+
*/
|
|
35
|
+
fetchJson?(resolvedUrl: string, options?: {signal?: AbortSignal}): Promise<unknown>;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/** Options for a {@link QuarksLoader}, shared by every load it does. */
|
|
29
39
|
export interface QuarksLoaderOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Prefix joined to relative geometry and image urls (see {@link QuarksLoader.joinUrl}).
|
|
42
|
+
* Unset, {@link QuarksLoader.load} uses the effect file's own folder and the other loads `''`.
|
|
43
|
+
*/
|
|
30
44
|
baseUrl?: string;
|
|
31
45
|
/**
|
|
32
46
|
* Reuse one Texture per scene for every effect that references the same image with the same
|
|
@@ -39,10 +53,34 @@ export interface QuarksLoaderOptions {
|
|
|
39
53
|
shareResources?: boolean;
|
|
40
54
|
/** Custom url / fetch backend (File picker, CDN, IndexedDB). */
|
|
41
55
|
resolver?: QuarksResourceResolver;
|
|
56
|
+
/**
|
|
57
|
+
* Makes `load`, `loadFromJSON`, `loadAll` and `loadFromJSONAll` resolve only once every texture
|
|
58
|
+
* the effect uses has loaded or failed, so it does not draw its first frames without them.
|
|
59
|
+
* A texture a scene with `useDelayedTextureLoading` would load only once drawn is loaded at
|
|
60
|
+
* once. `parse` never waits. Default false: they resolve when the nodes are built.
|
|
61
|
+
*/
|
|
62
|
+
waitForTextures?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Called for a geometry file or image that fails to load. The effect still loads: a geometry
|
|
65
|
+
* keeps its link unresolved, and a texture draws nothing. Default: a console warning.
|
|
66
|
+
*/
|
|
67
|
+
onResourceError?: (kind: QuarksResourceKind, url: string, error: unknown) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Options for one {@link QuarksLoader.load} or {@link QuarksLoader.loadFromJSON}. */
|
|
71
|
+
export interface QuarksLoadOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Cancels the load, e.g. on a level change. The promise rejects with the signal's reason as
|
|
74
|
+
* soon as it is aborted, and no node of the effect is left in the scene. The effect document's
|
|
75
|
+
* own fetch is cancelled; a geometry fetch shared with other loads runs on, and its result is
|
|
76
|
+
* dropped. A scene disposed during the load rejects it the same way.
|
|
77
|
+
*/
|
|
78
|
+
signal?: AbortSignal;
|
|
42
79
|
}
|
|
43
80
|
|
|
44
81
|
/** One in-memory effect for {@link QuarksLoader.loadFromJSONAll}. */
|
|
45
82
|
export interface QuarksJSONLoadEntry {
|
|
83
|
+
/** The effect document, as {@link serializeEffect} or the editor writes it. Left unchanged. */
|
|
46
84
|
json: unknown;
|
|
47
85
|
/** Overrides loader `baseUrl` for relative texture / geometry links in this effect. */
|
|
48
86
|
baseUrl?: string;
|
|
@@ -53,7 +91,7 @@ export interface QuarksJSONLoadEntry {
|
|
|
53
91
|
* Batch loads share the per-scene fetch cache and `shareResources`, so N effects that point at
|
|
54
92
|
* the same `assets/geometries/*.json` or texture only pay for that asset once.
|
|
55
93
|
*/
|
|
56
|
-
export interface QuarksBatchLoadOptions {
|
|
94
|
+
export interface QuarksBatchLoadOptions extends QuarksLoadOptions {
|
|
57
95
|
/**
|
|
58
96
|
* Max concurrent effect-document fetches (phase 1). Linked geometry/image resolve still runs
|
|
59
97
|
* across the whole pack in parallel so shared urls coalesce. Default: all at once.
|
|
@@ -75,6 +113,7 @@ interface ParsedGeometry {
|
|
|
75
113
|
positions: Float32Array;
|
|
76
114
|
indices: Uint32Array | Uint16Array;
|
|
77
115
|
uvs?: Float32Array;
|
|
116
|
+
uvs2?: Float32Array;
|
|
78
117
|
normals?: Float32Array;
|
|
79
118
|
}
|
|
80
119
|
|
|
@@ -93,6 +132,12 @@ function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<st
|
|
|
93
132
|
return map;
|
|
94
133
|
}
|
|
95
134
|
|
|
135
|
+
/** Image entries {@link QuarksLoader.resolveLinkedResources} rewrote, with the url it wrote. */
|
|
136
|
+
const resolvedImageUrls = new WeakMap<object, string>();
|
|
137
|
+
|
|
138
|
+
/** Image entries {@link QuarksLoader.resolveLinkedResources} rewrote, with the url the file had. */
|
|
139
|
+
const sourceImageUrls = new WeakMap<object, string>();
|
|
140
|
+
|
|
96
141
|
/** Content key for a geometry: two independent 32-bit hashes over every array's bytes, plus lengths. */
|
|
97
142
|
function geometryKey(geometry: ParsedGeometry): string {
|
|
98
143
|
let h1 = 0x811c9dc5;
|
|
@@ -101,6 +146,7 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
101
146
|
geometry.positions,
|
|
102
147
|
geometry.indices,
|
|
103
148
|
geometry.uvs,
|
|
149
|
+
geometry.uvs2,
|
|
104
150
|
geometry.normals,
|
|
105
151
|
];
|
|
106
152
|
let lengths = '';
|
|
@@ -119,12 +165,109 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
119
165
|
return `${lengths}${(h1 >>> 0).toString(16)}${(h2 >>> 0).toString(16)}`;
|
|
120
166
|
}
|
|
121
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Thrown for a document the loader cannot read at all, such as one with no `object`. A part it
|
|
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
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A copy of an effect document that loading can rewrite (resolved image urls, inlined geometry
|
|
182
|
+
* files) without touching the caller's. Only the entries that change are copied.
|
|
183
|
+
*/
|
|
184
|
+
function detachDocument(json: any): any {
|
|
185
|
+
if (json === null || typeof json !== 'object') {
|
|
186
|
+
return json;
|
|
187
|
+
}
|
|
188
|
+
const copy = {...json};
|
|
189
|
+
if (Array.isArray(json.images)) {
|
|
190
|
+
copy.images = json.images.map((image: unknown) => {
|
|
191
|
+
if (!image || typeof image !== 'object') {
|
|
192
|
+
return image;
|
|
193
|
+
}
|
|
194
|
+
const imageCopy: {url?: unknown} = {...(image as object)};
|
|
195
|
+
// An entry resolveResources() already rewrote stays resolved in its copy, so loading the
|
|
196
|
+
// document it prepared, with the same base, does not join the base a second time.
|
|
197
|
+
if (resolvedImageUrls.get(image) === imageCopy.url && typeof imageCopy.url === 'string') {
|
|
198
|
+
resolvedImageUrls.set(imageCopy, imageCopy.url);
|
|
199
|
+
const source = sourceImageUrls.get(image);
|
|
200
|
+
if (source !== undefined) {
|
|
201
|
+
sourceImageUrls.set(imageCopy, source);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return imageCopy;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(json.geometries)) {
|
|
208
|
+
copy.geometries = json.geometries.map((geometry: unknown) =>
|
|
209
|
+
geometry && typeof geometry === 'object' ? {...(geometry as object)} : geometry
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return copy;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Settles when a texture the loader made has loaded or failed. */
|
|
216
|
+
const textureSettled = new WeakMap<Texture, Promise<void>>();
|
|
217
|
+
|
|
218
|
+
const abortReason = (signal: AbortSignal) => signal.reason ?? new Error('[QuarksLoader] The load was aborted.');
|
|
219
|
+
const sceneDisposed = () => new Error('[QuarksLoader] The scene was disposed while the effect loaded.');
|
|
220
|
+
|
|
221
|
+
/** Throws the reason a load stops early: its signal was aborted, or its scene disposed. */
|
|
222
|
+
function checkLoad(scene: Scene, signal: AbortSignal | undefined): void {
|
|
223
|
+
if (signal?.aborted) {
|
|
224
|
+
throw abortReason(signal);
|
|
225
|
+
}
|
|
226
|
+
if (scene.isDisposed) {
|
|
227
|
+
throw sceneDisposed();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Follows `promise`, or rejects as {@link checkLoad} would as soon as the load is stopped. */
|
|
232
|
+
function untilStopped<T>(promise: Promise<T>, scene: Scene, signal: AbortSignal | undefined): Promise<T> {
|
|
233
|
+
checkLoad(scene, signal);
|
|
234
|
+
return new Promise<T>((resolve, reject) => {
|
|
235
|
+
const aborted = () => reject(abortReason(signal!));
|
|
236
|
+
signal?.addEventListener('abort', aborted, {once: true});
|
|
237
|
+
const disposed = scene.onDisposeObservable.addOnce(() => reject(sceneDisposed()));
|
|
238
|
+
promise.then(resolve, reject).finally(() => {
|
|
239
|
+
signal?.removeEventListener('abort', aborted);
|
|
240
|
+
scene.onDisposeObservable.remove(disposed);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
122
245
|
interface LoadedMeta {
|
|
246
|
+
/** The materials this load made, which the effect owns. */
|
|
247
|
+
ownedMaterials: Material[];
|
|
248
|
+
/** Image uuid to the url its textures load. */
|
|
249
|
+
images: {[uuid: string]: string | null};
|
|
250
|
+
/** Image uuid to the url the file wrote, before any base url was joined. */
|
|
251
|
+
imageSources: {[uuid: string]: string};
|
|
123
252
|
textures: {[uuid: string]: Texture | null};
|
|
124
253
|
geometries: {[uuid: string]: ParsedGeometry};
|
|
125
254
|
materials: {[uuid: string]: any};
|
|
126
255
|
}
|
|
127
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Loads effects saved as JSON into a scene: fetches the document and the geometry and image files
|
|
259
|
+
* it links, builds the nodes, particle systems, textures and materials, and returns the root.
|
|
260
|
+
* Add the systems to a {@link BatchedRenderer} with {@link QuarksUtil.addToBatchRenderer} to draw
|
|
261
|
+
* them, and free the effect with {@link QuarksLoader.disposeEffect}.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* const loader = new QuarksLoader(scene, {waitForTextures: true});
|
|
266
|
+
* const effect = await loader.load('effects/fireball.json');
|
|
267
|
+
* QuarksUtil.addToBatchRenderer(effect, renderer);
|
|
268
|
+
* QuarksUtil.play(effect);
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
128
271
|
export class QuarksLoader {
|
|
129
272
|
private scene: Scene;
|
|
130
273
|
private options: QuarksLoaderOptions;
|
|
@@ -134,20 +277,77 @@ export class QuarksLoader {
|
|
|
134
277
|
this.options = options;
|
|
135
278
|
}
|
|
136
279
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Fetches an effect JSON from `url`, resolves linked sources, and parses it. Free the effect
|
|
282
|
+
* with {@link disposeEffect}.
|
|
283
|
+
*/
|
|
284
|
+
async load(url: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
285
|
+
const {json, baseUrl} = await untilStopped(
|
|
286
|
+
this.fetchEffectDocument(url, options.signal),
|
|
287
|
+
this.scene,
|
|
288
|
+
options.signal
|
|
289
|
+
);
|
|
290
|
+
return this.loadFromJSON(json, baseUrl, options);
|
|
141
291
|
}
|
|
142
292
|
|
|
143
293
|
/**
|
|
144
|
-
* Resolves linked geometry / image urls
|
|
145
|
-
*
|
|
294
|
+
* 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` itself is
|
|
296
|
+
* left as it was, so the same document can be loaded again, into another scene or with
|
|
297
|
+
* another base url.
|
|
146
298
|
*/
|
|
147
|
-
async loadFromJSON(json: any, baseUrl?: string): Promise<TransformNode> {
|
|
299
|
+
async loadFromJSON(json: any, baseUrl?: string, options: QuarksLoadOptions = {}): Promise<TransformNode> {
|
|
300
|
+
const signal = options.signal;
|
|
301
|
+
checkLoad(this.scene, signal);
|
|
148
302
|
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
149
|
-
|
|
150
|
-
|
|
303
|
+
json = detachDocument(json);
|
|
304
|
+
await untilStopped(this.resolveResources(json, resolvedBase), this.scene, signal);
|
|
305
|
+
// The urls are resolved now; joining them with the base again would load `fx/fx/...`.
|
|
306
|
+
const loaded = this.parseDocument(json, '');
|
|
307
|
+
await this.finishLoad(loaded, signal);
|
|
308
|
+
return loaded.root;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Disposes an effect a loader built: its nodes and systems, the materials made for it, and
|
|
313
|
+
* each texture it uses once no other loaded effect uses it (textures are shared per scene,
|
|
314
|
+
* see {@link QuarksLoaderOptions.shareResources}). Copies made with
|
|
315
|
+
* `QuarksUtil.instantiate` hold the textures too, so disposing the source effect leaves them
|
|
316
|
+
* drawing. The same happens when the root is disposed any other way (`root.dispose()`, its
|
|
317
|
+
* parent's dispose, the scene's). The materials and textures are freed even when other code
|
|
318
|
+
* was handed them; give that code its own. For a root the loader did not build, this is
|
|
319
|
+
* `root.dispose()`.
|
|
320
|
+
*/
|
|
321
|
+
static disposeEffect(root: TransformNode): void {
|
|
322
|
+
disposeEffect(root);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Waits for the effect's textures when the loader is asked to, and frees the effect when the
|
|
327
|
+
* load is stopped meanwhile.
|
|
328
|
+
*/
|
|
329
|
+
private async finishLoad(loaded: {root: TransformNode; textures: Texture[]}, signal?: AbortSignal): Promise<void> {
|
|
330
|
+
try {
|
|
331
|
+
if (this.options.waitForTextures) {
|
|
332
|
+
for (const texture of loaded.textures) {
|
|
333
|
+
// A delayed texture would load only once drawn, which it is not until the load resolves.
|
|
334
|
+
if (texture.delayLoadState === Constants.DELAYLOADSTATE_NOTLOADED) {
|
|
335
|
+
texture.delayLoad();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
await untilStopped(
|
|
339
|
+
Promise.all(loaded.textures.map((texture) => textureSettled.get(texture))),
|
|
340
|
+
this.scene,
|
|
341
|
+
signal
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
checkLoad(this.scene, signal);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (!this.scene.isDisposed) {
|
|
347
|
+
disposeEffect(loaded.root);
|
|
348
|
+
}
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
151
351
|
}
|
|
152
352
|
|
|
153
353
|
/**
|
|
@@ -168,17 +368,21 @@ export class QuarksLoader {
|
|
|
168
368
|
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
169
369
|
): Promise<(TransformNode | null)[]>;
|
|
170
370
|
async loadAll(urls: string[], options: QuarksBatchLoadOptions = {}): Promise<(TransformNode | null)[]> {
|
|
171
|
-
|
|
371
|
+
checkLoad(this.scene, options.signal);
|
|
372
|
+
const fetching = QuarksLoader.mapPool(urls, options.concurrency, async (url, index) => {
|
|
373
|
+
checkLoad(this.scene, options.signal);
|
|
172
374
|
try {
|
|
173
|
-
return await this.fetchEffectDocument(url);
|
|
375
|
+
return await this.fetchEffectDocument(url, options.signal);
|
|
174
376
|
} catch (err) {
|
|
175
|
-
|
|
377
|
+
// A stopped load is never one effect's failure.
|
|
378
|
+
if (!options.continueOnError || options.signal?.aborted || this.scene.isDisposed) {
|
|
176
379
|
throw err;
|
|
177
380
|
}
|
|
178
381
|
console.warn(`[QuarksLoader] Failed to fetch effect "${url}" (${index + 1}/${urls.length}):`, err);
|
|
179
382
|
return null;
|
|
180
383
|
}
|
|
181
384
|
});
|
|
385
|
+
const docs = await untilStopped(fetching, this.scene, options.signal);
|
|
182
386
|
return this.loadDocuments(docs, options);
|
|
183
387
|
}
|
|
184
388
|
|
|
@@ -199,44 +403,59 @@ export class QuarksLoader {
|
|
|
199
403
|
options: QuarksBatchLoadOptions = {}
|
|
200
404
|
): Promise<(TransformNode | null)[]> {
|
|
201
405
|
const docs: Array<{json: any; baseUrl: string} | null> = entries.map((entry) => {
|
|
406
|
+
// Copied, as loadFromJSON does, so the caller's documents stay as they were.
|
|
202
407
|
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
203
408
|
const typed = entry as QuarksJSONLoadEntry;
|
|
204
|
-
return {json: typed.json
|
|
409
|
+
return {json: detachDocument(typed.json), baseUrl: typed.baseUrl ?? this.options.baseUrl ?? ''};
|
|
205
410
|
}
|
|
206
|
-
return {json: entry
|
|
411
|
+
return {json: detachDocument(entry), baseUrl: this.options.baseUrl ?? ''};
|
|
207
412
|
});
|
|
208
413
|
return this.loadDocuments(docs, options);
|
|
209
414
|
}
|
|
210
415
|
|
|
211
416
|
/**
|
|
212
417
|
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
213
|
-
* {@link QuarksResourceResolver}
|
|
214
|
-
* and
|
|
418
|
+
* {@link QuarksResourceResolver}, in place on `json`. Each document is resolved once:
|
|
419
|
+
* geometries already inlined and image urls this method already rewrote are left alone, so
|
|
420
|
+
* loading the same document again does not join its base url twice. Failures warn and leave
|
|
421
|
+
* the entry as is.
|
|
215
422
|
*/
|
|
216
423
|
async resolveResources(json: any, baseUrl: string = ''): Promise<void> {
|
|
217
424
|
await QuarksLoader.resolveLinkedResources(json, {
|
|
218
425
|
baseUrl,
|
|
219
426
|
resolver: this.options.resolver,
|
|
220
427
|
scene: this.scene,
|
|
428
|
+
onResourceError: this.options.onResourceError,
|
|
221
429
|
});
|
|
222
430
|
}
|
|
223
431
|
|
|
224
432
|
/**
|
|
225
433
|
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
226
434
|
* editor import that only prepares JSON). Pass `scene` to share the per-scene fetch cache.
|
|
435
|
+
* Like it, resolves each document once. A geometry file that fails to load goes to
|
|
436
|
+
* `onResourceError` (default: a console warning) and keeps its link.
|
|
227
437
|
*/
|
|
228
438
|
static async resolveLinkedResources(
|
|
229
439
|
json: any,
|
|
230
|
-
options: {
|
|
440
|
+
options: {
|
|
441
|
+
baseUrl?: string;
|
|
442
|
+
resolver?: QuarksResourceResolver;
|
|
443
|
+
scene?: Scene;
|
|
444
|
+
onResourceError?: QuarksLoaderOptions['onResourceError'];
|
|
445
|
+
} = {}
|
|
231
446
|
): Promise<void> {
|
|
232
447
|
const baseUrl = options.baseUrl ?? '';
|
|
233
448
|
const resolver = options.resolver;
|
|
234
449
|
const scene = options.scene;
|
|
235
450
|
|
|
236
|
-
await QuarksLoader.inlineGeometryEntries(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
451
|
+
await QuarksLoader.inlineGeometryEntries(
|
|
452
|
+
json,
|
|
453
|
+
async (geometryUrl) => {
|
|
454
|
+
const resolved = await QuarksLoader.resolveWith(resolver, geometryUrl, 'geometry', baseUrl);
|
|
455
|
+
return QuarksLoader.fetchJsonWith(resolver, resolved, scene);
|
|
456
|
+
},
|
|
457
|
+
options.onResourceError
|
|
458
|
+
);
|
|
240
459
|
|
|
241
460
|
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
242
461
|
await Promise.all(
|
|
@@ -244,7 +463,19 @@ export class QuarksLoader {
|
|
|
244
463
|
if (typeof img?.url !== 'string' || !img.url) {
|
|
245
464
|
return;
|
|
246
465
|
}
|
|
466
|
+
// An entry this loader already rewrote keeps its url: resolving the same document
|
|
467
|
+
// again (a second load, or a fetch cached for several loads) would join it twice.
|
|
468
|
+
if (resolvedImageUrls.get(img) === img.url) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const source: string = img.url;
|
|
247
472
|
img.url = await QuarksLoader.resolveWith(resolver, img.url, 'image', baseUrl);
|
|
473
|
+
resolvedImageUrls.set(img, img.url);
|
|
474
|
+
// A resolver that inlined the file (`data:`, `blob:`) made the url the image itself;
|
|
475
|
+
// the file's path no longer names what the texture holds, so a save writes the url.
|
|
476
|
+
if (!/^(data|blob):/i.test(img.url)) {
|
|
477
|
+
sourceImageUrls.set(img, source);
|
|
478
|
+
}
|
|
248
479
|
})
|
|
249
480
|
);
|
|
250
481
|
}
|
|
@@ -259,8 +490,32 @@ export class QuarksLoader {
|
|
|
259
490
|
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
260
491
|
}
|
|
261
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Builds the node tree of an effect document whose linked files are already inline (see
|
|
495
|
+
* {@link loadFromJSON}). `baseUrl` is joined to relative image urls. Parts the loader cannot
|
|
496
|
+
* read warn and take their defaults; a document with no `object` throws
|
|
497
|
+
* {@link QuarksFormatError}. Free the effect with {@link disposeEffect}.
|
|
498
|
+
*/
|
|
262
499
|
parse(json: any, baseUrl: string = ''): TransformNode {
|
|
500
|
+
return this.parseDocument(json, baseUrl).root;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** {@link parse}, and the textures the effect uses. */
|
|
504
|
+
private parseDocument(json: any, baseUrl: string): {root: TransformNode; textures: Texture[]} {
|
|
505
|
+
if (json === null || typeof json !== 'object' || json.object === null || typeof json.object !== 'object') {
|
|
506
|
+
throw new QuarksFormatError('[QuarksLoader] Not an effect document: it has no "object" to load.');
|
|
507
|
+
}
|
|
508
|
+
const format = json.metadata?.quarksFormat;
|
|
509
|
+
if (typeof format === 'number' && format > QUARKS_FORMAT_VERSION) {
|
|
510
|
+
console.warn(
|
|
511
|
+
`[QuarksLoader] This effect was written in format ${format}, newer than the ${QUARKS_FORMAT_VERSION} ` +
|
|
512
|
+
'this version of babylon.quarks reads; settings it does not know are left out.'
|
|
513
|
+
);
|
|
514
|
+
}
|
|
263
515
|
const meta: LoadedMeta = {
|
|
516
|
+
ownedMaterials: [],
|
|
517
|
+
images: {},
|
|
518
|
+
imageSources: {},
|
|
264
519
|
textures: {},
|
|
265
520
|
geometries: {},
|
|
266
521
|
materials: {},
|
|
@@ -281,7 +536,9 @@ export class QuarksLoader {
|
|
|
281
536
|
|
|
282
537
|
const root = this.parseObject(json.object, meta);
|
|
283
538
|
this.linkReferences(root);
|
|
284
|
-
|
|
539
|
+
const textures = [...new Set(Object.values(meta.textures).filter((t): t is Texture => t !== null))];
|
|
540
|
+
retainEffectResources(root, textures, meta.ownedMaterials);
|
|
541
|
+
return {root, textures};
|
|
285
542
|
}
|
|
286
543
|
|
|
287
544
|
private parseGeometries(geometries: any[], meta: LoadedMeta): void {
|
|
@@ -313,6 +570,7 @@ export class QuarksLoader {
|
|
|
313
570
|
positions: new Float32Array(geom.positions ?? []),
|
|
314
571
|
indices: geom.indices ? new Uint32Array(geom.indices) : new Uint32Array(0),
|
|
315
572
|
uvs: geom.uvs ? new Float32Array(geom.uvs) : undefined,
|
|
573
|
+
uvs2: geom.uvs2 ? new Float32Array(geom.uvs2) : undefined,
|
|
316
574
|
normals: geom.normals ? new Float32Array(geom.normals) : undefined,
|
|
317
575
|
});
|
|
318
576
|
}
|
|
@@ -427,7 +685,10 @@ export class QuarksLoader {
|
|
|
427
685
|
let positions = new Float32Array(0);
|
|
428
686
|
let indices: Uint32Array | Uint16Array = new Uint32Array(0);
|
|
429
687
|
let uvs: Float32Array | undefined;
|
|
688
|
+
let uvs2: Float32Array | undefined;
|
|
430
689
|
let normals: Float32Array | undefined;
|
|
690
|
+
// three.js names the second UV set `uv1` (r151 on), `uv2` before.
|
|
691
|
+
const uv2Attribute = data.attributes?.uv1 ?? data.attributes?.uv2;
|
|
431
692
|
|
|
432
693
|
if (data.interleavedBuffers && data.arrayBuffers) {
|
|
433
694
|
const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
|
|
@@ -453,6 +714,16 @@ export class QuarksLoader {
|
|
|
453
714
|
const buf = buffers[attr.data];
|
|
454
715
|
if (buf) uvs = this.extractInterleavedAttribute(buf.data, buf.stride, attr.offset, attr.itemSize);
|
|
455
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
|
+
}
|
|
456
727
|
if (data.attributes.normal?.isInterleavedBufferAttribute) {
|
|
457
728
|
const attr = data.attributes.normal;
|
|
458
729
|
const buf = buffers[attr.data];
|
|
@@ -467,6 +738,9 @@ export class QuarksLoader {
|
|
|
467
738
|
if (data.attributes.uv?.array) {
|
|
468
739
|
uvs = new Float32Array(data.attributes.uv.array);
|
|
469
740
|
}
|
|
741
|
+
if (uv2Attribute?.array) {
|
|
742
|
+
uvs2 = new Float32Array(uv2Attribute.array);
|
|
743
|
+
}
|
|
470
744
|
if (data.attributes.normal?.array) {
|
|
471
745
|
normals = new Float32Array(data.attributes.normal.array);
|
|
472
746
|
}
|
|
@@ -482,7 +756,7 @@ export class QuarksLoader {
|
|
|
482
756
|
|
|
483
757
|
indices = ensureTriangleIndices(positions, indices);
|
|
484
758
|
|
|
485
|
-
return {positions, indices, uvs, normals};
|
|
759
|
+
return {positions, indices, uvs, uvs2, normals};
|
|
486
760
|
}
|
|
487
761
|
|
|
488
762
|
private extractInterleavedAttribute(buffer: Float32Array, stride: number, offset: number, itemSize: number): any {
|
|
@@ -510,10 +784,10 @@ export class QuarksLoader {
|
|
|
510
784
|
}
|
|
511
785
|
|
|
512
786
|
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
513
|
-
private async fetchEffectDocument(url: string): Promise<{json: any; baseUrl: string}> {
|
|
787
|
+
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json: any; baseUrl: string}> {
|
|
514
788
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
515
789
|
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
516
|
-
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined);
|
|
790
|
+
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined, signal);
|
|
517
791
|
return {json, baseUrl};
|
|
518
792
|
}
|
|
519
793
|
|
|
@@ -529,12 +803,30 @@ export class QuarksLoader {
|
|
|
529
803
|
if (total === 0) {
|
|
530
804
|
return [];
|
|
531
805
|
}
|
|
806
|
+
checkLoad(this.scene, options.signal);
|
|
807
|
+
// Stops the whole batch on abort, and the effects still waiting for textures once one fails.
|
|
808
|
+
const stopAll = new AbortController();
|
|
809
|
+
const abort = () => stopAll.abort(abortReason(options.signal!));
|
|
810
|
+
options.signal?.addEventListener('abort', abort, {once: true});
|
|
811
|
+
try {
|
|
812
|
+
return await this.loadResolvedDocuments(docs, options, stopAll);
|
|
813
|
+
} finally {
|
|
814
|
+
options.signal?.removeEventListener('abort', abort);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
532
817
|
|
|
533
|
-
|
|
818
|
+
private async loadResolvedDocuments(
|
|
819
|
+
docs: Array<{json: any; baseUrl: string} | null>,
|
|
820
|
+
options: QuarksBatchLoadOptions,
|
|
821
|
+
stopAll: AbortController
|
|
822
|
+
): Promise<(TransformNode | null)[]> {
|
|
823
|
+
const total = docs.length;
|
|
824
|
+
const signal = stopAll.signal;
|
|
825
|
+
const results: (TransformNode | null)[] = new Array(total).fill(null);
|
|
534
826
|
const settled = new Array<boolean>(total).fill(false);
|
|
535
827
|
let done = 0;
|
|
536
828
|
const settle = (index: number, value: TransformNode | null) => {
|
|
537
|
-
if (settled[index]) {
|
|
829
|
+
if (settled[index] || signal.aborted) {
|
|
538
830
|
return;
|
|
539
831
|
}
|
|
540
832
|
settled[index] = true;
|
|
@@ -543,42 +835,67 @@ export class QuarksLoader {
|
|
|
543
835
|
options.onProgress?.(done, total);
|
|
544
836
|
};
|
|
545
837
|
|
|
546
|
-
await
|
|
547
|
-
|
|
838
|
+
await untilStopped(
|
|
839
|
+
Promise.all(
|
|
840
|
+
docs.map(async (doc, i) => {
|
|
841
|
+
if (!doc) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
await this.resolveResources(doc.json, doc.baseUrl);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
if (!options.continueOnError) {
|
|
848
|
+
throw err;
|
|
849
|
+
}
|
|
850
|
+
console.warn(`[QuarksLoader] Failed to resolve resources for effect ${i + 1}/${total}:`, err);
|
|
851
|
+
docs[i] = null;
|
|
852
|
+
settle(i, null);
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
),
|
|
856
|
+
this.scene,
|
|
857
|
+
signal
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
const loaded: Array<{root: TransformNode; textures: Texture[]}> = [];
|
|
861
|
+
const finishing: Promise<void>[] = [];
|
|
862
|
+
try {
|
|
863
|
+
for (let i = 0; i < total; i++) {
|
|
864
|
+
if (settled[i]) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const doc = docs[i];
|
|
548
868
|
if (!doc) {
|
|
549
|
-
|
|
869
|
+
settle(i, null);
|
|
870
|
+
continue;
|
|
550
871
|
}
|
|
872
|
+
let effect: {root: TransformNode; textures: Texture[]};
|
|
551
873
|
try {
|
|
552
|
-
|
|
874
|
+
// Resolved above, so the base must not be joined a second time.
|
|
875
|
+
effect = this.parseDocument(doc.json, '');
|
|
553
876
|
} catch (err) {
|
|
554
877
|
if (!options.continueOnError) {
|
|
555
878
|
throw err;
|
|
556
879
|
}
|
|
557
|
-
console.warn(`[QuarksLoader] Failed to
|
|
558
|
-
docs[i] = null;
|
|
880
|
+
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
559
881
|
settle(i, null);
|
|
882
|
+
continue;
|
|
560
883
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
for (let i = 0; i < total; i++) {
|
|
565
|
-
if (settled[i]) {
|
|
566
|
-
continue;
|
|
884
|
+
loaded.push(effect);
|
|
885
|
+
// An effect counts as loaded once its textures have too, when the loader waits for them.
|
|
886
|
+
finishing.push(this.finishLoad(effect, signal).then(() => settle(i, effect.root)));
|
|
567
887
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
888
|
+
await Promise.all(finishing);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
// One failure, or the load stopped: stop the rest and free every effect already built.
|
|
891
|
+
if (!signal.aborted) {
|
|
892
|
+
stopAll.abort(err);
|
|
572
893
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (!options.continueOnError) {
|
|
577
|
-
throw err;
|
|
578
|
-
}
|
|
579
|
-
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
580
|
-
settle(i, null);
|
|
894
|
+
void Promise.allSettled(finishing);
|
|
895
|
+
if (!this.scene.isDisposed) {
|
|
896
|
+
loaded.forEach((effect) => disposeEffect(effect.root));
|
|
581
897
|
}
|
|
898
|
+
throw err;
|
|
582
899
|
}
|
|
583
900
|
return results;
|
|
584
901
|
}
|
|
@@ -624,13 +941,15 @@ export class QuarksLoader {
|
|
|
624
941
|
private static fetchJsonWith(
|
|
625
942
|
resolver: QuarksResourceResolver | undefined,
|
|
626
943
|
resolvedUrl: string,
|
|
627
|
-
scene: Scene | undefined
|
|
944
|
+
scene: Scene | undefined,
|
|
945
|
+
signal?: AbortSignal
|
|
628
946
|
): Promise<unknown> {
|
|
629
947
|
const load = async () => {
|
|
630
948
|
if (resolver?.fetchJson) {
|
|
631
|
-
return resolver.fetchJson(resolvedUrl);
|
|
949
|
+
return signal ? resolver.fetchJson(resolvedUrl, {signal}) : resolver.fetchJson(resolvedUrl);
|
|
632
950
|
}
|
|
633
|
-
|
|
951
|
+
// Only an uncached fetch (an effect document) can be cancelled: a cached one is shared.
|
|
952
|
+
const response = await (signal ? fetch(resolvedUrl, {signal}) : fetch(resolvedUrl));
|
|
634
953
|
if (!response.ok) {
|
|
635
954
|
throw new Error(`HTTP ${response.status}`);
|
|
636
955
|
}
|
|
@@ -656,7 +975,11 @@ export class QuarksLoader {
|
|
|
656
975
|
return pending;
|
|
657
976
|
}
|
|
658
977
|
|
|
659
|
-
private static async inlineGeometryEntries(
|
|
978
|
+
private static async inlineGeometryEntries(
|
|
979
|
+
json: any,
|
|
980
|
+
loadGeometry: (url: string) => Promise<any>,
|
|
981
|
+
onResourceError: QuarksLoaderOptions['onResourceError'] = QuarksLoader.warnResourceError
|
|
982
|
+
): Promise<void> {
|
|
660
983
|
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
661
984
|
await Promise.all(
|
|
662
985
|
geometries
|
|
@@ -666,7 +989,7 @@ export class QuarksLoader {
|
|
|
666
989
|
const data = await loadGeometry(geom.url);
|
|
667
990
|
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
668
991
|
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
669
|
-
for (const key of ['positions', 'indices', 'uvs', 'normals']) {
|
|
992
|
+
for (const key of ['positions', 'indices', 'uvs', 'uvs2', 'normals']) {
|
|
670
993
|
const value = data?.[key];
|
|
671
994
|
if (value === undefined) {
|
|
672
995
|
continue;
|
|
@@ -675,21 +998,34 @@ export class QuarksLoader {
|
|
|
675
998
|
}
|
|
676
999
|
delete geom.url;
|
|
677
1000
|
} catch (err) {
|
|
678
|
-
|
|
1001
|
+
onResourceError('geometry', geom.url, err);
|
|
679
1002
|
}
|
|
680
1003
|
})
|
|
681
1004
|
);
|
|
682
1005
|
}
|
|
683
1006
|
|
|
684
|
-
private
|
|
685
|
-
(
|
|
1007
|
+
private static warnResourceError(kind: QuarksResourceKind, url: string, error: unknown): void {
|
|
1008
|
+
console.warn(`[QuarksLoader] Could not load ${kind} "${url}":`, error);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private reportResourceError(kind: QuarksResourceKind, url: string, error: unknown): void {
|
|
1012
|
+
(this.options.onResourceError ?? QuarksLoader.warnResourceError)(kind, url, error);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private parseImages(images: any[], baseUrl: string, meta: LoadedMeta): void {
|
|
686
1016
|
for (const img of images) {
|
|
687
1017
|
if (img.url) {
|
|
688
1018
|
QuarksLoader.warnIfNotAnImageUrl(img.url);
|
|
689
1019
|
}
|
|
690
1020
|
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
691
1021
|
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
692
|
-
|
|
1022
|
+
meta.images[img.uuid] = img.url ? QuarksLoader.joinUrl(baseUrl, img.url) : null;
|
|
1023
|
+
// The url the file wrote, for serializeEffect to write back. An inlined image
|
|
1024
|
+
// (`data:`, `blob:`) has no path to write; its texture writes its url.
|
|
1025
|
+
const source: string | undefined = sourceImageUrls.get(img) ?? img.url;
|
|
1026
|
+
if (source && !/^(data|blob):/i.test(source)) {
|
|
1027
|
+
meta.imageSources[img.uuid] = source;
|
|
1028
|
+
}
|
|
693
1029
|
}
|
|
694
1030
|
}
|
|
695
1031
|
|
|
@@ -713,9 +1049,8 @@ export class QuarksLoader {
|
|
|
713
1049
|
}
|
|
714
1050
|
|
|
715
1051
|
private parseTextures(textures: any[], meta: LoadedMeta): void {
|
|
716
|
-
const images = (this as any)._images || {};
|
|
717
1052
|
for (const texDef of textures) {
|
|
718
|
-
const imageUrl = images[texDef.image];
|
|
1053
|
+
const imageUrl = meta.images[texDef.image];
|
|
719
1054
|
if (imageUrl) {
|
|
720
1055
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
721
1056
|
const noMipmap = !!texDef.noMipmap;
|
|
@@ -741,11 +1076,41 @@ export class QuarksLoader {
|
|
|
741
1076
|
meta.textures[texDef.uuid] = cached;
|
|
742
1077
|
continue;
|
|
743
1078
|
}
|
|
744
|
-
|
|
1079
|
+
let settle!: () => void;
|
|
1080
|
+
const settled = new Promise<void>((resolve) => (settle = resolve));
|
|
1081
|
+
let failed = false;
|
|
1082
|
+
// Babylon calls onError from inside the constructor, before `texture` is assigned, and
|
|
1083
|
+
// then throws, when the engine cannot make the texture at all.
|
|
1084
|
+
let texture: Texture | undefined = undefined;
|
|
1085
|
+
const options: ITextureCreationOptions = {
|
|
745
1086
|
noMipmap,
|
|
746
1087
|
invertY,
|
|
747
1088
|
samplingMode: typeof texDef.samplingMode === 'number' ? texDef.samplingMode : undefined,
|
|
748
|
-
|
|
1089
|
+
onLoad: () => settle(),
|
|
1090
|
+
onError: (message, exception) => {
|
|
1091
|
+
failed = true;
|
|
1092
|
+
// A later load tries the image again rather than reuse the failed texture.
|
|
1093
|
+
if (cache !== undefined && cache.get(key) === texture) {
|
|
1094
|
+
cache.delete(key);
|
|
1095
|
+
}
|
|
1096
|
+
this.reportResourceError('image', imageUrl, exception ?? message);
|
|
1097
|
+
settle();
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
1100
|
+
try {
|
|
1101
|
+
texture = new Texture(imageUrl, this.scene, options);
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Reported through onError: the effect draws without this texture.
|
|
1104
|
+
meta.textures[texDef.uuid] = null;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
textureSettled.set(texture, settled);
|
|
1108
|
+
const source = meta.imageSources[texDef.image];
|
|
1109
|
+
if (source !== undefined) {
|
|
1110
|
+
setTextureSourceUrl(texture, source, imageUrl);
|
|
1111
|
+
}
|
|
1112
|
+
// A texture disposed before it loads never will.
|
|
1113
|
+
texture.onDisposeObservable.addOnce(() => settle());
|
|
749
1114
|
if (Array.isArray(texDef.wrap)) {
|
|
750
1115
|
texture.wrapU = this.mapWrapMode(texDef.wrap[0]);
|
|
751
1116
|
texture.wrapV = this.mapWrapMode(texDef.wrap[1] ?? texDef.wrap[0]);
|
|
@@ -761,7 +1126,7 @@ export class QuarksLoader {
|
|
|
761
1126
|
if (typeof texDef.rotation === 'number') {
|
|
762
1127
|
texture.wAng = texDef.rotation;
|
|
763
1128
|
}
|
|
764
|
-
if (cache !== undefined) {
|
|
1129
|
+
if (cache !== undefined && !failed) {
|
|
765
1130
|
cache.set(key, texture);
|
|
766
1131
|
// Something outside the library may dispose it; never hand out a dead texture.
|
|
767
1132
|
texture.onDisposeObservable.addOnce(() => {
|
|
@@ -824,13 +1189,18 @@ export class QuarksLoader {
|
|
|
824
1189
|
if (matDef.rim && typeof matDef.rim === 'object') {
|
|
825
1190
|
matInfo.rim = matDef.rim;
|
|
826
1191
|
}
|
|
827
|
-
// Texture effects (QuarksMaterial):
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
1192
|
+
// Texture effects (QuarksMaterial): mapUV {channel, tiling, offset, scroll},
|
|
1193
|
+
// gradientMap {ramp (texture uuid), channel},
|
|
1194
|
+
// erosion {mask (texture uuid), maskUV {channel, tiling, offset, scroll}, softness, edgeWidth,
|
|
1195
|
+
// edgeColor}.
|
|
1196
|
+
if (matDef.mapUV && typeof matDef.mapUV === 'object') {
|
|
1197
|
+
matInfo.mapUV = matDef.mapUV;
|
|
831
1198
|
}
|
|
832
|
-
if (matDef.gradientMap) {
|
|
833
|
-
matInfo.gradientMap =
|
|
1199
|
+
if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
|
|
1200
|
+
matInfo.gradientMap = {
|
|
1201
|
+
...matDef.gradientMap,
|
|
1202
|
+
ramp: matDef.gradientMap.ramp ? meta.textures[matDef.gradientMap.ramp] || null : null,
|
|
1203
|
+
};
|
|
834
1204
|
}
|
|
835
1205
|
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
836
1206
|
matInfo.erosion = {
|
|
@@ -838,8 +1208,15 @@ export class QuarksLoader {
|
|
|
838
1208
|
mask: matDef.erosion.mask ? meta.textures[matDef.erosion.mask] || null : null,
|
|
839
1209
|
};
|
|
840
1210
|
}
|
|
1211
|
+
// uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
|
|
1212
|
+
if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
|
|
1213
|
+
matInfo.uvDistortion = {
|
|
1214
|
+
...matDef.uvDistortion,
|
|
1215
|
+
map: matDef.uvDistortion.map ? meta.textures[matDef.uvDistortion.map] || null : null,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
841
1218
|
|
|
842
|
-
// QuarksMaterial (what
|
|
1219
|
+
// QuarksMaterial (what `materialMeta` in system/serialization.ts writes) writes `alphaMode`
|
|
843
1220
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
|
844
1221
|
// etc.) instead use `blending` with three.js's own 1..4 numbering, which does NOT
|
|
845
1222
|
// line up with Babylon's constants (e.g. three.js Additive=2 vs Babylon ALPHA_ADD=1)
|
|
@@ -887,6 +1264,9 @@ export class QuarksLoader {
|
|
|
887
1264
|
if (geometry.uvs && geometry.uvs.length > 0) {
|
|
888
1265
|
vertexData.uvs = geometry.uvs;
|
|
889
1266
|
}
|
|
1267
|
+
if (geometry.uvs2 && geometry.uvs2.length > 0) {
|
|
1268
|
+
vertexData.uvs2 = geometry.uvs2;
|
|
1269
|
+
}
|
|
890
1270
|
if (geometry.normals && geometry.normals.length > 0) {
|
|
891
1271
|
vertexData.normals = geometry.normals;
|
|
892
1272
|
}
|
|
@@ -896,6 +1276,7 @@ export class QuarksLoader {
|
|
|
896
1276
|
const materialInfo = this.getMaterial(meta, data.material);
|
|
897
1277
|
if (materialInfo) {
|
|
898
1278
|
const material = new StandardMaterial(`quarks_loader_material_${materialInfo.uuid}`, this.scene);
|
|
1279
|
+
meta.ownedMaterials.push(material);
|
|
899
1280
|
material.alphaMode = materialInfo.alphaMode ?? Constants.ALPHA_COMBINE;
|
|
900
1281
|
material.backFaceCulling = materialInfo.side !== 2;
|
|
901
1282
|
material.alphaCutOff = materialInfo.alphaTest ?? 0;
|
|
@@ -983,6 +1364,7 @@ export class QuarksLoader {
|
|
|
983
1364
|
}
|
|
984
1365
|
|
|
985
1366
|
if (data.visible !== undefined) node.setEnabled(data.visible);
|
|
1367
|
+
if (data.isVisible === false && node instanceof Mesh) node.isVisible = false;
|
|
986
1368
|
|
|
987
1369
|
if (data.children) {
|
|
988
1370
|
for (const childData of data.children) {
|
|
@@ -1027,6 +1409,12 @@ export class QuarksLoader {
|
|
|
1027
1409
|
const targetNode = nodesMap[meshSurfaceUUID];
|
|
1028
1410
|
if (targetNode instanceof Mesh) {
|
|
1029
1411
|
(system.emitterShape as any).mesh = targetNode;
|
|
1412
|
+
} else {
|
|
1413
|
+
console.warn(
|
|
1414
|
+
`[QuarksLoader] Emitter "${node.name}" has a mesh surface shape on "${meshSurfaceUUID}", ` +
|
|
1415
|
+
(targetNode ? 'which is not a mesh' : 'which is not in the effect') +
|
|
1416
|
+
'; it emits nothing until a mesh is set.'
|
|
1417
|
+
);
|
|
1030
1418
|
}
|
|
1031
1419
|
}
|
|
1032
1420
|
for (let i = 0; i < system.behaviors.length; i++) {
|