DreamFXLang
语言参考

`.dfm` — 模块与动态输入

`.dfm` — 模块或动态输入

.dfm — 模块或动态输入

生成一个 UNiagaraScript。两种,差别在两行:

// 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);
    }
}

从 .dfs 里像其他任何东西一样使用:

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

哪些引擎能生成

把 HLSL 写到 Niagara 自定义节点上需要 UNiagaraNodeCustomHlsl::SetCustomHlsl,而围绕它构建图表还需要另外四个声明。MoonEngine 给这五个都加了导出宏;原版引擎一个都不导出。DreamFXEditor.Build.cs 探测引擎头文件并据此定义 DREAMFX_HAS_CUSTOMHLSL_WRITE。

故事并未就此结束,因为导出和可达不是一回事。公有数据成员不需要导出宏,公有虚函数通过虚表分发,而作为 UPROPERTY 的私有字段可以按名称写入。这五个声明背后的每一个行为最终都被证明可以这样够到,因此有两个后端和三种结果:

何时行为
direct引擎导出这五个声明调用它们;这就是 MoonEngine
reflection没有导出,但它依赖的形状都能对上从公有表面重建每个操作
degraded它依赖的某个形状发生了变化拒绝生成,并说明是哪一个

反射后端是与直接后端对照验证的,而不是假定等价:同一个 .dfm 用两种方式构建,读回的 schema 逐字节相同,而且这在跨引擎时也成立——在原版 UE 5.8 上生成的模块与在 MoonEngine 上生成的同一模块相匹配。

-DreamFXForceReflectionBackend(或 dfx.ps1 -ForceReflectionBackend)在不需要它的引擎上选择反射后端,这就是在同一台机器上对两者进行 diff 的方式。

产物从来不是受限的部分。 生成的模块是一个普通的 UNiagaraScript:任何引擎都能加载它、从 .dfs 引用它、cook 它并运行它。在能运行的地方生成,提交资产,所有人消费它——而在有 MoonEngine 的团队里,在那里生成仍然是推荐路径,只因为整个语料库都是照着它验证的。

当两个后端都无法运行时:

情况会发生什么
资产已提交且与源文件匹配构建跳过它,CI 保持绿
源文件已编辑但未重新生成DFX5107 —— 在后端能运行的地方重新生成
完全没有资产DFX5100 —— 指出是哪项检查失败了

溯源检查两种情况下都会运行;区别只在于补救办法。


Settings

键含义
Usage模块可以被放入哪个(哪些)栈,或 DynamicInput。取六个栈名之一(L1),或它们的数组:Usage = [ParticleSpawn, ParticleUpdate];
OutputDynamicInput 的返回类型。必填(DFX3031);是值类型,不是数据接口(DFX3039)
Category它在栈的添加菜单中的位置
Description模块的 tooltip

没有显式栈列表的 DynamicInput 默认为粒子生成和更新——这两个覆盖了几乎所有情况。用数组形式来拓宽它。

Inputs

每一条都会变成一个带声明默认值和描述的 Module.<Name> 输入。默认值必须是字面量或枚举项,因为模块输入的默认值存储在资产上,不能引用模块之外的任何东西(DFX3044)。

普通的整数输入可以用 int(1.9) 作为默认值:它会在存储前被求值为 1。参数必须是一个有限数值字面量,其截断值能放入 int32(DFX4044)。对于 [StaticSwitch] 默认值,直接写出结果字面量。

数据接口(DI<X> 以及诸如 Texture2D 的简写)和仅实例对象类型(标记为 DefaultToInstanced 的 actor、组件和类)不能声明输入默认值(DFX4043)。声明输入时不带 = ...,并从系统或栈输入提供它的实例。这些值不是资产路径。Object<Texture> 和其他资产引用类型仍然与数据接口不同;普通资产引用在系统参数和栈输入上是支持的。

属性:[ Description="..." ]、[ Advanced ]、[ StaticSwitch ]。

[StaticSwitch] 会被接受、校验(bool/int/enum,常量默认值——DFX3034/DFX3035),然后按普通输入 lower,并由 DFX5102 说明这一点。一级生成把整个 body 放入一个自定义 HLSL 节点,那里没有供 switch 选择的分支。body 读取它的方式相同;丢失的是编译期折叠。

Body

body 是 HLSL,上面加了 DreamFX 的命名空间。

它自己的输入是裸名称。 SpinRate,而不是 Module.SpinRate —— 在模块内部命名空间是隐含的。限定形式会被接受并归一化掉,因为它对语言的描述并没有错,但裸名才是描述实际发生情况的写法:输入以引脚的形式到达。

Engine.、User.、System. 和 Emitter. 照写照读。 它们直接对参数映射解析。

Particles.* 照写照读写,DreamFX 为它连接引脚:

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

Niagara 已经知道的属性(Particles.SpriteRotation、Particles.Color……)不需要任何东西。自定义属性需要在首次使用时声明其类型,就像 .dfs 声明新属性一样——否则引脚会按猜测的宽度连接(DFX3046):

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

Particles.Color.rgb 通过 swizzle 解析为 Particles.Color:最长的、已知或已声明的点号前缀胜出。

整值赋值、swizzle 和索引分量支持 =、算术复合赋值(+=、-=、*=、/=、%=)、整数复合赋值(&=、|=、^=、<<=、>>=),以及前缀/后缀 ++ 和 --。诸如 == 和 <= 的比较仍是读取。普通 HLSL 操作数类型规则仍然适用。注释可以分隔声明、属性和运算符。原版 CPU VM 编译器拒绝整数取余(int %= int);在合适的地方使用浮点取余。不支持的操作会报告 Niagara 编译错误,而不是丢弃其写入。

对已有属性的条件写入在分支不执行时保留其传入值。新的自定义属性必须先接收一个无条件的整值初始化器,然后才能有分支、循环、预处理器条件或可能的提前返回(DFX3057);后续的读取和分量写入使用该已初始化值,而不需要传入属性。要使用由较早模块提供的自定义属性,只需在条件更新前声明其类型:

float Particles.Moon.Existing;
if (bEnabled) { Particles.Moon.Existing = 1.0; }

生成的 HLSL 引脚名称是内部的:诸如 Particles.Moon.Spin 和 Particles.Moon_Spin 这样的不同名称保持不同,包括当输入或局部变量使用类似名称时。

调用数据接口输入

声明为 DI<X> Name; 的输入是自定义节点上的一个引脚,其函数按 Niagara 自定义 HLSL 的方式调用:Name.Function(args)。将 out 参数声明为普通的、未初始化的局部变量:

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

float Gust = 1.0; 会在调用前让 CPU VM 编译失败,报 internal compiler error: out/inout parameters must be lvalues in call to 'SampleWind_Module_Wind'(DFX6006):VM 的 HLSL 前端 hlslcc 会先把带初值的局部常量折进实参表,再去检查 out 形参,而常量不是 lvalue。GPU 模拟两种写法都接受; 按未初始化写,一份 body 两端都通。

模块吃语句,动态输入吃一个表达式

模块的 body 逐字输出 —— 局部变量、分支、想写几条语句都行。这正是 DFX4030 一直在指的那件能力,也是把 .dfm 生成解锁掉值得做的 理由:.dfs 里的内联 hlsl { } 是一个 rvalue,永远装不下更多。

动态输入的 body 被 Niagara 翻译器包成 Output = (Type)( <body> );,所以它必须是一个表达式, 写不写 return 都行。前面放语句只会产出无效 HLSL 而不是一条说清真正问题的错误, 所以 DreamFX 先接住它(DFX3037),并指向模块形式。


一级生成的代价

整个 body 变成一个 UNiagaraNodeCustomHlsl,所以在节点编辑器里这个模块是个黑盒 (plan 3.3)。对一个文本优先的工作流来说这就是本意 —— 文本才是源。它比内联 hlsl { } 多买到的是栈输入装不下的一切:语句、带默认值和 tooltip 的具名输入、跨系统按名复用。

把 body 铺成真正的节点图是二级,且不在计划内:那是 DreamShader 已经背着的那个约 1.3 万行的问题, 再来一遍。

本页目录