DreamShaderLang
DreamShaderLang 2.0

Substrate 语法糖

Substrate 值上的运算符与 lerp、Slab 上的旧参数化实参、运行时分支、按成员逐步构建的值,以及一份源码同时适配开与不开 Substrate 的工程。

给 Substrate 图准备的更短的写法。每一条都只是拼写:它绑定到一组固定的 Substrate.* 节点模式上, 生成的图和长写法完全一样,反编译器还会把短写法写回来。两门语言都有 —— .dsm 和 .dss 一视同仁。

项目值
需要引擎自带的 Substrate 节点(UE 5.4+);Substrate.Select 需要 UE 5.6
会不会改变图不会 —— A + B 就是 Substrate.Add(A = A, B = B)
起始版本since 2.0.0
#pragma material(Substrate = Native)

uniform Texture2D Albedo;
uniform float Rust = 0.3;
uniform float Coat = 1.0;

export void M_CoatedMetal(inout material m)
{
    float4 Tex = Albedo.Sample(UE.TexCoord(Index = 0));

    Substrate Metal = Substrate.Slab(BaseColor = Tex.rgb, Metallic = 1.0, Roughness = 0.35);
    Substrate RustS = Substrate.Slab(BaseColor = Tex.rgb * float3(0.6, 0.3, 0.1), Roughness = 0.9);
    Substrate Body  = lerp(Metal, RustS, saturate(Tex.a * Rust));
    Substrate Clear = Substrate.Slab(IOR = 1.5, Roughness = 0.1);

    m.FrontMaterial = Substrate.Layer(Clear * Coat, Body, 0.01);
}

运算符与 lerp

Substrate 值不是数。它有两个运算符和一个内建函数,每一个都是一个节点。

你写的它是引脚
A + BSubstrate.AddA、B
A * w、w * ASubstrate.WeightA、Weight
lerp(A, B, t)Substrate.HorizontalMixBackground、Foreground、Mix

优先级就是语言本身的:A + B * w 先给 B 加权,再相加。Substrate 值上的其他运算符 —— -、/、A * B、A += B —— 是 DSH5293;把一个 Substrate 值和一个数混在一起的 lerp 也是。

五个组合节点还可以按位置传实参,其中两个有短名字: Substrate.Add(A, B)、Substrate.Weight(A, w)、Substrate.Mix(Bg, Fg, t)、 Substrate.Layer(Top, Base, Thickness)、Substrate.Select(A, B, t)。BSDF 有十八个引脚,所以 BSDF 只接受按名字传参。

运算符取引擎对参数混合(parameter blending)的默认值 —— 关。需要它的源码写具名调用: Substrate.Mix(A, B, t, UseParameterBlending = true)。

Substrate 值上的分支

条件是分支变成
/// @static 的 uniform 或函数形参类型为 Substrate 的 StaticSwitch —— 其中一边在编译期被裁掉
任何运行时才定的东西Substrate.Select:else 一侧接 A,then 一侧接 B,条件接 SelectValue

Substrate.Select 从 UE 5.6 起才有;更老的引擎上运行时形式是 DSH4378,消息里给出两条出路:把条件改成静态的, 或者对两个值做 lerp。Select 会对两个输入做参数混合,而引擎会拒绝某些不同类 BSDF 的组合, 所以两边是不同节点类时会得到一条警告 DSH4380,节点照样生成。

Slab 上的旧参数

Substrate.Slab 的参数化是 DiffuseAlbedo 和 F0,而没人手里有这两样的贴图。下面这些实参不是引脚; 每一族都是某个转换节点的输入侧,编译器会把这个转换节点接在它所代表的引脚前面。

实参转换节点接到
BaseColor、Metallic、SpecularSubstrate.MetalnessToDiffuseAlbedoF0DiffuseAlbedo、F0
Haziness —— 相对于本次调用自己的 RoughnessSubstrate.HazinessToSecondaryRoughnessSecondRoughness、SecondRoughnessWeight
Transmittance、ThicknessSubstrate.TransmittanceToMFPSSSMFP
IOR没有节点:F0 = ((n − 1) / (n + 1))²F0

一个节点只要具备某一族所接的全部引脚,就接受那一族 —— Slab 四族全收,SimpleClearCoat 收落在 F0 上的那两族 —— 而名字本身就是该节点引脚的,就按引脚处理。IOR 是数字时折成常量 F0(1.5 得 0.04); 两个 slab 转换的是同一组值时共用一个转换节点。给同一组引脚起了两个名字是 DSH5295; 有 Haziness 没 Roughness、有 Thickness 没 Transmittance 是 DSH5296。

按成员逐步构建

用不带实参的节点调用声明的 Substrate 局部变量,是一个「还在构建中」的值。

Substrate S = Substrate.Slab();      // 没有实参:builder
S.BaseColor = Albedo;                // 引脚,或上表里的实参 —— 顺序随意
S.Metallic  = 1.0;
#if DS_HAS_FUZZ
S.FuzzAmount = Fuzz;                 // 预处理器按行裁剪,这种写法正是为它准备的
#endif
m.FrontMaterial = S;                 // 第一次使用 S 时才生成节点
规则说明
最后一次写入生效S.Pin *= x 和 ++S.Pin 从成员当前持有的值出发
读成员不产生节点读到的就是当初写进去的值;读一个从没写过的成员是 DSH5299
第一次使用即封印之后再写成员是 DSH5297
声明在 if 外面时,不能在 if 里面写成员那会变成「同一个节点的两个版本」—— DSH5298。在分支内部声明的 builder 属于那个分支自己;#if 不是 if。

节点在值第一次被使用的地方生成,而且走的是和调用相同的路径, 所以 builder 与 Substrate.Slab(Roughness = r, BaseColor = c) 不可能生成出不同的图。

承载 Substrate 的层混合

凡是有 material 的地方,m.FrontMaterial 就是一个 Substrate 值 —— 材质经由引脚进来时也一样, 此时通过 GetMaterialAttributes 节点读取。

/// @layerblend
export void MLB_Wet(material Bottom, material Top, float Alpha, inout material R)
{
    R.FrontMaterial = lerp(Bottom.FrontMaterial, Top.FrontMaterial, Alpha);
}

一份源码,两种工程

#pragma material(Substrate = ...) 说明:一个按旧属性写的材质,在开了 Substrate 的工程里该怎么读。

模式工程没开 Substrate工程开了 Substrate
Legacy(默认)照原样照原样 —— 引擎自己转换旧属性
Bridge照原样Surface 材质的着色属性被收进接在 FrontMaterial 上的一个 Substrate.ShadingModels 节点,逐个引脚与引擎自己的转换一致 —— 见下表
Native驱动 FrontMaterial 是 DSH4382照原样

Bridge 收哪些属性,与引擎转换旧 Surface 材质时收的完全一样,所以资产读起来就是引擎加载时会转出来的那一个 —— 区别只在于:这个节点在你构建、diff、反编译的那张图里。

属性Substrate.ShadingModels 的引脚之后还留在材质上吗
BaseColor、Metallic、Specular、Roughness、Anisotropy、EmissiveColor、Tangent同名引脚否
SubsurfaceColorSubSurfaceColor否
ClearCoat、ClearCoatRoughness(即 CustomData0 / CustomData1)ClearCoat、ClearCoatRoughness否
Normal、Opacity同名引脚是 —— 引擎对这两个是「复制」,Bridge 也一样
ShadingModel(设置为 FromMaterialExpression 的材质)ShadingModel是
其余 —— OpacityMask、WorldPositionOffset、PixelDepthOffset、Displacement、Refraction、AmbientOcclusion、SurfaceThickness、Customized UV—留在原处

节点的 ShadingModelOverride 取材质的 ShadingModel 设置;自己计算着色模型的材质不动 override,改为驱动那个引脚。 一个材质如果没有写任何「会被移走」的属性,就没有可桥接的东西,保持原样。

「开了 Substrate」就是内建宏 DS_SUBSTRATE;凡是写了 Bridge 或 Native 的材质,它都会进入 build key —— 切换工程设置时,恰好重建这些材质。

在 1.x 源码里

Shader(Name = "Materials/M_CoatedMetal")
{
    Outputs {
        Base.FrontMaterial = Substrate.Layer(Clear * Coat, Body, 0.01);   // 是表达式,不是变量名
    }
    Graph {
        Substrate Metal = Substrate.Slab(BaseColor = Tint.rgb, Metallic = 1.0, Roughness = 0.35);
        Substrate Body  = lerp(Metal, RustS, saturate(Rust));
        Substrate Clear = Substrate.Slab(IOR = 1.5, Roughness = 0.1);
    }
}

Outputs 绑定的右侧本身就是一个表达式,在 Graph 跑完之后求值;Outputs 把一切都算完的 Shader 不需要 Graph 段。

本页目录