Skip to content

FX 2 - #299

Open
trethaller wants to merge 234 commits into
masterfrom
fx-reboot
Open

FX 2#299
trethaller wants to merge 234 commits into
masterfrom
fx-reboot

Conversation

@trethaller

@trethaller trethaller commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Major rework of the data structure of FX files, in particular how Emitter and Shader parameters are defined and animated.
Everything linked to "migration" is supposed to be temporary, since we don't want live games to upgrade files on the fly for performance reasons.
The current plan is to give this a try and if all goes well apply a one shot data migration on all projects and deactivate migration code.

ncannasse and others added 30 commits October 9, 2026 17:49
vLerp folds into the existing flat values when it matches one of them
(the usual cases evaluate exactly as before), and falls back to VLerp,
which getFast evaluates, instead of a non flat VAdd/VMult.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Params are stored under their plain name (scale, startSpeed...), one mode
per component: Constant, Range, Curve, RandomCurve, CurveRange or unset,
the shapes being a curve referenced by path or an easing (EmitterParam).
Particle shaders under an emitter get the same model (Shader.particleParams),
colors keep their curves.

Old files are upgraded on load into equivalent modes (EmitterUpgrade) and
saved in the new format (props.paramsVersion). Upgraded emitters keep the
random slot layout of the old code (skippedRandomSlots), so they draw the
same randoms. The animation curves of the spawn and multiplier params, and
of the emitter settings, keep multiplying the value by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nes on load

An animation curve (over the FX time) is now the value of the property: the
object transforms (position, scale, rotation in degrees) of FX and FX2D, the
local rotation offset (degrees), the shader params out of the emitters, the
emitter settings, spawn params (value, min, max) and multipliers. The life
time can be animated too.

The emitter params reference their animation curves by path (Emitter /
Emitter2D.animCurves, saved in props.animCurves), curve names carry no meaning
there. The FX objects and shaders still find theirs by name.

Curves are saved with a version; the ones without it are upgraded on load
(AnimUpgrade, old format only), on the raw data from FX.load / FX2D.load and
the emitter load: the old constants are baked into the keys (exact for the
linear cases, a Reference curve takes them in its scale / offset, a Blend
curve bakes its children, the curves referencing a baked one are
compensated), the old emitter curves are bound by path where they are (a
range gets a second curve referencing the first). The rotation / scale of an
object composed with a base rotation is folded exactly when the base
rotation allows it, re-baked from samples otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An animation curve holds the value of the property: a new track starts with a
key at the current value of the property (transform, shader param, light,
renderer FX field, emitter setting or spawn / multiplier bound). The tracks
of the emitter params are bound to them by path (animCurves), a range
component gets a min and a max track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Overrides saved from now on carry `#anim` (Diff.apply skips the `#` fields
of a diff root). Old overrides set old curve values and the properties they
were composed with: Reference.applyOverrides marks the curves they set as old
and, when the referenced .fx is already upgraded, puts the curves of the
prefabs they touch back in the old format (AnimUpgrade.legacyOverride, the
inverse of the exact bakes), so the upgrade on load bakes them with the
overridden values. A spawn range min that references its max follows the
re-baked bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he transform, tiny base angles count as none

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
trethaller and others added 26 commits November 9, 2026 11:21
Setting loop writes it to every data (a blend or a Curve Range samples each one with its own), and loading takes the one of the first data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only short comments where the code needs one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`particleParams` of a shader under an emitter is saved by param name, a mode or a vector of component modes (`{ alpha: { mode: "Curve" }, UVOffset: { y: { mode: "Curve" } } }`) instead of a list of curve names, so that other modes can be added without another migration. In memory a map of the modes by curve name (`alpha`, `UVOffset:y`), null without any. The migration writes the new format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the end of the upgrade, the numbers that were not in the data are written with 6 significant digits, 0 under 1e-6, removing the float noise of the bakes; the ones of the data are kept as they are. A RandomCurve spread negligible next to the other one (the overshoot of the curve beyond its keys) is 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
randomColor1 / randomColor2 (and the color of Emitter2D) are RGB ints, as the other colors of the prefabs: their alpha was never read. The migration converts the arrays, rounded to 8 bits. The kit edits a PColor prop as an int when its value is one, as an array otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	hrt/prefab/fx/Emitter.hx
…om rate fixed

Params are plain Values for every kind, SpawnValue is gone. The kinds only differ by their clock and their random:
Particle (over the particle life, random per particle), Spawn (read at spawn, emitter time, random per particle),
Emission (emitter time, random drawn by the emitter: a hash of the seed and the step, the same whatever the ticks).
Every kind takes Constant, Range, Curve, RandomCurve and CurveRange; the curve of a param is always named after its
component (no more `:min` / `:max` curves). Random slots keep the old order (Particle, then Spawn), Emission takes none.

- The rate is one Emission param: InfinityRandom, emitRateMin / emitRateMax are gone. A random rate ramps to a new value
  every Rate Change Time (the old ramp went away from its target and could pause the emission).
- Particles Per Burst is an Emission param: a Range draws an integer count per burst, both bounds included.
- 3D Duration stops at its duration, as 2D (it kept emitting at its last rate).
- Evaluator.getAtRandom evaluates a value with a given random; getFast shares its body (an inlined callback).
- Editor: the curves of the Spawn / Emission params are modes, edited inline over the emitter time, no animation tracks.
- Migration: spawn (k ± a)·c becomes RandomCurve (exact, parameter curves included, references copied), a spawn random
  blend a Curve Range, the rate and burst count curves a Curve mode, InfinityRandom a Range or a Curve Range.
- Spec updated (kinds, modes, param curves, upgrade rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n looping FX

- A Follow sub-emitter that stops emitting no longer crashes: it is in the list of its particle until the particle dies,
  only a sub-emitter in its parent's list is auto removed.
- Burst in a looping FX: the bursts before the tick of the loop are done, the first burst of each loop is no longer skipped.
- BurstDuration in a looping FX: the loop tests its new time against the duration (it used the previous loop's), it emits
  on every loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of scale upgraded to Point

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for both

The emitter colors are ARGB ints, alpha read (the inspector edits an int PColor with its alpha); the migration makes the old ones opaque, their alpha was never read.

- Fade Color (fadeColorMode, fadeColor1 / fadeColor2, fadeGradient): a color over the particle life, from a start color to an end color or along a gradient. Default: white to transparent white.
- The random color and the fade color multiply the pixel color, alpha included, in a single hrt.shader.ParticleShader added to the batch when one is set (lazily, by its first feature). It reads the per-particle random of BaseEmitter: no per-particle color on the CPU, ColorMult and ParticleInstance.colorMult are gone. Its modes are hxsl enum consts, its settings are read at init. Meant to take over ParticleColorRandom, ParticleColorLife...
- RandomColorMode moved into Emitter.hx (used by both settings).
- Emitter2D: ARGB colors, alpha read.
- Spec: Colors section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The switch on the rate mode in getHiddenFields had no null case: an unset
rate (default value) read _hx_index of null in JS. The check is also split
for readability: a random rate mode, and an emit type that reads the rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o the emitter fade

- The Fade In / Fade Out of ParticleShader are the linear ramps of ParticleFade (min of the two), 0 for no ramp.
- Migration: a ParticleFade on the particles of an emitter becomes its Fade In / Fade Out (its defaults 0.1 / 0.1 when unset) and is removed, when it is the only enabled one the emitter applies (outside sub-emitters and trails), static, with a power of 1, not under a disabled or editor only prefab. The others stay a shader (GPU emitters, powers, several fades...). The overrides of the references to it are made again by the reference pass.
- Spec: upgrade row.

Games (dry run): 247 upgraded in Farever, 34 in Spacecraft, 52 in Northgard 2, 227 in Wartales. fxbench FX_Emitter_ParticleFade: old hide with the shader against the upgraded emitter fade, same sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n declaration order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…des as arrays

The macro generates a single FIELDS table (properties and params, in declaration order) instead of
PROPERTIES / PARAMS / FIELDS_ORDER, and the emitters implement IEmitter. The modes of a vector param
are an array indexed by component, saved as { x, y, z } as before. The shader particle modes use the
size of the shader param for their key in both getMode and setMode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… values and their getters

The macro generates the evaluator of each emitter, with a Value per param and animated property
(the Particle ones first) and an inline getter for each, instead of the hand-written fields of the
emitter object. The prefab builds it (makeEvaluator), each emitter object gets its own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	hrt/prefab/fx/BaseFX.hx
…filter

A Constant gradient steps at its stops: read from its texture with the default linear filter, its
colors blended around each stop. The stops stay quantized to the resolution of the gradient.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kind

kit-param-emission, kit-param-spawn, kit-param-particle: the clock of the param, to style them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e of its curves, Constant Speed renamed Speed

- The tooltip of a param line has a second line from its kind: read at each emission step / once at spawn (curves over the emitter time), or over the particle life (curves over its life).
- "Constant Speed" / "Constant World Speed" are "Speed" / "World Speed": "Constant" read as the Constant mode. The start speeds no longer say "set at the spawn of the particle", the tooltip does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is rebuilt after a reset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame it

The random colors / gradient and the fade colors / gradient share a line, Alpha Fade In / Out too.
Without @:line the line takes the label of its first property and the others show none; the first widget has the same width on every such line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trethaller trethaller changed the title Fx reboot, WIP FX 2 Nov 10, 2026
@trethaller
trethaller marked this pull request as ready for review November 10, 2026 14:15
trethaller and others added 2 commits November 10, 2026 15:18
…he RandomColorMode constructor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants