DreamShaderLang
HLSL / GLSL 背景

HLSL 简介

写给 DreamShaderLang 作者的 HLSL 背景 —— HLSL 究竟在哪里运行、类型与签名怎么对应、@custom 与 Function 体里能写什么,以及插件自带的 DreamShaderBuiltins.ush。

HLSL 是 Unreal 材质 Custom 节点使用的着色语言。DreamShaderLang 不是 HLSL 编译器,但这里的 HLSL 也不是摆设: .dss 里的 /// @custom 函数,以及 1.x 的 Function / GraphFunction,函数体就是 HLSL,几乎原样写进生成的 Custom 节点,由 Unreal 编译。.dss 的其余代码长得像 HLSL, 却会被降级成一张材质节点图。

本页写给从 HLSL 过来的读者:哪些 HLSL 经验能直接用、哪些不能,以及分界线在哪里。

HLSL 在哪里运行,在哪里不运行

构造里面的文本是什么
.dss 里带 /// @custom 的函数HLSL,原样捕获;每次调用变成一个 UMaterialExpressionCustom 节点,函数体就是节点的代码
1.x Function Name(…) { … }HLSL —— legacy 前端把它读成一个 /// @custom 函数
1.x GraphFunction Name(…) { … }HLSL,但其中每个 UE.* 调用会被提升成真正的材质节点,再作为额外输入 pin 接回来
.dss 的其余函数和入口HLSL 的语法,不是 HLSL 的执行 —— 被绑定、降级成材质节点;有 if、?:、&&、可证明次数的循环
1.x Graph = { … }不是 HLSL —— 一门用来搭 UMaterialExpression 节点的小型语句语言
Properties、Inputs、Outputs、Settings、uniform、#pragma material声明,不是代码

since 2.0.0 起 HLSL 体直接写进 Custom 节点的代码;它调用的其他函数作为 DreamShaderFn_* 成员嵌进一个生成的 wrapper struct。不再生成逐源文件的 .ush include。见生成流程。

所以:节点图写起来啰嗦的算术就交给 HLSL,引擎输入、参数和绑定就留在图代码里。两者在函数调用处汇合。

// 纯 HLSL
float3 ApplyTint(float3 color, float3 tint)
{
    return color * tint;
}
// .dss —— 同一个 helper,作为一个 Custom 节点
/// @custom
float3 ApplyTint(float3 color, float3 tint)
{
    return color * tint;
}
// 1.x —— 同一个 helper
Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
    result = color * tint;
}

1.x 里声明返回类型也是合法的,会降级成单个输出,所以 Function float3 ApplyTint(in vec3 color, in vec3 tint) { return color * tint; } 是同一个 helper 的 HLSL 式写法。

类型

DreamShaderLang 的类型 token 是一个封闭集合。它接受的 HLSL 写法如下:

HLSL 写法是否接受说明
float、float1、half、double是1 分量
int、uint、bool是1 分量;在图里都折叠到同一套浮点宽度
float2..4、half2..4、double2..4、int2..4、uint2..4、bool2..4是2 / 3 / 4 分量
Texture2D、TextureCube、Texture2DArray、Texture3D是纹理对象;可作函数的输入,不能作函数结果(DSH6254)
VolumeTexture是生成 HLSL 时写成 Texture3D
SamplerState是一个采样器 since 2.0.0(1.x 把它读成 Texture2D)
float2x2、float3x3、float4x4只在 HLSL 体内图里没有矩阵值;见下面的矩阵一节
struct只在 .dss字段可以带自己的 /// 块和数组维度;没有方法,不能嵌套

还有几个 HLSL 不认识的 token:.dss 的 material(材质属性集),1.x 的 MaterialAttributes、StaticBool 和 Substrate。 作为函数结果,MaterialAttributes 写成 FMaterialAttributes、StaticBool 写成 bool since 2.0.0;作为 Custom 节点的输入, MaterialAttributes 是 DSH6210,Substrate 在任何位置都是 DSH6253 —— Custom 节点没有这两种 pin。这类管线请写成普通图代码、 ShaderFunction 或 GraphFunction。完整的有效性矩阵见类型与值。

矩阵

图里没有矩阵值。float2x2 / float3x3 / float4x4 —— 以及 1.x 会规范化成它们的 mat2 / mat3 / mat4 —— 在 Properties 里是 DSH3252,在其他任何会变成节点或 pin 的位置都是 DSH4361。不过 1.x Function 签名在词法上接受 mat3, 所以声明能解析,失败发生在调用时:

Function float3 Rotate(in mat3 basis, in vec3 v) { return mul(basis, v); }
// 能解析;Graph 调用把矩阵传给 'basis' 时报 DSH4361

矩阵运算可以写在 /// @custom / Function 体内部,作为局部变量;或者通过 transform 内置 —— UE.TransformVector 和 UE.TransformPosition,见 UE.* 节点。

签名

HLSL 的函数签名和 DreamShaderLang 的函数签名长得像,但规则在几处不同。

规则HLSL.dss 函数1.x Function
参数限定符in、out、inoutin、out、inout只有 in 和 out —— inout 是 DSH6302
参数默认值无有 —— float Strength = 1.0;导出函数上它让输入变成可选无(opt 和默认值属于 ShaderFunction 的 Inputs)
返回类型任意,外加 void任意可选;写了就意味着恰好一个输出,并且禁止任何 out 参数(DSH6304)
多个结果out 参数与返回值并存out 参数out 参数,且不写返回类型
重载支持不支持 —— 一个名字只声明一样东西(DSH4210)同左
与内置同名允许遮蔽DSH6206DSH6206

1.x 函数必须产出点什么:至少一个 out 参数,或者一个返回类型,都没有是 DSH6305。

Function float Luma(in vec3 color) {
    return dot(color, float3(0.299, 0.587, 0.114));      // 一个结果,HLSL 风格
}

Function SplitChannels(in vec4 src, out vec3 rgb, out float alpha) {
    rgb   = src.rgb;                                      // 两个结果,out 风格
    alpha = src.a;
}

since 2.0.0 起 return 保持原样:函数体现在就是返回节点第一个输出的那个 HLSL 函数的体,所以任何花括号深度上的 return expr; 都按 HLSL 的意思返回(1.x 只改写深度 0 上的 return,嵌套的会绕过 __return)。仍然检查的是:带返回类型的函数在 顶层写裸 return;(DSH6308)、没有返回类型的函数里任何位置的 return;(DSH6256),以及有返回类型却一个 return 都没有 (warning DSH6257,输出为 0)。

纹理与采样器

纹理类型的输入在节点上是一个携带纹理对象的 pin,引擎在它旁边声明 SamplerState <ParamName>Sampler。在函数体里就用这个名字采样:

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

since 2.0.0 起也可以写 tex.Sample(uv) —— 以及参数比 HLSL 自己的形式少一个的 SampleLevel、SampleBias、SampleGrad —— 它们会被改写成引擎的 Texture2DSample(tex, texSampler, uv) 一族。被嵌入的 helper 的签名会在每个纹理参数后面多出 SamplerState <Name>Sampler,别的函数体调用它时也会传入对应实参;这样的调用必须传全部实参(DSH6262),纹理实参必须是一个 名字(DSH6263)。SamplerState 不在触发这个展开的集合里。

控制流

位置可用的控制流
/// @custom / Function / GraphFunction 体HLSL 自己的全部 —— if、else、for、while、do、switch、break、continue、discard。函数体是透传的,能在 Custom 节点里编过的,在这里也能编过
.dss 图代码if / else、?:、&& / ||;for / while / do 在次数可证明(默认最多 64 次)时展开,否则是 DSH4360 —— 运行时才知道次数的循环写进 /// @custom 函数。没有 switch;分支里的 discard 是 DSH4362
1.x Graph 块只有 if / else,条件必须带括号、分支体必须带花括号

1.x Graph 块里的 &&、||、%、?:、&、|、^、<< 和 v[i] since 2.0.0 起都是带码的错误(DSH2200–DSH2210), 不再被静默截断。在 Function 体和 .dss 里它们是正常的运算符。见不支持的写法。

内建函数

上下文可用范围
/// @custom / Function / GraphFunction 体完整 HLSL 内建函数集,以及 Unreal Common.ush 引入作用域的一切
图代码 —— .dss 和 1.x Graph数学内置:44 个 HLSL 名字,按精确拼写匹配;1.x 源文件里另外认四个 GLSL 拼写(warning DSH5277)。其余节点靠 UE.* / UE.Expression(…)

since 2.0.0 起 distance、exp、exp2、log、log2、log10、tan、sign、round、trunc、ddx、ddy、fwidth、rcp、 rsqrt 等都是内置了。仍然没有的是 sinh / cosh / tanh(没有对应的材质节点 —— 写进 @custom 体)和矩阵。

图里的 sin / cos 建出的 Sine / Cosine 节点按周期计(Period = 1),HLSL 体里的 sin / cos 才按弧度。round 四舍六入五取偶,和 HLSL 一样 since 2.1.2。

把自己的函数命名成数学内置的名字(lerp、dot、pow……)是 DSH6206,报在声明处 since 2.0.0。1.9.x 及以前它能编译、 能生成,却在 Graph 里永远调不到。构造器名(float3、vec4……)也是同样的保留方式。

你的函数体会被怎么处理

每个 1.x Function 和 GraphFunction 体在存储前都会经过一次标识符级改写,扫描过程能识别注释和字符串。.dss 的 /// @custom 体原样保留,不做这层改写。

写的变成
vec2 / vec3 / vec4float2 / float3 / float4
ivec2..4、uvec2..4、bvec2..4int2..4、uint2..4、bool2..4
mat2 / mat3 / mat4float2x2 / float3x3 / float4x4
mixlerp
fractfrac
modfmod
A::BA_B —— 正是带命名空间函数自己的名字,所以函数体里的 Ns::Fn(…) 能调到它 since 2.0.0

匹配的是整个标识符,且不区分大小写。1.x 函数体里名为 Mix、Mod、Fract、Vec3 或 Mat4 的局部变量、helper 或结构体成员 都会被静默改名。没有任何诊断;故障表现为 HLSL 编译错误,或者悄悄算错。请改名 —— MixColor、ModValue。

函数体最前面的 #include(之前只能有空白和注释)不属于函数:它被从函数体里拿掉(用空格覆盖,行列不变),挂到每个包含这个体的 Custom 节点的 include 列表上,引擎把它放在文件作用域。写在语句之后的 #include 留在函数体里。完整的双向别名表见 GLSL 简介。

DreamShaderBuiltins.ush

插件自带一个 HLSL 头文件,定义了一批 DS_* 宏和函数,对应 UE.* 节点在材质翻译器里产生的 HLSL。

磁盘位置<Plugin>/Shaders/DreamShaderBuiltins.ush(随发布压缩包一起提供)
虚拟 shader 路径/Plugin/DreamShader/DreamShaderBuiltins.ush
include guardDREAMSHADER_BUILTINS_USH
定义25 个 DS_* 符号 —— 22 个宏、3 个函数
插件是否会发出 include不会
#include "/Plugin/DreamShader/DreamShaderBuiltins.ush"

模块启动时,DreamShader 会注册一条从虚拟目录 /Plugin/DreamShader 到插件 Shaders 目录的 shader 源目录映射,除非该虚拟目录 已经有映射。这个映射是无条件的 —— 不依赖任何项目设置、backend 选择或引擎版本 —— 所以上面那条路径对引擎的 shader 预处理器永远 可解析。它和 /DreamShaderGenerated 是两条不同的映射;后者 since 2.0.0 起不再有编译往里写文件。

插件里没有任何地方为这个头文件发出 include,也没有任何地方引用 DS_* 符号。由 Function 做成的 Custom 节点,它的 include 列表里只有函数体开头那些 #include;Graph 和 ThinCustom 两个 backend 都不包含它。每个符号在 UE.* 目录 里都有对应的节点,所以这个头文件只在手写的 HLSL 体里有用。头文件自己的注释说它由生成的 DSI_*.ush 包含 —— 那是已退役的 Instance backend 的安排,见 Backend。

符号

Parameters 是 include 处作用域内的材质参数结构体;Tex 是一个纹理参数标识符, 采样器名由 token 粘接得到(Tex##Sampler)。

符号种类展开对应节点
DS_TIME宏(View.GameTime)UE.Time()
DS_REAL_TIME宏(View.RealTime)—
DS_DELTA_TIME宏(View.DeltaTime)—
DS_PERIODIC_TIME(Period)宏(fmod(View.GameTime, (Period)))UE.Time(Period=…)
DS_TexCoord(Parameters, CoordinateIndex)float2 函数Parameters.TexCoords[CoordinateIndex].xy;NUM_TEX_COORD_INTERPOLATORS 为 0 时是 float2(0, 0)UE.TexCoord
DS_VertexColor(Parameters)float4 函数Parameters.VertexColorUE.VertexColor
DS_CameraVector(Parameters)宏((Parameters).CameraVector)UE.CameraVector
DS_ReflectionVector(Parameters)宏((Parameters).ReflectionVector)UE.ReflectionVector
DS_PixelNormalWS(Parameters)宏((Parameters).WorldNormal)UE.PixelNormalWS
DS_VertexNormalWS(Parameters)宏((Parameters).TangentToWorld[2])UE.VertexNormalWS
DS_TwoSidedSign(Parameters)宏((Parameters).TwoSidedSign)UE.TwoSidedSign
DS_ViewportUV(Parameters)宏(GetViewportUV(Parameters))UE.ViewportUV
DS_PixelDepth(Parameters)宏(GetPixelDepth(Parameters))UE.PixelDepth
DS_WorldPosition(Parameters)宏(WSDemote(GetWorldPosition(Parameters)))UE.WorldPosition
DS_TranslatedWorldPosition(Parameters)宏(GetTranslatedWorldPosition(Parameters))UE.TranslatedWorldPosition
DS_ObjectPosition(Parameters)宏(WSDemote(GetObjectWorldPosition(Parameters)))UE.ObjectPosition
DS_ObjectRadius(Parameters)宏(GetPrimitiveData(Parameters).ObjectRadius)UE.ObjectRadius
DS_ObjectBounds(Parameters)宏(float3(GetPrimitiveData(Parameters).ObjectBoundsX, …ObjectBoundsY, …ObjectBoundsZ))UE.ObjectBounds
DS_CameraPosition(Parameters)宏(WSDemote(GetWorldCameraOrigin(Parameters)))UE.CameraPosition
DS_PerInstanceRandom(Parameters)宏(GetPerInstanceRandom(Parameters))UE.PerInstanceRandom
DS_PerInstanceFadeAmount(Parameters)宏(GetPerInstanceFadeAmount(Parameters))UE.PerInstanceFadeAmount
DS_Panner(UV, Time, Speed)float2 函数UV + float2(frac(Time * Speed.x), frac(Time * Speed.y))UE.Panner
DS_SampleTexture2D(Tex, UV)宏Texture2DSample(Tex, Tex##Sampler, UV)Graph 块中的 SampleTexture2D(tex, uv)
DS_SampleTexture2DLod(Tex, UV, Lod)宏Texture2DSampleLevel(Tex, Tex##Sampler, UV, Lod)—
DS_SampleTexture2DBias(Tex, UV, Bias)宏Texture2DSampleBias(Tex, Tex##Sampler, UV, Bias)—

WSDemote 调用把引擎的大世界坐标向量降到 float3。camera-relative 的那个位置形式本身就是 float3,在相机附近保留精度。

怎么安全地用

把 #include 写在函数体最前面。 那样它会被挂到 Custom 节点的 include 列表上,在文件作用域被包含,头文件里的三个函数定义 (DS_TexCoord、DS_VertexColor、DS_Panner)也能正常编译。写在某条语句之后的 #include 留在函数体里、被粘贴到生成的函数 内部 —— 那里 22 个宏照样能用(#define 在哪里都合法),三个函数定义则不合法。

DS_TexCoord 和 DS_VertexColor 依赖的翻译器副作用现在没人安排了。 DS_TexCoord 读 Parameters.TexCoords[…],它只在材质 翻译期间至少分配了一个插值器槽位时才存在;一个都没分配时函数能编过并返回 float2(0, 0)。DS_VertexColor 读 Parameters.VertexColor,只有在材质以顶点色用途翻译时才被填充。已退役的 Instance backend 会安排这两件事,而 Graph 和 ThinCustom 不会。请改用 UE.TexCoord 和 UE.VertexColor,或者接一个函数输入。

  • 其余的读取都没有副作用。它们碰到的每个字段和 Get*(Parameters) helper 都在像素入口无条件填充, 所以在任何不透明 Surface 像素求值里都能编过,不需要任何用途标记或插值器申请。
  • DS_PerInstanceRandom 和 DS_PerInstanceFadeAmount 只对实例化或 GPU 剔除的绘制有意义。 在普通网格上它们读回安全常量(分别是 0 和 1),不会报编译错误。
  • 有 include guard,所以同一个翻译单元里重复包含无害。
  • 1.x 函数体的 GLSL 别名改写跳过字符串字面量,所以 #include "…" 里的路径永远不会被改写。
Function vec2 Wobble(in vec2 uv, in float speed)
{
    #include "/Plugin/DreamShader/DreamShaderBuiltins.ush"
    float t = DS_TIME * speed;
    return uv + float2(sin(t), cos(t)) * 0.02;
}

基于节点的等价写法既不需要 include,也不需要任何 HLSL helper:

GraphFunction vec2 Wobble(in vec2 uv, in float speed)
{
    float t = UE.Time() * speed;
    return uv + vec2(sin(t), cos(t)) * 0.02;
}

HLSL 常见关键字

作为参考 —— 这些是 HLSL 自己的词汇,在 /// @custom 和 Function 体里都有效。DreamShaderLang 中一列标明函数体之外认得的部分。

分类关键字DreamShaderLang 中
类型void、bool、int、uint、half、float、double除 void 外都是类型 token;.dss 函数可以返回 void
控制流if、else、for、while、do、switch、case、default.dss:除 switch / case 外都有;1.x Graph:只有 if / else;default 在 1.x 里还能作调用实参
跳转return、break、continue、discard.dss 都有,但分支里的 discard 是 DSH4362;1.x Graph 一个都没有
参数in、out、inout.dss 三个都有;1.x 只有 in 和 out
修饰const、static、uniform、groupshared.dss:uniform 是材质参数,static const 是常量;1.x:Properties 里的 const
纹理Texture2D、TextureCube、SamplerState、SamplerComparisonState前三个,作为类型 token
聚合struct只在 .dss

接下来

本页目录