babylon.quarks 0.27.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 (52) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +3 -1
  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 +4256 -831
  10. package/dist/types/index.d.ts +4256 -831
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +93 -78
  13. package/src/EffectPool.ts +91 -12
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +31 -3
  16. package/src/ParticleSystem.ts +92 -25
  17. package/src/QuarksLoader.ts +289 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +43 -15
  20. package/src/QuarksUtil.ts +64 -9
  21. package/src/SpriteBatch.ts +21 -6
  22. package/src/TrailBatch.ts +12 -6
  23. package/src/VFXBatch.ts +70 -528
  24. package/src/batchKey.ts +4 -2
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +21 -24
  28. package/src/envAtlas.ts +8 -2
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +39 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +110 -16
  33. package/src/shaderVariants.ts +24 -4
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/soft.ts +6 -5
  37. package/src/shaders/chunks/surface.ts +2 -2
  38. package/src/shaders/particle_frag.glsl.ts +3 -1
  39. package/src/shaders/particle_frag.wgsl.ts +2 -0
  40. package/src/shaders/particle_physics_frag.glsl.ts +5 -2
  41. package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
  42. package/src/shaders/trail_frag.glsl.ts +4 -1
  43. package/src/shaders/trail_frag.wgsl.ts +2 -0
  44. package/src/system/EmissionController.ts +2 -4
  45. package/src/system/EventDispatcher.ts +26 -10
  46. package/src/system/geometry.ts +22 -29
  47. package/src/system/materialResolve.ts +56 -7
  48. package/src/system/normalize.ts +6 -1
  49. package/src/system/serialization.ts +54 -33
  50. package/src/system/settingsSchema.ts +32 -10
  51. package/src/system/types.ts +67 -28
  52. package/src/trailSettings.ts +9 -6
package/CHANGELOG.md CHANGED
@@ -4,6 +4,431 @@ All notable changes to the `babylon.quarks` package are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project follows the `quarks.core` 0.x version line.
6
6
 
7
+ ## [0.28.0] — 2026-10-23
8
+
9
+ The release after phase 6 of the audit and the 0.28 cleanup: alpha means coverage in every blend
10
+ mode, as in Unity; shapes fill like Unity's; `play()`, `resume()` and the `QuarksPrefab` timeline
11
+ behave like Unity's `Play` and `PlayableDirector`; the renderer's transform and the camera layers
12
+ no longer change what is drawn; effect files hold only what differs from the defaults; and no
13
+ `any` is left in the public types. The Unity exporter is out of experimental, and the editor gets a
14
+ headless `createEditorSession` and WebGPU. 0.27.1 was never published to npm: its packaging changes
15
+ (below) ship in this release.
16
+
17
+ ### Upgrading from 0.27
18
+
19
+ Most effects load and play unchanged. Check these, in this order:
20
+
21
+ 1. **Effects under additive, premultiplied, screen, subtract or max blending whose alpha falls
22
+ below 1** now fade with it. If one looks dimmer than you want, raise its alpha or its colour; a
23
+ texture authored premultiplied should be re-exported with straight alpha. Multiply and min
24
+ particles lighten toward white as their alpha falls instead of staying dark.
25
+ 2. **Fog follows the blend mode.** Subtract and screen effects fade toward black with distance, like
26
+ additive ones, and multiply effects toward white; only those that lay over the frame still mix
27
+ toward the fog colour.
28
+ 3. **Soft particles** fade linearly instead of quadratically under `ALPHA_ADD` and
29
+ `ALPHA_COMBINE`; re-tune `softFarFade` if a soft edge looks different.
30
+ 4. **Shapes with a thickness above 0, cones, and grids** spread particles differently (evenly over
31
+ the volume, every cone particle at its start speed, grid cells centred). Re-tune an effect that
32
+ relied on the old crowding at the centre.
33
+ 5. **Behaviors no longer reset at every loop.** A looping `Noise` or `TurbulenceField` keeps going
34
+ instead of jumping back to its start once a loop, and the sub emissions of particles still alive
35
+ carry on into the next loop.
36
+ 6. **`play()` on a finished system restarts it.** To undo a pause and leave a finished system
37
+ finished, call `resume()` (or `QuarksUtil.resume(root)`).
38
+ 7. **`QuarksPrefab`'s end lets the particles die out.** Call `stop()` yourself where you want them
39
+ cleared on the spot. `update()` with no argument now steps by the engine's frame time, and
40
+ `autoUpdate` drives the timeline for you. A prefab saved with a `timeScale` other than 1 now
41
+ plays its systems at that speed, and `addParticleSystemAnimation` no longer pauses the prefab.
42
+ 8. **Move emitters, not the `BatchedRenderer`.** Its transform no longer moves the particles.
43
+ 9. **Camera layers count for culling.** Only systems with a `cullingMode` other than the default
44
+ `'always'` are affected: one whose `layers.mask` no active camera renders now pauses as its
45
+ `cullingMode` says. Give the camera a `layerMask` that includes it.
46
+ 10. **Files are written with only their non-default fields.** babylon.quarks 0.27 and later read
47
+ them; three.quarks and babylon.quarks before 0.27 do not. Keep the old files if a three.quarks
48
+ tool must read them.
49
+ 11. **TypeScript and plugins:**
50
+ - `v.isVector2` instead of `Vector2.isVector2`; `setAxisAngleFromRotationMatrix` takes a
51
+ `Matrix4`.
52
+ - JSON readers and `QuarksLoader` take typed JSON or `unknown` (see the `any` items below), and
53
+ a behavior's `toJSON` returns a `BehaviorJSON` with a `type`.
54
+ - A plugin's `loadJSON` must only read its JSON: saving now calls it to find the defaults.
55
+ - `BatchedRenderer.batches` and `systemToBatchIndex` are read-only.
56
+ - A `VFXBatch` subclass implements `resizeBuffers()` and `initialCapacity` and creates its own
57
+ `mesh` in `setupBuffers`.
58
+ - `EulerGenerator.eular` is now `euler` (the old name is a deprecated alias).
59
+ 12. **Editor:** a `resolveTexture` callback returns a Babylon `Texture`, and `ValueField` with
60
+ `curve={false}` reports a `ValueGenerator` (see the editor's changelog). Install
61
+ `babylon.quarks-editor` 0.28 with `babylon.quarks` 0.28.
62
+
63
+ ### Breaking
64
+
65
+ - **Alpha means coverage in every blend mode.** A particle's alpha (its colour over life, a soft
66
+ fade, erosion, a rim fade, a `tint`) now fades it out whatever `blendMode` it draws with, as
67
+ Unity's particle shaders do for their Alpha, Premultiply, Additive and Multiply modes. Before,
68
+ modes whose blend takes the colour at full weight ignored alpha. What changes on screen:
69
+ - `ALPHA_ONEONE`, `ALPHA_PREMULTIPLIED`, `ALPHA_SCREENMODE`, `ALPHA_SUBTRACT`, `ALPHA_MAXIMIZED`,
70
+ `ALPHA_MAX` and the other modes that take the colour at full weight multiply it by its alpha
71
+ in the shader. An effect whose alpha is below 1 under these modes draws dimmer than before, as
72
+ it would in Unity; one with alpha 1 draws as before. Textures stay straight alpha, as in
73
+ Unity's Premultiply mode: a texture authored premultiplied should be re-exported straight.
74
+ - `ALPHA_MULTIPLY` and `ALPHA_MIN` move the colour toward white as alpha falls, so a fading
75
+ multiply particle lightens away instead of staying dark (Babylon's own particles do the same).
76
+ - The soft-particle fade lowers alpha only. Under `ALPHA_ADD` and `ALPHA_COMBINE` it used to fade
77
+ colour and alpha together, so the blend weighed it twice and the fade looked quadratic; it is
78
+ now linear over the soft distance, as in Unity and Unreal's Depth Fade. An opaque system
79
+ (`transparent: false`) or one under `ALPHA_INTERPOLATE` or `ALPHA_REPLACE_COLOR` has no
80
+ coverage to fade, so soft particles no longer darken it; they never faded it out either.
81
+ - `ALPHA_MAXIMIZED` takes the colour by alpha twice (its blend weighs it once more), so its fade
82
+ is quadratic; every other coverage mode fades linearly.
83
+ - **Fog follows the blend mode.** Particles that subtract from or screen over the frame fade
84
+ toward black with distance, like additive ones; multiply particles fade toward white, which
85
+ leaves the frame alone; particles that lay over the frame (`ALPHA_COMBINE`,
86
+ `ALPHA_PREMULTIPLIED`, opaque) still mix toward the fog colour. Before, every mode other than
87
+ `ALPHA_ADD` and `ALPHA_ONEONE` mixed toward the fog colour, which fogged a multiply or subtract
88
+ effect into a coloured haze.
89
+ - **`Vector2.isVector2` is an instance flag**, set on the prototype as `isVector3` and `isVector4`
90
+ are, instead of a static field. Read it on a vector (`v.isVector2`); `Vector2.isVector2` on the
91
+ class is gone.
92
+ - **`Vector4.setAxisAngleFromRotationMatrix` takes a `Matrix4`**, the matrix it has always read
93
+ (see Fixed). Code that passed a `Matrix3` no longer compiles; it got a wrong axis and angle.
94
+ - **Shape thickness fills evenly, as in Unity.** A sphere's or hemisphere's Radius Thickness now
95
+ spreads particles evenly over the volume of the shell, and a circle's and donut tube's over its
96
+ area (the cone already did); a rectangle spreads each edge's particles evenly over its band. Before, the radius was rolled linearly, so a
97
+ full sphere put half its particles within half its radius instead of an eighth. Effects with a
98
+ thickness above 0 look less crowded at the centre.
99
+ - **Cone particles all start at their start speed.** The direction tilted toward the rim was not
100
+ normalised, so particles inside the base disc moved slower than the rim's, by up to 3 % at the
101
+ default 30° and up to 24 % at 90°.
102
+ - **A grid emits from its cells' centres**, symmetric about the origin: a grid of width 2 with two
103
+ columns emits from x = ±0.5 instead of −1 and 0.
104
+ - Every shape's `fromJSON` takes a `ShapeJSON` instead of `any`, `ShapeJSON.speed` is a
105
+ `FunctionJSON`, and `ValueGenerator.startGen` and `genValue` take a `GeneratorMemory`.
106
+ - `EmitterShapes` lists the fields its shapes have: the grid's `row` (it said `rows`), no `angle`
107
+ on the sphere and hemisphere, the box's `emitFrom` as the new `'boxEmitFrom'` field type, and a
108
+ `transform` (the new `'shapeTransform'` type) on every shape.
109
+ - **Files hold only the shape and behavior fields that differ from their defaults.** A system's
110
+ shape and each behavior are written through the new `EmitterToJSON` and `BehaviorToJSON`, which
111
+ leave out every field equal to the default their type reads a missing field as, as the system's
112
+ own settings already were; a field the type needs, such as a behavior's curve, stays. babylon.quarks
113
+ 0.27 and later read these files; **three.quarks and babylon.quarks before 0.27 no longer do**.
114
+ `toJSON()` on a shape or behavior still writes every field.
115
+ - `Behavior.toJSON()` returns the new `BehaviorJSON` (a `type` and the settings) instead of `any`. A
116
+ behavior whose `toJSON` was declared to return something without a `type` no longer compiles.
117
+ `EmitterShapePlugin.loadJSON` and `BehaviorPlugin.loadJSON` are declared to take `ShapeJSON` and
118
+ `BehaviorJSON`, as methods, so a plugin that types its own JSON still compiles; they must only
119
+ read, since saving calls them to find the defaults.
120
+ - **`play()` on a finished system plays it again**, as Unity's `Play` does: it restarts it from
121
+ the beginning. Before, it only unpaused, and a finished system stayed empty. A paused or stopped
122
+ system still resumes where it was. `QuarksUtil.play(root)` does the same for each system, so a
123
+ short burst that had finished inside a longer effect plays again; to undo a pause without that,
124
+ use the new `resume()` and `QuarksUtil.resume(root)`.
125
+ - **Behaviors no longer reset at every loop.** A looping system called each behavior's `reset()`
126
+ as it began its next loop, so a `Noise` or `TurbulenceField` jumped back to its start offset
127
+ once a loop and the sub emissions of particles still alive were dropped. `reset()` now runs only
128
+ on `restart()` (and `stop()`), as `Behavior.reset` documents.
129
+ - **`QuarksPrefab` timeline:**
130
+ - The end of the timeline ends the emission of the systems still playing and lets their
131
+ particles die out, stops the clips, stops the prefab and rewinds it; it used to call `stop()`,
132
+ which cleared every particle on the spot. Call `stop()` yourself for that.
133
+ - A track's window excludes its end in `update` too, as `setTime` did: a system whose track ends
134
+ at 0.5 s stops emitting at 0.5 s, not one frame later.
135
+ - `update()` with no argument advances by the engine's frame time (game time) instead of the
136
+ wall clock.
137
+ - `addParticleSystemAnimation` no longer pauses the prefab.
138
+ - A looping track that ends with the timeline does not start over, and one restarted earlier
139
+ ends its emission with the timeline too.
140
+ - `timeScale` now speeds up and slows down the particle systems on the timeline (see Added), and
141
+ a negative, NaN or infinite one is read as 0. A prefab saved with a `timeScale` other than 1
142
+ plays its systems at that speed now.
143
+ - `pause()` then `play()` resumes exactly the systems the pause paused, those dying out after
144
+ their window included (they stayed frozen before), and `setTime` on a paused prefab leaves the
145
+ systems it starts paused.
146
+ - `ParticleEmitter.toJSON` writes `visible` from the emitter's own enabled flag, as `clone()` and
147
+ `serializeEffect` do, not from its parents': an emitter under a disabled template is saved
148
+ visible.
149
+ - `retainEffectResources` takes the effect's materials as any iterable, and counts holders of a
150
+ material as it does of a texture.
151
+ - **The renderer's transform no longer moves particles.** The batch meshes stay under the
152
+ `BatchedRenderer` node (disabling or disposing it still hides or frees them), but their world
153
+ matrix is frozen at identity: particles are drawn where their emitters put them, wherever the
154
+ renderer is, and the batch bounds Babylon culls by are the particles' own. Before, a moved,
155
+ turned or scaled renderer moved every particle again and only warned once; the warning is gone.
156
+ - `BatchedRenderer.batches` and `systemToBatchIndex` are read-only (`ReadonlyArray`,
157
+ `ReadonlyMap`); the renderer has always owned them, and code that wrote to them no longer
158
+ compiles; assigning `batches` throws at runtime.
159
+ - **Culling only counts the cameras that render a system's layer**: a camera whose `layerMask`
160
+ shares no bit with the system's `layers.mask` no longer keeps it simulating, so a system on a
161
+ layer only an inactive camera renders pauses as its `cullingMode` says. The `CullingMode` docs
162
+ say which cameras are tested, and that one rendering into a render target texture is not.
163
+ - A `VFXBatch` subclass implements `resizeBuffers()` and `initialCapacity`, which the shrinking
164
+ instance buffers call, and creates `mesh` in `setupBuffers`; the base class no longer makes one.
165
+ - **quarks.core's JSON and data types have no `any` left.** What changes for code that
166
+ compiles against them:
167
+ - `FunctionJSON` is the union of each generator's JSON (`ConstantValueJSON`, `GradientJSON`,
168
+ `EulerJSON` and the rest, grouped as `ValueGeneratorJSON`, `ColorGeneratorJSON`,
169
+ `RotationGeneratorJSON` and `Vector3GeneratorJSON`), with `type` as the tag. It was an open
170
+ record of `any`. Each generator's `toJSON` and `fromJSON` use its own type, and the
171
+ `…GeneratorFromJSON` readers take their group's. A generator class of your own that
172
+ implements `ValueGenerator` or a sibling interface writes one of these, or casts its `toJSON`;
173
+ no reader would know its type anyway.
174
+ - `BehaviorJSON` is `{type: string}`, no longer an open record, and every built-in behavior has its
175
+ own JSON type (`ColorOverLifeJSON`, `EmitSubParticleSystemJSON`, …) for its `toJSON` and
176
+ `fromJSON`. A plugin's `loadJSON` may take its own JSON interface with a `type`, as
177
+ `examples/src/demos/customPlugin.ts` does. An object literal passed to `BehaviorFromJSON` is
178
+ typed as its behavior's JSON, e.g. `const json: NoiseJSON = {type: 'Noise', …}`.
179
+ - `GeneratorMemory` is `number[]`. `ContinuousLinearFunction`'s values are `number`, `Vector3` or
180
+ `Vector4`, its `subType` follows from them, and its `fromJSON` returns the matching type for
181
+ the `subType`. `GradientJSON`'s `color` and `alpha` say their `subType`.
182
+ - `IEmitter.matrixWorld` is a `Matrix4`; `IParticleSystem.toJSON` and `IAnimationData.target` are
183
+ `unknown`, which a renderer narrows; `Constructable` takes `never[]` arguments.
184
+ - `EmitSubParticleSystem.subParticleSystem` is declared `IEmitter | string | undefined`: a loaded
185
+ behavior holds the uuid until the loader links it, as it always has.
186
+ - `Sequencer.toJSON` returns `SequencerJSON`; `Euler.fromArray` and `toArray` take numbers and an
187
+ order.
188
+ - In babylon.quarks, `ParticleSystemJSONParameters.rendererEmitterSettings` is the new
189
+ `RendererEmitterSettingsJSON`: a trail's `TrailSettingsJSON` or a stretched billboard's
190
+ settings.
191
+ - The `isVector3`-style flags are set on the prototypes as non-enumerable, read-only properties,
192
+ so they no longer show up in a `for…in` over a vector.
193
+ - **babylon.quarks's effect format and loader have no `any` left either.** The parts of an effect
194
+ file have named types: `EffectDocumentJSON` for what `QuarksLoader` reads, with
195
+ `QuarksMaterialJSON`, `EffectGeometryJSON` (quarks' own, three.js `BufferGeometry`,
196
+ `PlaneGeometry` and `SphereGeometry`), `EffectImageJSON`, `QuarksPrefabJSON`,
197
+ `ParticleEmitterJSON` and the rest. What changes for code that compiles against them:
198
+ - `QuarksLoader`'s `loadFromJSON`, `parse`, `resolveResources`, `resolveLinkedResources` and
199
+ `resolveExternalGeometries` take the document as `unknown`, as parsed from its file. `resolveLinkedResources` takes the new `QuarksResolveOptions`.
200
+ - `ParticleSystem.material` and `ParticleSystemParameters.material` are a
201
+ `ParticleMaterialSource`: the fields a system reads from a Babylon material or a loaded
202
+ material record. Any Babylon material fits.
203
+ - `BabylonMetaData` holds `BaseTexture | null` textures, `QuarksMaterialJSON` materials and
204
+ `GeometryEntry` geometries; its unused maps are `unknown`.
205
+ - `QuarksNodeJSON` lists its fields instead of an open record, `matrix` is optional, `visible`
206
+ is a boolean, and it has the prefab and three.js transform fields the loader reads.
207
+ `QuarksEffectJSON.materials` is `QuarksMaterialJSON[]`.
208
+ - `ParticleSystemJSONParameters` types its generators by group (`ValueGeneratorJSON`,
209
+ `ColorGeneratorJSON`, …), its `shape` as `ShapeJSON`, its `behaviors` as `BehaviorJSON[]`,
210
+ and `instancingGeometry` as a uuid or an entry. `BurstParametersJSON` has the older
211
+ `cycleCount`.
212
+ - `QuarksPrefab.toJSON` returns a `QuarksPrefabJSON`, `ParticleEmitter.toJSON` a
213
+ `ParticleEmitterJSON`, and `MeshSurfaceEmitter.fromJSON` takes a `ShapeJSON`.
214
+ - quarks.core's `JsonMetaData.textures` may hold null.
215
+
216
+ ### Deprecated
217
+
218
+ - `RecordState`, the sample of the old linked-list trail history. Trails record into the flat ring
219
+ buffer of `TrailParticle` (`historyPositions` and its siblings); nothing fills or draws the list.
220
+ - `TrailParticle.previous`, that list, is `@internal` and out of the documented API.
221
+ - `MeshSettings.startRotationX`, `startRotationY` and `startRotationZ`, which nothing reads, are
222
+ optional and deprecated; a mesh particle's start rotation about each axis is the system's
223
+ `startRotation`, an `EulerGenerator`.
224
+ - `EulerGenerator.eular`, a misspelling: the Euler is `EulerGenerator.euler`, and `eular` is an
225
+ alias of it.
226
+
227
+ ### Added
228
+
229
+ - **`blendCategory(settings)`** says how a blend mode's colour leaves the shader (`'straight'`,
230
+ `'premultiply'` or `'multiply'`) and where fog takes it (`'color'`, `'black'` or `'white'`), with
231
+ the `BlendCategory`, `BlendOutput` and `FogTarget` types. Opaque materials, `ALPHA_INTERPOLATE`,
232
+ `ALPHA_REPLACE_COLOR` and `ALPHA_DUAL_SRC0_ADD_SRC1xDST` have no coverage and stay `'straight'`.
233
+ - The shader variants cover every blend resolve with and without fog and soft particles, so
234
+ `validate:shaders` compiles the new `BLEND_PREMULTIPLY`, `BLEND_MULTIPLY` and `FOG_MULTIPLY`
235
+ defines in GLSL and WGSL.
236
+ - **The whole simulation core is documented.** Every vector, quaternion, matrix and math helper,
237
+ every shape, value, colour and rotation generator, behavior field and plugin type re-exported from
238
+ `quarks.core` now carries TSDoc, and `npm run docs:check` fails on one that does not, as it does
239
+ for the rest of the package.
240
+ - `ContinuousLinearFunction` (the keys of a `Gradient`) and its `ObjectValueType` are exported, and
241
+ `Bezier` has `BezierJSON` and `BezierSplit` types for what `toJSON` and `split` return.
242
+ - **Format warnings name the part of the file they are about**, by its JSON path
243
+ (`object.children[2].ps.behaviors[3]`). `QuarksLoaderOptions.onFormatWarning` receives them
244
+ instead of the console, and `QuarksLoaderOptions.strict` makes a load throw `QuarksFormatError`
245
+ for them, after reading the whole effect and freeing what it built, with every part in its
246
+ `problems`, to check in CI that a set of assets loads cleanly. The
247
+ loader's own format warnings (a newer format, an unresolved sub emitter or mesh surface link, an
248
+ image url that is not an image) go the same way. `readFormat`, `readFormatPart`,
249
+ `currentFormatPath`, `warnFormat` and `QuarksFormatError` (now defined in `quarks.core`) give plugin
250
+ readers and other hosts the same.
251
+ - `BehaviorTypes.EmitSubParticleSystem` lists `inherit`, with the new `'subEmitterInherit'` field
252
+ type.
253
+ - **`EffectPool.acquire` takes a placement** (`EffectPlacement`: `position`, `rotation` or
254
+ `rotationQuaternion`, `scaling`, `parent`), so a hit can face its surface normal or a muzzle
255
+ flash its barrel. Every acquire first puts the copy back to the template's transform, so a
256
+ rotation or scale from its last use does not stay. `acquire(position, parent)` still works, with
257
+ any vector that has `x`, `y` and `z`.
258
+ - **`ParticleSystem.timeDilation`**, a runtime multiplier on `playbackSpeed` that is not part of
259
+ the effect (never saved, cloned or loaded), like Unreal's Custom Time Dilation or Unity's
260
+ `VisualEffect.playRate`: for slow motion, a pause menu, or a timeline.
261
+ - **`QuarksPrefab.timeScale` reaches the systems**: it sets the `timeDilation` of each track's
262
+ system, so they slow down and speed up with the timeline; it is normalised like
263
+ `playbackSpeed`. Removing a system's last track sets it back to 1. Clips keep their own speed.
264
+ - **`ParticleSystem.resume()` and `QuarksUtil.resume(root)`** undo a pause and leave a finished
265
+ system finished, for a pause button over a scene; the editor's transport uses them.
266
+ - **`QuarksPrefab.autoUpdate`** advances the timeline before every render by the engine's frame
267
+ time, as a Unity `PlayableDirector` on Game Time does; it is turned off on dispose.
268
+ - `removeAllEventListeners()` with no event type removes every listener.
269
+ - A **Timeline** demo: a charge, a release burst on a window shorter than a frame and lingering
270
+ smoke on a `QuarksPrefab`, every other blast in slow motion.
271
+ - **`renderFace`** on a system (`'both'`, `'front'`, `'back'`; the new `RenderFace` type), Unity's
272
+ Render Face and Unreal's Two Sided: a mesh particle can cull its back faces, or show only its
273
+ inside. `'both'` by default, as before; written to JSON only when set; mesh systems that differ
274
+ are batched apart; the flat render modes (billboards, stretched, beam, trail) always draw both
275
+ faces. The culling is per batch, so a mesh mirrored by a negative size on an odd number of axes
276
+ swaps which side `'front'` and `'back'` show. The editor has a Render face field for mesh systems.
277
+
278
+ ### Fixed
279
+
280
+ - `new Gradient()` with no keys is opaque white from 0 to 1, as Unity's default gradient is. Its two
281
+ colour keys were both at 0 (black, then white), so it already looked white after the start; now
282
+ its keys span the range.
283
+ - `PiecewiseBezier.toSVG` returns an empty path for a curve with no pieces instead of throwing.
284
+ - A shape transform's rotation is read as a unit quaternion, and a zero one as the identity, so a
285
+ hand-written or rounded rotation no longer scales the shape.
286
+ - `Rotation3DOverLife` no longer allocates a quaternion for each mesh particle it starts.
287
+ - `BatchedRenderer.addSystem` throws for a system of another scene instead of building a batch
288
+ that holds two scenes' meshes and materials.
289
+ - `configureAdaptivePerformance` keeps `minQuality` at 0.1 or more, the floor
290
+ `ParticleSystem.setQualityFactor` clamps to and the documented range; it allowed 0.05.
291
+ - The 1×1 white map mesh batches share is made again after something outside disposes it, instead
292
+ of binding a disposed texture.
293
+ - An env atlas that fails to build from a cube's faces warns once instead of once per system: it
294
+ is not tried again until the cube's faces change or `disposeEnvAtlas` is called for the cube.
295
+ - `ApplySequences` keeps each particle's place in a `WeakMap` instead of fields added to the
296
+ particle, reuses that state when a pooled particle is emitted again instead of allocating two
297
+ vectors, and moves a particle straight to its point when a sequence's range has no length (it
298
+ divided by zero).
299
+ - An `EmitSubParticleSystem` read from a file without `useVelocityAsBasis` saves without it, as a
300
+ default, instead of writing `false`.
301
+ - `PiecewiseBezier.toSVG` draws the curve itself, not its sampled table.
302
+ - A `Gradient` read from JSON without its `color` or `alpha` keys warns, and keeps the default for
303
+ the missing part.
304
+ - A particle system reads its texture from the first material slot that holds a `Texture`; a cube
305
+ or other non-`Texture` in a slot is passed over instead of drawn as a broken 2D texture.
306
+ - A list of an effect file (`images`, `textures`, `materials`, `geometries`, a node's `children`)
307
+ that is not a list, or an entry in one that is not an object, warns and is skipped, like any
308
+ other part the loader cannot read, instead of failing the load with a `TypeError`.
309
+ - `ParticleSystem.fromJSON` with a material entry whose `reflectionAtlas` is a texture uuid
310
+ resolves it, as it does the other slots, instead of failing on the string.
311
+ - The fragment's `alpha < 0.01` discard is right in every blend mode: under one-one or
312
+ premultiplied blending a discarded fragment used to be one that would still have drawn.
313
+ - Erosion and a mesh's rim fade no longer show a hard cut under one-one and premultiplied
314
+ blending; they fade like under alpha blending.
315
+ - **`Bezier.split(t)` split the curve at `1 - t`.** Its de Casteljau steps weighted the two points
316
+ the wrong way round, so the halves met at the mirrored point; a curve split in the editor away
317
+ from a segment's middle changed shape. It now splits at `t`, and both halves trace the curve.
318
+ - `Matrix3.makeTranslation(vector)` read a `Vector2` as a number and built a broken matrix; it
319
+ checked a flag that `Vector2` only had as a static.
320
+ - `Vector4.setAxisAngleFromRotationMatrix` is typed to take the `Matrix4` it reads (as three.js
321
+ does); it was typed `Matrix3` and read a 3×3 matrix's entries from the wrong places.
322
+ - Shapes read out-of-range settings safely: a thickness is clamped to [0, 1] (NaN reads as 1), a negative radius,
323
+ width or height is read as 0, a cone's angle is clamped to [0, π/2] and a box's thickness to 1, a
324
+ grid's rows and columns are read as whole numbers of at least 1 (0 or Infinity gave NaN
325
+ positions), and a
326
+ short `boxThickness` array reads its missing axes as 0.
327
+ - A donut with no tube, and a rectangle with no size, started particles with no speed; they head
328
+ out from the centre line, or along the edge's normal, at their start speed.
329
+ - `AxisAngleGenerator` normalises its axis when it rotates (a zero axis is read as +z): an axis of
330
+ any other length gave a quaternion that was not a unit and skewed mesh particles.
331
+ - `RandomQuatGenerator` and `RandomColorBetweenGradient` used before `startGen` read slots that
332
+ belong to other generators, and the gradient mix rolled a new value every frame, flickering; both
333
+ now roll their own slots once, as `IntervalValue` does.
334
+ - `PiecewiseFunction.insertFunction` at a `t` outside every piece inserted at the front and
335
+ unsorted the pieces; it inserts in order and clamps `t` to [0, 1].
336
+ - A circle or rectangle in `Random` mode no longer samples its sweep speed every frame, as the
337
+ other shapes do not; a random sweep speed no longer draws its roll, which shifts a seeded
338
+ effect's random sequence by one draw.
339
+ - Shape docs give the units and axes of `arc` (radians around +z from +x), the ranges of every
340
+ size and thickness, and say that `RectangleEmitter` emits from the outline (Unity's Rectangle is a
341
+ `BoxEmitter` with no depth); the donut's `donutRadius` is the tube's radius, not the hole's.
342
+ - `docs/FORMAT.md` gave 0.28 as the release of format version 1; 0.27.0 is. It now says how a
343
+ system's blend mode is read when its material entry, or the system, has none, and that
344
+ `ps.version` is written for three.quarks only.
345
+ - **`QuarksPrefab` plays a window a single frame steps over.** A track shorter than a frame (a
346
+ release burst, say) was never entered when one update jumped from before its start to after its
347
+ end; its system now restarts and ends its emission once it has simulated a step (checked on
348
+ each update and after each render), and its clip plays and stops.
349
+ - **An `EffectPool` never hands out a disposed copy.** A copy disposed from outside, by its own
350
+ `dispose()` or with a parent it was placed under, leaves the pool, which makes a new one when
351
+ it needs it; before, `acquire` handed out the disposed node and it never came back.
352
+ - **`QuarksUtil.instantiate` copies meshes as meshes**, sharing the source's geometry and
353
+ material, where it made plain transform nodes of them; an instance of a mesh in the tree becomes
354
+ an instance of its copy, and a `MeshSurfaceEmitter` whose mesh is in the copied tree emits from
355
+ the copy. The copy holds the source's materials as it held its
356
+ textures, so `disposeEffect(source)` leaves a copy's meshes and materials whole.
357
+ - **`MeshSurfaceEmitter` emits from a mesh without indices**, read as a triangle list, instead of
358
+ from the origin. A stray vertex count past the last whole triangle is ignored.
359
+ - **A listener removed while an event fires no longer makes the next one skip**: the listener
360
+ lists are copied on write, so every listener registered when the event fired is called once,
361
+ and one added during it waits for the next event.
362
+ - `particleDied` allocates nothing per death: one event object is reused, its `particle` set only
363
+ while the listeners run (documented, as is when a trail's death fires).
364
+ - `EmitSubParticleSystem.reset()` drops the running sub emissions, as `ParticleSystem.restart`
365
+ did through a special case it no longer needs; any host gets the same reset.
366
+ - Culling reuses its frustum planes; it allocated a new array per extra camera every frame.
367
+ - **Instance buffers shrink.** A batch whose buffers a burst grew halves them after it has used
368
+ under a quarter of them for 300 renderer updates (about five seconds at 60 fps), down to where
369
+ it started, so one big explosion no longer holds that memory for the rest of the session.
370
+ Trails too.
371
+ - A batch no longer creates a placeholder mesh that it disposes straight away.
372
+ - `SortMode` documents that `'distance'` sorts once a frame, for the first active camera.
373
+
374
+ ### Internal
375
+
376
+ - Tests cover the vectors, quaternions, Euler angles, matrices and math helpers, and the coverage
377
+ floors rise: the simulation core's from 54 % to 88 % of lines, the library's and the editor's to
378
+ one point under what the suites reach.
379
+ - Behaviour tests for every shape: even filling, bounds, start speed, the rectangle's outline,
380
+ grid cells, out-of-range settings, JSON round trips of every field and the registry's fields.
381
+ - Golden fixtures in `packages/babylon.quarks/test/fixtures`: effects written by three.quarks, by
382
+ the Unity exporter and by babylon.quarks 0.27.0. Each loads with the warnings its snapshot holds and
383
+ saves to the format it holds, saving is stable, and each 0.27.0 file loads as the one it was
384
+ written from.
385
+ - Loaded node ids and mesh surface links live in module `WeakMap`s instead of `as any` fields on
386
+ nodes and systems.
387
+ - The batch settings, their defaults and the helpers that normalise and compare them move from
388
+ `VFXBatch.ts` to `batchSettings.ts`, which `VFXBatch.ts` re-exports; the batch key reads them
389
+ there, so the build no longer reports a circular import. Exports are unchanged.
390
+ - A clean build and install print no warnings: the core build stops passing `declarationDir`
391
+ without declarations, CI runs on Node 24 with current GitHub Actions, dev dependencies have no
392
+ known vulnerabilities, and the demo smoke test fails on a console warning as well as an error.
393
+ - The toolchain moves to ESLint 10 (with `defineConfig` and react-hooks 7's React Compiler rules),
394
+ TypeScript 6, Vite 8 (Rolldown: the examples' chunks all fit under its 500 kB warning) and
395
+ cross-env 10. Babel stays on 7, which Jest's own preset still needs.
396
+ - No explicit `any` is left in the repository, tests included, and ESLint now reports one as an
397
+ error. Tests reach private members with `obj['member']`, and use typed helpers in
398
+ `test/internals.ts` for shader options, empty resources, fetch responses and values the types
399
+ rule out.
400
+
401
+ ## [0.27.1] — 2026-10-23 (not published)
402
+
403
+ Packaging and release checks; the runtime is the same as 0.27.0. It was never published to npm; its
404
+ changes ship in 0.28.0.
405
+
406
+ ### Changed
407
+
408
+ - **`@babylonjs/core` peer range is `^9.0.0`** instead of `>=9.0.0`. The package is built and
409
+ tested against Babylon 9; a future major is a separate release, so npm no longer accepts it
410
+ silently. No Babylon 10 exists yet, so no install that worked stops working.
411
+ - **`sideEffects` names the built bundles**, the only files with import-time work (they register
412
+ the mesh-surface emitter with the shape table, so a bare `import 'babylon.quarks'` keeps doing
413
+ that); bundlers may treat everything else in the package as side-effect free.
414
+ - The README says what `require('babylon.quarks')` needs: `@babylonjs/core` is ES modules only, so
415
+ CommonJS consumers need a Node that can `require` ES modules (20.19 or 22.12 and later).
416
+
417
+ ### Internal
418
+
419
+ - **`npm run check:pack` checks that the editor moves with the library.** The editor's peer
420
+ dependency on `babylon.quarks` must be the library's own minor line (`~X.Y.0`), and CONTRIBUTING's
421
+ release steps say so: an editor published with an older range would install a babylon.quarks
422
+ without the API it imports.
423
+ - The examples compile with `strict` and are part of `npm run typecheck`, in CI too.
424
+ - The vendored `quarks.core` workspace is marked `private` (it is bundled into babylon.quarks and
425
+ never published on its own), points at this repository, and lists `types` first in its
426
+ `exports`. Its unused Babel and ESLint configs are gone, and so is `composite` in the library's
427
+ `tsconfig.json`, which nothing referenced.
428
+ - The examples dev server accepts only localhost and Cloudflare quick tunnel hosts instead of
429
+ any `Host` header. The repository's Node requirement is 20.19, the version `check:pack`'s
430
+ CommonJS import needs.
431
+
7
432
  ## [0.27.0] — 2026-10-23
8
433
 
9
434
  The release that follows the October 2026 audit: every domain (emitters, behaviors, system and
package/README.md CHANGED
@@ -32,7 +32,9 @@ High-performance particle system for [Babylon.js](https://www.babylonjs.com/). B
32
32
  npm install babylon.quarks @babylonjs/core
33
33
  ```
34
34
 
35
- Peer dependency: `@babylonjs/core` >= 9.
35
+ Peer dependency: `@babylonjs/core` 9.x. The package ships ES module and CommonJS builds;
36
+ `@babylonjs/core` is ES modules only, so `require('babylon.quarks')` needs a Node that can
37
+ `require` ES modules (20.19 or 22.12 and later). Bundlers and `import` work everywhere.
36
38
 
37
39
  ## Quick start
38
40