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.
Files changed (79) hide show
  1. package/CHANGELOG.md +769 -8
  2. package/README.md +10 -8
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +6683 -0
  10. package/dist/types/index.d.ts +1959 -588
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +204 -101
  13. package/src/EffectPool.ts +4 -1
  14. package/src/MeshSurfaceEmitter.ts +29 -4
  15. package/src/ParticleEmitter.ts +45 -15
  16. package/src/ParticleSystem.ts +909 -2061
  17. package/src/QuarksLoader.ts +421 -67
  18. package/src/QuarksPrefab.ts +91 -4
  19. package/src/QuarksSerializer.ts +484 -0
  20. package/src/QuarksUtil.ts +15 -0
  21. package/src/SpriteBatch.ts +106 -246
  22. package/src/TrailBatch.ts +533 -342
  23. package/src/VFXBatch.ts +236 -136
  24. package/src/batchKey.ts +237 -0
  25. package/src/effectResources.ts +79 -0
  26. package/src/envAtlas.ts +119 -17
  27. package/src/index.ts +32 -33
  28. package/src/nodeId.ts +13 -0
  29. package/src/shaderProgram.ts +325 -0
  30. package/src/shaderVariants.ts +225 -0
  31. package/src/shaders/chunks/fog.ts +4 -2
  32. package/src/shaders/chunks/sheet.ts +111 -0
  33. package/src/shaders/chunks/soft.ts +84 -0
  34. package/src/shaders/chunks/surface.ts +10 -0
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  37. package/src/shaders/particle_frag.glsl.ts +3 -22
  38. package/src/shaders/particle_frag.wgsl.ts +3 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +12 -25
  40. package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
  41. package/src/shaders/particle_vert.glsl.ts +5 -27
  42. package/src/shaders/particle_vert.wgsl.ts +5 -26
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  45. package/src/shaders/trail_frag.glsl.ts +11 -1
  46. package/src/shaders/trail_frag.wgsl.ts +11 -0
  47. package/src/shaders/trail_vert.glsl.ts +45 -36
  48. package/src/shaders/trail_vert.wgsl.ts +46 -40
  49. package/src/softDepth.ts +136 -0
  50. package/src/system/BehaviorPlan.ts +85 -0
  51. package/src/system/BoundsTracker.ts +181 -0
  52. package/src/system/EmissionController.ts +360 -0
  53. package/src/system/EventDispatcher.ts +44 -0
  54. package/src/system/ParticleSpawner.ts +274 -0
  55. package/src/system/SimulationClock.ts +130 -0
  56. package/src/system/geometry.ts +111 -0
  57. package/src/system/integrate.ts +428 -0
  58. package/src/system/materialResolve.ts +116 -0
  59. package/src/system/normalize.ts +95 -0
  60. package/src/system/serialization.ts +401 -0
  61. package/src/system/settingsSchema.ts +317 -0
  62. package/src/system/types.ts +410 -0
  63. package/src/textureSource.ts +30 -0
  64. package/src/trailSettings.ts +51 -0
  65. package/src/BatchedParticleRenderer.ts +0 -3
  66. package/src/materials/ParticleMaterials.ts +0 -23
  67. package/src/materials/index.ts +0 -1
  68. package/src/shaders/chunks/index.ts +0 -1
  69. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  70. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  71. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  72. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  73. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  74. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  75. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  76. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  77. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  78. package/src/shaders/index.ts +0 -9
  79. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,237 @@
1
+ import {Constants} from '@babylonjs/core/Engines/constants';
2
+ import type {Vector3} from 'quarks.core';
3
+ import type {VFXBatchSettings} from './BatchedRenderer';
4
+ import {DEFAULT_SOFT_FAR_FADE, validSoftFade, validTileCount} from './system/normalize';
5
+ import {
6
+ normalizeErosion,
7
+ normalizeGradientMap,
8
+ normalizeMapUV,
9
+ normalizeMeshRim,
10
+ normalizeUVDistortion,
11
+ readsUV1,
12
+ RenderMode,
13
+ StoredBatchSettings,
14
+ TextureUV,
15
+ } from './VFXBatch';
16
+
17
+ // What decides whether two systems draw from one batch.
18
+ //
19
+ // A batch is one draw call with one material, so its systems share everything the material
20
+ // holds. That falls into two groups, as a Unity material's shader keywords and texture slots
21
+ // against its MaterialPropertyBlock values, or an Unreal material against its dynamic
22
+ // instance's parameters:
23
+ //
24
+ // - the shader key: the render mode, the geometry, the textures, the blend and depth state,
25
+ // and which effects are compiled in. A change here needs a new material;
26
+ // - the live parameters: the numbers the effects run on, such as a soft fade distance, an
27
+ // erosion edge width or a rim colour. They are uniforms, so a change here is written to the
28
+ // material in place.
29
+ //
30
+ // Each render mode reads only part of the settings, so before two settings are compared they
31
+ // are reduced to a canonical form in which every field the mode ignores is at its default. Two
32
+ // billboards that differ only in a mesh rim then share a batch.
33
+
34
+ /** Geometry a trail batch's settings hold, since a trail draws its own strip. */
35
+ const NO_GEOMETRY = new Float32Array(0);
36
+ const NO_INDICES = new Uint16Array(0);
37
+
38
+ /** Whether `renderMode` draws camera-facing quads that take a pivot and screen-size limits. */
39
+ export function takesPivotAndSizeLimits(renderMode: RenderMode): boolean {
40
+ return (
41
+ renderMode === RenderMode.BillBoard ||
42
+ renderMode === RenderMode.StretchedBillBoard ||
43
+ renderMode === RenderMode.VerticalBillBoard ||
44
+ renderMode === RenderMode.HorizontalBillBoard
45
+ );
46
+ }
47
+
48
+ /** Whether the settings draw a flipbook: more than one cell in either direction. */
49
+ export function tilesSheet(settings: {uTileCount: number; vTileCount: number}): boolean {
50
+ return settings.uTileCount > 1 || settings.vTileCount > 1;
51
+ }
52
+
53
+ /** Whether the settings put a screen-size limit on the particles. */
54
+ export function hasSizeLimits(settings: {minParticleSize: number; maxParticleSize: number}): boolean {
55
+ return settings.minParticleSize > 0 || settings.maxParticleSize !== Infinity;
56
+ }
57
+
58
+ /**
59
+ * A batch's copy of a system's settings: every optional field defaulted, and every field the
60
+ * render mode ignores at its default, so that systems that draw the same share a batch whatever
61
+ * their unused settings say. See {@link batchSettingsEqual}.
62
+ */
63
+ export function storedBatchSettings(settings: VFXBatchSettings): StoredBatchSettings {
64
+ const renderMode = settings.renderMode;
65
+ const isMesh = renderMode === RenderMode.Mesh;
66
+ const isTrail = renderMode === RenderMode.Trail;
67
+ const quad = takesPivotAndSizeLimits(renderMode);
68
+ const softParticles = settings.softParticles;
69
+ const transparent = settings.materialTransparent;
70
+ const pivot = settings.pivot;
71
+ const mapUV = normalizeMapUV(settings.mapUV);
72
+ const uvDistortion = normalizeUVDistortion(settings.uvDistortion);
73
+ const gradientMap = normalizeGradientMap(settings.gradientMap);
74
+ const erosion = normalizeErosion(settings.erosion);
75
+ const uTileCount = validTileCount(settings.uTileCount);
76
+ const vTileCount = validTileCount(settings.vTileCount);
77
+ const stored: StoredBatchSettings = {
78
+ renderMode,
79
+ // A trail draws its own strip and reads none of the instanced geometry.
80
+ instancingGeometry: isTrail ? NO_GEOMETRY : settings.instancingGeometry,
81
+ instancingIndices: isTrail ? NO_INDICES : settings.instancingIndices,
82
+ instancingUVs: isTrail ? undefined : settings.instancingUVs,
83
+ // Normals, and the second UV set when a texture reads it, only reach mesh particles.
84
+ instancingUVs2: isMesh && readsUV1({mapUV, uvDistortion, erosion}) ? settings.instancingUVs2 : undefined,
85
+ instancingNormals: isMesh ? settings.instancingNormals : undefined,
86
+ renderOrder: settings.renderOrder,
87
+ uTileCount,
88
+ vTileCount,
89
+ // A trail cuts its cell on the CPU; a 1×1 sheet has no second cell to blend into.
90
+ blendTiles: !isTrail && settings.blendTiles && tilesSheet({uTileCount, vTileCount}),
91
+ softParticles,
92
+ softNearFade: softParticles ? validSoftFade(settings.softNearFade, 0) : 0,
93
+ softFarFade: softParticles ? validSoftFade(settings.softFarFade, DEFAULT_SOFT_FAR_FADE) : 0,
94
+ // An opaque batch draws with blending off, whatever mode it would blend with.
95
+ materialBlendMode: transparent ? settings.materialBlendMode : Constants.ALPHA_DISABLE,
96
+ materialTransparent: transparent,
97
+ materialDepthTest: settings.materialDepthTest,
98
+ materialDepthWrite: settings.materialDepthWrite,
99
+ materialAlphaTest: settings.materialAlphaTest > 0 ? settings.materialAlphaTest : 0,
100
+ texture: settings.texture,
101
+ reflectionTexture: isMesh ? (settings.reflectionTexture ?? null) : null,
102
+ reflectionLevel: isMesh ? (settings.reflectionLevel ?? 1) : 1,
103
+ reflectionAtlas: isMesh ? (settings.reflectionAtlas ?? null) : null,
104
+ layerMask: settings.layerMask,
105
+ renderingGroupId: settings.renderingGroupId ?? 0,
106
+ applyFog: settings.applyFog ?? true,
107
+ pivot: quad && pivot && (pivot.x !== 0 || pivot.y !== 0 || pivot.z !== 0) ? pivot.clone() : undefined,
108
+ sortMode: settings.sortMode ?? 'none',
109
+ minParticleSize: quad ? Math.max(0, settings.minParticleSize ?? 0) : 0,
110
+ maxParticleSize: quad ? positiveOrUnlimited(settings.maxParticleSize) : Infinity,
111
+ lighting: isMesh ? (settings.lighting ?? 'fixed') : 'fixed',
112
+ rim: isMesh ? normalizeMeshRim(settings.rim) : undefined,
113
+ mapUV,
114
+ uvDistortion,
115
+ gradientMap,
116
+ erosion,
117
+ };
118
+ return stored;
119
+ }
120
+
121
+ /** `size` when it limits something, else no limit. */
122
+ function positiveOrUnlimited(size: number | undefined): number {
123
+ return size !== undefined && size > 0 ? size : Infinity;
124
+ }
125
+
126
+ /**
127
+ * Whether two canonical settings compile to the same material: the same render mode, geometry,
128
+ * textures, blend and depth state, and the same effects. Batches with equal shader keys differ
129
+ * only in {@link liveParamsEqual | live parameters}.
130
+ */
131
+ export function shaderKeyEqual(a: StoredBatchSettings, b: StoredBatchSettings): boolean {
132
+ return (
133
+ a.renderMode === b.renderMode &&
134
+ a.instancingGeometry === b.instancingGeometry &&
135
+ a.instancingIndices === b.instancingIndices &&
136
+ a.instancingUVs === b.instancingUVs &&
137
+ a.instancingUVs2 === b.instancingUVs2 &&
138
+ a.instancingNormals === b.instancingNormals &&
139
+ a.texture === b.texture &&
140
+ a.reflectionTexture === b.reflectionTexture &&
141
+ a.reflectionAtlas === b.reflectionAtlas &&
142
+ a.lighting === b.lighting &&
143
+ a.rim?.mode === b.rim?.mode &&
144
+ a.uTileCount === b.uTileCount &&
145
+ a.vTileCount === b.vTileCount &&
146
+ a.blendTiles === b.blendTiles &&
147
+ a.softParticles === b.softParticles &&
148
+ a.materialBlendMode === b.materialBlendMode &&
149
+ a.materialTransparent === b.materialTransparent &&
150
+ a.materialDepthTest === b.materialDepthTest &&
151
+ a.materialDepthWrite === b.materialDepthWrite &&
152
+ a.materialAlphaTest > 0 === b.materialAlphaTest > 0 &&
153
+ a.layerMask === b.layerMask &&
154
+ a.renderingGroupId === b.renderingGroupId &&
155
+ a.renderOrder === b.renderOrder &&
156
+ a.applyFog === b.applyFog &&
157
+ a.sortMode === b.sortMode &&
158
+ !a.pivot === !b.pivot &&
159
+ hasSizeLimits(a) === hasSizeLimits(b) &&
160
+ a.mapUV?.channel === b.mapUV?.channel &&
161
+ a.uvDistortion?.map === b.uvDistortion?.map &&
162
+ a.uvDistortion?.mapUV.channel === b.uvDistortion?.mapUV.channel &&
163
+ !a.gradientMap === !b.gradientMap &&
164
+ a.gradientMap?.ramp === b.gradientMap?.ramp &&
165
+ !a.erosion === !b.erosion &&
166
+ a.erosion?.mask === b.erosion?.mask &&
167
+ a.erosion?.maskUV.channel === b.erosion?.maskUV.channel
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Whether two canonical settings with the same shader key set the material's uniforms to the
173
+ * same values: the fade distances, alpha cutoff, pivot, size limits, reflection level, rim
174
+ * colour, each effect's parameters and each texture's layout.
175
+ */
176
+ export function liveParamsEqual(a: StoredBatchSettings, b: StoredBatchSettings): boolean {
177
+ return (
178
+ a.softNearFade === b.softNearFade &&
179
+ a.softFarFade === b.softFarFade &&
180
+ a.materialAlphaTest === b.materialAlphaTest &&
181
+ a.reflectionLevel === b.reflectionLevel &&
182
+ pivotEqual(a.pivot, b.pivot) &&
183
+ a.minParticleSize === b.minParticleSize &&
184
+ a.maxParticleSize === b.maxParticleSize &&
185
+ rimParamsEqual(a.rim, b.rim) &&
186
+ uvLayoutEqual(a.mapUV, b.mapUV) &&
187
+ a.uvDistortion?.strength === b.uvDistortion?.strength &&
188
+ uvLayoutEqual(a.uvDistortion?.mapUV, b.uvDistortion?.mapUV) &&
189
+ a.gradientMap?.channel === b.gradientMap?.channel &&
190
+ a.erosion?.softness === b.erosion?.softness &&
191
+ a.erosion?.edgeWidth === b.erosion?.edgeWidth &&
192
+ colorEqual(a.erosion?.edgeColor, b.erosion?.edgeColor) &&
193
+ uvLayoutEqual(a.erosion?.maskUV, b.erosion?.maskUV)
194
+ );
195
+ }
196
+
197
+ /** Whether two canonical settings draw from one batch: the same shader key and the same live parameters. */
198
+ export function batchSettingsEqual(a: StoredBatchSettings, b: StoredBatchSettings): boolean {
199
+ return shaderKeyEqual(a, b) && liveParamsEqual(a, b);
200
+ }
201
+
202
+ /** True when both pivots put the quad in the same place; a missing pivot is zero. */
203
+ function pivotEqual(a: Vector3 | undefined, b: Vector3 | undefined): boolean {
204
+ return (a?.x ?? 0) === (b?.x ?? 0) && (a?.y ?? 0) === (b?.y ?? 0) && (a?.z ?? 0) === (b?.z ?? 0);
205
+ }
206
+
207
+ function rimParamsEqual(
208
+ a: {color: readonly number[]; power: number; strength: number} | undefined,
209
+ b: {color: readonly number[]; power: number; strength: number} | undefined
210
+ ): boolean {
211
+ if (!a || !b) {
212
+ return !a === !b;
213
+ }
214
+ return a.power === b.power && a.strength === b.strength && colorEqual(a.color, b.color);
215
+ }
216
+
217
+ function colorEqual(a: readonly number[] | undefined, b: readonly number[] | undefined): boolean {
218
+ if (!a || !b) {
219
+ return !a === !b;
220
+ }
221
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
222
+ }
223
+
224
+ /** Whether two layouts tile, offset and scroll the texture the same; the channel is part of the shader key. */
225
+ function uvLayoutEqual(a: TextureUV | undefined, b: TextureUV | undefined): boolean {
226
+ if (!a || !b) {
227
+ return !a === !b;
228
+ }
229
+ return (
230
+ a.tiling[0] === b.tiling[0] &&
231
+ a.tiling[1] === b.tiling[1] &&
232
+ a.offset[0] === b.offset[0] &&
233
+ a.offset[1] === b.offset[1] &&
234
+ a.scroll[0] === b.scroll[0] &&
235
+ a.scroll[1] === b.scroll[1]
236
+ );
237
+ }
@@ -0,0 +1,79 @@
1
+ import type {Material} from '@babylonjs/core/Materials/material';
2
+ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
3
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
4
+ import type {Node} from '@babylonjs/core/node';
5
+
6
+ /** What an effect root owns: the textures it holds a count on, and the materials made for it alone. */
7
+ interface EffectResources {
8
+ textures: Set<BaseTexture>;
9
+ materials: Material[];
10
+ }
11
+
12
+ const owned = new WeakMap<Node, EffectResources>();
13
+ /** How many effect roots hold each texture. */
14
+ const holders = new WeakMap<BaseTexture, number>();
15
+
16
+ /**
17
+ * Records that `root` uses `textures` and owns `materials`. They are freed when the root is
18
+ * disposed, however that happens: {@link disposeEffect}, `root.dispose()`, a parent's dispose or
19
+ * the scene's.
20
+ */
21
+ export function retainEffectResources(
22
+ root: TransformNode,
23
+ textures: Iterable<BaseTexture>,
24
+ materials: Material[]
25
+ ): void {
26
+ if (owned.has(root)) {
27
+ return;
28
+ }
29
+ const resources: EffectResources = {textures: new Set(textures), materials};
30
+ for (const texture of resources.textures) {
31
+ holders.set(texture, (holders.get(texture) ?? 0) + 1);
32
+ }
33
+ owned.set(root, resources);
34
+ root.onDisposeObservable.addOnce(() => releaseEffectResources(root));
35
+ }
36
+
37
+ /**
38
+ * Gives `copy` a hold on the textures the effect `source` belongs to holds, as a copy of the
39
+ * effect, or of a node in it, draws with them too.
40
+ */
41
+ export function shareEffectResources(source: TransformNode, copy: TransformNode): void {
42
+ for (let node: Node | null = source; node !== null; node = node.parent) {
43
+ const resources = owned.get(node);
44
+ if (resources !== undefined) {
45
+ retainEffectResources(copy, resources.textures, []);
46
+ return;
47
+ }
48
+ }
49
+ }
50
+
51
+ function releaseEffectResources(root: Node): void {
52
+ const resources = owned.get(root);
53
+ if (resources === undefined) {
54
+ return;
55
+ }
56
+ owned.delete(root);
57
+ for (const material of resources.materials) {
58
+ material.dispose();
59
+ }
60
+ for (const texture of resources.textures) {
61
+ const count = (holders.get(texture) ?? 1) - 1;
62
+ if (count > 0) {
63
+ holders.set(texture, count);
64
+ } else {
65
+ holders.delete(texture);
66
+ texture.dispose();
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Disposes an effect root and its nodes and systems, the materials made for it, and each texture it
73
+ * holds once no other effect root holds it. A root nothing was recorded for is only disposed.
74
+ */
75
+ export function disposeEffect(root: TransformNode): void {
76
+ root.dispose();
77
+ // A root disposed earlier, before its hold was recorded, still lets go.
78
+ releaseEffectResources(root);
79
+ }
package/src/envAtlas.ts CHANGED
@@ -2,8 +2,18 @@ import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
2
  import {Texture} from '@babylonjs/core/Materials/Textures/texture';
3
3
  import {Scene} from '@babylonjs/core/scene';
4
4
 
5
- const atlasByCube = new WeakMap<BaseTexture, Texture>();
6
- const pendingByCube = new WeakMap<BaseTexture, Promise<Texture | null>>();
5
+ /**
6
+ * Atlases by cube, then by the scene they were built for: a texture belongs to one scene and one
7
+ * engine, so a second scene (an editor preview, a second canvas) gets an atlas of its own.
8
+ */
9
+ const atlasesByCube = new WeakMap<BaseTexture, Map<Scene, Texture>>();
10
+ const pendingByCube = new WeakMap<BaseTexture, Map<Scene, Promise<Texture | null>>>();
11
+ /** Atlases this module built, which it disposes; an atlas given to {@link cacheEnvAtlas} stays the caller's. */
12
+ const builtAtlases = new WeakSet<Texture>();
13
+ /** Atlases whose dispose this module already listens to. */
14
+ const watchedAtlases = new WeakSet<Texture>();
15
+ /** Cubes whose dispose this module already listens to. */
16
+ const watchedCubes = new WeakSet<BaseTexture>();
7
17
 
8
18
  /** Reads the six face URLs from a Babylon CubeTexture (px py pz nx ny nz). */
9
19
  export function cubeFaceUrls(reflection: BaseTexture | null | undefined): string[] | null {
@@ -15,14 +25,85 @@ export function cubeFaceUrls(reflection: BaseTexture | null | undefined): string
15
25
  return files?.length === 6 ? files.slice() : null;
16
26
  }
17
27
 
18
- /** Returns a previously built atlas for this cube, if any. */
19
- export function getCachedEnvAtlas(cube: BaseTexture): Texture | null {
20
- return atlasByCube.get(cube) ?? null;
28
+ /** The scene a texture belongs to, or null for a disposed texture or one with no scene. */
29
+ function sceneOf(texture: BaseTexture): Scene | null {
30
+ return (texture as {getScene?: () => Scene | null}).getScene?.() ?? null;
21
31
  }
22
32
 
23
- /** Stores an atlas for a cube source (tests / prebuilt atlases). */
33
+ /**
34
+ * Returns the atlas already built or cached for this cube in `scene`, if any.
35
+ *
36
+ * @param cube - The cube texture the atlas was made from.
37
+ * @param scene - The scene the atlas must belong to. Defaults to the cube's own scene.
38
+ */
39
+ export function getCachedEnvAtlas(cube: BaseTexture, scene: Scene | null = sceneOf(cube)): Texture | null {
40
+ return (scene && atlasesByCube.get(cube)?.get(scene)) || null;
41
+ }
42
+
43
+ /**
44
+ * Stores a prebuilt atlas for a cube in the atlas's scene, so systems that use the cube in that
45
+ * scene take it instead of building one. The atlas stays the caller's: quarks never disposes it,
46
+ * and drops it from the cache when it, or the cube, is disposed.
47
+ *
48
+ * @param cube - The cube texture the atlas stands in for.
49
+ * @param atlas - A 3×2 atlas in the layout of {@link createEnvAtlasFromFaceUrls}.
50
+ */
24
51
  export function cacheEnvAtlas(cube: BaseTexture, atlas: Texture): void {
25
- atlasByCube.set(cube, atlas);
52
+ const scene = sceneOf(atlas) ?? sceneOf(cube);
53
+ if (scene) {
54
+ store(cube, scene, atlas);
55
+ } else {
56
+ console.warn('[babylon.quarks] cacheEnvAtlas: neither the atlas nor the cube has a scene; not cached');
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Drops the atlases made for `cube` from the cache and disposes the ones quarks built. Runs by
62
+ * itself when the cube is disposed; call it when an environment is swapped out but the cube is
63
+ * kept. Systems that use the cube build a new atlas the next time their material is resolved.
64
+ *
65
+ * @param cube - The cube texture whose atlases to drop.
66
+ * @param scene - Only the atlas for this scene. Defaults to every scene.
67
+ */
68
+ export function disposeEnvAtlas(cube: BaseTexture, scene?: Scene): void {
69
+ const atlases = atlasesByCube.get(cube);
70
+ if (!atlases) {
71
+ return;
72
+ }
73
+ for (const [atlasScene, atlas] of [...atlases]) {
74
+ if (scene && atlasScene !== scene) {
75
+ continue;
76
+ }
77
+ atlases.delete(atlasScene);
78
+ if (builtAtlases.has(atlas)) {
79
+ atlas.dispose();
80
+ }
81
+ }
82
+ }
83
+
84
+ function store(cube: BaseTexture, scene: Scene, atlas: Texture): void {
85
+ let atlases = atlasesByCube.get(cube);
86
+ if (!atlases) {
87
+ atlases = new Map();
88
+ atlasesByCube.set(cube, atlases);
89
+ }
90
+ atlases.set(scene, atlas);
91
+ // A scene disposes its textures, so this also lets go of a disposed scene.
92
+ if (!watchedAtlases.has(atlas) && atlas.onDisposeObservable) {
93
+ watchedAtlases.add(atlas);
94
+ atlas.onDisposeObservable.addOnce(() => {
95
+ if (atlasesByCube.get(cube)?.get(scene) === atlas) {
96
+ atlasesByCube.get(cube)!.delete(scene);
97
+ }
98
+ });
99
+ }
100
+ if (!watchedCubes.has(cube) && cube.onDisposeObservable) {
101
+ watchedCubes.add(cube);
102
+ cube.onDisposeObservable.addOnce(() => {
103
+ watchedCubes.delete(cube);
104
+ disposeEnvAtlas(cube);
105
+ });
106
+ }
26
107
  }
27
108
 
28
109
  /**
@@ -63,23 +144,30 @@ export const envAtlasBuilder = {
63
144
  };
64
145
 
65
146
  /**
66
- * Ensures a 3×2 env atlas exists for a CubeTexture. Returns a cached atlas
67
- * synchronously when available; otherwise starts (or joins) an async build and
68
- * invokes onReady when the atlas is usable.
147
+ * Ensures a 3×2 env atlas exists for a CubeTexture in `scene`. Returns a cached atlas
148
+ * synchronously when available; otherwise starts (or joins) an async build and invokes onReady
149
+ * when the atlas is usable. A cube disposed before its build finishes gets no atlas.
69
150
  */
70
151
  export function ensureEnvAtlasFromCube(
71
152
  cube: BaseTexture,
72
153
  scene: Scene,
73
154
  onReady: (atlas: Texture) => void
74
155
  ): Texture | null {
75
- const cached = atlasByCube.get(cube);
156
+ const cached = getCachedEnvAtlas(cube, scene);
76
157
  if (cached) {
77
158
  onReady(cached);
78
159
  return cached;
79
160
  }
80
161
 
162
+ let pendingByScene = pendingByCube.get(cube);
163
+ if (!pendingByScene) {
164
+ pendingByScene = new Map();
165
+ pendingByCube.set(cube, pendingByScene);
166
+ }
167
+ const pendings = pendingByScene;
168
+
81
169
  const kick = () => {
82
- const existingPending = pendingByCube.get(cube);
170
+ const existingPending = pendings.get(scene);
83
171
  if (existingPending) {
84
172
  existingPending.then((atlas) => {
85
173
  if (atlas) {
@@ -94,20 +182,34 @@ export function ensureEnvAtlasFromCube(
94
182
  return;
95
183
  }
96
184
 
185
+ let cubeDisposed = false;
186
+ // Removed once the build settles, so it does not keep the scene and onReady alive.
187
+ const cubeObserver = cube.onDisposeObservable?.addOnce(() => (cubeDisposed = true));
188
+ const settle = () => {
189
+ pendings.delete(scene);
190
+ if (cubeObserver) {
191
+ cube.onDisposeObservable.remove(cubeObserver);
192
+ }
193
+ };
97
194
  const pending = envAtlasBuilder
98
195
  .createFromFaceUrls(urls, scene)
99
196
  .then((atlas) => {
100
- atlasByCube.set(cube, atlas);
101
- pendingByCube.delete(cube);
197
+ settle();
198
+ if (cubeDisposed || scene.isDisposed) {
199
+ atlas.dispose();
200
+ return null;
201
+ }
202
+ builtAtlases.add(atlas);
203
+ store(cube, scene, atlas);
102
204
  onReady(atlas);
103
205
  return atlas;
104
206
  })
105
207
  .catch((err) => {
106
- pendingByCube.delete(cube);
208
+ settle();
107
209
  console.warn('[babylon.quarks] Failed to build env atlas from cubemap faces', err);
108
210
  return null;
109
211
  });
110
- pendingByCube.set(cube, pending);
212
+ pendings.set(scene, pending);
111
213
  };
112
214
 
113
215
  if (cube.isReady()) {
@@ -121,7 +223,7 @@ export function ensureEnvAtlasFromCube(
121
223
  }
122
224
  }
123
225
 
124
- return atlasByCube.get(cube) ?? null;
226
+ return getCachedEnvAtlas(cube, scene);
125
227
  }
126
228
 
127
229
  /** Loads one image URL as an HTMLImageElement. */
package/src/index.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import {loadPlugin} from 'quarks.core';
2
2
  import {MeshSurfaceEmitterPlugin} from './MeshSurfaceEmitter';
3
- import {registerShaderChunks} from './shaders';
4
3
 
5
4
  export * from 'quarks.core';
6
- export {BatchedParticleRenderer} from './BatchedParticleRenderer';
7
5
  export {BatchedRenderer} from './BatchedRenderer';
8
6
  export type {AdaptivePerformanceOptions, AdaptivePerformanceState, VFXBatchSettings} from './BatchedRenderer';
9
7
  export {EffectPool} from './EffectPool';
@@ -12,11 +10,12 @@ export {
12
10
  cacheEnvAtlas,
13
11
  createEnvAtlasFromFaceUrls,
14
12
  cubeFaceUrls,
13
+ disposeEnvAtlas,
15
14
  ensureEnvAtlasFromCube,
16
15
  getCachedEnvAtlas,
17
16
  } from './envAtlas';
18
- export * from './materials/';
19
17
  export {MeshSurfaceEmitter, MeshSurfaceEmitterPlugin} from './MeshSurfaceEmitter';
18
+ export {quarksNodeId} from './nodeId';
20
19
  export {ParticleEmitter} from './ParticleEmitter';
21
20
  export {ParticleSystem} from './ParticleSystem';
22
21
  export type {
@@ -29,22 +28,47 @@ export type {
29
28
  ParticleSystemParameters,
30
29
  ScalingMode,
31
30
  } from './ParticleSystem';
32
- export {QuarksLoader} from './QuarksLoader';
31
+ export {QuarksFormatError, QuarksLoader} from './QuarksLoader';
33
32
  export type {
34
33
  QuarksBatchLoadOptions,
35
34
  QuarksJSONLoadEntry,
36
35
  QuarksLoaderOptions,
36
+ QuarksLoadOptions,
37
37
  QuarksResourceKind,
38
38
  QuarksResourceResolver,
39
39
  } from './QuarksLoader';
40
40
  export {QuarksPrefab} from './QuarksPrefab';
41
41
  export type {AnimationData, QuarksTimelineClip} from './QuarksPrefab';
42
+ export {QUARKS_FORMAT_VERSION, serializeEffect} from './QuarksSerializer';
43
+ export type {
44
+ QuarksEffectJSON,
45
+ QuarksGeometryJSON,
46
+ QuarksImageJSON,
47
+ QuarksNodeJSON,
48
+ QuarksTextureJSON,
49
+ SerializeEffectOptions,
50
+ } from './QuarksSerializer';
42
51
  export {QuarksUtil} from './QuarksUtil';
43
- export * from './shaders/';
44
- export {SpriteBatch} from './SpriteBatch';
45
- export {TrailBatch} from './TrailBatch';
52
+ export {shaderProgram} from './shaderProgram';
53
+ export type {ShaderContext, ShaderDepth, ShaderProgram, ShaderSettings} from './shaderProgram';
54
+ export type {ShaderSources} from './shaders/shaderLanguageSupport';
55
+ export {shaderVariants} from './shaderVariants';
56
+ export type {ShaderVariant} from './shaderVariants';
57
+ export {decodeSoftDepth, softDepthParams} from './softDepth';
58
+ export type {DepthTextureOptions, SoftDepthEncoding} from './softDepth';
59
+ export {textureSourceUrl} from './textureSource';
60
+ export {
61
+ MAX_TRAIL_TESSELLATION,
62
+ trailCurveTension,
63
+ trailLifetime,
64
+ trailMinVertexDistance,
65
+ trailTessellation,
66
+ trailTextureMode,
67
+ trailTextureScale,
68
+ } from './trailSettings';
46
69
  export {
47
70
  defaultMeshLighting,
71
+ EROSION_DEFAULT_SOFTNESS,
48
72
  erosionEqual,
49
73
  GRADIENT_MAP_WEIGHTS,
50
74
  gradientMapEqual,
@@ -60,6 +84,7 @@ export {
60
84
  RenderMode,
61
85
  textureUVEqual,
62
86
  textureUVToJSON,
87
+ UV_DISTORTION_DEFAULT_STRENGTH,
63
88
  UV_DISTORTION_DEFAULT_UV,
64
89
  uvDistortionEqual,
65
90
  VFXBatch,
@@ -80,30 +105,4 @@ export type {
80
105
  UVChannel,
81
106
  } from './VFXBatch';
82
107
 
83
- // Re-export quarks.core types (interfaces - type-only)
84
- export type {
85
- Behavior,
86
- BillBoardSettings,
87
- ColorGenerator,
88
- EmissionState,
89
- EmitterShape,
90
- FunctionColorGenerator,
91
- FunctionValueGenerator,
92
- IEmitter,
93
- IParticle,
94
- IParticleSystem,
95
- MeshSettings,
96
- Particle,
97
- ParticleSystemEvent,
98
- ParticleSystemEventType,
99
- RendererEmitterSettings,
100
- RotationGenerator,
101
- SerializationOptions,
102
- StretchedBillBoardSettings,
103
- TrailSettings,
104
- ValueGenerator,
105
- Vector3Generator,
106
- } from 'quarks.core';
107
-
108
- registerShaderChunks();
109
108
  loadPlugin(MeshSurfaceEmitterPlugin);
package/src/nodeId.ts ADDED
@@ -0,0 +1,13 @@
1
+ import {RandomGUID} from '@babylonjs/core/Misc/guid';
2
+
3
+ /**
4
+ * The id an effect file knows a node by: the uuid it was loaded under, or, for a node made in code,
5
+ * a GUID given to it the first time it is asked for and kept from then on. Every link written into
6
+ * a file (a sub emitter's target, a mesh surface shape's mesh, a prefab animation's target) and
7
+ * every node's own `uuid` use this, so an effect loaded, saved and loaded again keeps its links,
8
+ * and a node made in code never takes the id of one loaded from a file.
9
+ */
10
+ export function quarksNodeId(node: object): string {
11
+ const ided = node as {_quarksUUID?: string};
12
+ return (ided._quarksUUID ??= RandomGUID());
13
+ }