DreamShaderLang
HLSL / GLSL 背景

HLSL 简介

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

HLSL 是 Unreal 材质 Custom 节点使用的着色语言。DreamShaderLang 不是 HLSL 编译器,但这里的 HLSL 也不是摆设:FunctionGraphFunction 的函数体就是 HLSL, 几乎原样输出到生成的 include 里,由 Unreal 编译。

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

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

构造里面的文本是什么
Function Name(…) { … }HLSL,原样输出到生成的 .ush 中,符号名 DreamShaderFn_<Name>;每个调用点变成一个 UMaterialExpressionCustom 节点
GraphFunction Name(…) { … }HLSL,但其中每个 UE.* 调用会被提升成真正的材质节点,再作为额外输入 pin 接回来
Graph = { … }不是 HLSL —— 一门用来搭 UMaterialExpression 节点的小型语句语言
PropertiesInputsOutputsSettings声明,不是代码

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

// 纯 HLSL
float3 ApplyTint(float3 color, float3 tint)
{
    return color * tint;
}
// 同一个 helper 的 DreamShaderLang 声明
Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
    result = color * tint;
}

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

类型

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

HLSL 写法是否接受说明
floatfloat11 分量
halfhalf11 分量;独立 token,行为与 float 一致
intuintbool1 分量;都折叠到同一套浮点宽度
float2 / float3 / float42 / 3 / 4 分量
half2 / half3 / half42 / 3 / 4 分量
int2..4uint2..4bool2..42 / 3 / 4 分量
Texture2DTextureCubeTexture2DArrayTexture3D纹理对象;可作 Functionin 参数,绝不能作结果
VolumeTexture生成 HLSL 时改写为 Texture3D —— 唯一的一处改写
SamplerState解析为一个 Texture2D 对象,而不是独立的 sampler 值
float2x2float3x3float4x4没有矩阵类型;见下面的矩阵一节
doubledouble2..4不是类型 token
struct不是声明形式

还有两个 HLSL 根本不认识的 token:MaterialAttributesStaticBool。它们在 DreamShaderLang 这边能解析,但 token 会被原样写进生成的签名里,于是用了它们的 Function 会产出一个 Unreal 编不过的 helper。这类管线请改用 ShaderFunctionGraphFunction。完整的有效性矩阵见 类型与值

矩阵

没有矩阵类型。float2x2 / float3x3 / float4x4 —— 以及会被规范化成它们的 mat2 / mat3 / mat4 —— 在每个声明位置都被拒绝。不过 mat3Function 签名里词法上是被接受的,因为签名 规范化器在任何校验之前就把它改写掉了;声明能解析,失败发生在调用时:

Function float3 Rotate(in mat3 basis, in vec3 v) { return mul(basis, v); }
// 能解析;调用时失败:
//   DreamShader Function 'Rotate' input 'basis' uses unsupported type 'float3x3'.

矩阵形状的工作只能通过 transform 内置触达 —— UE.TransformVectorUE.TransformPosition, 见 UE.* 节点

签名

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

规则HLSLDreamShaderLang Function
参数限定符inoutinout只有 inout —— inout 不存在,会被拒绝
默认限定符inin,当参数只有两个空白分隔的 token 时
返回类型任意,外加 void可选;写了就意味着恰好一个输出,并且禁止任何 out 参数
多个结果out 参数与返回值并存out 参数,且不写返回类型
重载支持没有重载解析;名字不区分大小写地冲突
参数默认值无 —— opt 和默认值属于 ShaderFunctionInputs,不属于这里

函数必须产出点什么:至少一个 out 参数,或者一个返回类型。都没有会报 Function '{Name}' must declare at least one out parameter.

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

声明了返回类型时,处在花括号深度 0 的每个 return 都会被改写成对合成变量 __return 的赋值。 深度 0 上的裸 return; 是硬错误。嵌在 if { … } 里的 return 仍然是真正的 HLSL return —— 合法,但绕过了 __return

纹理与采样器

纹理类型的 in 参数会在生成的签名里紧随其后多出一个 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;
}

SamplerState 不在触发这个展开的集合里。声明为 SamplerState S 的参数在签名里就写成 SamplerState S,并在调用点收到一个纹理对象。

控制流

Function 体内部,HLSL 自己的控制流完全可用 —— ifelseforwhiledoswitchbreakcontinuediscard。函数体是透传的,所以能在 Custom 节点里编过的,在这里也能编过。

Graph 块里词汇量小得多:只有 if / else,条件必须带括号、分支体必须带花括号,没有循环。

&&||Graph 块里不存在,而且会被静默丢弃 —— if (a > 0 && b > 0) 会按 if (a > 0) 编译,没有任何诊断。同样的截断也适用于 %?:&|^<<v[i]。 在 Function 体内部这些都是普通 HLSL,正常工作。见不支持的写法

内建函数

上下文可用范围
Function / GraphFunction完整 HLSL 内建函数集,以及 Unreal Common.ush 引入作用域的一切
Graph恰好 19 个数学内置写法,其余靠 UE.Expression(…)

Graph 这一面是:absceilclampcosdotfloorfmodfracfractlerpmaxminmixmodnormalizepowsaturatesinsqrt。明显缺席的有:stepsmoothsteplengthcrossreflectrefractexplog,以及全部反三角函数。 要用它们,走 UE.Expression(Class = …),或者写进 Function 体里。

把自己的 helper 命名成数学内置的名字,会让它在 Graph 里不可达,而且没有任何诊断。名为 lerpdotpowFunction 照样编译、照样生成 —— 只是 Graph 调用点解析到了内置。构造器名 (float3vec4 …)也是同样的保留方式。

你的函数体会被怎么处理

每个 FunctionGraphFunction 体 —— 语言中仅此两处 —— 在存储前都会经过一次标识符级改写。 扫描过程能识别注释和字符串。

写的变成
vec2 / vec3 / vec4float2 / float3 / float4
ivec2..4uvec2..4bvec2..4int2..4uint2..4bool2..4
mat2 / mat3 / mat4float2x2 / float3x3 / float4x4
mixlerp
fractfrac
modfmod
A::BA_B

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

完整的双向别名表见 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 预处理器永远可解析。它和生成的逐源文件 helper include 用的是两条不同的映射。

目前插件里没有任何地方为这个头文件发出 include,也没有任何地方引用 DS_* 符号。两个 backend 都不包含它;生成的逐源文件 .ush 里只有由你的 Function 块产出的 DreamShaderFn_* 定义。 这个头文件只能通过在 Function 的 HLSL 体里手写 #include 来触达。

头文件自己的注释说它由生成的 DSI_*.ush 包含。那是已退役的 Instance backend 的安排; 现在解析出的 backend 集合只有 GraphThinCustom,不再产出任何 DSI_*.ush, 那条注释描述的路径已经不再运行。见 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].xyNUM_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,在相机附近保留精度。

怎么安全地用

写在 Function 体里的 #include 会落在一个函数内部。 Function 块的 HLSL 是原样输出到生成的 DreamShaderFn_* 定义的花括号之间的,所以 #include —— 以及头文件的全部内容 —— 会被插到那个位置。 22 个宏不受影响;#define 在哪里都合法。但三个函数定义(DS_TexCoordDS_VertexColorDS_Panner)在另一个函数体内部是不合法的。手写 include 请限制在只用宏那一半的函数体里, 或者把想要的函数体内联复制过来。

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

  • 其余的读取都没有副作用。它们碰到的每个字段和 Get*(Parameters) helper 都在像素入口无条件填充, 所以在任何不透明 Surface 像素求值里都能编过,不需要任何用途标记或插值器申请。
  • DS_PerInstanceRandomDS_PerInstanceFadeAmount 只对实例化或 GPU 剔除的绘制有意义。 在普通网格上它们读回安全常量(分别是 01),不会报编译错误。
  • 有 include guard,所以同一个翻译单元里重复包含无害。
  • 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 自己的词汇,在 Function 体里都有效。DreamShaderLang 中一列标明 声明语法在函数体之外认得的部分。

分类关键字DreamShaderLang 中
类型voidboolintuinthalffloatdoublevoiddouble 外都是类型 token
控制流ifelseforwhiledoswitchcasedefaultGraph 中的 if / elsedefault 作为调用实参
跳转returnbreakcontinuediscard带返回类型的 Function 体中的 return
参数inoutinout只有 inout
修饰conststaticuniformgroupsharedProperties 声明中的 const,含义不同
纹理Texture2DTextureCubeSamplerStateSamplerComparisonState前三个,作为类型 token
聚合struct

接下来

本页目录