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、inout | in、out、inout | 只有 in 和 out —— inout 是 DSH6302 |
| 参数默认值 | 无 | 有 —— float Strength = 1.0;导出函数上它让输入变成可选 | 无(opt 和默认值属于 ShaderFunction 的 Inputs) |
| 返回类型 | 任意,外加 void | 任意 | 可选;写了就意味着恰好一个输出,并且禁止任何 out 参数(DSH6304) |
| 多个结果 | out 参数与返回值并存 | out 参数 | out 参数,且不写返回类型 |
| 重载 | 支持 | 不支持 —— 一个名字只声明一样东西(DSH4210) | 同左 |
| 与内置同名 | 允许遮蔽 | DSH6206 | DSH6206 |
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 / vec4 | float2 / float3 / float4 |
ivec2..4、uvec2..4、bvec2..4 | int2..4、uint2..4、bool2..4 |
mat2 / mat3 / mat4 | float2x2 / float3x3 / float4x4 |
mix | lerp |
fract | frac |
mod | fmod |
A::B | A_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 guard | DREAMSHADER_BUILTINS_USH |
| 定义 | 25 个 DS_* 符号 —— 22 个宏、3 个函数 |
| 插件是否会发出 include | 不会 |
#include "/Plugin/DreamShader/DreamShaderBuiltins.ush"模块启动时,DreamShader 会注册一条从虚拟目录 /Plugin/DreamShader 到插件 Shaders 目录的 shader 源目录映射,除非该虚拟目录
已经有映射。这个映射是无条件的 —— 不依赖任何项目设置、backend 选择或引擎版本 —— 所以上面那条路径对引擎的 shader 预处理器永远
可解析。它和 /DreamShaderGenerated 是两条不同的映射;后者 since 2.0.0 起不再有编译往里写文件。
符号
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.VertexColor | UE.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 |