DreamShaderLang
语言核心

函数

Function、GraphFunction 与 Namespace —— 参数、返回类型、SelfContained 内嵌、UE.* 提升,以及调用如何解析。

FunctionGraphFunction 把可复用逻辑从裸的 Graph 语句里挪出来。两者都用 C 风格签名加一段 HLSL 函数体;区别在于这段函数体的去向。

FunctionGraphFunction
函数体去向生成的 .ush include,符号名 DreamShaderFn_<Name>直接内联进调用方的 Custom 节点
函数体里的 UE.* 调用原样留作 HLSL 文本求值成材质表达式,作为 Custom 节点输入传入
Inline / SelfContained接受在这里不是修饰符
需要活动的 Graph 构建不需要需要
可被其他 HLSL 调用可以不可以 —— 它没有生成符号

Namespace 把这两种块归到 Ns:: 前缀之下。

语法概览

Function      [ { Inline | SelfContained } ] [ <return-type> ] <name> ( [ <parameter-list> ] ) { <hlsl> }
GraphFunction                                [ <return-type> ] <name> ( [ <parameter-list> ] ) { <hlsl> }

<parameter-list> := <parameter> [ , <parameter> ] …
<parameter>      := [ { in | out } ] <type-token> <parameter-name>
记号含义示例
<x>占位符——替换成实际内容,尖括号本身不写出来。Name = <string>
[ x ]可选——整段可以整体省略。[, Root = <string>]
{ a | b }多选一——从竖线分隔的写法里取其中一个。{ Node( … ) | Comment( … ) }
可重复——前一项可以出现任意多次。<property-declaration> …

它没有 (Key = Value) 头部、没有 Settings、也没有任何 section。参数列表之后的 { … } 是原始 HLSL, 通过感知 ///* */"…" 的花括号配平扫描提取出来。

关键字 FunctionGraphFunction 大小写敏感;修饰符、in / out 限定符和所有类型 token 不敏感。

声明顺序

解析器从左到右读标识符,靠一个字符的前瞻消歧:读完一个标识符后跳过空白和注释,如果下一个字符不是 (, 那么刚读到的标识符就是返回类型

形式读作
Function Name(…)名字
Function Type Name(…)返回类型,然后名字
Function SelfContained Name(…)修饰符,然后名字
Function SelfContained Type Name(…)修饰符、返回类型、名字
Function Type SelfContained Name(…)返回类型 Type、名字 SelfContained,然后多出一个 Name —— 解析错误

所以修饰符的位置是固定的:紧跟在关键字之后第一位。

函数不能取名InlineSelfContainedFunction SelfContained(in float a, out float b) { … } 会走修饰符分支,然后在本该是名字的位置发现 (,失败于 Function declaration is missing a valid function name after SelfContained.

参数

参数先按顶层 , 切分,每个参数再按任意一段空白切分。接受 2 或 3 个 token:

Token 数形式限定符
2<type> <name>默认为 in
3<qualifier> <type> <name>按写的来
少于 2 或多于 3Function '{Name}' has an invalid parameter declaration '{Text}'.
规则行为
限定符只有 inout,比较前会小写化,所以 InOUTiN 都可以。
inout不存在。 inout float x 会被拒绝:Function '{Name}' parameter '{Text}' uses unsupported qualifier 'inout'. Supported qualifiers are in and out.
空白任意空白都能分隔 token,制表符也行 —— 这一点不同于 Inputs / Outputs section,那里必须是字面空格。
空参数被静默跳过,所以 ) 前的尾随逗号是可以的。
顺序in 参数按声明顺序成为输入,out 参数按声明顺序成为结果。两者可以交错。
保留名名为 __return(忽略大小写)的参数会被拒绝 —— 这个名字保留给返回类型降级使用。
不可用opt、默认值和 [ … ] 元数据不属于这套语法,它们属于 Inputs / Outputs section

至少一个输出

函数必须产出点什么:解析完成后结果列表不能为空。这意味着声明里要么至少有一个 out 参数,要么 有返回类型。两者都没有会得到 Function '{Name}' must declare at least one out parameter.

返回类型

形式合法结果
Function Name(in T a, out U r)一个结果 r
Function Name(in T a, out U r1, out V r2)两个结果,先 r1r2
Function U Name(in T a) { return expr; }一个名为 __return、类型为 U 的合成结果
Function U Name(in T a, out V r)Function '{Name}' has a return type and cannot also declare out parameters. Use out parameters without a return type for multiple outputs.
Function Name(in T a)完全没有输出
Function U Name(…) { return; }有返回类型却写了裸 return;

有返回类型就意味着恰好一个输出,并且禁止任何显式 out 多输出必须用 out 参数且不写返回类型。

return 降级

声明了返回类型时,函数体在 codegen 之前会被改写:花括号深度 0 上、两侧是非标识符字符的每个 return 都变成字面文本 __return =。后面的表达式和它的 ; 原样保留,扫描会跳过注释、"…"'…',所以 returnValuemy_return 不受影响。

深度 0 的裸 return; 是硬错误: A function with a return type cannot use a bare 'return;'. Return a value, e.g. 'return expr;'.

只有深度 0 的 return 会被降级。嵌在 if { … } 里的 return expr; 在产出的 DreamShaderFn_* 函数体 中仍然是真正的 HLSL return。那是合法 HLSL 且返回同样的类型,但它绕过了 __return 变量 —— 而在 GraphFunction 里,因为函数体被内联进 Custom 节点,它会直接从节点返回,完全跳过结果回写。

接受的参数类型

15 个 GLSL 别名规范化之后,解析器接受:

Token可作 in可作结果 / 返回
float float1 half half1 int uint bool可以可以
float2 half2 int2 uint2 bool2可以可以
float3 half3 int3 uint3 bool3可以可以
float4 half4 int4 uint4 bool4可以可以
MaterialAttributes可以可以
StaticBoolStaticBoolParameter可以不可以
Texture2DSamplerState可以不可以
TextureCube可以不可以
Texture2DArray可以不可以
Texture3DVolumeTexture可以不可以
Substrate不可以不可以

未知 token 在解析阶段不会被校验 —— 它被透传过去,只有函数被调用时才失败。矩阵就走这条路径: DreamShader Function 'Rotate' input 'basis' uses unsupported type 'float3x3'.

SubstrateFunctionGraphFunction 上永远不可用。UE 5.4+ 时 Function… uses Substrate, which is not supported by HLSL Custom node functions. Use GraphFunction or ShaderFunction instead.;UE 5.3 时报 … requires Unreal Engine 5.4 or newer. Substrate 值根本无法 跨越 Custom 节点边界 —— 请在 ShaderFunctionShaderGraph 里构建它们。

纹理参数与采样器

有五个 token 被当作纹理函数参数Texture2DTextureCubeTexture2DArrayTexture3DVolumeTexture。每遇到一个,生成的 HLSL 签名就会在它后面多出一个 SamplerState <ParamName>Sampler 参数,每个生成的调用点也会传入对应的 <argument>Sampler。在函数体里用这个名字来采样纹理。

SamplerState 不在这个列表里:声明为 SamplerState S 的参数解析成普通的 Texture2D 对象,不会 获得伴随参数。

Function SampleTinted(in Texture2D tex, in vec2 uv, in vec3 tint, out vec3 rgb, out float alpha)
{
    float4 texel = Texture2DSample(tex, texSampler, uv);
    rgb   = texel.rgb * tint;
    alpha = texel.a;
}

函数体规范化

每个 FunctionGraphFunction 的函数体 —— 语言里也只有它们 —— 在存储之前会过一遍标识符级别的改写。 扫描感知注释和字符串,做两件事:替换 18 个别名(15 个 GLSL 类型拼法,加上 mixlerpfractfracmodfmod),以及把 A::B 压平成净化后的标识符 A_B

因为别名匹配忽略大小写、且作用于函数体内任意位置的整个标识符,你自己写的名为 MixModFractVec3Mat4 的 helper 或变量会在产出的 HLSL 里被静默改名。没有任何诊断。请改名,或者换一个不会 撞上的拼法(MixColorModValue)。

生成的 HLSL

每个 Function 以这种形态写进生成的 include:

<ResultType0> DreamShaderFn_<SanitizedName>(<params>)
{
	<ResultType0> <Result0Name> = (<ResultType0>)0;
	<Result1Name> = (<ResultType1>)0;
	<body, indented>
	return <Result0Name>;
}
元素规则
符号名DreamShaderFn_ 加上函数的完整名字,经过净化。Common::ApplyTintDreamShaderFn_Common_ApplyTint
为什么要前缀不带前缀的 Luminance(float3) 会重定义 Common.ush 里的引擎内建,导致 shader 编译失败于 redefinition of 'Luminance'
返回值Results[0] 是 HLSL 返回值,永远不是参数。
参数每个 in 参数;每个纹理类型输入之后一个 SamplerState <Name>Sampler;然后其余每个结果作为 out <Type> <Name>
类型改写VolumeTextureTexture3D;其他一律原样。

一个解析单元里的每个 Function 都会进 include —— 包括 SelfContained 的。GraphFunction 永远不会。 见 生成流程

Inline / SelfContained

InlineSelfContained完全同义词 —— 同一个解析分支、同一个标志位,行为上毫无差别。两者都 不被 GraphFunction 接受。

不带修饰符时,调用点产出的 Custom 节点代码会去调用 include。带修饰符时,这个函数以及它调用的普通 Function 的传递闭包会被内嵌进调用节点自己的代码里,包在一个结构体中。

方面默认Inline / SelfContained
函数体所在位置共享的生成 .ush在每个调用它的 Custom 节点里复制一份
IncludeFilePaths总是包含生成的 include只有当某个其他直接被调方没有被内嵌时才包含
结构体包装一个 generated_wrapper_* 类型加一个 __ds_wrapper_* 实例
成员产出顺序依赖顺序,被调方在调用方之前
递归include 允许,但 HLSL 会拒绝在产出之前就被检测并报错
仍然写进 include —— 修饰符只是增加内嵌,不会移除 include 条目

内嵌是由调用点驱动的,不是只看声明。只有从内嵌根可达的函数才会被拉进包装结构体;被内嵌闭包调用的 普通 Function 会跟着一起被内嵌。

GraphFunctionUE.* 提升

Custom 节点的代码被组装之前,函数体会被扫描一遍,每个 UE.* 调用都会被替换成一个生成输入引脚的 名字,那个引脚承载这次调用求值得到的值。

只有下列条件按顺序全部满足时才会被提升:

步骤条件
1U 前面的字符是标识符边界
2接下来三个字符是 UE. —— 忽略大小写匹配,所以 ue.Ue.uE. 都会触发
3. 后面的字符能开始一个标识符
4跳过可选空白后,下一个字符是 (
5存在配对的 ) —— 搜索感知字符串

第 1–4 步不满足时,文本原样静默透传。第 5 步失败时生成停止,报 DreamShader GraphFunction '{Name}' contains an unterminated UE.* call. 扫描会跳过注释和字符串字面量, 所以写在注释里的 UE.Time() 不会被提升。

提取出的文本会被重新按 Graph 表达式解析,并在一个包含外层 Graph 块变量和本次调用实参的作用域中求值, 所以任何 UE.* 内置(包括 UE.Expression)都可用。

生成的引脚名

步骤规则
1基础名是 __ds_<SanitizedFunctionName>_UE<N>N 在每个调用点都从 0 开始
2整个名字经过标识符净化,开头的 __ 被塌缩成单个 _
3结果为空时变成 __ds_input
4只要和已有输入名冲突,就追加 _1_2

所以函数体里含一个 UE.Time() 调用的 GraphFunction WindPulse 会产出一个字面名为:

_ds_WindPulse_UE0

的引脚,而带命名空间的 Common::Pulse 产出 _ds_Common_Pulse_UE0。这些名字在生成的节点上、以及任何 提到它们的 shader 编译错误里都是用户可见的。

提升恰好消费 UE.Name( … ) 到配对的 ) 为止,一个字符都不多。尾随的 swizzle 或成员访问会留在原地, 作为作用于引脚的 HLSL,所以 UE.CameraVector().xy 变成 _ds_<Fn>_UE0.xy,由 shader 编译器求值。引脚 本身承载的是内置的完整分量数。

只有 UE. 会被提升。 Substrate.* 调用不被扫描识别,会原样抄进 Custom 节点的 HLSL,而 Substrate.Slab(…) 不是合法 HLSL,shader 编译器会拒绝它。请在 ShaderFunctionShaderGraph 里构建 Substrate 图。

被提升的表达式必须产出普通数值。纹理对象、MaterialAttributes 值或 Substrate 材质会失败于 DreamShader GraphFunction '{Name}' UE input '{CallText}' cannot be passed into a Custom node input.

GraphFunction WindPulse(in float2 uv, out float pulse)
{
    float t = UE.Time();
    pulse = sin(uv.x * 8.0 + t);
}

一次调用会生成:

UMaterialExpressionTime                →  Custom pin "_ds_WindPulse_UE0"
UMaterialExpressionTextureCoordinate   →  Custom pin "uv"
UMaterialExpressionCustom  Description="WindPulse"  OutputType=CMOT_Float1
float pulse = (float)0;
float t = _ds_WindPulse_UE0;
pulse = sin(uv.x * 8.0 + t);
return pulse;

Namespace

Namespace 给它包含的 FunctionGraphFunction 声明的名字加前缀。它不是一个实体:不会为它存储 任何对象,它也不创建作用域。它唯一的作用是改写成员的记录名。

Namespace(Name = "Common") { Function ApplyTint(…) }   →   名字 "Common::ApplyTint"
规则行为
Name必须是合法标识符,逐字符校验。Namespace(Name="A::B") 是错误 —— 没有多段声明形式
函数体只能是 FunctionGraphFunction。嵌套命名空间、section 和资产块都会失败于 Namespace '{Name}' may only contain Function or GraphFunction blocks.
重复打开允许且不作检查。同一文件或跨 import 的两个 Namespace(Name="Common") 块都会加 Common:: 前缀。
查找成员只能通过完整限定名访问。没有 using,没有名字导入,也没有不限定的回退。
大小写比较对整个限定串忽略大小写,所以 common::applytint(…) 能解析。
Namespace(Name="Common")
{
    Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
        result = color * tint;
    }

    Function float Remap01(in float value) {
        return saturate(value * 0.5 + 0.5);
    }

    GraphFunction Pulse(in float speed, out float value) {
        value = sin(UE.Time() * speed);
    }
}
Graph = {
    vec3 Tinted;
    Common::ApplyTint(BaseColor, Tint, Tinted);   // 语句式调用
    float K = Common::Remap01(Tinted.r);          // 值式调用
}

因为 ::_ 净化后是同一个符号,Namespace(Name="Common") 的成员 ApplyTint 和顶层的 Function Common_ApplyTint 会产出同一个 DreamShaderFn_Common_ApplyTint。include 写出器会拒绝这一对: DreamShader Function '{Name}' collides with another generated helper symbol '{Symbol}'. Rename the Function or Namespace.

写在另一个 FunctionGraphFunction 函数体里的 Ns::Fn(…) 调用不会解析。 函数体规范化会在 codegen 看到它之前把限定 token Common::ApplyTint 改写成 Common_ApplyTint,而改写表的键是 Common::ApplyTintDreamShaderFn_Common_ApplyTint —— 从来不是 Common_ApplyTint。症状是一条指出 未定义 Common_ApplyTint 的 shader 编译错误;DreamShader 侧没有任何诊断。

绕过办法:在 Graph 块里调用这个带命名空间的函数,把结果作为参数传进来;或者把 helper 声明在顶层、 不限定地调用;或者在函数体里直接写净化后的符号 DreamShaderFn_Common_ApplyTint(…),规范化器不会动它。 从 Graph 块发起的调用不受影响。

调用

两种调用形式都写在 Graph 块里。

形式要求
x = Fn(a, b); —— 值式调用 since 1.3.1恰好一个声明的输出;实参个数必须等于输入个数
Fn(a, b, OutX, OutY); —— 语句式调用先全部输入,再按声明顺序每个 out 结果一个纯变量名

单输出的 FunctionGraphFunction 调用可以当作值使用,从 1.3.1 起就可以。单输出的 ShaderFunctionShaderLayerShaderLayerBlendVirtualFunction 调用同样可以 since 1.5.0。只有输出的 helper 才需要语句式。

FunctionGraphFunction 在两种形式下都支持命名参数 —— 传 Key = Value 会失败于 … currently uses positional arguments only. out 目标必须是裸名字、非空,且在一次调用内互不重复。 因为查找也接受净化后的拼法,DreamShaderFn_Luma(c) 是合法调用。

完整的参数规则、default、输出选择器和跨类型歧义见 调用

说明

  • 没有重载解析。 名字不按 arity 或参数类型区分;忽略大小写后第一个匹配的声明胜出。
  • 忽略大小写的冲突就是真冲突。 Lumaluma 会被判为"声明了多次",A::BA_B 也是。
  • 解析单元是整个 import 闭包,所以跨导入 header 的重名就是冲突。见 import 与命名空间
  • 只含 Function 块的 .dsm.dsf 能编译成功且不产出资产: Generated DreamShader helper include '{Path}' from {File}.
  • 如果一个 Function 和一个 GraphFunction 同名,所有调用都会有歧义。
  • UE 5.3 上生成的 Custom 节点会在材质图里显示代码;UE 5.4 起 ShowCode 被设为 false
  • 旧的 section 式函数体 Function Name { Inputs = { … } Code = { … } } 不是受支持的形式。

示例

// DShader/Lib/Color.dsh

Function float Luma(in vec3 color)
{
    return dot(color, float3(0.299, 0.587, 0.114));
}

Function SelfContained Remap01(in float value, out float result)
{
    result = saturate(value * 0.5 + 0.5);
}
import "Lib/Color.dsh";

Shader(Name="Materials/M_Tinted")
{
    Properties = {
        TextureSampleParameter2D BaseTex = Path(Game, "Textures/T_Base");
        vec3                     Tint    = vec3(1.0, 0.6, 0.2);
    }
    Outputs = {
        vec3 Color;
        Base.EmissiveColor = Color;
    }
    Graph = {
        vec2 UV  = UE.TexCoord(Index = 0);
        vec4 Tex = BaseTex(Coordinates = UV);

        float Key;
        Remap01(Luma(Tex.rgb), Key);      // 语句式调用,内层是值式调用

        Color = Tex.rgb * Key;
    }
}

诊断

消息触发原因处理
Function declaration is missing a valid function name.Function 后面的 token 不是标识符。
Function declaration is missing a valid function name after SelfContained.写成了 Function SelfContained( 或 Function Inline( —— 修饰符分支在本该是名字的位置发现了 (。
Function declaration is missing a function name after the return type '{Token}'.返回类型后面没跟标识符 —— 在参数列表 ( 的位置写了 { 也会走到这里。
Function '{Name}' has an invalid parameter declaration '{Text}'.参数按空白切分后少于 2 个或多于 3 个 token,或类型 / 名字为空。
Function '{Name}' parameter '{Text}' uses unsupported qualifier '{Qualifier}'. Supported qualifiers are in and out.使用了 in / out 之外的限定符,inout 也在其中。把这个参数拆成一个 in 参数和一个 out 参数。
Function '{Name}' parameter name '__return' is reserved for return-type lowering.参数字面名为 __return。
Function '{Name}' has a return type and cannot also declare out parameters. Use out parameters without a return type for multiple outputs.返回类型和任意 out 参数同时出现 —— GraphFunction SelfContained Foo(… out …) 也会走到这里。
Function '{Name}' must declare at least one out parameter.既没有 out 参数也没有返回类型。
A function with a return type cannot use a bare 'return;'. Return a value, e.g. 'return expr;'.有返回类型的函数体里出现了深度 0 的裸 return。
DreamShader Function '{Name}' is declared more than once.import 闭包内有两个忽略大小写后同名的声明。
DreamShader Function '{Name}' collides with another generated helper symbol '{Symbol}'. Rename the Function or Namespace.两个名字净化成同一个 DreamShaderFn_* 符号 —— 典型是 Ns::Fn 撞上 Ns_Fn。
SelfContained Function cycle detected: {Path}. HLSL Custom nodes cannot compile recursive DreamShader functions.内嵌闭包里出现了环。
GraphFunction cycle detected: {Path}.GraphFunction 直接或间接递归;{Path} 是当时的调用栈。
Unknown Graph function '{Name}'.没有这个名字的可调用项 —— 常见原因是不限定地调用了带命名空间的成员。改写成 Ns::Fn(…)。
Graph call '{Name}' is ambiguous because multiple definitions use that name: {Kinds}.两种可调用类型声明了同一个名字。
DreamShader Function '{Name}' has {N} outputs and must be called with explicit out variables, for example {Name}(..., ResultA, ResultB).对多输出函数使用了值式调用。
DreamShader Function '{Name}' currently uses positional arguments only.给 Function 或 GraphFunction 传了命名参数。
DreamShader Function '{Name}' out argument {N} must be a plain variable name.out 目标不是裸标识符。序号从 1 开始。
DreamShader GraphFunction '{Name}' contains an unterminated UE.* call.函数体里的 UE.*( 没有配对的 )。
DreamShader GraphFunction '{Name}' UE input '{CallText}' cannot be passed into a Custom node input.被提升的调用产出了纹理对象、MaterialAttributes 值或 Substrate 值。
GraphFunction call requires an active Graph build context.在 Graph 块之外调用了 GraphFunction。
Namespace '{Name}' may only contain Function or GraphFunction blocks.Namespace 体内出现了其他任何 token,包括嵌套的 Namespace。
Expected function name after '::'.限定调用的 :: 后面什么都没有。

该选哪一种

场景
复用的纯数学Function,放在 .dsh
复用的、需要真实材质节点的逻辑GraphFunction
供其他材质调用的可复用资产ShaderFunction,放在 .dsf
已经存在的 UMaterialFunctionVirtualFunction
MaterialAttributes 或 Substrate 传递ShaderFunction —— 绝不用 Function
公共库 API放进 Namespace 的稳定名字,作为 package 发布

继续阅读

本页目录