DreamFXLang
Language

Values, types and rules

Everything that can appear on the right of an =, the curve literal, the inline-expression whitelist, the type table, and L2 / L3 / L6 / L7.

Values, types and the L* rules

Everything that can appear on the right of an =, and the rules that decide what it means.


The eight rules, by name

Rule
L1A stack is an ordered statement block. Six of them: SystemSpawn, SystemUpdate, EmitterSpawn, EmitterUpdate, ParticleSpawn, ParticleUpdate. Writing order is module order.
L2Two statement forms: a module call, and an assignment. Consecutive assignments with the same enabled state fold into one Set Parameters module; a module call or a change of disabled state breaks the run. A new attribute is declared by its first write.
L3Four value modes: literal, linked, dynamic input, HLSL.
L4Module names resolve through Settings.ModulePaths; write a longer path only when a short name is ambiguous.
L5#Region is a comment. It does not reach the asset — the external edit API has no stack-note function.
L6Inline expressions lower to one HLSL expression, and only whitelisted functions are allowed.
L7Numeric conversion widens, never narrows. int -> float is implicit; float -> int needs int(...).
L8Renderer properties are schema-driven generic assignment; every renderer type is supported without per-type syntax.

L3 — the four value modes

InitializeParticle(
    Lifetime       = 0.25,                                     // literal
    Color          = User.TintA,                               // linked
    SpriteSize     = RandomRangeFloat(Minimum = 1, Maximum = 4), // dynamic input
);

Color Particles.Color = hlsl {                                 // HLSL expression
    float4(Particles.Color.rgb, saturate(1.0 - Particles.NormalizedAge))
};

Literals

450.0        24           true
(1, 0, 0)    (1.0, 0.72, 0.25, 1.0)      // 2, 3 or 4 components
"/Game/FX/M_Spark"                        // asset path
box(-200, -200, -50, 200, 200, 300)       // six numbers: min xyz, max xyz

24 and 24.0 are different: the first is an integer literal, and L7 uses that distinction.

Names that are not identifiers

Niagara names come from a UI with no restrictions. Real content in this project has a user parameter called PillarPower(0~1) and a module input called Ring/DiscDistributionMode; neither is an identifier in any language. Back-quotes hold one:

Properties = {
    float `PillarPower(0~1)`;
}

ParticleSpawn = {
    SphereLocation(
        `Ring/DiscDistributionMode` = Direct,
        Alpha                       = `User.PillarPower(0~1)`,
    );
}

A back-quoted name is one token, dots included — `User.PillarPower(0~1)`, not User.`PillarPower(0~1)` . Anywhere a name may appear, this may appear.

Only quote what needs it: `Speed` is legal but reads worse than Speed. The decompiler applies exactly that rule, so an export shows which names in a given asset need quoting. A name containing a back-quote has no written form at all — rename the parameter. An unterminated one is DFX1005.

Enum entries are written by name, with spaces and hyphens removed — Niagara's Direct Set is DirectSet. The names are read from the live asset, because a user-defined Niagara enum stores NewEnumerator0 internally and keeps the real name in display text; an unknown one reports the real list (DFX4006).

Linked

SpawnCount = User.SparkCount;
ConeAxis   = Particles.Velocity;

Any namespace-qualified parameter: User., Particles., Emitter., System., Engine..

A link binds the parameter directly — there is no conversion step anywhere in it. So the two types have to match exactly, and DFX4027 is the one L7 case an explicit cast cannot fix, because there is nowhere to put one. A float user parameter cannot drive an int32 spawn count; declare it int.

Dynamic input

UniformScaleFactor = FloatFromCurve(
    FloatCurve = curve { 0.0 -> 1.0; 1.0 -> 0.0; },
    CurveIndex = Particles.NormalizedAge
)

Nests to any depth — a chain is just a longer address, written parent-first.

HLSL

Color Particles.Color = hlsl {
    float4(Particles.Color.rgb, saturate(1.0 - Particles.NormalizedAge))
};

Raw and multi-line, no escaping. Namespaced parameters are read as written.

One expression only (DFX4030). A stack input's HLSL lowers to a node with one typed output pin and no body, so there is nowhere for a statement to go. Multi-statement logic belongs in a .dfm Module, whose body is emitted verbatim.

An hlsl block carries no type of its own, so an assignment to a new attribute needs the annotation: Color Particles.Color = hlsl { ... }; (DFX4022).


curve { }

curve {
    0.0 -> 1.0;
    0.7 -> 0.85 [ Interp=Cubic; Arrive=-0.4; Leave=-1.2 ];
    0.9 -> 0.40 [ Interp=Cubic; Tangent=Break; Arrive=0.0; Leave=-2.25 ];
    1.0 -> 0.0  [ Interp=Linear ];
}

Fills a curve data interface, so it goes where one is expected — usually a dynamic input such as FloatFromCurve (DFX4037).

Tangents are per key and are exported per key. They matter: a hand-tuned shape written back without its tangents is a different curve, and losing shape is losing data. Interp defaults to Auto.

Tangent names the tangent mode — Auto, User, Break or None — and may be left out, in which case a key with a tangent means User and a key without means Auto. Only Break and None ever have to be written, plus the Auto key that carries a stored slope, so ordinary hand-written sources never mention it. Break is the one that had to be spelled: it is how a key holds two independent tangents — a corner — and until 2026-08-12 the exporter wrote tangents only under User and dropped everything else, so every corner came back smooth and every Auto key's stored slope came back zero. A curve is evaluated from its stored tangents whatever the mode says; the engine re-derives them only when something edits the curve, so "the mode is Auto, the engine will work them out" is false for a curve nobody is editing.

A curve data interface with anything set outside its keys — an exposed curve, a LUT turned off, an external curve asset — is written as raw configuration JSON instead of a curve { } literal, and a multi-channel interface (vector, colour) takes the readable form only when its channels are the same shape. The readable form is a convenience, and a convenience does not get to lose data.


L6 — inline expressions

VelocityStrength = RandomRangeFloat(Minimum = User.SparkSpeed * 0.6, Maximum = User.SparkSpeed)
StretchDir       = normalize(Particles.Velocity)

Arithmetic (+ - * /), unary minus, parentheses, and these functions:

normalize  saturate  clamp  lerp  frac  min  max
abs  floor  ceil  pow  sqrt  dot  cross  length
int

The whole expression lowers to one HLSL expression dynamic input, with the type inferred from the operands. Only namespace-qualified parameters exist inside one — there are no locals (DFX4032). The builtins are positional, not named (DFX4033).

int(expression) explicitly converts a scalar value to a signed 32-bit integer, truncating toward zero. For example, int(User.CountScale * 3.0) remains a runtime HLSL conversion. Floating constants retain their numeric precision during expression lowering, including small coefficients such as 1e-7.

Anything outside the list is an error (DFX4031), and the list is short on purpose. Widening it is the first step toward re-implementing a general expression compiler, which is the ~13k-line component DreamShader has and DreamFX exists partly to avoid. hlsl { } is the escape hatch: inside one, any HLSL is allowed.

Expressions are not recovered by the decompiler — they come back as an equivalent hlsl { } block. The round trip is semantic, not textual.


L7 — numeric conversion

From → to
int → floatimplicit
float → interror; write int(...) (DFX4003)
mismatched vector widthserror (DFX4002)

A silently truncated spawn count is among the hardest effect bugs to find, which is the whole reason the rule is one-directional.

int(1.9) also works for ordinary parameter and module-input defaults, producing the literal 1. The default argument must be one finite numeric literal, and its truncated value must fit int32 (DFX4044). A runtime expression such as int(User.Scale) cannot be stored as a literal default; put it in a stack assignment or module input call instead. [StaticSwitch] defaults require the resulting literal directly.


Types

DSLNiagara
float int boolNiagaraFloat / NiagaraInt32 / NiagaraBool
Vector2 Vector Vector42 / 3 / 4 floats
ColorLinearColor
PositionNiagaraPosition
QuatQuat
Texture2D, DI<X>data interfaces

Data interface parameters carry their configuration as a quoted JSON object, the same verbatim form a module's data interface input uses and the same form the exporter writes. Declared bare, they stay a slot to fill at runtime. curve { } is the readable spelling for a curve interface. DFX5098 means the value is the wrong shape — it no longer means "declared only, will be ignored".

On this page