DreamFXLang
Language

.dfm — modules and dynamic inputs

Writing a Niagara module or dynamic input as text — Usage, Inputs, the HLSL Body, and which engines can generate one.

A .dfm produces one UNiagaraScript. Two kinds, differing in two lines:

// A module: goes in a stack, reads and writes attributes.
Module(Name="Modules/Moon/ToonSpin", Root="Plugin.DreamFX")
{
    Settings = {
        Usage       = ParticleUpdate;
        Category    = "MoonToon|Motion";
        Description = "Spins a sprite at a constant rate, optionally reversed.";
    }

    Inputs = {
        float SpinRate   = 90.0  [ Description="Degrees per second." ];
        bool  bClockwise = true  [ StaticSwitch ];
        float RateScale  = 1.0   [ Advanced ];
    }

    Body = {
        float Dir = bClockwise ? 1.0 : -1.0;
        Particles.SpriteRotation += SpinRate * RateScale * Dir * Engine.DeltaTime;
    }
}
// A dynamic input: computes a value for an input slot.
DynamicInput(Name="Modules/Moon/ToonPulse", Root="Plugin.DreamFX")
{
    Settings = { Usage = DynamicInput; Output = float; Category = "MoonToon|Math"; }
    Inputs   = { float Frequency = 6.0; float Sharpness = 2.0; }
    Body     = {
        return pow(0.5 + 0.5 * sin(Engine.Time * Frequency * 6.2831853), Sharpness);
    }
}

Used from a .dfs like anything else:

ToonSpin(SpinRate = 220.0, bClockwise = true);
ScaleSpriteSize(ScaleSpriteSizeMode = Uniform, UniformScaleFactor = ToonPulse(Frequency = 4.0));

Settings

KeyMeaning
Usagewhich stack(s) the module may be placed in, or DynamicInput. One of the six stack names (L1), or an array of them: Usage = [ParticleSpawn, ParticleUpdate];
Outputa DynamicInput's return type. Required (DFX3031); a value type, not a data interface (DFX3039)
Categorywhere it sits in the stack's add menu
Descriptionthe module's tooltip

A DynamicInput with no explicit stack list defaults to particle spawn and update — the two that cover nearly everything. Widen it with the array form.

Inputs

Each becomes a Module.<Name> input with the declared default and description. Defaults must be literals or enum entries, because a module input's default is stored on the asset and cannot reference anything outside the module (DFX3044).

Attributes: [ Description="..." ], [ Advanced ], [ StaticSwitch ].

[StaticSwitch] is accepted, validated (bool/int/enum, constant default — DFX3034/DFX3035) and then lowered as an ordinary input, with DFX5102 saying so. Tier-one generation puts the whole body in one custom HLSL node, which has no branch for a switch to select. The body reads it identically; what is lost is the compile-time folding.

Body

The body is HLSL, with DreamFX's namespaces on top.

Its own inputs are bare names. SpinRate, not Module.SpinRate — inside a module the namespace is implied. The qualified form is accepted and normalised away, because it is not wrong about the language, but bare is the spelling that describes what is happening: the input arrives as a pin.

Engine., User., System. and Emitter. are read as written. They resolve against the parameter map directly.

Particles.* is read and written as written, and DreamFX wires the pins for it:

Body = {
    Particles.SpriteRotation += SpinRate * Engine.DeltaTime;   // read and write, both handled
}

An attribute Niagara already knows (Particles.SpriteRotation, Particles.Color, …) needs nothing. A custom one needs its type at first use, the same way a .dfs declares a new attribute — otherwise the pin would be wired at a guessed width (DFX3046):

Body = {
    float Particles.Moon.SpinPhase = 0.0;
    Particles.Moon.SpinPhase += Engine.DeltaTime;
}

Particles.Color.rgb resolves to Particles.Color with a swizzle: the longest dotted prefix that is a known or declared attribute wins.

Calling a data-interface input

An input declared DI<X> Name; is a pin on the custom node, and its functions are called the way Niagara custom HLSL calls them: Name.Function(args). Declare the out arguments as plain, uninitialised locals:

Inputs = { DI<DreamWind> Wind; }
Body = {
    float3 WindVelocity;
    float  Gust;
    Wind.SampleWind(Particles.Position, WindVelocity, Gust);
    Particles.Velocity = WindVelocity;
}

float Gust = 1.0; before the call fails the CPU VM compile with internal compiler error: out/inout parameters must be lvalues in call to 'SampleWind_Module_Wind' (DFX6006): the VM's HLSL front end, hlslcc, constant-folds the initialised local into the argument list before it checks the out parameter, and a constant is not an lvalue. GPU sims accept both spellings; write the uninitialised form so one body compiles on both targets.

Modules take statements; dynamic inputs take one expression

A module's body is emitted verbatim — locals, branches, as many statements as you like. That is the capability DFX4030 has always pointed at, and the reason .dfm generation was worth unblocking: an inline hlsl { } in a .dfs is a single rvalue and can never hold more.

A dynamic input's body is wrapped by the Niagara translator as Output = (Type)( <body> );, so it has to be one expression, with or without the return. Statements before it produce invalid HLSL rather than an error naming the real problem, so DreamFX catches it first (DFX3037) and points at the module form.

Which engines can generate one

Writing HLSL onto a Niagara custom node needs UNiagaraNodeCustomHlsl::SetCustomHlsl, and building the graph around it needs four more declarations. MoonEngine puts an export macro on all five; a stock engine exports none of them. DreamFXEditor.Build.cs probes the engine headers and defines DREAMFX_HAS_CUSTOMHLSL_WRITE accordingly.

That is not the end of the story, because exported and reachable are not the same thing. Public data members need no export macro, public virtuals dispatch through the vtable, and a private field that is a UPROPERTY can be written by name. Every behaviour behind those five declarations turned out to be reachable that way, so there are two backends and three outcomes:

WhenBehaviour
directthe engine exports the five declarationscalls them; this is MoonEngine
reflectionit does not, but the shapes it depends on all check outrebuilds each operation from the public surface
degradeda shape it depends on has movedrefuses to generate, and says which one

The reflection backend is verified against the direct one rather than assumed equivalent: the same .dfm built both ways reads back with a byte-identical schema, and that holds across engines — a module generated on stock UE 5.8 matches the same module generated on MoonEngine.

-DreamFXForceReflectionBackend (or dfx.ps1 -ForceReflectionBackend) selects the reflection backend on an engine that does not need it, which is how the two are diffed on one machine.

The product was never the limited part. A generated module is an ordinary UNiagaraScript: any engine loads it, references it from a .dfs, cooks it and runs it. Generate anywhere it works, commit the asset, everyone consumes it — and on a team with MoonEngine, generating there stays the recommended path simply because it is the one the whole corpus is verified against.

When neither backend can run:

SituationWhat happens
asset committed and matching the sourcebuild skips it, CI stays green
source edited without regeneratingDFX5107 — regenerate where a backend runs
no asset at allDFX5100 — names the check that failed

The provenance check runs either way; only the remedy differs.

What tier one costs

The whole body becomes one UNiagaraNodeCustomHlsl, so the module is a black box in the node editor. For a text-first workflow that is the intent — the text is the source. What it buys over an inline hlsl { } is everything a stack input cannot hold: statements, named inputs with defaults and tooltips, and reuse by name across systems.

Lowering a body into a real node graph is tier two, and is not planned: it is the ~13k-line problem DreamShader already has, reproduced.

On this page