babylon.quarks 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +1194 -8
  2. package/README.md +13 -9
  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 +10108 -0
  10. package/dist/types/index.d.ts +5794 -998
  11. package/package.json +18 -7
  12. package/src/BatchedRenderer.ts +274 -156
  13. package/src/EffectPool.ts +94 -12
  14. package/src/MeshSurfaceEmitter.ts +50 -14
  15. package/src/ParticleEmitter.ts +75 -17
  16. package/src/ParticleSystem.ts +981 -2066
  17. package/src/QuarksLoader.ts +666 -194
  18. package/src/QuarksPrefab.ts +367 -73
  19. package/src/QuarksSerializer.ts +512 -0
  20. package/src/QuarksUtil.ts +77 -7
  21. package/src/SpriteBatch.ts +127 -252
  22. package/src/TrailBatch.ts +545 -348
  23. package/src/VFXBatch.ts +238 -596
  24. package/src/batchKey.ts +239 -0
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +76 -0
  28. package/src/envAtlas.ts +126 -18
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +69 -35
  31. package/src/nodeId.ts +36 -0
  32. package/src/shaderProgram.ts +419 -0
  33. package/src/shaderVariants.ts +245 -0
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +12 -5
  36. package/src/shaders/chunks/sheet.ts +111 -0
  37. package/src/shaders/chunks/soft.ts +85 -0
  38. package/src/shaders/chunks/surface.ts +12 -2
  39. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  40. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  41. package/src/shaders/particle_frag.glsl.ts +5 -22
  42. package/src/shaders/particle_frag.wgsl.ts +5 -23
  43. package/src/shaders/particle_physics_frag.glsl.ts +16 -26
  44. package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
  45. package/src/shaders/particle_vert.glsl.ts +5 -27
  46. package/src/shaders/particle_vert.wgsl.ts +5 -26
  47. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  48. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  49. package/src/shaders/trail_frag.glsl.ts +13 -0
  50. package/src/shaders/trail_frag.wgsl.ts +13 -0
  51. package/src/shaders/trail_vert.glsl.ts +45 -36
  52. package/src/shaders/trail_vert.wgsl.ts +46 -40
  53. package/src/softDepth.ts +136 -0
  54. package/src/system/BehaviorPlan.ts +85 -0
  55. package/src/system/BoundsTracker.ts +181 -0
  56. package/src/system/EmissionController.ts +358 -0
  57. package/src/system/EventDispatcher.ts +60 -0
  58. package/src/system/ParticleSpawner.ts +274 -0
  59. package/src/system/SimulationClock.ts +130 -0
  60. package/src/system/geometry.ts +104 -0
  61. package/src/system/integrate.ts +428 -0
  62. package/src/system/materialResolve.ts +165 -0
  63. package/src/system/normalize.ts +100 -0
  64. package/src/system/serialization.ts +422 -0
  65. package/src/system/settingsSchema.ts +339 -0
  66. package/src/system/types.ts +449 -0
  67. package/src/textureSource.ts +30 -0
  68. package/src/trailSettings.ts +54 -0
  69. package/src/BatchedParticleRenderer.ts +0 -3
  70. package/src/materials/ParticleMaterials.ts +0 -23
  71. package/src/materials/index.ts +0 -1
  72. package/src/shaders/chunks/index.ts +0 -1
  73. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  74. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  75. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  76. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  77. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  78. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  79. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  80. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  81. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  82. package/src/shaders/index.ts +0 -9
  83. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,261 @@
1
+ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
+ import type {
3
+ MeshLighting,
4
+ MeshRim,
5
+ ParticleErosionOptions,
6
+ ParticleGradientMap,
7
+ ParticleUVDistortionOptions,
8
+ TextureUV,
9
+ } from './batchSettings';
10
+ import type {QuarksEffectJSON, QuarksNodeJSON, QuarksTextureJSON} from './QuarksSerializer';
11
+ import type {ParticleMaterialSource} from './system/materialResolve';
12
+
13
+ // The parts of an effect document `QuarksLoader` reads: what `serializeEffect` writes, and the
14
+ // three.js object format three.quarks files use. Every field a file may leave out is optional; the
15
+ // loader checks what it reads, so a field of the wrong kind warns or is skipped, never throws.
16
+
17
+ /** Mesh data as typed arrays, the form a loaded geometry takes. */
18
+ export interface GeometryArrays {
19
+ /** `x, y, z` per vertex. */
20
+ positions: Float32Array;
21
+ /** Three per triangle. */
22
+ indices: Uint32Array | Uint16Array;
23
+ /** First UV set, `u, v` per vertex. */
24
+ uvs?: Float32Array;
25
+ /** Second UV set, `u, v` per vertex. */
26
+ uvs2?: Float32Array;
27
+ /** `x, y, z` per vertex. */
28
+ normals?: Float32Array;
29
+ }
30
+
31
+ /** A three.js `BufferAttribute`, inline or as a view into an interleaved buffer. */
32
+ export interface BufferAttributeJSON {
33
+ /** The values, when the attribute holds its own. */
34
+ array?: number[];
35
+ /** Components per vertex. */
36
+ itemSize?: number;
37
+ /** The typed array type, e.g. `'Float32Array'` or `'Uint16Array'`. */
38
+ type?: string;
39
+ /** Set when the attribute reads an interleaved buffer. */
40
+ isInterleavedBufferAttribute?: boolean;
41
+ /** The interleaved buffer's uuid. */
42
+ data?: string;
43
+ /** Where the attribute starts in each interleaved vertex. */
44
+ offset?: number;
45
+ }
46
+
47
+ /** A three.js interleaved buffer: a stride over one of the geometry's array buffers. */
48
+ export interface InterleavedBufferJSON {
49
+ /** The array buffer's uuid. */
50
+ buffer: string;
51
+ /** Floats per vertex. */
52
+ stride: number;
53
+ }
54
+
55
+ /** The vertex attributes of a three.js `BufferGeometry` that quarks reads. */
56
+ export interface BufferGeometryAttributesJSON {
57
+ /** Positions, `x, y, z` per vertex. */
58
+ position?: BufferAttributeJSON;
59
+ /** The first UV set. */
60
+ uv?: BufferAttributeJSON;
61
+ /** The second UV set, as three.js r151 on names it. */
62
+ uv1?: BufferAttributeJSON;
63
+ /** The second UV set, as three.js before r151 names it. */
64
+ uv2?: BufferAttributeJSON;
65
+ /** Normals, `x, y, z` per vertex. */
66
+ normal?: BufferAttributeJSON;
67
+ }
68
+
69
+ /** The index of a three.js `BufferGeometry`. */
70
+ export interface BufferIndexJSON {
71
+ /** `'Uint16Array'` or `'Uint32Array'`; 32-bit when absent. */
72
+ type?: string;
73
+ /** Three per triangle. */
74
+ array: number[];
75
+ }
76
+
77
+ /** The `data` of a three.js `BufferGeometry`. */
78
+ export interface BufferGeometryDataJSON {
79
+ /** The vertex attributes. */
80
+ attributes?: BufferGeometryAttributesJSON;
81
+ /** The triangle indices. */
82
+ index?: BufferIndexJSON;
83
+ /** Interleaved buffers by uuid. */
84
+ interleavedBuffers?: {[uuid: string]: InterleavedBufferJSON};
85
+ /** The bytes of the interleaved buffers by uuid, as 32-bit words. */
86
+ arrayBuffers?: {[uuid: string]: number[] | ArrayBuffer};
87
+ }
88
+
89
+ /** A three.js `BufferGeometry`. */
90
+ export interface BufferGeometryJSON {
91
+ /** Unique within the file. */
92
+ uuid: string;
93
+ /** Always `'BufferGeometry'`. */
94
+ type: 'BufferGeometry';
95
+ /** The vertex data. */
96
+ data?: BufferGeometryDataJSON;
97
+ }
98
+
99
+ /** A three.js `PlaneGeometry`: a quad in XY. */
100
+ export interface PlaneGeometryJSON {
101
+ /** Unique within the file. */
102
+ uuid: string;
103
+ /** Always `'PlaneGeometry'`. */
104
+ type: 'PlaneGeometry';
105
+ /** Default 1. */
106
+ width?: number;
107
+ /** Default 1. */
108
+ height?: number;
109
+ }
110
+
111
+ /** A three.js `SphereGeometry`. */
112
+ export interface SphereGeometryJSON {
113
+ /** Unique within the file. */
114
+ uuid: string;
115
+ /** Always `'SphereGeometry'`. */
116
+ type: 'SphereGeometry';
117
+ /** Default 0.5. */
118
+ radius?: number;
119
+ /** Default 16, at least 3. */
120
+ widthSegments?: number;
121
+ /** Default 8, at least 2. */
122
+ heightSegments?: number;
123
+ /** Default 0. */
124
+ phiStart?: number;
125
+ /** Default 2π. */
126
+ phiLength?: number;
127
+ /** Default 0. */
128
+ thetaStart?: number;
129
+ /** Default π. */
130
+ thetaLength?: number;
131
+ }
132
+
133
+ /**
134
+ * Quarks' own mesh data, inline (see `QuarksGeometryJSON`) or in its own file at `url`, which
135
+ * `QuarksLoader.resolveResources` inlines before parsing.
136
+ */
137
+ export interface QuarksGeometryEntryJSON {
138
+ /** Unique within the file. */
139
+ uuid: string;
140
+ /** Always `'QuarksGeometry'`. */
141
+ type: 'QuarksGeometry';
142
+ /** The file that holds the data, until it is inlined. */
143
+ url?: string;
144
+ /** `x, y, z` per vertex. */
145
+ positions?: number[];
146
+ /** Three per triangle. */
147
+ indices?: number[];
148
+ /** First UV set. */
149
+ uvs?: number[];
150
+ /** Second UV set. */
151
+ uvs2?: number[];
152
+ /** `x, y, z` per vertex. */
153
+ normals?: number[];
154
+ }
155
+
156
+ /** A geometry entry of an effect document. */
157
+ export type EffectGeometryJSON = QuarksGeometryEntryJSON | BufferGeometryJSON | PlaneGeometryJSON | SphereGeometryJSON;
158
+
159
+ /** An image entry of an effect document. */
160
+ export interface EffectImageJSON {
161
+ /** Unique within the file. */
162
+ uuid: string;
163
+ /** Relative to the file's base url, absolute, or a `data:` url; absent for an image with no file. */
164
+ url?: string;
165
+ }
166
+
167
+ /**
168
+ * A texture slot of a {@link QuarksMaterialJSON}: a texture uuid in a file, the texture itself once
169
+ * a loader resolved it, or null for a texture that could not be made.
170
+ */
171
+ export type MaterialTextureRef = string | BaseTexture | null;
172
+
173
+ /** The gradient mapping of a {@link QuarksMaterialJSON}; see `ParticleSystem.gradientMap`. */
174
+ export interface GradientMapJSON extends Omit<Partial<ParticleGradientMap>, 'ramp'> {
175
+ /** The ramp texture. */
176
+ ramp?: MaterialTextureRef;
177
+ }
178
+
179
+ /** The erosion of a {@link QuarksMaterialJSON}; see `ParticleSystem.erosion`. */
180
+ export type ErosionJSON = Omit<ParticleErosionOptions, 'mask'> & {
181
+ /** The mask texture. */
182
+ mask?: MaterialTextureRef;
183
+ };
184
+
185
+ /** The UV distortion of a {@link QuarksMaterialJSON}; see `ParticleSystem.uvDistortion`. */
186
+ export type UVDistortionJSON = Omit<ParticleUVDistortionOptions, 'map'> & {
187
+ /** The distortion texture. */
188
+ map?: MaterialTextureRef;
189
+ };
190
+
191
+ /**
192
+ * A material entry of an effect document: what a particle system's material writes (`type`
193
+ * `'QuarksMaterial'`), or a three.js material, whose `map` and `blending` the loader reads too.
194
+ */
195
+ export interface QuarksMaterialJSON {
196
+ /** Unique within the file. */
197
+ uuid: string;
198
+ /** `'QuarksMaterial'`, or the three.js material type. */
199
+ type?: string;
200
+ /** Whether the particles blend. Default true. */
201
+ transparent?: boolean;
202
+ /** A Babylon alpha mode constant. */
203
+ alphaMode?: number;
204
+ /** A three.js blending code (1 normal, 2 additive, 3 subtractive, 4 multiply), read without `alphaMode`. */
205
+ blending?: number;
206
+ /** A three.js side code; 2 draws both faces. */
207
+ side?: number;
208
+ /** Default true. */
209
+ depthTest?: boolean;
210
+ /** Default false. */
211
+ depthWrite?: boolean;
212
+ /** Default 0. */
213
+ alphaTest?: number;
214
+ /** The particle texture. */
215
+ texture?: MaterialTextureRef;
216
+ /** A three.js material's texture, read when there is no `texture`. */
217
+ map?: string;
218
+ /** A 3×2 environment atlas for reflections. */
219
+ reflectionAtlas?: MaterialTextureRef;
220
+ /** How strong the reflection is. */
221
+ reflectionLevel?: number;
222
+ /** Mesh particle lighting; `'fixed'` when absent. */
223
+ lighting?: MeshLighting;
224
+ /** Mesh particle rim. */
225
+ rim?: Partial<MeshRim>;
226
+ /** The particle texture's UV transform. */
227
+ mapUV?: Partial<TextureUV>;
228
+ /** Gradient mapping. */
229
+ gradientMap?: GradientMapJSON;
230
+ /** Erosion. */
231
+ erosion?: ErosionJSON;
232
+ /** UV distortion. */
233
+ uvDistortion?: UVDistortionJSON;
234
+ /** While writing: the material the system read its settings from. Never written to a file. */
235
+ sourceMaterial?: ParticleMaterialSource;
236
+ }
237
+
238
+ /**
239
+ * A geometry as an effect's resources hold it: an entry of the document, or, once loaded, its
240
+ * typed arrays.
241
+ */
242
+ export type GeometryEntry = EffectGeometryJSON | GeometryArrays;
243
+
244
+ /**
245
+ * An effect document as `QuarksLoader` reads it: what `serializeEffect` writes
246
+ * (`QuarksEffectJSON`), or a three.quarks / three.js object file. Only `object` is required.
247
+ */
248
+ export interface EffectDocumentJSON {
249
+ /** What wrote the file. */
250
+ metadata?: Partial<QuarksEffectJSON['metadata']>;
251
+ /** Meshes the nodes and particle systems use. */
252
+ geometries?: EffectGeometryJSON[];
253
+ /** Materials, referred to by uuid. */
254
+ materials?: QuarksMaterialJSON[];
255
+ /** Textures, referred to by uuid. */
256
+ textures?: QuarksTextureJSON[];
257
+ /** Image files the textures load. */
258
+ images?: EffectImageJSON[];
259
+ /** The effect's root node. */
260
+ object: QuarksNodeJSON;
261
+ }
@@ -0,0 +1,76 @@
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 holds a count on: the textures it draws with and the materials made for the effect. */
7
+ interface EffectResources {
8
+ textures: Set<BaseTexture>;
9
+ materials: Set<Material>;
10
+ }
11
+
12
+ const owned = new WeakMap<Node, EffectResources>();
13
+ /** How many effect roots hold each texture and material. */
14
+ const holders = new WeakMap<BaseTexture | Material, number>();
15
+
16
+ /**
17
+ * Records that `root` uses `textures` and the `materials` made for the effect. Each is freed once
18
+ * the last root holding it is disposed, however that happens: {@link disposeEffect},
19
+ * `root.dispose()`, a parent's dispose or the scene's.
20
+ */
21
+ export function retainEffectResources(
22
+ root: TransformNode,
23
+ textures: Iterable<BaseTexture>,
24
+ materials: Iterable<Material>
25
+ ): void {
26
+ if (owned.has(root)) {
27
+ return;
28
+ }
29
+ const resources: EffectResources = {textures: new Set(textures), materials: new Set(materials)};
30
+ for (const resource of [...resources.textures, ...resources.materials]) {
31
+ holders.set(resource, (holders.get(resource) ?? 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 and materials the effect `source` belongs to holds, as a
39
+ * copy of the 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, resources.materials);
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 resource of [...resources.materials, ...resources.textures]) {
58
+ const count = (holders.get(resource) ?? 1) - 1;
59
+ if (count > 0) {
60
+ holders.set(resource, count);
61
+ } else {
62
+ holders.delete(resource);
63
+ resource.dispose();
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Disposes an effect root and its nodes and systems, and each material and texture it holds once no
70
+ * other effect root holds it. A root nothing was recorded for is only disposed.
71
+ */
72
+ export function disposeEffect(root: TransformNode): void {
73
+ root.dispose();
74
+ // A root disposed earlier, before its hold was recorded, still lets go.
75
+ releaseEffectResources(root);
76
+ }
package/src/envAtlas.ts CHANGED
@@ -2,8 +2,20 @@ 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
+ /** The faces each cube's atlas failed to build from: not tried, or warned about, again. */
14
+ const failedFaces = new WeakMap<BaseTexture, string>();
15
+ /** Atlases whose dispose this module already listens to. */
16
+ const watchedAtlases = new WeakSet<Texture>();
17
+ /** Cubes whose dispose this module already listens to. */
18
+ const watchedCubes = new WeakSet<BaseTexture>();
7
19
 
8
20
  /** Reads the six face URLs from a Babylon CubeTexture (px py pz nx ny nz). */
9
21
  export function cubeFaceUrls(reflection: BaseTexture | null | undefined): string[] | null {
@@ -15,14 +27,86 @@ export function cubeFaceUrls(reflection: BaseTexture | null | undefined): string
15
27
  return files?.length === 6 ? files.slice() : null;
16
28
  }
17
29
 
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;
30
+ /** The scene a texture belongs to, or null for a disposed texture or one with no scene. */
31
+ function sceneOf(texture: BaseTexture): Scene | null {
32
+ return (texture as {getScene?: () => Scene | null}).getScene?.() ?? null;
21
33
  }
22
34
 
23
- /** Stores an atlas for a cube source (tests / prebuilt atlases). */
35
+ /**
36
+ * Returns the atlas already built or cached for this cube in `scene`, if any.
37
+ *
38
+ * @param cube - The cube texture the atlas was made from.
39
+ * @param scene - The scene the atlas must belong to. Defaults to the cube's own scene.
40
+ */
41
+ export function getCachedEnvAtlas(cube: BaseTexture, scene: Scene | null = sceneOf(cube)): Texture | null {
42
+ return (scene && atlasesByCube.get(cube)?.get(scene)) || null;
43
+ }
44
+
45
+ /**
46
+ * Stores a prebuilt atlas for a cube in the atlas's scene, so systems that use the cube in that
47
+ * scene take it instead of building one. The atlas stays the caller's: quarks never disposes it,
48
+ * and drops it from the cache when it, or the cube, is disposed.
49
+ *
50
+ * @param cube - The cube texture the atlas stands in for.
51
+ * @param atlas - A 3×2 atlas in the layout of {@link createEnvAtlasFromFaceUrls}.
52
+ */
24
53
  export function cacheEnvAtlas(cube: BaseTexture, atlas: Texture): void {
25
- atlasByCube.set(cube, atlas);
54
+ const scene = sceneOf(atlas) ?? sceneOf(cube);
55
+ if (scene) {
56
+ store(cube, scene, atlas);
57
+ } else {
58
+ console.warn('[babylon.quarks] cacheEnvAtlas: neither the atlas nor the cube has a scene; not cached');
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Drops the atlases made for `cube` from the cache and disposes the ones quarks built. Runs by
64
+ * itself when the cube is disposed; call it when an environment is swapped out but the cube is
65
+ * kept. Systems that use the cube build a new atlas the next time their material is resolved.
66
+ *
67
+ * @param cube - The cube texture whose atlases to drop.
68
+ * @param scene - Only the atlas for this scene. Defaults to every scene.
69
+ */
70
+ export function disposeEnvAtlas(cube: BaseTexture, scene?: Scene): void {
71
+ failedFaces.delete(cube);
72
+ const atlases = atlasesByCube.get(cube);
73
+ if (!atlases) {
74
+ return;
75
+ }
76
+ for (const [atlasScene, atlas] of [...atlases]) {
77
+ if (scene && atlasScene !== scene) {
78
+ continue;
79
+ }
80
+ atlases.delete(atlasScene);
81
+ if (builtAtlases.has(atlas)) {
82
+ atlas.dispose();
83
+ }
84
+ }
85
+ }
86
+
87
+ function store(cube: BaseTexture, scene: Scene, atlas: Texture): void {
88
+ let atlases = atlasesByCube.get(cube);
89
+ if (!atlases) {
90
+ atlases = new Map();
91
+ atlasesByCube.set(cube, atlases);
92
+ }
93
+ atlases.set(scene, atlas);
94
+ // A scene disposes its textures, so this also lets go of a disposed scene.
95
+ if (!watchedAtlases.has(atlas) && atlas.onDisposeObservable) {
96
+ watchedAtlases.add(atlas);
97
+ atlas.onDisposeObservable.addOnce(() => {
98
+ if (atlasesByCube.get(cube)?.get(scene) === atlas) {
99
+ atlasesByCube.get(cube)!.delete(scene);
100
+ }
101
+ });
102
+ }
103
+ if (!watchedCubes.has(cube) && cube.onDisposeObservable) {
104
+ watchedCubes.add(cube);
105
+ cube.onDisposeObservable.addOnce(() => {
106
+ watchedCubes.delete(cube);
107
+ disposeEnvAtlas(cube);
108
+ });
109
+ }
26
110
  }
27
111
 
28
112
  /**
@@ -63,23 +147,32 @@ export const envAtlasBuilder = {
63
147
  };
64
148
 
65
149
  /**
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.
150
+ * Ensures a 3×2 env atlas exists for a CubeTexture in `scene`. Returns a cached atlas
151
+ * synchronously when available; otherwise starts (or joins) an async build and invokes onReady
152
+ * when the atlas is usable. A cube disposed before its build finishes gets no atlas. A build that
153
+ * fails warns once and is not tried again until the cube's faces change or
154
+ * {@link disposeEnvAtlas} is called for it; {@link cacheEnvAtlas} still supplies an atlas.
69
155
  */
70
156
  export function ensureEnvAtlasFromCube(
71
157
  cube: BaseTexture,
72
158
  scene: Scene,
73
159
  onReady: (atlas: Texture) => void
74
160
  ): Texture | null {
75
- const cached = atlasByCube.get(cube);
161
+ const cached = getCachedEnvAtlas(cube, scene);
76
162
  if (cached) {
77
163
  onReady(cached);
78
164
  return cached;
79
165
  }
80
166
 
167
+ let pendingByScene = pendingByCube.get(cube);
168
+ if (!pendingByScene) {
169
+ pendingByScene = new Map();
170
+ pendingByCube.set(cube, pendingByScene);
171
+ }
172
+ const pendings = pendingByScene;
173
+
81
174
  const kick = () => {
82
- const existingPending = pendingByCube.get(cube);
175
+ const existingPending = pendings.get(scene);
83
176
  if (existingPending) {
84
177
  existingPending.then((atlas) => {
85
178
  if (atlas) {
@@ -90,24 +183,39 @@ export function ensureEnvAtlasFromCube(
90
183
  }
91
184
 
92
185
  const urls = cubeFaceUrls(cube);
93
- if (!urls) {
186
+ if (!urls || failedFaces.get(cube) === urls.join('\n')) {
94
187
  return;
95
188
  }
96
189
 
190
+ let cubeDisposed = false;
191
+ // Removed once the build settles, so it does not keep the scene and onReady alive.
192
+ const cubeObserver = cube.onDisposeObservable?.addOnce(() => (cubeDisposed = true));
193
+ const settle = () => {
194
+ pendings.delete(scene);
195
+ if (cubeObserver) {
196
+ cube.onDisposeObservable.remove(cubeObserver);
197
+ }
198
+ };
97
199
  const pending = envAtlasBuilder
98
200
  .createFromFaceUrls(urls, scene)
99
201
  .then((atlas) => {
100
- atlasByCube.set(cube, atlas);
101
- pendingByCube.delete(cube);
202
+ settle();
203
+ if (cubeDisposed || scene.isDisposed) {
204
+ atlas.dispose();
205
+ return null;
206
+ }
207
+ builtAtlases.add(atlas);
208
+ store(cube, scene, atlas);
102
209
  onReady(atlas);
103
210
  return atlas;
104
211
  })
105
212
  .catch((err) => {
106
- pendingByCube.delete(cube);
213
+ settle();
214
+ failedFaces.set(cube, urls.join('\n'));
107
215
  console.warn('[babylon.quarks] Failed to build env atlas from cubemap faces', err);
108
216
  return null;
109
217
  });
110
- pendingByCube.set(cube, pending);
218
+ pendings.set(scene, pending);
111
219
  };
112
220
 
113
221
  if (cube.isReady()) {
@@ -121,7 +229,7 @@ export function ensureEnvAtlasFromCube(
121
229
  }
122
230
  }
123
231
 
124
- return atlasByCube.get(cube) ?? null;
232
+ return getCachedEnvAtlas(cube, scene);
125
233
  }
126
234
 
127
235
  /** Loads one image URL as an HTMLImageElement. */
@@ -1,4 +1,4 @@
1
- /** Sequential 0..N-1 indices for non-indexed triangle-list BufferGeometry (Three.js drawArrays). */
1
+ /** `indices` when there are some, else `0..N-1`: each three vertices of `positions` one triangle. */
2
2
  export function ensureTriangleIndices(
3
3
  positions: Float32Array,
4
4
  indices?: Uint32Array | Uint16Array