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.
- package/CHANGELOG.md +1194 -8
- package/README.md +13 -9
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.cts +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,1193 @@ 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
|
+
|
|
432
|
+
## [0.27.0] — 2026-10-23
|
|
433
|
+
|
|
434
|
+
The release that follows the October 2026 audit: every domain (emitters, behaviors, system and
|
|
435
|
+
loader, shaders, trails) was brought to the working rules in `CLAUDE.md`. A 0.27.0 dated
|
|
436
|
+
2026-10-21 was prepared but never published; its entries are folded in here, so this is the only
|
|
437
|
+
0.27.0 and the whole list applies when upgrading from 0.26.
|
|
438
|
+
|
|
439
|
+
### Breaking changes
|
|
440
|
+
|
|
441
|
+
Each item is described in full in the section it points to.
|
|
442
|
+
|
|
443
|
+
- **Exports removed** that nothing in the package read: `VFXBatchSettings.reflectionFaces`,
|
|
444
|
+
`MeshSettings.rotationAxis`, the `ParticleMesh*Material` classes, the shader chunk registry and
|
|
445
|
+
GLSL string exports, `BatchedParticleRenderer`, the `SpriteBatch` / `TrailBatch` exports,
|
|
446
|
+
`ParticleEmitter.extractFromCache`, `ParticleSystemParameters.startLength` and the duplicate
|
|
447
|
+
`quarks.core` type re-exports (Removed); `ApplyForce` loses its runtime fields `magnitudeValue`
|
|
448
|
+
and `memory` (Changed).
|
|
449
|
+
- **Trails stretch their texture by distance** by default; set the texture mode to
|
|
450
|
+
`'distributePerSegment'` to keep the old layout (Changed).
|
|
451
|
+
- **`WidthOverLength` spans the samples the trail holds**, not its `startLength` capacity
|
|
452
|
+
(Changed).
|
|
453
|
+
- **Trails face the camera in view space**; the trail material loses its `lineWidth`,
|
|
454
|
+
`resolution`, `sizeAttenuation`, `world` and `worldViewProjection` uniforms (Changed).
|
|
455
|
+
- **Flipbook frames wrap into the sheet** and the last cell blends into the first (Changed).
|
|
456
|
+
- **`softFarFade` defaults to 1**, so soft particles fade without a fade distance set (Changed).
|
|
457
|
+
- **Soft particles name their depth encoding**: `setDepthTexture(depth, {encoding})` with
|
|
458
|
+
`'linear'` as the default; a hand-built non-linear depth needs `'nonLinear'` (Changed).
|
|
459
|
+
- **`ApplyForce` is a world force**; a local-space effect that relied on the old reading sets
|
|
460
|
+
`space: 'local'` (Changed).
|
|
461
|
+
- **`OrbitOverLife` orbits the emitter about the emitter's normalised axis**; set `space: 'world'`
|
|
462
|
+
for the old orbit (Changed).
|
|
463
|
+
- **`Noise` turns mesh particles by `rotationAmount`** instead of ignoring it (Changed).
|
|
464
|
+
- **`TurbulenceField` is deterministic**: two fields with the same settings and no `seed` swirl
|
|
465
|
+
identically (Changed).
|
|
466
|
+
- **Bursts take the file defaults in code too**: `interval` is 0.1 s when left out, and the
|
|
467
|
+
system holds its own copy of `emissionBursts` (Changed).
|
|
468
|
+
- **A system belongs to one `BatchedRenderer` at a time**, and a disposed renderer refuses
|
|
469
|
+
`addSystem` with an error (Changed).
|
|
470
|
+
- **`ParticleSystem.toJSON()` ids are numbered per `meta`**, so two separate calls both write
|
|
471
|
+
`quarks_geometry_1`; merge outputs through one `meta` or use `serializeEffect` (Changed).
|
|
472
|
+
- **A disposed system stays disposed**: `restart()` no longer brings it back (Changed).
|
|
473
|
+
- **Seeded systems roll differently from 0.26**; a saved seed gives a different, still repeatable,
|
|
474
|
+
effect (Changed).
|
|
475
|
+
- **`serializeEffect` writes the url a texture's image had in its source file** rather than the
|
|
476
|
+
joined url, so a save next to the source loads; a save elsewhere passes `imageUrl` (Changed).
|
|
477
|
+
- **TypeScript:** the fields of `ParticleSystemJSONParameters` other than `version` are optional,
|
|
478
|
+
and `StoredBatchSettings.texture` is `BaseTexture | null` (Changed).
|
|
479
|
+
|
|
480
|
+
### Added
|
|
481
|
+
|
|
482
|
+
- **Trail texture modes** (`TrailSettings.textureMode`, Unity's Texture Mode): `'stretch'` (the
|
|
483
|
+
default) runs the texture once along the ribbon by distance; `'distributePerSegment'` once with
|
|
484
|
+
each recorded segment an equal share (Niagara's Scaled Uniformly, what trails did before);
|
|
485
|
+
`'tile'` repeats it per world unit, anchored where the particle was born so a sample keeps its U
|
|
486
|
+
while it lives (Niagara's Tiling Distance); `'repeatPerSegment'` once per segment.
|
|
487
|
+
`TrailSettings.textureScale` (Unity's Texture Scale) sets the repeats: per unit, per segment or
|
|
488
|
+
over the ribbon. `uvFromHead` runs any mode from the particle back. The texture's UV channel 1
|
|
489
|
+
follows the same U. Each sample now records its distance along the trail
|
|
490
|
+
(`TrailParticle.historyDistances`, `measureHeadDistance()`).
|
|
491
|
+
- **`TrailSettings.minVertexDistance`** (Unity's Min Vertex Distance): a point is fixed only once
|
|
492
|
+
the particle has moved that far from the last fixed one, the newest sample following the
|
|
493
|
+
particle meanwhile (`TrailParticle.update(replaceHead)`), so a slow or standing particle no
|
|
494
|
+
longer fills its history with points on one spot, and the trail's length is a distance rather
|
|
495
|
+
than a number of steps.
|
|
496
|
+
- **`TrailSettings.lifetime`** (Unity's Time): samples older than that many seconds are dropped
|
|
497
|
+
from the tail (`TrailParticle.historyAges`, `dropOldest()`), within the `startLength` cap.
|
|
498
|
+
- **`ParticleSystem.clearTrails()`** (Unity's `TrailRenderer.Clear`): drops every ribbon and keeps
|
|
499
|
+
the particles, which start new ones where they are. With `pause()`, the move and `play()`, it is
|
|
500
|
+
how an emitter teleports without a ribbon across the jump; the advice to `pause()` alone, given
|
|
501
|
+
below, never cleared the samples already recorded.
|
|
502
|
+
- Normalisers `trailTextureMode`, `trailTextureScale`, `trailMinVertexDistance`, `trailLifetime`,
|
|
503
|
+
beside `trailTessellation` and `trailCurveTension`; the trail settings' JSON writes each only
|
|
504
|
+
when set, and `fromJSON`, `clone()`, `serializeEffect` and `QuarksLoader` carry them all.
|
|
505
|
+
- Demo: Trail Texture Modes and Sampling.
|
|
506
|
+
- **`shaderProgram()` and `shaderVariants()`**: the defines, attributes, uniforms, samplers and
|
|
507
|
+
sources (GLSL and WGSL) of the material a batch builds from its settings, and every distinct
|
|
508
|
+
program the library can compile (Unity's ShaderVariantCollection), for warming the shader cache
|
|
509
|
+
before the first frame. The batches build their materials through `shaderProgram`, so the list
|
|
510
|
+
is the code's own account of what it compiles; `npm run validate:shaders` compiles every variant
|
|
511
|
+
in both languages in CI (the former `validate:wgsl` compiled a hand-written WGSL subset that
|
|
512
|
+
missed, among others, the env atlas, lit meshes with texture effects, and effects with fog, soft
|
|
513
|
+
particles or alpha test). Types `ShaderProgram`, `ShaderSettings`, `ShaderContext`,
|
|
514
|
+
`ShaderVariant`, `ShaderSources`.
|
|
515
|
+
- **`ParticleSystem.clear()`** (Unity's `Clear`): removes the particles alive and nothing else; the
|
|
516
|
+
emission clock, the bursts and the pause stay where they were, and the sub emissions the
|
|
517
|
+
removed particles were driving end with them.
|
|
518
|
+
- **`space` on `ApplyForce`, `ForceOverLife` and `OrbitOverLife`** (quarks.core), the space the
|
|
519
|
+
force or the orbit is authored in (Unity's Space on Force over Lifetime): `'world'` keeps gravity
|
|
520
|
+
down and wind steady however the emitter is turned, and orbits the world origin about a world
|
|
521
|
+
axis; `'local'` turns and scales the force with the emitter, and orbits the emitter about one of
|
|
522
|
+
its own axes. The forces default to `'world'`, the orbit to `'local'`. The behavior converts into
|
|
523
|
+
the space the system simulates in, so a setting means the same in a local and a world-space
|
|
524
|
+
system, and the compiled behavior pass does the same. Written to JSON only when not the default.
|
|
525
|
+
The type is `BehaviorSpace` (`VelocityOverLifeSpace` is now an alias of it); the decomposed
|
|
526
|
+
emitter transform the conversions use is `EmitterFrame`, for behaviors of your own. The editor
|
|
527
|
+
shows Space on Force over Lifetime and Gravity, and Seed on Turbulence. The Force Space demo
|
|
528
|
+
shows a world gravity and a local thrust on turning emitters.
|
|
529
|
+
- **`seed` on `TurbulenceField`** (quarks.core): the field is built from it, 0 by default, so the
|
|
530
|
+
same settings swirl the same way on every run and in every copy; give each system its own seed
|
|
531
|
+
for different fields. Written to JSON only when set. `ParticleSystem.seed` now covers the whole
|
|
532
|
+
effect, since no behavior rolls outside the step any more.
|
|
533
|
+
- **`PhysicsResolver`** (quarks.core) is exported, and `setPhysicsResolver(undefined)` unregisters
|
|
534
|
+
the host's resolver. `ApplyCollision` asks the registered resolver when a particle is tested,
|
|
535
|
+
so one registered after the effect loaded takes effect; a resolver given to the constructor is
|
|
536
|
+
still preferred. `getPhysicsResolver()` is typed as possibly undefined.
|
|
537
|
+
- **`prewarmTime` on `ParticleSystem`**: how much of the cycle `prewarm` simulates before the first
|
|
538
|
+
frame, in seconds (Niagara's Warmup Time), on a looping system like `prewarm` itself. 0, the
|
|
539
|
+
default, warms up a whole cycle as Unity's Prewarm does; a shorter time warms up the cycle's last part only, with the bursts before it
|
|
540
|
+
counted as fired, which is enough for an effect whose particles live shorter than its cycle and
|
|
541
|
+
keeps a long cycle's first frame cheap. Cut to the cycle and to the static
|
|
542
|
+
`ParticleSystem.maxPrewarmSeconds` guard; written to JSON only when set.
|
|
543
|
+
- **`transparent`, `depthTest`, `depthWrite` and `alphaTest` on `ParticleSystem`**, the material
|
|
544
|
+
flags a system is built with, readable and settable afterwards like `blending`. Setting one
|
|
545
|
+
moves the system to a matching batch. `alphaTest` is kept to 0…1; the material flags were
|
|
546
|
+
only constructor parameters and JSON fields before.
|
|
547
|
+
- **`instancingIndices` on `ParticleSystem`**: the mesh particles' index list, settable after
|
|
548
|
+
construction. Mesh systems that share a vertex buffer but not an index list (sub-meshes of one
|
|
549
|
+
buffer) are now batched apart; they used to share a batch and draw with the first system's
|
|
550
|
+
indices.
|
|
551
|
+
- **`serializeEffect(root, options?)` writes a whole effect** as one document `QuarksLoader`
|
|
552
|
+
loads back, the counterpart of `loader.load()`. It writes the node tree (groups, emitters, mesh
|
|
553
|
+
nodes with their geometry, prefabs with `timeScale`), each texture with its sampler state, and a
|
|
554
|
+
format version. The result is plain data, so `JSON.stringify` takes it; a loaded effect saved
|
|
555
|
+
again writes the same file. A texture with no url a file can point at is reported through
|
|
556
|
+
`onWarning`, as are links to nodes it does not write and mesh settings it cannot hold. Options:
|
|
557
|
+
`filter` to leave out helper nodes, `imageUrl` to write image urls relative to where the file is
|
|
558
|
+
saved, `generator`, and `name` for several roots. A mesh node that is not drawn keeps its
|
|
559
|
+
children drawn (`isVisible: false`, which the loader now reads). The document types are exported (`QuarksEffectJSON` and the rest), and the format is
|
|
560
|
+
described in `docs/FORMAT.md`. Before, `ParticleSystem.toJSON` left live textures and materials
|
|
561
|
+
in its meta, and every caller wrote its own envelope.
|
|
562
|
+
- **Effect files carry a format version**, `metadata.quarksFormat` (`QUARKS_FORMAT_VERSION`). The
|
|
563
|
+
loader warns about a file from a newer version. Files without one still load.
|
|
564
|
+
- **`QuarksFormatError`**, thrown by `QuarksLoader.parse` for a document with no `object`.
|
|
565
|
+
- **Loads can wait for textures, report what failed, and be cancelled.** The loader option
|
|
566
|
+
`waitForTextures` makes `load`, `loadFromJSON`, `loadAll` and `loadFromJSONAll` resolve once every
|
|
567
|
+
texture of the effect has loaded or failed, so it does not draw its first frames blank, like an
|
|
568
|
+
Addressables or Unreal streamable handle completing. `onResourceError(kind, url, error)` reports
|
|
569
|
+
each geometry file or image that fails; the effect still loads. A `signal` (`AbortSignal`) in the
|
|
570
|
+
new `QuarksLoadOptions`, and in `QuarksBatchLoadOptions`, cancels a load: it rejects with the
|
|
571
|
+
signal's reason as soon as it is aborted and leaves no node of the effect in the scene, whatever
|
|
572
|
+
`continueOnError` says. The effect document's fetch is cancelled, and a custom
|
|
573
|
+
`QuarksResourceResolver.fetchJson` gets the signal for it. A scene disposed during a load rejects
|
|
574
|
+
it the same way, and a batch whose effect fails stops waiting on the others' textures.
|
|
575
|
+
- **A loaded effect frees what it owns when it is disposed**: the materials made for it, and each
|
|
576
|
+
texture once no other loaded effect, or copy of one, uses it. This holds however the root goes
|
|
577
|
+
(`root.dispose()`, its parent's or the scene's); `QuarksLoader.disposeEffect(root)` says so in
|
|
578
|
+
code. Before, those materials and textures stayed in the scene. A material or texture of a loaded
|
|
579
|
+
effect handed to other code is freed with the effect.
|
|
580
|
+
- **Soft particles and alpha test on trails.** `softParticles` and `alphaTest` now apply to trails
|
|
581
|
+
as to every other render mode, in GLSL and WGSL. Before, a trail accepted them, batched on them
|
|
582
|
+
and drew hard with a fixed 0.01 cutoff.
|
|
583
|
+
- **Soft-particle depth encodings:** `SoftDepthEncoding`, `DepthTextureOptions`,
|
|
584
|
+
`BatchedRenderer.depthEncoding`, and the decode helpers `softDepthParams` and `decodeSoftDepth`
|
|
585
|
+
(see Changed).
|
|
586
|
+
- **`disposeEnvAtlas(cube, scene?)`** drops the env atlases built for a cube.
|
|
587
|
+
- **Trail tessellation** (`TrailSettings.tessellation`, `curveTension`), like Niagara's ribbon
|
|
588
|
+
tessellation and curve tension:
|
|
589
|
+
- Each segment between two recorded samples is drawn as `tessellation + 1` segments along a
|
|
590
|
+
centripetal cardinal spline through the samples: Catmull-Rom at `curveTension` 0 (the
|
|
591
|
+
default), straight at 1. Centripetal spacing keeps the curve from overshooting where samples
|
|
592
|
+
lie unevenly apart.
|
|
593
|
+
- A fast or sharply turning trail, such as a weapon swing that moves far between frames, stays
|
|
594
|
+
round instead of showing its corners. The curve passes through every sample, so the trail
|
|
595
|
+
keeps its shape.
|
|
596
|
+
- Swept trails curve their sides with the points, keeping each side's length, so a blade's trail
|
|
597
|
+
stays as wide as the blade.
|
|
598
|
+
- Widths, colors and the texture's U are eased along the curve. The live tip drawn between
|
|
599
|
+
steps joins it with a straight segment, so the recorded part stays still as the tip moves.
|
|
600
|
+
- `tessellation` is a whole number from 0 (the default: off, no cost) to 16
|
|
601
|
+
(`MAX_TRAIL_TESSELLATION`). JSON writes both settings only when set. Helpers:
|
|
602
|
+
`trailTessellation`, `trailCurveTension`.
|
|
603
|
+
- **`textureSourceUrl(texture)`**: the url a loaded texture's image had in its effect file,
|
|
604
|
+
before the loader joined a base url, which `serializeEffect` now writes by default. It is
|
|
605
|
+
`undefined` for a texture made in code, one whose image a resolver inlined as a `data:` url, or
|
|
606
|
+
one whose `url` changed since it was loaded, which all write their `url` as before.
|
|
607
|
+
|
|
608
|
+
### Removed (breaking)
|
|
609
|
+
|
|
610
|
+
The public API is now what the docs describe; these exports were left over from the three.js port
|
|
611
|
+
or duplicated something else, and nothing in the package used them.
|
|
612
|
+
|
|
613
|
+
- **`VFXBatchSettings.reflectionFaces`**: six face textures were read off a material
|
|
614
|
+
(`material.reflectionFaces`) and kept in the batch key, but no shader ever sampled them.
|
|
615
|
+
`reflectionTexture` is documented as it works: a cube texture is unfolded into `reflectionAtlas`
|
|
616
|
+
and sampled from there; nothing else is sampled.
|
|
617
|
+
- **`MeshSettings.rotationAxis`** (quarks.core), which was declared but never read, written or
|
|
618
|
+
cloned: a mesh particle's rotation is its start rotation (`startRotationX/Y/Z`, or a
|
|
619
|
+
`RandomQuatGenerator` / `EulerGenerator` start rotation) and `Rotation3DOverLife`. A file that
|
|
620
|
+
wrote it loses nothing, since no runtime ever read it.
|
|
621
|
+
- **`ParticleMeshStandardMaterial`, `ParticleMeshPhysicsMaterial` and
|
|
622
|
+
`BabylonParticleMaterialParameters`.** Mesh particles are drawn by the batch's own material; set
|
|
623
|
+
their look with the system's `material`, `texture` and lighting settings.
|
|
624
|
+
- **The three.js shader chunk registry**: `registerShaderChunks`, `getShaderChunk`,
|
|
625
|
+
`getRegisteredShaderChunks`, and the side effect that ran it on import. The batches never read
|
|
626
|
+
it.
|
|
627
|
+
- **The GLSL shader string exports** (`particleVertShader`, `particleFragShader`,
|
|
628
|
+
`localParticleVertShader`, `trailVertShader` and the rest). The batches build their shaders
|
|
629
|
+
themselves, in GLSL and WGSL.
|
|
630
|
+
- **`BatchedParticleRenderer`**, an alias of `BatchedRenderer`.
|
|
631
|
+
- **The `SpriteBatch` and `TrailBatch` exports.** `BatchedRenderer` makes the batches; `VFXBatch`
|
|
632
|
+
stays exported as the type of `renderer.batches`.
|
|
633
|
+
- **`ParticleEmitter.extractFromCache`**, a three.js serialisation helper.
|
|
634
|
+
- **`ParticleSystemParameters.startLength`**, which nothing read. A trail's length is
|
|
635
|
+
`rendererEmitterSettings.startLength`.
|
|
636
|
+
- **The duplicate type re-exports of `quarks.core`.** The same types still come from
|
|
637
|
+
`export * from 'quarks.core'`.
|
|
638
|
+
|
|
639
|
+
### Changed
|
|
640
|
+
|
|
641
|
+
- **A trail's texture is stretched by distance** (breaking: an existing effect file has no
|
|
642
|
+
`textureMode`, so its trails now stretch by distance, which shows wherever a trail's speed
|
|
643
|
+
varies along its length): the default texture mode is Unity's Stretch, so a fast stretch of
|
|
644
|
+
the ribbon shows more of the texture than a slow one. Trails used to give each recorded segment
|
|
645
|
+
an equal share whatever its length; set `textureMode: 'distributePerSegment'` to keep that. A
|
|
646
|
+
ribbon that has not moved is laid out per segment, since it has no distance to stretch over.
|
|
647
|
+
- **`WidthOverLength` spans the samples the trail holds** (breaking for a young trail): the curve's
|
|
648
|
+
end is the oldest sample, as Unity's width curve runs over the trail as it is, so a trail cut
|
|
649
|
+
short by `lifetime` or `minVertexDistance` reaches the curve's end and a trail still growing
|
|
650
|
+
shows the whole curve compressed. It used to span the `startLength` capacity, so a growing or
|
|
651
|
+
shortened trail never tapered to the curve's end.
|
|
652
|
+
- **`followLocalOrigin: false` is no longer written to JSON**, as the other trail settings at
|
|
653
|
+
their defaults are not (three.quarks's loader reads a missing flag as off); `startLength` is
|
|
654
|
+
still always written, for loaders that read it without a default. A JSON `sideAxis` that is
|
|
655
|
+
not three finite numbers (numeric strings still count), or has no length, is dropped with a
|
|
656
|
+
`[quarks]` warning instead of making a ribbon of no width (or of NaN).
|
|
657
|
+
- **A particle at the end of its life is dead for its trail too.** The trail path recorded one
|
|
658
|
+
more sample at `age === life`, the step the removal pass already counted the particle dead, and
|
|
659
|
+
the renderer drew a tip for it; the samples, the world-space rewrite, the side axis and the tip
|
|
660
|
+
now all stop at `age < life`.
|
|
661
|
+
- **Flipbook frames wrap into the sheet, and the last cell blends into the first** (breaking for a
|
|
662
|
+
frame outside the sheet). A frame past the last cell used to read outside the texture, a
|
|
663
|
+
negative one differed between GLSL (floored) and WGSL (truncated), and with `blendTiles` the last
|
|
664
|
+
frame cross-faded into a cell that does not exist. The cell is now computed by one shared chunk in
|
|
665
|
+
both languages and by the trail's CPU path (`sheetCell`), frames wrapping as Unity's Texture
|
|
666
|
+
Sheet Animation loops. `blendTiles` compiles nothing on a 1×1 sheet, where the `TILE_BLEND` shader
|
|
667
|
+
read varyings the vertex stage never wrote (undefined behaviour on some GLSL drivers; the common
|
|
668
|
+
content of imported files).
|
|
669
|
+
- **`uTileCount`, `vTileCount`, `softNearFade` and `softFarFade` are normalised**: tile counts are
|
|
670
|
+
whole numbers of at least 1 (a 0 or fractional count divided by zero in the shader), fade
|
|
671
|
+
distances are finite and at least 0. **`softFarFade` defaults to 1** (breaking): with the old
|
|
672
|
+
default of 0 a soft particle was cut off hard at the scene instead of fading, so soft particles
|
|
673
|
+
did nothing unless a fade distance was set; a file that wrote neither fade now fades over one
|
|
674
|
+
unit, Unreal's Depth Fade default.
|
|
675
|
+
- **Mesh particles light both faces and keep their normals under a non-uniform size.** The normal
|
|
676
|
+
is scaled by the inverse of the size, so a stretched mesh stays lit as a stretched mesh rather
|
|
677
|
+
than as its unscaled shape, and a back face (meshes draw without culling) is lit, reflected and
|
|
678
|
+
rimmed as the face the camera sees instead of as the hidden one; a mirrored size (a negative
|
|
679
|
+
component) turns the normal with the winding, so a mirrored mesh lights the same way.
|
|
680
|
+
- **A stretched billboard seen along its velocity no longer vanishes**: the side vector was
|
|
681
|
+
`normalize(0)` (NaN) for a particle moving straight at the camera; it falls back as the beam's
|
|
682
|
+
already did.
|
|
683
|
+
- **A rim fade can discard**: `MESH_RIM_FADE` lowered the alpha after the alpha test had run, so a
|
|
684
|
+
face faded to nothing still passed the test; the test now runs after the rim.
|
|
685
|
+
- **A gradient map without a map tints every render mode the same**: sprites ignored it and meshes
|
|
686
|
+
(which sample a white placeholder) took the ramp's end colour; both now look the ramp up at white.
|
|
687
|
+
- **Linear fog with `fogEnd == fogStart`** divided by zero; it is a cut at that distance.
|
|
688
|
+
- **Erosion and distortion write only non-default values** to JSON (`softness`, `edgeWidth`,
|
|
689
|
+
`edgeColor`, `strength`, a missing `mask`), like every other setting; a gradient map, erosion mask
|
|
690
|
+
or distortion map that is not a url-backed `Texture` warns when it is left out of a file instead
|
|
691
|
+
of vanishing silently.
|
|
692
|
+
- Documented: the fixed discard of fragments under alpha 0.01, fog on multiply and subtract blend
|
|
693
|
+
modes, `blendTiles` needing a sheet and not applying to trails, trails cutting their flipbook
|
|
694
|
+
cell on the CPU.
|
|
695
|
+
- Internal: the unused `worldView` and `worldViewProjection` uniforms and the `USE_COLOR_AS_ALPHA`
|
|
696
|
+
define, which nothing set, are gone from the sprite shaders.
|
|
697
|
+
- **A system belongs to one `BatchedRenderer` at a time.** `addSystem` on a second renderer takes
|
|
698
|
+
the system out of the first (both used to step and draw it), and adding a system again after
|
|
699
|
+
its settings changed moves it to the matching batch instead of leaving it in two. A disposed
|
|
700
|
+
renderer lets its systems go (they can join another renderer) and refuses `addSystem` with an
|
|
701
|
+
error; a setter on a system after its renderer was disposed used to rebuild a batch, mesh and
|
|
702
|
+
material under the dead node. The renderer warns once when it is itself transformed: particles
|
|
703
|
+
are placed by their emitters, so its transform would move them again.
|
|
704
|
+
- **Opaque materials no longer draw a depth pre-pass.** `needDepthPrePass` was set whenever a
|
|
705
|
+
system wrote depth, so an opaque mesh system drew twice; it is now set only for a transparent
|
|
706
|
+
material that writes depth, which is what the pre-pass is for.
|
|
707
|
+
- **Setting `renderMode` or `instancingGeometry` keeps the pause.** Both rebuild the particles
|
|
708
|
+
through `restart()`, which also unpaused a stopped system; the pause state is kept now.
|
|
709
|
+
Switching to `Mesh` at runtime gives the system normals when it has none (the mesh shader reads
|
|
710
|
+
them; a mesh without the attribute was the iOS `GL_INVALID_OPERATION`), and a geometry with a
|
|
711
|
+
different vertex count takes its own index sequence instead of keeping the old geometry's
|
|
712
|
+
indices (a cube given to a quad system drew two triangles). `play()` is documented as resuming
|
|
713
|
+
only; a finished system needs `restart()`.
|
|
714
|
+
- **Bursts are normalised, in code and from a file.** A burst given to the constructor and one
|
|
715
|
+
read from JSON take the same defaults: `interval` 0.1 s when left out (the file default; the
|
|
716
|
+
constructor used to fire the waves all at once), `probability` kept to 0..1, `cycle` at least 1,
|
|
717
|
+
a non-finite `time` 0, a numeric `count` a constant; and the bursts are kept in time order, which
|
|
718
|
+
emission relies on: `restart()` puts bursts edited in place back in order, so an editor that
|
|
719
|
+
appends a burst at 0 after a later one no longer fires it late. The constructor used to keep the
|
|
720
|
+
caller's array; it now holds its own copy, so edit `emissionBursts` rather than the array that
|
|
721
|
+
was passed in. `BurstParameters.count` is documented as rolled per play and read at the wave's
|
|
722
|
+
time as a fraction of the duration, which is what the code has done since 0.27's burst fix.
|
|
723
|
+
- **Setting `ParticleSystem.time` resynchronises the bursts and is normalised.** The bursts
|
|
724
|
+
before the new time count as fired and the ones after are due again (a burst the clock was set
|
|
725
|
+
past used to fire on the next step anyway); NaN or a negative value is 0 (NaN used to hang the
|
|
726
|
+
system, never finishing), a value past the duration wraps on a looping system. The waves of a
|
|
727
|
+
burst in progress are counted with the emission's own arithmetic, so a wave on a rounding edge is
|
|
728
|
+
no longer skipped (this also affected prewarm).
|
|
729
|
+
- **`emissionMultiplier` is kept finite and at or above 0**; a negative, NaN or infinite value
|
|
730
|
+
counts as 0 (a negative value used to be ignored, an infinite one hung the spawner). An infinite
|
|
731
|
+
burst `cycle`, which hung the burst loop, is 1.
|
|
732
|
+
- **`ApplyForce` is a world force (breaking for turned or scaled emitters in local-space systems,
|
|
733
|
+
and for `motionScale`).** It used to push along its direction in whatever space the system
|
|
734
|
+
simulated in, so a local-space system with a turned emitter had its "gravity" turned with it,
|
|
735
|
+
and a world-space system in `'hierarchy'` scaling mode did not scale it with the emitter, unlike
|
|
736
|
+
`ForceOverLife`. Both forces now follow the same rule: a `'world'` force is converted into the
|
|
737
|
+
emitter's axes for a local-space system and multiplied by `motionScale` in a world-space one. A
|
|
738
|
+
file that relied on the old reading sets `space: 'local'`. Converting a world vector into a
|
|
739
|
+
non-uniformly scaled emitter's axes now undoes the rotation before the scale, as the matrix's
|
|
740
|
+
inverse does; `ForceOverLife` used to do it the other way round. `GravityForce` is documented as
|
|
741
|
+
pulling toward a point in the simulation space.
|
|
742
|
+
- **`OrbitOverLife` orbits the emitter, about the emitter's axis (breaking for a non-unit axis,
|
|
743
|
+
and for world-space systems).** The axis is normalised: an axis of length 2 used to turn
|
|
744
|
+
particles twice as far and push them off their circle. In a world-space system the pivot is now
|
|
745
|
+
the emitter's world position and the axis turns with the emitter, where it used to orbit the
|
|
746
|
+
world origin about the world axis; an effect that wants the old orbit sets `space: 'world'`.
|
|
747
|
+
A zero axis orbits nothing. The compiled behavior pass does the same.
|
|
748
|
+
- **`Noise` turns mesh particles by `rotationAmount` (breaking for mesh particles with a
|
|
749
|
+
`rotationAmount`).** A quaternion particle used to be turned by
|
|
750
|
+
a normalised four-noise quaternion, which ignored the amount and the power altogether: a
|
|
751
|
+
`rotationAmount` of 0.001 wobbled as hard as 1. It now turns about an axis the X, Y and Z noise
|
|
752
|
+
choose by W noise × π × amount, the angle a billboard would turn by.
|
|
753
|
+
- **`TurbulenceField` is deterministic and restarts cleanly (breaking: two fields with the same
|
|
754
|
+
settings and no `seed` now swirl identically).** Its noise permutation and time offsets were
|
|
755
|
+
rolled from the global random source when the behavior was constructed, so a clone, a reloaded
|
|
756
|
+
file and a seeded system all swirled differently, and `reset()` did nothing;
|
|
757
|
+
a zero `scale` component divided by zero and filled the velocities with NaN. The field now comes
|
|
758
|
+
from `seed` (see Added), `reset()` rewinds the drift, and a zero scale samples the field at 0
|
|
759
|
+
along that axis (no variation there) instead.
|
|
760
|
+
- **`ForceOverLife` before its first particle.** `update`, `updateAll` and the compiled pass
|
|
761
|
+
disagreed on a behavior that had not seen a particle yet (one applied the force, one skipped
|
|
762
|
+
it); all three now apply it as a world force.
|
|
763
|
+
- **The `Behavior` interface documents its contract**: the order the host calls `initialize`,
|
|
764
|
+
`frameUpdate`, `update`/`updateAll` and `reset` in, what the particle looks like at each, that
|
|
765
|
+
randomness goes through `quarks.core`'s `random()` so a host that wraps its steps in
|
|
766
|
+
`setRandomSource` replays an effect, and that everything else a behavior keeps derives from its
|
|
767
|
+
settings. `BehaviorTypes[...].params` lists every field a behavior writes (`Noise`'s
|
|
768
|
+
`scrollSpeed` and `remap`, `VelocityOverLife`'s `radial` and `orbitalOffset`, `FrameOverLife`'s
|
|
769
|
+
options, `WidthOverLength.absolute`, the new `space` and `seed`), and the serialisation contract
|
|
770
|
+
test fails when a field is written that the table does not list, so an editor cannot miss one.
|
|
771
|
+
- **A moving emitter's motion is spread over the steps of a frame.** The simulation runs in fixed
|
|
772
|
+
1/60 s steps, several per frame on a slow display, and every step of a frame used the emitter
|
|
773
|
+
where the frame ended: the frame's spawns clumped there, and a trail recorded in world space
|
|
774
|
+
stair-stepped behind a moving emitter. Each step now gets the emitter interpolated between
|
|
775
|
+
where the last simulated step had it and where it is now, position, rotation and scale apart,
|
|
776
|
+
the way Niagara's interpolated spawning does; emission over distance already spawned along the
|
|
777
|
+
path, and the rate's spawns and world-space trail samples now follow it too; a burst due on a
|
|
778
|
+
mid-frame step fires where the emitter was then. A single-step frame, the first frame after
|
|
779
|
+
`play()`, `restart()`, a catch-up or a spell out of view, and a still emitter are unchanged, so
|
|
780
|
+
a copy placed and restarted (as `EffectPool.acquire` does) starts where it was put. A large move
|
|
781
|
+
of a playing system is motion like any other: a frame of two or more steps lays that frame's
|
|
782
|
+
spawns and trail samples along the jump, so teleport with `pause()` or `stop()` first.
|
|
783
|
+
`emitterVelocity` is still the frame's displacement over its time, which is the per-step
|
|
784
|
+
velocity of a linear path, so Inherit Velocity is unaffected. A seeded system with a moving
|
|
785
|
+
emitter plays differently from 0.27 when a frame runs more than one step; the simulation
|
|
786
|
+
snapshot test was re-recorded.
|
|
787
|
+
- **Batches are keyed on what the render mode draws.** The renderer used to split systems on
|
|
788
|
+
every setting, including the ones their mode ignores: two billboards with different mesh rims,
|
|
789
|
+
two meshes with different billboard pivots or size limits, two opaque systems with different
|
|
790
|
+
blend modes, two systems with different soft fade distances and soft particles off, or two
|
|
791
|
+
trails with different instanced geometry each cost a draw call. They now share a batch. The
|
|
792
|
+
settings a mode ignores are at their defaults in the batch's `settings`, so an opaque batch's
|
|
793
|
+
`materialBlendMode` reads `ALPHA_DISABLE` and a billboard batch's `rim` is unset.
|
|
794
|
+
- **Live parameters change a batch's material in place.** The values that only set uniforms (the
|
|
795
|
+
soft fade distances, the alpha cutoff, the pivot, the size limits, the reflection level, the rim
|
|
796
|
+
colour, power and strength, each texture effect's numbers and each texture's tiling, offset and
|
|
797
|
+
scroll) are the batch's live parameters, as a Unity MaterialPropertyBlock's values or an
|
|
798
|
+
Unreal dynamic material instance's parameters. Changing one on a system that is alone in its
|
|
799
|
+
batch writes the material and keeps the batch; it used to make a new batch, mesh and material
|
|
800
|
+
per value, and leave the old one for 120 frames. A change that needs a different shader (a
|
|
801
|
+
cutoff or pivot switched on or off, a rim mode, a texture) still rebuilds the material, and a
|
|
802
|
+
system that shares its batch still moves to a batch with its values. `VFXBatchSettings`
|
|
803
|
+
documents the two groups.
|
|
804
|
+
- **A particle system's JSON leaves out settings at their defaults.** `toJSON`, and so
|
|
805
|
+
`serializeEffect` and the editor's saves, used to write every setting; now a setting is written
|
|
806
|
+
only when it differs from the default, and a missing field reads as the default. The shape, the
|
|
807
|
+
start and emission curves except `startDelay`, `startTileIndex`, the geometry, the material and
|
|
808
|
+
the behaviors are still always written, since loaders up to 0.27 and three.quarks need them, so
|
|
809
|
+
both still load the files. Files from earlier releases load as before. The fields of
|
|
810
|
+
`ParticleSystemJSONParameters` other than `version` are therefore optional (breaking for
|
|
811
|
+
TypeScript code that read them without a check).
|
|
812
|
+
- **Every export has TSDoc**, and `npm run docs:check`, part of `npm run check` and CI, keeps it
|
|
813
|
+
that way. Members that are public only for other files of the package are tagged `@internal`
|
|
814
|
+
and left out of the API docs: the batches' buffers and lifecycle methods, `VFXBatch.addSystem`,
|
|
815
|
+
`applyDepthTexture`, `applyMeshLighting` and the like. They still compile, but are not part of
|
|
816
|
+
the contract.
|
|
817
|
+
- **`StoredBatchSettings.texture` is typed `BaseTexture | null`** instead of `any` (breaking for
|
|
818
|
+
TypeScript code that assigned something else), and `ParticleSystemParameters.material` is
|
|
819
|
+
`unknown`: a Babylon material, or the material record a loaded system carries.
|
|
820
|
+
- **`ParticleSystemParameters.speedFactor` is deprecated** in favour of
|
|
821
|
+
`rendererEmitterSettings.speedFactor`, and the demos use the latter.
|
|
822
|
+
- **`BatchedRenderer.deleteSystem` is typed to return `boolean`**, whether the system was in a
|
|
823
|
+
batch, which it already returned.
|
|
824
|
+
- **`QuarksPrefab.toJSON` writes `timeScale`** when it is not 1, so `toJSON` and `fromJSON` round-trip;
|
|
825
|
+
only `serializeEffect` wrote it before.
|
|
826
|
+
- **The package compiles with TypeScript's `strict` mode.** The published declarations are the
|
|
827
|
+
same as before.
|
|
828
|
+
- **`loadFromJSON` and `loadFromJSONAll` no longer change the document they are given.** They
|
|
829
|
+
resolve urls and inline geometry files on a copy, so one document can be loaded many times, into
|
|
830
|
+
several scenes or with several base urls. Code that read the resolved urls back from its own
|
|
831
|
+
document should call `resolveResources` (which still works in place) or read the loaded nodes.
|
|
832
|
+
- **`EffectPool` frees a copy with `QuarksLoader.disposeEffect`**, and `QuarksUtil.instantiate`
|
|
833
|
+
gives the copy a hold on the source's textures, so disposing the source leaves its copies drawing
|
|
834
|
+
and the last one to go frees the textures.
|
|
835
|
+
- **A geometry file that fails to load is reported through `onResourceError`** (default: the
|
|
836
|
+
same console warning as before). With `waitForTextures`, `onProgress` counts an effect once its
|
|
837
|
+
textures have settled.
|
|
838
|
+
- **Unreadable parts of an effect warn and take their default** instead of throwing a `TypeError`
|
|
839
|
+
or going silent: an unknown shape gives a point emitter, an unknown behavior is skipped with a
|
|
840
|
+
warning, an unknown curve or colour type warns, and a setting the file leaves out (a system's
|
|
841
|
+
`startLife`) takes the constructor's default. A behavior's missing number, vector or speed
|
|
842
|
+
range takes a fixed default where the constructor has none (see `docs/FORMAT.md`). A mesh surface shape whose mesh is not in the effect warns, as a sub emitter does.
|
|
843
|
+
- **Systems of one document share geometry and texture entries.** `ParticleSystem.toJSON(meta)`
|
|
844
|
+
writes a geometry or texture once per `meta`, however many systems or slots use it, and numbers
|
|
845
|
+
its ids per document, so the same effect serializes to the same file. Ids are no longer unique across
|
|
846
|
+
calls: two separate `toJSON()` calls both write `quarks_geometry_1`, so merge their outputs
|
|
847
|
+
through one `meta`, or use `serializeEffect`.
|
|
848
|
+
- **Trails face the camera in view space (breaking for custom trail shaders)**, across their
|
|
849
|
+
tangent and the line of sight, like Unity's View alignment and Niagara's ribbons. The width is
|
|
850
|
+
the particle's size in world units, measured across the line of sight. Before, the facing was
|
|
851
|
+
worked out on screen, which spiked across the view when a trail passed the camera and put NaN
|
|
852
|
+
vertices (holes) at repeated points and hairpins. The trail material no longer has the
|
|
853
|
+
`lineWidth`, `resolution`, `sizeAttenuation`, `world` and `worldViewProjection` uniforms. Code
|
|
854
|
+
that set or read them, or a shader built on them, should drop them; to change a trail's width,
|
|
855
|
+
scale `startSize` or add `WidthOverLength`.
|
|
856
|
+
- **A trail system made without `rendererEmitterSettings`** gets `startLength: 30` and
|
|
857
|
+
`followLocalOrigin: false`, as setting `renderMode` to Trail does, instead of throwing on its
|
|
858
|
+
first particle. `startLength` and `followLocalOrigin` are documented.
|
|
859
|
+
- **Box and edge shapes now apply their shape transform.** `BoxEmitter` and `EdgeEmitter` take a
|
|
860
|
+
`transform` (offset, rotation and scale within the system) like the other shapes, and write and
|
|
861
|
+
read it in JSON. Before, a Unity export that moved, turned or scaled a Box, Rectangle or Edge
|
|
862
|
+
shape lost that transform, and its particles started from the system's origin. Effects with such
|
|
863
|
+
a shape now emit where they were authored.
|
|
864
|
+
- **`ApplyForce` no longer has the public runtime fields `magnitudeValue` and `memory`.** The
|
|
865
|
+
magnitude is rolled per particle (see Fixed); read `magnitude` for the setting.
|
|
866
|
+
- **Texture sequencer JSON names its type and writes `position` as `[x, y, z]`.** Files written
|
|
867
|
+
before this release, with no type and an `{x, y, z}` position, still load.
|
|
868
|
+
- **Color, Size and Rotation by Speed read the current speed**, like Unity's modules and
|
|
869
|
+
Niagara's Scale by Speed: the velocity's length times the speed modifier, clamped to
|
|
870
|
+
`speedRange`. Motion `VelocityOverLife` adds does not count, since it moves particles without
|
|
871
|
+
changing their velocity. Before, they read the start speed, so a particle slowed by drag, a limit or a
|
|
872
|
+
collision kept its birth colour and size. Effects that use them on particles whose speed changes
|
|
873
|
+
now look different. An empty `speedRange` is a step at its value instead of NaN. The helper is
|
|
874
|
+
exported as `speedFraction`.
|
|
875
|
+
- **`LimitSpeedOverLife` dampen is clamped to 0..1 and no longer depends on the frame rate.** At
|
|
876
|
+
60 steps a second a step still removes `dampen / 3` of the excess speed, so babylon.quarks
|
|
877
|
+
effects, which step at 60 Hz, look the same. Other step lengths now remove the share that
|
|
878
|
+
compounds to the same loss per second; before, a long step could reverse the velocity.
|
|
879
|
+
- **A disposed system stays disposed.** `restart()` no longer brings it back, `update()` and
|
|
880
|
+
`emit()` do nothing, and `dispose()` drops its particles, also when called from a
|
|
881
|
+
`particleDied` listener. `isDisposed` (also on `IParticleSystem`) says so.
|
|
882
|
+
- **Node ids in saved effects.** `ParticleEmitter.uuid`, and every link a file writes to a node
|
|
883
|
+
(sub emitter targets, mesh surface meshes, prefab animation targets), is now
|
|
884
|
+
`quarksNodeId(node)`: the uuid the node was loaded under, or a GUID given once to a node made in
|
|
885
|
+
code. Before, nodes made in code were written under their Babylon `uniqueId`, which differs per
|
|
886
|
+
session and can equal a loaded node's id. The editor writes every node this way too. Files
|
|
887
|
+
saved earlier with numeric ids still load; a prefab resolves a numeric id only to a node with no
|
|
888
|
+
file uuid.
|
|
889
|
+
- **A Death sub emitter ignores `inherit: ['lifetime']`**, since its parent has no life left;
|
|
890
|
+
its particles live their own life.
|
|
891
|
+
- **Soft particles: the depth texture now says what it stores (breaking for hand-built depth).**
|
|
892
|
+
`setDepthTexture(depth, {encoding})` takes `'linear'` (Babylon's default depth renderer, the
|
|
893
|
+
default), `'nonLinear'` (the depth buffer value, Unity's `_CameraDepthTexture`) or `'viewZ'`
|
|
894
|
+
(view-space Z, Unreal's SceneDepth). Pass the `DepthRenderer` itself and its encoding and
|
|
895
|
+
packing are read off it: `setDepthTexture(scene.enableDepthRenderer(camera))`. Perspective and
|
|
896
|
+
orthographic cameras, WebGL and WebGPU depth ranges, a reverse depth buffer and an infinite far
|
|
897
|
+
plane (`'nonLinear'` or `'viewZ'`) are handled. Before, the shaders always decoded a depth buffer
|
|
898
|
+
value, which no Babylon depth renderer writes by default. If you fed a non-linear depth by hand,
|
|
899
|
+
pass `{encoding: 'nonLinear'}`. The `projParams` uniform now holds the decode coefficients from
|
|
900
|
+
`softDepthParams`; the `soft_pars_fragment` chunk uses them too.
|
|
901
|
+
- **Soft particles are compiled in only while a depth texture is set**, as Unity turns them off for
|
|
902
|
+
a camera with no depth texture. Without one they draw hard instead of sampling an unbound
|
|
903
|
+
texture; setting or clearing it rebuilds the soft batches.
|
|
904
|
+
- **Batch meshes are not pickable, take no collisions and are not serialized** by Babylon's scene
|
|
905
|
+
serializer. Their geometry is one quad or mesh at the origin, not the particles.
|
|
906
|
+
- **Env atlases are cached per scene**, so a second scene or engine (an editor preview) gets its
|
|
907
|
+
own instead of a texture of the first scene. `getCachedEnvAtlas(cube, scene?)` takes the scene.
|
|
908
|
+
An atlas quarks built is disposed with its cube; `disposeEnvAtlas(cube, scene?)` drops one
|
|
909
|
+
sooner. A system whose atlas is disposed while its cube is kept builds a new one.
|
|
910
|
+
- **`BatchedRenderer.depthTexture` is an accessor**, and setting it calls `setDepthTexture`.
|
|
911
|
+
`VFXBatch.applyDepthTexture` takes a depth binding as well as a texture.
|
|
912
|
+
- **Seeded systems roll differently from 0.26.** A seeded system now rolls its emission rates,
|
|
913
|
+
burst counts and start delay on its own generator when it is created and on every restart, so
|
|
914
|
+
a restart replays them too. A seed saved with 0.26 gives a different, still repeatable, effect,
|
|
915
|
+
and lock-step sessions must not mix 0.26 and 0.27 clients.
|
|
916
|
+
- **`serializeEffect` writes each image's url as its source file had it** (breaking for a save
|
|
917
|
+
that relied on the joined url): a texture loaded from `fx/hit.json` as `tex/a.png` holds the url
|
|
918
|
+
`fx/tex/a.png`, and the saved file used to point at that, so a save next to the source loaded
|
|
919
|
+
`fx/fx/tex/a.png` the next time. The file now gets `tex/a.png` (see `textureSourceUrl`), as
|
|
920
|
+
Unity and Unreal keep asset links relative to the project; `imageUrl` still overrides it for a
|
|
921
|
+
save elsewhere, and a texture made in code still writes its `url`.
|
|
922
|
+
|
|
923
|
+
### Fixed
|
|
924
|
+
|
|
925
|
+
- **A trail re-emitted with a random `startLength` reallocated its history every time.** The ring
|
|
926
|
+
buffers were replaced whenever the capacity changed; they now only grow
|
|
927
|
+
(`TrailParticle.ensureHistoryCapacity`), and `TrailParticle.length` defaults to 30, the
|
|
928
|
+
documented value, instead of 100.
|
|
929
|
+
- **Trail batches allocated twice the vertices they could draw.** The buffers were sized in
|
|
930
|
+
vertex pairs but compared against vertex counts, so every trail batch held two unused copies
|
|
931
|
+
of itself on the CPU and the GPU; `maxParticles` now counts vertices.
|
|
932
|
+
- **A tessellated trail bunched its points at the samples at `curveTension: 1`** and on the live
|
|
933
|
+
tip's segment: the Hermite basis eased a segment with no tangents while its widths, colours and
|
|
934
|
+
U ran linearly. A straight segment is now walked evenly.
|
|
935
|
+
- **Local-space sub emitter particles jumped to the next parent.** A sub emitter target in local
|
|
936
|
+
space (`worldSpace: false`) draws each particle through the matrix of the emission that spawned
|
|
937
|
+
it; that matrix object was reused for the next emission once the first ended, so particles that
|
|
938
|
+
outlived their emission (a child whose particles live longer than its `duration`) moved to where
|
|
939
|
+
the next parent died. The matrix now stays with the particles born through it, so they keep
|
|
940
|
+
following their parent while it lives (Frame and Birth modes), and the next emission gets its own.
|
|
941
|
+
- **`ChangeEmitDirection` with a random angle (`IntervalValue`) threw on every spawn**
|
|
942
|
+
(`memory.push is not a function`): its scratch memory was in a format the generators no longer
|
|
943
|
+
use. It is a generator memory now, cleared before each spawn, so one sample is rolled per
|
|
944
|
+
particle and the memory does not grow. The behavior and `restart()` reset it.
|
|
945
|
+
- **Arc modes keep their position within the arc.** `Loop` and `PingPong` at a negative speed, and
|
|
946
|
+
`Burst` for particles emitted by rate (not in a burst), gave positions outside 0…1, so the particle landed outside the arc or on the wrong side
|
|
947
|
+
of it; `PingPong` also started at the arc's far end. `Loop` wraps, `PingPong` starts at the arc's
|
|
948
|
+
start and sweeps back and forth, `Burst` places rate-emitted particles at random, and `spread`
|
|
949
|
+
snaps the result afterwards, as Unity's Arc Mode and Spread do.
|
|
950
|
+
- **`restart()` starts a sweeping shape over.** A `Loop`/`PingPong` shape resumed its arc from where
|
|
951
|
+
the system stopped, so a seeded replay or a pooled copy did not spawn where the original did.
|
|
952
|
+
`EmitterShape` gained an optional `reset()`, which the built-in sweeping shapes implement and
|
|
953
|
+
`restart()` calls; a custom shape with sweep state can implement it too.
|
|
954
|
+
- **`PointEmitter` emits at the start speed.** It scaled the speed by a random cube root, so with
|
|
955
|
+
`startSpeed` 5 the particles left at anything from 0.4 to 5, which no other shape and no Unity
|
|
956
|
+
shape does. Directions are uniform over the sphere as before. An effect tuned to the old spread of
|
|
957
|
+
speeds should set `startSpeed` to a random range (`IntervalValue`) to keep it.
|
|
958
|
+
- **Cone `thickness` maps to the radius as Unity's Radius Thickness does.** The ring started at
|
|
959
|
+
`sqrt(1 - thickness)` (0.71 of the radius for a thickness of 0.5) instead of `1 - thickness`, and
|
|
960
|
+
the Unity exporter already wrote the value 1:1. Emission is uniform over the ring's area, and
|
|
961
|
+
the thickness is kept to 0…1 (a value above 1 used to give NaN positions).
|
|
962
|
+
- **Curves and gradients hold their last key past 1.** `ContinuousLinearFunction` (vector and colour
|
|
963
|
+
curves, gradient keys) returned the _first_ key for a time past 1, so a particle that outlived its
|
|
964
|
+
curve snapped back to its start colour on the exact path while the sampled table held the end.
|
|
965
|
+
Both paths agree now.
|
|
966
|
+
- **An empty or unknown sequencer no longer breaks the system.** `TextureSequencer` with no
|
|
967
|
+
locations (before `fromImage`, or `locations: []` in a file) threw on the first particle it moved;
|
|
968
|
+
it leaves the particle where it is. A sequencer of an unknown type in a file used to become a
|
|
969
|
+
silent empty one and throw the same way; it warns and is skipped, like an unknown behavior.
|
|
970
|
+
- **The start colour and a burst's count curve run over the cycle like every other start curve.**
|
|
971
|
+
Both were sampled in seconds, so with a 4 s cycle a start-colour gradient was read past its end
|
|
972
|
+
from 1 s on, and a burst count curve was read at the burst's time in seconds. They now get the
|
|
973
|
+
normalised time of the cycle (0 to 1), as `startLife`, `startSpeed` and `startSize` do and as
|
|
974
|
+
Unity evaluates them. A sub emission reads the time since it began (its parent particle's birth
|
|
975
|
+
or death) over the target's duration, as its other start curves already did; before, a sub
|
|
976
|
+
emitter target's burst count curve was always read at 0, since the target's own clock never
|
|
977
|
+
runs. An effect authored to the old sampling changes: a start-colour gradient or burst count
|
|
978
|
+
curve meant to span the cycle now does.
|
|
979
|
+
- **Distance covered while emission over distance is 0 is not owed later.** With a rate curve
|
|
980
|
+
that rises, or a rate switched on in code or in the editor, the emitter used to spawn a line of
|
|
981
|
+
particles for every metre it had moved while the rate was 0.
|
|
982
|
+
- **A system stopped, paused or disposed from a listener stays stopped for the rest of the
|
|
983
|
+
frame.** `stop()` from a `finished` listener (which `EffectPool` does on release), `dispose()`
|
|
984
|
+
from `particleDied` and `autoDestroy` used to be followed by the frame's remaining steps, which
|
|
985
|
+
emitted into the stopped system and could fire `finished` again.
|
|
986
|
+
- **A fast playback speed runs at full speed.** The 0.1 s frame clamp now applies to the host's
|
|
987
|
+
frame before it is scaled by `playbackSpeed`, and the per-frame step cap grows with the speed, so
|
|
988
|
+
speed 10 advances 10 seconds per second; before, it was held to about 6. A host frame longer than
|
|
989
|
+
0.1 s is still simulated as 0.1 s at any speed.
|
|
990
|
+
- **Prewarm runs on the fixed step directly**, not through `update()`, so it always simulates one
|
|
991
|
+
whole cycle: before, `playbackSpeed` scaled it too, so at speed 0.5 only half the cycle was
|
|
992
|
+
warmed up and at speed 0 none of it. It costs one step per 1/60 s of the cycle and no more. A
|
|
993
|
+
cycle longer than `ParticleSystem.maxPrewarmSeconds` (10 by default, a performance guard for the
|
|
994
|
+
whole app) is warmed up over its last part only, with the burst waves due before that counted as
|
|
995
|
+
fired, so a long looping effect does not stall its first frame.
|
|
996
|
+
- **Invalid clock settings are normalised.** A negative, NaN or infinite `playbackSpeed` reads as
|
|
997
|
+
0 (the clock never runs backwards) and a `duration` shorter than one simulation step (1/60 s),
|
|
998
|
+
0 or NaN reads as one step, so `timeRatio` stays finite; both are accessors now and normalise
|
|
999
|
+
what is set after construction too. The constructor also reads an invalid `cullingMode` as
|
|
1000
|
+
`'always'` and a negative or NaN `maxParticles` as no limit, as `fromJSON` already did. A
|
|
1001
|
+
negative or NaN frame given to `update()` is no time at all.
|
|
1002
|
+
- **A culled `'pauseAndCatchUp'` system owes at most 0.1 s per frame it missed**, the same clamp a
|
|
1003
|
+
visible system's frame gets; before, one stalled frame off screen could owe up to the 2 s cap.
|
|
1004
|
+
- **`QuarksPrefab.play()` after `pause()` resumes the timeline's systems and clips**; before, a
|
|
1005
|
+
system paused in the middle of its track stayed paused until the track ended.
|
|
1006
|
+
- **Setting `MeshSurfaceEmitter.mesh` to nothing, or to a mesh without positions or indices,
|
|
1007
|
+
emits from the origin** as documented, instead of from the previous mesh.
|
|
1008
|
+
- **A stretched billboard system no longer writes its defaults into the
|
|
1009
|
+
`rendererEmitterSettings` object it was given**, as trails already did not.
|
|
1010
|
+
- **A texture whose image failed to load is no longer shared.** The next load of the same image
|
|
1011
|
+
tries it again instead of reusing the broken texture. A texture the engine cannot make at all is
|
|
1012
|
+
reported and left out instead of failing the load.
|
|
1013
|
+
- **A loader read the images of the document it parsed before.** A document with textures but no
|
|
1014
|
+
`images` loaded the previous document's pictures. Each parse now has its own.
|
|
1015
|
+
- **`loadPlugin` did not record the plugin**, so loading it again ran `initialize` again and
|
|
1016
|
+
`unloadPlugin` removed nothing. `unloadPlugin` now removes only the types that plugin registered.
|
|
1017
|
+
- **`QuarksPrefab.timeScale` was not read from files.**
|
|
1018
|
+
- **A trail's live tip folded back over its head** on frames where the last step ran ahead of the
|
|
1019
|
+
display, about every other frame at 120 Hz. The head is now drawn back along its last segment
|
|
1020
|
+
instead. Float noise in the step accumulator no longer adds a tip of no length.
|
|
1021
|
+
- **A dead trail particle's tip kept moving**, drawing a spike that grew while the trail shrank.
|
|
1022
|
+
The trail now ends at its last sample.
|
|
1023
|
+
- **The live tip ignored `WidthOverLength`**, so a trail narrowed by it flared at the tip on
|
|
1024
|
+
frames between steps.
|
|
1025
|
+
- **`WidthOverLength` read its curve past the end** for a fractional or zero `startLength`.
|
|
1026
|
+
- **Camera-dependent uniforms reached the GPU one draw late.** The eye position (rim, env
|
|
1027
|
+
reflection), the soft-particle depth decode and the `'scene'` light were
|
|
1028
|
+
written after the material had uploaded its uniforms, so each draw used the previous bind's
|
|
1029
|
+
values: a frame late with one camera, and the other camera's with `activeCameras`. They now reach
|
|
1030
|
+
the effect of the draw they are for.
|
|
1031
|
+
- **Soft particles faded against the wrong depth**, both from the decode (see Changed) and because
|
|
1032
|
+
the particle's own depth had the wrong sign for a left-handed camera, Babylon's default.
|
|
1033
|
+
- **GLSL and WGSL applied fog and the soft fade in different orders**, so WebGPU kept a fog-coloured
|
|
1034
|
+
fringe where a fogged soft particle met geometry. Both now fade last, after fog.
|
|
1035
|
+
- **A mesh batch rebuilt for its env atlas lost the soft-particle depth texture.**
|
|
1036
|
+
- **A non-looping system whose rate curve starts at 0 never emitted.** It reported nothing
|
|
1037
|
+
pending on its first step and finished at once. A rate that rises later in the cycle now counts
|
|
1038
|
+
as pending.
|
|
1039
|
+
- **A looping system reported pending emission after `endEmit()`.**
|
|
1040
|
+
- **A disposed sub emitter target kept taking particles without limit**, since nothing updated it
|
|
1041
|
+
any more. Sub emitters now skip disposed targets.
|
|
1042
|
+
- **Death sub emissions followed a newborn.** The dying particle's object is reused for the next
|
|
1043
|
+
particle, and the emission followed that one back to the emitter, inheriting its colour and size.
|
|
1044
|
+
A Death emission now stays where its parent died and inherits what the parent died with. Birth
|
|
1045
|
+
and Frame emissions stop following when their parent dies (`Particle.serial`). A restart or
|
|
1046
|
+
`stop()` drops the emissions still running from the last play.
|
|
1047
|
+
- **`useVelocityAsBasis` stretched the sub emitter by the parent's speed**, and gave a NaN matrix
|
|
1048
|
+
for a parent moving along -Z or faster than 1 along +Z. The basis is now orthonormal.
|
|
1049
|
+
- **The fused `VelocityOverLife` ignored `radial` and `orbitalOffset`.** The fused pass, which runs
|
|
1050
|
+
by default, now matches the per-particle path.
|
|
1051
|
+
- **Sub emitter and mesh surface links broke when a loaded effect was saved again.** The links were
|
|
1052
|
+
written with Babylon's `uniqueId` and the nodes with the uuid they were loaded under.
|
|
1053
|
+
- **`clone()` shared state with the original**, so editing one changed the other:
|
|
1054
|
+
- `ChangeEmitDirection`, the `Euler` rotation generator and `Vector3Function` (and with it
|
|
1055
|
+
`SizeOverLife` with separate axes) reused their child generators.
|
|
1056
|
+
- A system's emission bursts shared their `count` generator, and `startTileIndex` was shared.
|
|
1057
|
+
- `emissionMultiplier` and a fixed `beamEnd` point were not copied. A `beamEnd` given as a
|
|
1058
|
+
Babylon `Vector3` is now copied by value too. A `beamEnd` node is still shared, since the
|
|
1059
|
+
clone follows the same node.
|
|
1060
|
+
- **`ApplyForce` with a random magnitude threw** when it was created or loaded, since its
|
|
1061
|
+
generator memory was the wrong shape for `IntervalValue`. The magnitude is now rolled per
|
|
1062
|
+
particle at emission, as Unity's Force over Lifetime does with Random Between Two Constants, on
|
|
1063
|
+
the system's seeded random source.
|
|
1064
|
+
- **`ApplySequences` lost its sequences in JSON.** A texture sequencer wrote no type, so loading
|
|
1065
|
+
the file could not tell which sequencer to build.
|
|
1066
|
+
- **Node ESM, webpack 5 and SSR could not import the package.** The ESM and CJS builds imported
|
|
1067
|
+
`@babylonjs/core` deep paths without `.js`, which only lenient bundlers such as Vite resolve.
|
|
1068
|
+
Every deep import now ends in `.js`. The `exports` map now gives `require` its own declaration
|
|
1069
|
+
file (`dist/types/index.d.cts`) and lists `./package.json`, so TypeScript with
|
|
1070
|
+
`node16`/`nodenext` resolution types both module kinds. `npm run check:pack` now installs the
|
|
1071
|
+
packed tarballs into a scratch project and imports them with Node ESM, Node CJS and TypeScript
|
|
1072
|
+
`nodenext`.
|
|
1073
|
+
- **`restart()` broke random emission.** It emptied the system's generator memory without
|
|
1074
|
+
rolling `emissionOverTime` again, so an `IntervalValue` rate became NaN and the system emitted
|
|
1075
|
+
nothing. Every pooled effect with a random emission rate was empty from its second play.
|
|
1076
|
+
- **`restart()` kept the emission-over-distance history.** A pooled effect placed somewhere new
|
|
1077
|
+
and restarted spawned a line of particles along the jump from where it last played.
|
|
1078
|
+
- **Disposing an emitter node left its system running.** `root.dispose()` on an effect tree now
|
|
1079
|
+
disposes every `ParticleSystem` in it and removes it from its renderer. A system that disposes
|
|
1080
|
+
itself (`dispose()`, or `autoDestroy` when it finishes) still leaves the systems of its child
|
|
1081
|
+
emitters playing until they finish.
|
|
1082
|
+
- **`depthTest: false` had no effect.** It was stored, batched and saved, but never reached the
|
|
1083
|
+
material. It now draws particles over all geometry (Unity's `ZTest Always`) in every render mode.
|
|
1084
|
+
- **A mesh batch leaked a material when its env atlas finished loading.** The rebuild now disposes
|
|
1085
|
+
the material it replaces, and does nothing when the batch was disposed before the atlas loaded.
|
|
1086
|
+
- **`QuarksLoader` joined a relative base url twice.** `load('fx/hit.json')` and
|
|
1087
|
+
`loadFromJSON(json, 'fx/')` loaded textures from `fx/fx/...`, and from `fx/fx/fx/...` when the
|
|
1088
|
+
same document was loaded again.
|
|
1089
|
+
- **Trails picked the wrong sprite-sheet cell.** The cell column was counted with `vTileCount`
|
|
1090
|
+
and fractional frames were not rounded down, so a sheet with more columns than rows, or a
|
|
1091
|
+
`FrameOverLife` curve, showed the wrong cell or a slice across two. Trails now pick the same cell
|
|
1092
|
+
as billboards.
|
|
1093
|
+
- **`OrbitOverLife.clone()` dropped the axis**, so a cloned system orbited about Y.
|
|
1094
|
+
- **`loadFromJSON` after `resolveResources()` joined the base url twice.** The copy
|
|
1095
|
+
`loadFromJSON` takes of the document lost the mark that its image urls were already resolved,
|
|
1096
|
+
so a document prepared with `resolveResources(json, 'fx/')` and then loaded with the same base
|
|
1097
|
+
got `fx/fx/tex/a.png`. The copy keeps the mark.
|
|
1098
|
+
|
|
1099
|
+
### Tests
|
|
1100
|
+
|
|
1101
|
+
- Trails: the texture modes and scale, the live tip's distance, minimum vertex distance, lifetime
|
|
1102
|
+
(alive and dead), the end-of-life boundary, `clearTrails()` and the teleport, the history
|
|
1103
|
+
buffers' reuse, the batch's vertex sizing, tessellation at tension 1, the trail `tint`, the
|
|
1104
|
+
trail settings through `serializeEffect` and `QuarksLoader`, the `sideAxis` JSON validation,
|
|
1105
|
+
and `WidthOverLength` on fractional and zero lengths.
|
|
1106
|
+
- **Coverage is enforced.** The package thresholds were never checked, because `npm test` ran
|
|
1107
|
+
without coverage, and the root run ignored each package's file list. `npm run test:coverage` (in
|
|
1108
|
+
CI and `npm run check`) now counts every source file and fails under a floor per package, set in
|
|
1109
|
+
the root `jest.config.json` one point under today's coverage and raised as tests are added. The
|
|
1110
|
+
floors show where tests are thinnest: the editor's core sits at 14%, quarks.core at 55%.
|
|
1111
|
+
- **Serialisation contract.** Every behavior, emitter shape, value and colour generator, and every
|
|
1112
|
+
`ParticleSystem` setting in each render mode, must survive `toJSON` → `fromJSON`, `clone()` and
|
|
1113
|
+
`QuarksLoader` unchanged, and a clone must share no mutable state with its original. A new
|
|
1114
|
+
behavior or shape type fails the test until it is added to it.
|
|
1115
|
+
- **Frame-rate independence.** Every quarks.core behavior that changes a particle over time reaches
|
|
1116
|
+
the same state after a second at 30, 60, 120 and 144 Hz, through both `update` and `updateAll`.
|
|
1117
|
+
A seeded `ParticleSystem` gives the same particles, finishes within a frame of the same time and
|
|
1118
|
+
pauses the same way at 20 to 240 Hz.
|
|
1119
|
+
- **Seeded simulation snapshot.** A seeded system in each render mode (billboard in world and
|
|
1120
|
+
local space, stretched, mesh, trail, beam), with a moving emitter, bursts and behaviors, is
|
|
1121
|
+
stepped on uneven frames and its particles (trail history included), bounds and JSON are compared
|
|
1122
|
+
with a stored snapshot; loaded back with `fromJSON`, it must write the same JSON and play the same.
|
|
1123
|
+
It was recorded before `ParticleSystem.ts` was split and pins the simulation while the rest of the
|
|
1124
|
+
split follows.
|
|
1125
|
+
- Loader: a document `resolveResources()` prepared loads with the same base once; a save next to
|
|
1126
|
+
the source writes the source url, through a custom resolver too.
|
|
1127
|
+
|
|
1128
|
+
### Docs
|
|
1129
|
+
|
|
1130
|
+
- **README** describes the WebGPU path as it is: native WGSL shaders, nothing transpiled or
|
|
1131
|
+
fetched at run time. The feature list names beams, the texture effects and the scene-integration
|
|
1132
|
+
options; the Unity-style snippet imports from `babylon.quarks` (the npm package `quarks.core` is
|
|
1133
|
+
the upstream three.quarks core, not this monorepo's fork of it); the demo count is current. `ROADMAP.md` and
|
|
1134
|
+
`docs/SHURIKEN_PARITY.md` match what shipped in 0.24 to 0.27.
|
|
1135
|
+
- **Three new demos** in the examples app: pooled one-shot hits (`EffectPool`,
|
|
1136
|
+
`QuarksUtil.getDuration`, `emissionMultiplier`), renderer options side by side (`sortMode`,
|
|
1137
|
+
`alignToDirection`, `minParticleSize` / `maxParticleSize`, `scalingMode`), and surface trails with
|
|
1138
|
+
a mesh second UV set (`MeshSurfaceEmitter`, the trail flags, `tessellation`, `curveTension`,
|
|
1139
|
+
`instancingUVs2` with a map on UV channel 1 through a gradient map).
|
|
1140
|
+
|
|
1141
|
+
### Internal
|
|
1142
|
+
|
|
1143
|
+
- **`npm run validate:wgsl` runs in CI** (job `wgsl`), on Chromium's software WebGPU adapter, so a
|
|
1144
|
+
WGSL variant that stops compiling fails the pull request. The script compiles the variants in
|
|
1145
|
+
batches instead of one round trip each, which brings a run from ten minutes to about fifteen
|
|
1146
|
+
seconds; it reports a missing WebGPU adapter or an empty run in plain words instead of a stack
|
|
1147
|
+
trace, fails when the device is lost mid-run (a lost device compiles nothing and reports no
|
|
1148
|
+
errors), and gives up after five minutes instead of holding the job.
|
|
1149
|
+
- **The demo smoke in CI opened the wrong pages.** The examples are built under `/<repository>/`
|
|
1150
|
+
on GitHub Actions, and the preview server answers a request outside that path with a 404 page
|
|
1151
|
+
that loads without an error, so `check:demos` had been passing on 404 pages there. Both
|
|
1152
|
+
`check:demos` and `validate:wgsl` now ask the server for its base path
|
|
1153
|
+
(`scripts/examplesBase.mjs`), and the smoke fails a page the viewer never comes up on.
|
|
1154
|
+
- **`batchKey.ts`** holds the batch key: `storedBatchSettings` reduces a system's settings to the
|
|
1155
|
+
canonical form a batch stores, `shaderKeyEqual` and `liveParamsEqual` compare the two groups
|
|
1156
|
+
and `batchSettingsEqual` both. `BatchedRenderer.updateSystem` writes live parameters through
|
|
1157
|
+
`VFXBatch.applyLiveSettings`. `VFXBatch` reads the texture layouts off its settings on every
|
|
1158
|
+
bind, and each batch writes its live uniforms from one place (`writeLiveUniforms`), on build and
|
|
1159
|
+
on a change.
|
|
1160
|
+
- **The stretched-billboard shaders no longer declare a `speedFactor` uniform.** The speed factor
|
|
1161
|
+
is baked into the velocity attribute on the CPU, so the uniform, which was set from
|
|
1162
|
+
`softNearFade`, was never read. The per-frame sync and its tests are gone with it.
|
|
1163
|
+
- **`integrate.ts`** (`src/system/`) holds the passes of a simulation step after emission: the
|
|
1164
|
+
snapshot the renderer extrapolates from, the behavior steps, the sprite path's one pass over
|
|
1165
|
+
the store (newborn collapse, motion, ageing, bounds and removal) and the trail path's passes
|
|
1166
|
+
(collapse, motion, history, removal), as plain functions over the store and the particle array.
|
|
1167
|
+
`ParticleSystem.stepOnce` calls them in order and is 60 lines; the file is now about 1700
|
|
1168
|
+
lines, from 3641 before phase 3c. The hot loops are the same code and run the same within
|
|
1169
|
+
benchmark noise (a local 88k-particle benchmark: 11.5, 11.6 and 10.4 ms before against 11.6,
|
|
1170
|
+
9.2 and 10.1 ms after, in interleaved runs).
|
|
1171
|
+
- **`EmissionController` and `ParticleSpawner`** (`src/system/`) take emission (rates, bursts and
|
|
1172
|
+
their waves, emission along the emitter's path, the system-level generator memory) and the
|
|
1173
|
+
making of newborn particles (pool growth, start values, placement, behavior initialisation) out
|
|
1174
|
+
of `ParticleSystem`, which goes from about 2460 to 1950 lines. The hot path is unchanged within
|
|
1175
|
+
measurement noise (a 20-system, 88k-particle Node benchmark: 7.5 ms before, 7.9 ms after, a
|
|
1176
|
+
spawn-heavy one 10.3 and 10.3 ms, with a run-to-run spread of about 1 ms).
|
|
1177
|
+
- **`SimulationClock`** (`src/system/SimulationClock.ts`) holds the fixed-step accumulator, the
|
|
1178
|
+
rounding rule that keeps the residual near zero, the start delay and the catch-up debt of a
|
|
1179
|
+
culled system, as pure numbers with their own tests. `ParticleSystem.update` is a client of it.
|
|
1180
|
+
- **One table of `ParticleSystem` settings** (`src/system/settingsSchema.ts`) gives each plain and
|
|
1181
|
+
generator setting its default, its JSON form and how it is copied. `toJSON`, `fromJSON` and
|
|
1182
|
+
`clone` are built from it, and a test fails when a constructor parameter is in neither the table
|
|
1183
|
+
nor the code that handles the meta, the shape or the behaviors.
|
|
1184
|
+
- **`ParticleSystem.ts` is being split into modules under `src/system/`.** This first part moves
|
|
1185
|
+
out the parameter and JSON types (`types.ts`), reading and writing JSON
|
|
1186
|
+
(`serialization.ts`), culling bounds (`BoundsTracker.ts`), event listeners
|
|
1187
|
+
(`EventDispatcher.ts`), the behavior fusion plan (`BehaviorPlan.ts`), reading draw settings from a
|
|
1188
|
+
material (`materialResolve.ts`), default and fallback geometry (`geometry.ts`) and the setting
|
|
1189
|
+
normalisers (`normalize.ts`). The file goes from 3641 to about 2500 lines. The public API and the
|
|
1190
|
+
simulation are unchanged; the types are still exported from the package root. Still to come: one
|
|
1191
|
+
table of settings for the constructor, setters, JSON and `clone`, the simulation clock, emission
|
|
1192
|
+
and spawning, and the integration passes.
|
|
1193
|
+
|
|
7
1194
|
## [0.26.0] — 2026-10-21
|
|
8
1195
|
|
|
9
1196
|
### Changed (breaking)
|
|
@@ -104,13 +1291,13 @@ renames below.
|
|
|
104
1291
|
unchanged and stays in the fused pass.
|
|
105
1292
|
- Texture effects on sprites, meshes and trails, GLSL and WGSL, each saved with the material in the
|
|
106
1293
|
effect JSON only when set and batched apart when they differ:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1294
|
+
- `ParticleSystem.uvScroll` `[u, v]`: scrolls the map in UV units per second on the renderer's clock.
|
|
1295
|
+
- `ParticleSystem.gradientMap`: recolours the map through a ramp texture picked by its red
|
|
1296
|
+
channel; the particle colour still tints it.
|
|
1297
|
+
- `ParticleSystem.erosion` (`ParticleErosion`: mask, softness, edgeWidth, edgeColor, maskScroll):
|
|
1298
|
+
alpha erosion. The particle's alpha decides how much of the shape is left instead of how
|
|
1299
|
+
transparent it is, so a `ColorOverLife` alpha curve dissolves it through a noise mask or wipes
|
|
1300
|
+
it through a gradient one, with an optional glowing edge.
|
|
114
1301
|
- `RenderMode.Beam` (6) with `ParticleSystem.beamEnd` (a point or anything with a world position,
|
|
115
1302
|
such as a `TransformNode`, read every frame; runtime only): a quad laid from each particle to
|
|
116
1303
|
the end and turned about its length to face the camera, U along it, the size as its width. With
|
|
@@ -353,7 +1540,6 @@ renames below.
|
|
|
353
1540
|
optional per-axis thickness, moving particles along +Z; a box with no depth is a flat rectangle.
|
|
354
1541
|
`EdgeEmitter` (`edge`) emits from a line along X, moving particles along +Y. Both match the
|
|
355
1542
|
Unity shapes of the same name.
|
|
356
|
-
|
|
357
1543
|
- Trail settings `uvFromHead`, `colorFromParticle`, `dieWithParticle` and `recordWorldSpace`
|
|
358
1544
|
(all optional, off by default) for trails that behave like Unity's: the texture runs from the
|
|
359
1545
|
particle back to the tail, the whole trail takes the particle's current color, the trail
|