babylon.quarks 0.26.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 +769 -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 +1959 -588
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +204 -101
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +909 -2061
- package/src/QuarksLoader.ts +421 -67
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +106 -246
- package/src/TrailBatch.ts +533 -342
- package/src/VFXBatch.ts +236 -136
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +32 -33
- 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 +10 -0
- 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 +3 -22
- package/src/shaders/particle_frag.wgsl.ts +3 -23
- package/src/shaders/particle_physics_frag.glsl.ts +12 -25
- package/src/shaders/particle_physics_frag.wgsl.ts +12 -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 +11 -1
- package/src/shaders/trail_frag.wgsl.ts +11 -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 +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.
|
|
@@ -94,6 +132,12 @@ function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<st
|
|
|
94
132
|
return map;
|
|
95
133
|
}
|
|
96
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
|
+
|
|
97
141
|
/** Content key for a geometry: two independent 32-bit hashes over every array's bytes, plus lengths. */
|
|
98
142
|
function geometryKey(geometry: ParsedGeometry): string {
|
|
99
143
|
let h1 = 0x811c9dc5;
|
|
@@ -121,12 +165,109 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
121
165
|
return `${lengths}${(h1 >>> 0).toString(16)}${(h2 >>> 0).toString(16)}`;
|
|
122
166
|
}
|
|
123
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
|
+
|
|
124
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};
|
|
125
252
|
textures: {[uuid: string]: Texture | null};
|
|
126
253
|
geometries: {[uuid: string]: ParsedGeometry};
|
|
127
254
|
materials: {[uuid: string]: any};
|
|
128
255
|
}
|
|
129
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
|
+
*/
|
|
130
271
|
export class QuarksLoader {
|
|
131
272
|
private scene: Scene;
|
|
132
273
|
private options: QuarksLoaderOptions;
|
|
@@ -136,20 +277,77 @@ export class QuarksLoader {
|
|
|
136
277
|
this.options = options;
|
|
137
278
|
}
|
|
138
279
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
143
291
|
}
|
|
144
292
|
|
|
145
293
|
/**
|
|
146
|
-
* Resolves linked geometry / image urls
|
|
147
|
-
*
|
|
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.
|
|
148
298
|
*/
|
|
149
|
-
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);
|
|
150
302
|
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
}
|
|
153
351
|
}
|
|
154
352
|
|
|
155
353
|
/**
|
|
@@ -170,17 +368,21 @@ export class QuarksLoader {
|
|
|
170
368
|
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
171
369
|
): Promise<(TransformNode | null)[]>;
|
|
172
370
|
async loadAll(urls: string[], options: QuarksBatchLoadOptions = {}): Promise<(TransformNode | null)[]> {
|
|
173
|
-
|
|
371
|
+
checkLoad(this.scene, options.signal);
|
|
372
|
+
const fetching = QuarksLoader.mapPool(urls, options.concurrency, async (url, index) => {
|
|
373
|
+
checkLoad(this.scene, options.signal);
|
|
174
374
|
try {
|
|
175
|
-
return await this.fetchEffectDocument(url);
|
|
375
|
+
return await this.fetchEffectDocument(url, options.signal);
|
|
176
376
|
} catch (err) {
|
|
177
|
-
|
|
377
|
+
// A stopped load is never one effect's failure.
|
|
378
|
+
if (!options.continueOnError || options.signal?.aborted || this.scene.isDisposed) {
|
|
178
379
|
throw err;
|
|
179
380
|
}
|
|
180
381
|
console.warn(`[QuarksLoader] Failed to fetch effect "${url}" (${index + 1}/${urls.length}):`, err);
|
|
181
382
|
return null;
|
|
182
383
|
}
|
|
183
384
|
});
|
|
385
|
+
const docs = await untilStopped(fetching, this.scene, options.signal);
|
|
184
386
|
return this.loadDocuments(docs, options);
|
|
185
387
|
}
|
|
186
388
|
|
|
@@ -201,44 +403,59 @@ export class QuarksLoader {
|
|
|
201
403
|
options: QuarksBatchLoadOptions = {}
|
|
202
404
|
): Promise<(TransformNode | null)[]> {
|
|
203
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.
|
|
204
407
|
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
205
408
|
const typed = entry as QuarksJSONLoadEntry;
|
|
206
|
-
return {json: typed.json
|
|
409
|
+
return {json: detachDocument(typed.json), baseUrl: typed.baseUrl ?? this.options.baseUrl ?? ''};
|
|
207
410
|
}
|
|
208
|
-
return {json: entry
|
|
411
|
+
return {json: detachDocument(entry), baseUrl: this.options.baseUrl ?? ''};
|
|
209
412
|
});
|
|
210
413
|
return this.loadDocuments(docs, options);
|
|
211
414
|
}
|
|
212
415
|
|
|
213
416
|
/**
|
|
214
417
|
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
215
|
-
* {@link QuarksResourceResolver}
|
|
216
|
-
* 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.
|
|
217
422
|
*/
|
|
218
423
|
async resolveResources(json: any, baseUrl: string = ''): Promise<void> {
|
|
219
424
|
await QuarksLoader.resolveLinkedResources(json, {
|
|
220
425
|
baseUrl,
|
|
221
426
|
resolver: this.options.resolver,
|
|
222
427
|
scene: this.scene,
|
|
428
|
+
onResourceError: this.options.onResourceError,
|
|
223
429
|
});
|
|
224
430
|
}
|
|
225
431
|
|
|
226
432
|
/**
|
|
227
433
|
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
228
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.
|
|
229
437
|
*/
|
|
230
438
|
static async resolveLinkedResources(
|
|
231
439
|
json: any,
|
|
232
|
-
options: {
|
|
440
|
+
options: {
|
|
441
|
+
baseUrl?: string;
|
|
442
|
+
resolver?: QuarksResourceResolver;
|
|
443
|
+
scene?: Scene;
|
|
444
|
+
onResourceError?: QuarksLoaderOptions['onResourceError'];
|
|
445
|
+
} = {}
|
|
233
446
|
): Promise<void> {
|
|
234
447
|
const baseUrl = options.baseUrl ?? '';
|
|
235
448
|
const resolver = options.resolver;
|
|
236
449
|
const scene = options.scene;
|
|
237
450
|
|
|
238
|
-
await QuarksLoader.inlineGeometryEntries(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
);
|
|
242
459
|
|
|
243
460
|
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
244
461
|
await Promise.all(
|
|
@@ -246,7 +463,19 @@ export class QuarksLoader {
|
|
|
246
463
|
if (typeof img?.url !== 'string' || !img.url) {
|
|
247
464
|
return;
|
|
248
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;
|
|
249
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
|
+
}
|
|
250
479
|
})
|
|
251
480
|
);
|
|
252
481
|
}
|
|
@@ -261,8 +490,32 @@ export class QuarksLoader {
|
|
|
261
490
|
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
262
491
|
}
|
|
263
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
|
+
*/
|
|
264
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
|
+
}
|
|
265
515
|
const meta: LoadedMeta = {
|
|
516
|
+
ownedMaterials: [],
|
|
517
|
+
images: {},
|
|
518
|
+
imageSources: {},
|
|
266
519
|
textures: {},
|
|
267
520
|
geometries: {},
|
|
268
521
|
materials: {},
|
|
@@ -283,7 +536,9 @@ export class QuarksLoader {
|
|
|
283
536
|
|
|
284
537
|
const root = this.parseObject(json.object, meta);
|
|
285
538
|
this.linkReferences(root);
|
|
286
|
-
|
|
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};
|
|
287
542
|
}
|
|
288
543
|
|
|
289
544
|
private parseGeometries(geometries: any[], meta: LoadedMeta): void {
|
|
@@ -529,10 +784,10 @@ export class QuarksLoader {
|
|
|
529
784
|
}
|
|
530
785
|
|
|
531
786
|
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
532
|
-
private async fetchEffectDocument(url: string): Promise<{json: any; baseUrl: string}> {
|
|
787
|
+
private async fetchEffectDocument(url: string, signal?: AbortSignal): Promise<{json: any; baseUrl: string}> {
|
|
533
788
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
534
789
|
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
535
|
-
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined);
|
|
790
|
+
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined, signal);
|
|
536
791
|
return {json, baseUrl};
|
|
537
792
|
}
|
|
538
793
|
|
|
@@ -548,12 +803,30 @@ export class QuarksLoader {
|
|
|
548
803
|
if (total === 0) {
|
|
549
804
|
return [];
|
|
550
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
|
+
}
|
|
551
817
|
|
|
552
|
-
|
|
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);
|
|
553
826
|
const settled = new Array<boolean>(total).fill(false);
|
|
554
827
|
let done = 0;
|
|
555
828
|
const settle = (index: number, value: TransformNode | null) => {
|
|
556
|
-
if (settled[index]) {
|
|
829
|
+
if (settled[index] || signal.aborted) {
|
|
557
830
|
return;
|
|
558
831
|
}
|
|
559
832
|
settled[index] = true;
|
|
@@ -562,42 +835,67 @@ export class QuarksLoader {
|
|
|
562
835
|
options.onProgress?.(done, total);
|
|
563
836
|
};
|
|
564
837
|
|
|
565
|
-
await
|
|
566
|
-
|
|
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];
|
|
567
868
|
if (!doc) {
|
|
568
|
-
|
|
869
|
+
settle(i, null);
|
|
870
|
+
continue;
|
|
569
871
|
}
|
|
872
|
+
let effect: {root: TransformNode; textures: Texture[]};
|
|
570
873
|
try {
|
|
571
|
-
|
|
874
|
+
// Resolved above, so the base must not be joined a second time.
|
|
875
|
+
effect = this.parseDocument(doc.json, '');
|
|
572
876
|
} catch (err) {
|
|
573
877
|
if (!options.continueOnError) {
|
|
574
878
|
throw err;
|
|
575
879
|
}
|
|
576
|
-
console.warn(`[QuarksLoader] Failed to
|
|
577
|
-
docs[i] = null;
|
|
880
|
+
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
578
881
|
settle(i, null);
|
|
882
|
+
continue;
|
|
579
883
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
for (let i = 0; i < total; i++) {
|
|
584
|
-
if (settled[i]) {
|
|
585
|
-
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)));
|
|
586
887
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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);
|
|
591
893
|
}
|
|
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);
|
|
894
|
+
void Promise.allSettled(finishing);
|
|
895
|
+
if (!this.scene.isDisposed) {
|
|
896
|
+
loaded.forEach((effect) => disposeEffect(effect.root));
|
|
600
897
|
}
|
|
898
|
+
throw err;
|
|
601
899
|
}
|
|
602
900
|
return results;
|
|
603
901
|
}
|
|
@@ -643,13 +941,15 @@ export class QuarksLoader {
|
|
|
643
941
|
private static fetchJsonWith(
|
|
644
942
|
resolver: QuarksResourceResolver | undefined,
|
|
645
943
|
resolvedUrl: string,
|
|
646
|
-
scene: Scene | undefined
|
|
944
|
+
scene: Scene | undefined,
|
|
945
|
+
signal?: AbortSignal
|
|
647
946
|
): Promise<unknown> {
|
|
648
947
|
const load = async () => {
|
|
649
948
|
if (resolver?.fetchJson) {
|
|
650
|
-
return resolver.fetchJson(resolvedUrl);
|
|
949
|
+
return signal ? resolver.fetchJson(resolvedUrl, {signal}) : resolver.fetchJson(resolvedUrl);
|
|
651
950
|
}
|
|
652
|
-
|
|
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));
|
|
653
953
|
if (!response.ok) {
|
|
654
954
|
throw new Error(`HTTP ${response.status}`);
|
|
655
955
|
}
|
|
@@ -675,7 +975,11 @@ export class QuarksLoader {
|
|
|
675
975
|
return pending;
|
|
676
976
|
}
|
|
677
977
|
|
|
678
|
-
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> {
|
|
679
983
|
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
680
984
|
await Promise.all(
|
|
681
985
|
geometries
|
|
@@ -694,21 +998,34 @@ export class QuarksLoader {
|
|
|
694
998
|
}
|
|
695
999
|
delete geom.url;
|
|
696
1000
|
} catch (err) {
|
|
697
|
-
|
|
1001
|
+
onResourceError('geometry', geom.url, err);
|
|
698
1002
|
}
|
|
699
1003
|
})
|
|
700
1004
|
);
|
|
701
1005
|
}
|
|
702
1006
|
|
|
703
|
-
private
|
|
704
|
-
(
|
|
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 {
|
|
705
1016
|
for (const img of images) {
|
|
706
1017
|
if (img.url) {
|
|
707
1018
|
QuarksLoader.warnIfNotAnImageUrl(img.url);
|
|
708
1019
|
}
|
|
709
1020
|
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
710
1021
|
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
711
|
-
|
|
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
|
+
}
|
|
712
1029
|
}
|
|
713
1030
|
}
|
|
714
1031
|
|
|
@@ -732,9 +1049,8 @@ export class QuarksLoader {
|
|
|
732
1049
|
}
|
|
733
1050
|
|
|
734
1051
|
private parseTextures(textures: any[], meta: LoadedMeta): void {
|
|
735
|
-
const images = (this as any)._images || {};
|
|
736
1052
|
for (const texDef of textures) {
|
|
737
|
-
const imageUrl = images[texDef.image];
|
|
1053
|
+
const imageUrl = meta.images[texDef.image];
|
|
738
1054
|
if (imageUrl) {
|
|
739
1055
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
740
1056
|
const noMipmap = !!texDef.noMipmap;
|
|
@@ -760,11 +1076,41 @@ export class QuarksLoader {
|
|
|
760
1076
|
meta.textures[texDef.uuid] = cached;
|
|
761
1077
|
continue;
|
|
762
1078
|
}
|
|
763
|
-
|
|
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 = {
|
|
764
1086
|
noMipmap,
|
|
765
1087
|
invertY,
|
|
766
1088
|
samplingMode: typeof texDef.samplingMode === 'number' ? texDef.samplingMode : undefined,
|
|
767
|
-
|
|
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());
|
|
768
1114
|
if (Array.isArray(texDef.wrap)) {
|
|
769
1115
|
texture.wrapU = this.mapWrapMode(texDef.wrap[0]);
|
|
770
1116
|
texture.wrapV = this.mapWrapMode(texDef.wrap[1] ?? texDef.wrap[0]);
|
|
@@ -780,7 +1126,7 @@ export class QuarksLoader {
|
|
|
780
1126
|
if (typeof texDef.rotation === 'number') {
|
|
781
1127
|
texture.wAng = texDef.rotation;
|
|
782
1128
|
}
|
|
783
|
-
if (cache !== undefined) {
|
|
1129
|
+
if (cache !== undefined && !failed) {
|
|
784
1130
|
cache.set(key, texture);
|
|
785
1131
|
// Something outside the library may dispose it; never hand out a dead texture.
|
|
786
1132
|
texture.onDisposeObservable.addOnce(() => {
|
|
@@ -870,7 +1216,7 @@ export class QuarksLoader {
|
|
|
870
1216
|
};
|
|
871
1217
|
}
|
|
872
1218
|
|
|
873
|
-
// QuarksMaterial (what
|
|
1219
|
+
// QuarksMaterial (what `materialMeta` in system/serialization.ts writes) writes `alphaMode`
|
|
874
1220
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
|
875
1221
|
// etc.) instead use `blending` with three.js's own 1..4 numbering, which does NOT
|
|
876
1222
|
// line up with Babylon's constants (e.g. three.js Additive=2 vs Babylon ALPHA_ADD=1)
|
|
@@ -930,6 +1276,7 @@ export class QuarksLoader {
|
|
|
930
1276
|
const materialInfo = this.getMaterial(meta, data.material);
|
|
931
1277
|
if (materialInfo) {
|
|
932
1278
|
const material = new StandardMaterial(`quarks_loader_material_${materialInfo.uuid}`, this.scene);
|
|
1279
|
+
meta.ownedMaterials.push(material);
|
|
933
1280
|
material.alphaMode = materialInfo.alphaMode ?? Constants.ALPHA_COMBINE;
|
|
934
1281
|
material.backFaceCulling = materialInfo.side !== 2;
|
|
935
1282
|
material.alphaCutOff = materialInfo.alphaTest ?? 0;
|
|
@@ -1017,6 +1364,7 @@ export class QuarksLoader {
|
|
|
1017
1364
|
}
|
|
1018
1365
|
|
|
1019
1366
|
if (data.visible !== undefined) node.setEnabled(data.visible);
|
|
1367
|
+
if (data.isVisible === false && node instanceof Mesh) node.isVisible = false;
|
|
1020
1368
|
|
|
1021
1369
|
if (data.children) {
|
|
1022
1370
|
for (const childData of data.children) {
|
|
@@ -1061,6 +1409,12 @@ export class QuarksLoader {
|
|
|
1061
1409
|
const targetNode = nodesMap[meshSurfaceUUID];
|
|
1062
1410
|
if (targetNode instanceof Mesh) {
|
|
1063
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
|
+
);
|
|
1064
1418
|
}
|
|
1065
1419
|
}
|
|
1066
1420
|
for (let i = 0; i < system.behaviors.length; i++) {
|