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