常用写法
DreamShaderLang 的文件级构件 —— 最小材质、参数、纹理采样、共享头文件、函数、UE.* 节点、Layer、VirtualFunction、GraphFunction 与 Layout,每一段都是完整源文件,1.x 写法旁边并列 .dss 写法。
本页的每一段都是一个完整文件或一个完整块,短到可以直接粘贴改名。顺序从"能产出资产的最小文件"开始,
一直排到需要 .dsh 头文件、.dsf 函数文件或特定引擎版本的写法。每段 1.x 写法后面都跟着同一个东西的 .dss 写法。
| 适用于 | DreamShaderLang 1.x(.dsm、.dsf、.dsh),由 2.0 编译器经 legacy 前端构建(自 2.0.0 起);.dss 写法是 DreamShaderLang 2.0 |
| 引擎 | UE 5.3 – 5.8;有版本门槛的都在正文里标出 |
| 假定源码根目录 | <Project>/DShader —— 即 SourceDirectory 项目设置 |
| 生成产物 | Graph backend 的材质和每个材质函数、layer、blend:每次构建成功都存盘。ThinCustom 材质 —— 默认 backend —— 保持 Ephemeral,只在内存里,直到被 Materialize;见内存材质 |
每段开头注释里的路径就是该文件假定所在的位置,import 和 #include 的路径按这个布局解析。用 Path(Engine, …)
或 /Engine/… 引用的资产随引擎自带,所以那几个例子照抄就能加载;引用项目资产的那个(VirtualFunction)已经标出。
注释里提到的诊断都写码。编译把每一条报成 <file>(<line>,<col>): DSHnnnn: <message>,位置就是它所说的那个构造;码怎么读见错误速查。
每段下面的「写成 .dss」是同一个文件用 DreamShaderLang 2.0 手写的样子:#pragma material(…) 换成
Settings,每个参数是一个带 /// 标签的 uniform,再加一个名字就是资产名的 export 函数。
.dss 把它的 export 构建在文件自己所在的文件夹里,所以这里每个都构建出和旁边 1.x 文件同一个资产路径 —— 两者留一个,别都留。
dsc migrate 会写出一个 1.x 文件的 .dss 并证明两者构建出同一张图;它的输出比这里的长,
因为它保留 1.x 的每个细节(每个参数上的 /// @sort 32、1.x 给每个输出的零初始值)。
本页讲的是形状。想要能跑出效果的完整材质,见完整示例。
最小材质
能产出资产的最小文件:一个参数、一条绑定、一次赋值。
// DShader/Materials/M_Minimal.dsm
Shader(Name="Materials/M_Minimal")
{
Properties = {
vec3 Tint = vec3(1.0, 0.2, 0.2);
}
Settings = {
Domain = "UI";
ShadingModel = "Unlit";
}
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
Color = Tint;
}
}生成的资产:
package /Game/Materials/M_Minimal
object path /Game/Materials/M_Minimal.M_Minimal
class UDreamShaderMaterialInstance over a hidden base UMaterial (the default ThinCustom backend)Shader区分大小写;section 名Properties、Settings、Outputs、Graph不区分。 见词法与大小写。- section 名后的
=是可选糖(自 1.5.0 起):Properties { … }解析结果完全一样。section 体内最后一个;同样可省。 Root默认是/Game,所以头部写成Shader(Name="Materials/M_Minimal", Root="Game")含义不变。 完整规则见资产路径。- 一个
Shader既没有Graphsection、Outputs里也没有东西时报DSH2255。没有Graph时,由Outputs自己算值 —— 带初始化式的输出声明,或者(自 2.0.0 起)右边是表达式的绑定。没有Outputssection 的Shader照样构建,只是什么都没接上,并给出 warningDSH2256(自 2.0.0 起)。
写成 .dss
// DShader/Materials/M_Minimal.dss
#pragma material(Domain = UI, ShadingModel = Unlit)
uniform float3 Tint = float3(1.0, 0.2, 0.2);
export void M_Minimal(inout material m)
{
m.EmissiveColor = Tint;
}签名 export void <Name>(inout material m) 让这个文件成为一个材质;1.x 的 Outputs 绑定就是对 m 某个成员的写入。
参数、分组与元数据
一个文件里覆盖所有参数族,带 Group("…") 作用域、[ … ] 元数据和 Slider(min, max) 简写。
// DShader/Materials/M_Params.dsm
Shader(Name="Materials/M_Params")
{
Properties = {
Group("Surface") {
ScalarParameter Roughness = 0.55 [Slider(0, 1)];
VectorParameter Albedo = float4(0.8, 0.8, 0.8, 1.0) [Description="Base albedo"];
}
Group("Detail") {
TextureSampleParameter2D DetailMap = Path(Engine, "EngineResources/WhiteSquareTexture") [
SamplerType = "LinearColor";
SamplerSource = "FromTextureAsset";
SortPriority = 99;
];
StaticSwitchParameter UseDetail = true;
}
Texture2D NoiseTex = Path(Engine, "EngineResources/WhiteSquareTexture");
const float DebugScale = 1.0;
}
Settings = {
Domain = "Surface";
ShadingModel = "DefaultLit";
BlendMode = "Opaque";
}
Outputs = {
float3 Color;
float Rough;
Base.BaseColor = Color;
Base.Roughness = Rough;
}
Graph = {
vec2 UV = UE.TexCoord(Index = 0);
vec4 Detail = DetailMap(Coordinates = UV);
vec4 Noise = SampleTexture2D(NoiseTex, UV);
Color = UseDetail(True = Detail.rgb * Albedo.rgb, False = Albedo.rgb);
Rough = Roughness * DebugScale * Noise.r;
}
}float/vec3/Texture2D是紧凑写法;ScalarParameter、VectorParameter、TextureSampleParameter2D则直接点名 Unreal 节点。两套写法都收录在 Properties 类型。Group("…") { … }作用域(自 1.5.0 起)会把组名盖到里面的参数上,并从每个Propertiessection 一个计数器 按0, 10, 20, …编号;显式写的SortPriority优先,且不占号。 这里Roughness是 0,Albedo是 10。嵌套的组用|拼接(Outer|Inner)。- (自 2.0.0 起)组名会盖到标量、向量、纹理和 static bool 参数上。带 pin 的参数和开关参数 ——
DetailMap、UseDetail—— 在每次使用处写成一个节点,那个节点只带它自己[ … ]里的元数据:DetailMap得到SortPriority = 99、 没有组,UseDetail两样都没有。 const声明的是谁也覆盖不了的值 —— 编译期常量,不是参数。它接受标量、向量或纹理类型; 其他类型是DSH3253。
警告:
裸读一个 StaticSwitchParameter 不是值。 Color = UseDetail; 报 DSH4200。用 True=
和 False=(或 A= / B=,或按位置)调用它;缺一边是 DSH5258,别的参数被丢掉并给出 warning DSH5254。
写成 .dss
// DShader/Materials/M_Params.dss
#pragma material(Domain = Surface, ShadingModel = DefaultLit, BlendMode = Opaque)
/// @group Surface @sort 0 @slider 0 1
uniform float Roughness = 0.55;
/// @group Surface @sort 10 @desc Base albedo
uniform float4 Albedo = float4(0.8, 0.8, 0.8, 1.0);
/// @group Detail @sort 99 @sampler LinearColor
/// @default /Engine/EngineResources/WhiteSquareTexture
uniform Texture2D DetailMap;
/// @static
/// @group Detail @sort 20
uniform bool UseDetail = true;
/// @default /Engine/EngineResources/WhiteSquareTexture
uniform Texture2D NoiseTex;
static const float DebugScale = 1.0;
export void M_Params(inout material m)
{
float2 UV = UE.TexCoord(Index = 0);
float4 Detail = DetailMap.Sample(UV);
float4 Noise = NoiseTex.Sample(UV);
float3 Color = Albedo.rgb;
if (UseDetail)
{
Color = Detail.rgb * Albedo.rgb;
}
m.BaseColor = Color;
m.Roughness = Roughness * DebugScale * Noise.r;
}- 纹理
uniform不写初始化式:它的资产写在/// @default,采样器类型写在/// @sampler。用.Sample(UV)采样时它建出一个TextureObjectParameter加一个TextureSample,而 1.x 的 token 建的是一个TextureSampleParameter2D;采样器来源取节点的默认值 From Texture Asset。 - 被
if读取的/// @staticuniform bool就是静态开关。/// @group和/// @sort写每个 参数的组和优先级;static const就是 1.x 的const。
采样纹理的两种写法
上面那两个 1.x 纹理声明的采样方式不同,而且这个区别不是写法偏好问题。
| 声明 | 生成的节点 | 采样方式 |
|---|---|---|
Texture2D NoiseTex = Path(…); | TextureObjectParameter —— 纹理对象,没有输入 pin | SampleTexture2D(NoiseTex, UV) |
TextureSampleParameter2D DetailMap = Path(…); | TextureSampleParameter2D —— 自带 Coordinates pin | DetailMap(Coordinates = UV) |
警告:
pin 调用形式 Name(Pin = …) 只属于十个参数 token,它们的节点自带输入 pin ——
ChannelMaskParameter、StaticComponentMaskParameter,以及八个 *SampleParameter* token —— 并且每个参数
都要带名字(否则 DSH5259)。纹理对象参数(紧凑的 Texture2D,或 TextureObjectParameter)
没有 pin、也不是函数,所以 NoiseTex(Coordinates = UV) 报 DSH4208。
采样纹理对象请改用保留写法 SampleTexture2D(textureObject, uv)。它
区分大小写,并且只接受两个位置参数(否则 DSH5256)。在 .dss 里两种都写成
Tex.Sample(UV)。
共享 .dsh 头文件
.dsh 头文件包含 Function、GraphFunction、Namespace 和 VirtualFunction 块、import 行以及 2.0
声明 —— 没有资产块(DSH2249)。它不生成任何资产。它被单独解析,其声明会成为
每个 import 它的文件中的名字(自 2.0.0 起)(1.x 会把它的文本粘贴进 import 它的文件)。
// DShader/Shared/Common.dsh
Namespace(Name="Common")
{
Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
result = color * tint;
}
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);
}
Function SplitChannels(in vec4 src, out vec3 rgb, out float alpha) {
rgb = src.rgb;
alpha = src.a;
}引入它:
// DShader/Materials/M_Tinted.dsm
import "Shared/Common.dsh";
Shader(Name="Materials/M_Tinted")
{
Properties = {
vec3 Albedo = vec3(0.6, 0.8, 1.0);
vec3 Tint = vec3(1.0, 0.4, 0.1);
}
Settings = { Domain = "Surface"; ShadingModel = "Unlit"; }
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
Common::ApplyTint(Albedo, Tint, Tinted);
Color = Tinted;
}
}import "Shared/Common"等价:当说明符完全不带扩展名时,会自动追加.dsh。只有.dsh才能被 import(自 2.0.0 起):.dsf或.dsm路径会报DSH2252。- 相对说明符会依次尝试 import 它的文件所在目录、它所属源码根的目录
(
DShader),然后是该根的Packages—— 从不尝试另一个根的。解析到其根之外的候选会被 跳过。参见 Imports 与命名空间。 - (自 2.0.0 起)
import在文件作用域内作为 token 读取:;是可选的,'single quotes'在 1.x 文件中也能用,而/* … */注释内的import会被注释掉(1.x 会处理它)。不过仍请让每条import独占一行:编辑器的依赖扫描器在保存头文件时会重编 import 它的文件,它仍然 逐行读取。 Function函数体是 HLSL,不是Graph语句:这里的dot、saturate和float3(…)是 HLSL 内建函数。GLSL 拼写会在这些函数体内被改写,不区分大小写且不给诊断(vec3→float3、mix→lerp、fract→frac、mod→fmod)。- (自 2.0.0 起)每个
Function节点都会嵌入其函数体调用的Function,而 1.x 只对SelfContained这样做,所以SelfContained不改变任何东西。Inline是它的旧拼写,带 warningDSH6306; 用在GraphFunction上两者都是DSH6307。 - (自 2.0.0 起)在另一个
Function或GraphFunction函数体内使用带命名空间的调用是可以的:函数体 规范化会把Common::ApplyTint拍平为Common_ApplyTint,这正是那个函数自己的名字,被调方 会被嵌入调用方的节点。1.x 会给 shader 编译器留下一个未定义符号。请书写::时 不要在它两边加空格。
写成 .dss
// DShader/Materials/M_Tinted.dss
#include "Shared/Common.dsh"
#pragma material(Domain = Surface, ShadingModel = Unlit)
uniform float3 Albedo = float3(0.6, 0.8, 1.0);
uniform float3 Tint = float3(1.0, 0.4, 0.1);
export void M_Tinted(inout material m)
{
float3 Tinted;
Common_ApplyTint(Albedo, Tint, Tinted);
m.EmissiveColor = Tinted;
}.dsh 同时容纳两种方言,所以 .dss 会按原样 include 这个头文件。带命名空间的函数按它声明的名字
调用,即 Common_ApplyTint,而 out 实参是先声明好的变量。头文件本身的 2.0
写法如下 —— .dsm 同样能 import 它:
// DShader/Shared/Common.dsh
/// @custom
/// @name Common::ApplyTint
void Common_ApplyTint(float3 color, float3 tint, out float3 result)
{
result = color * tint;
}
/// @custom
/// @name Common::Luma
float Common_Luma(float3 color)
{
return dot(color, float3(0.299, 0.587, 0.114));
}
/// @custom
void Remap01(float value, out float result)
{
result = saturate(value * 0.5 + 0.5);
}
/// @custom
void SplitChannels(float4 src, out float3 rgb, out float alpha)
{
rgb = src.rgb;
alpha = src.a;
}/// @custom 会原样保留函数体、并让每次调用成为一个 Custom 节点,如同 1.x 的 Function;/// @name 保留
节点标题 Common::ApplyTint。
调用函数:值形式与语句形式
函数的值是它的返回值,或者它的第一个 out。语句形式先传输入,再给每个输出一个目标;
其尾部实参就是接收结果的变量名。
// DShader/Materials/M_Calls.dsm
import "Shared/Common.dsh";
Shader(Name="Materials/M_Calls")
{
Properties = {
vec4 Source = vec4(0.4, 0.6, 0.9, 0.75);
vec3 Tint = vec3(1.0, 0.4, 0.1);
}
Settings = { Domain = "Surface"; ShadingModel = "Unlit"; BlendMode = "Translucent"; }
Outputs = {
vec3 Color;
float Alpha;
Base.EmissiveColor = Color;
Base.Opacity = Alpha;
}
Graph = {
// statement form: two out results, two target names
SplitChannels(Source, Rgb, A);
// statement form: one out result
Common::ApplyTint(Rgb, Tint, Tinted);
// value form: the return value, or the first out
float L = Common::Luma(Tinted);
float Soft = Remap01(L);
Color = Tinted * Soft;
Alpha = A;
}
}| 被调用者种类 | 值形式 | 语句形式 | 命名参数 |
|---|---|---|---|
Function | 可以 —— 返回值,或第一个 out(自 2.0.0 起)(1.x:只能一个结果) | 可以 | 可以(自 2.0.0 起) |
GraphFunction | 同 Function | 可以 | 可以(自 2.0.0 起) |
ShaderFunction | 可以 —— 第一个输出,除非用选择器挑别的;只能在它自己的文件里调用 | 可以 | 可以 |
ShaderLayer / ShaderLayerBlend | 不能调用:DSH6208(自 2.0.0 起) | —— | —— |
VirtualFunction | 可以 | 可以 | 可以 |
- 没人声明过的 out 目标会由调用方声明,成为输出类型的局部变量(info
DSH5283)。目标必须 可赋值 —— 一个变量,或者它的成员或 swizzle(DSH4239)—— 并且类型和输出一致(DSH4218)。 - 在全按位置的语句调用里,最后几个实参依声明顺序各接收一个输出,一个对一个;它们
之前的是输入。实参太多是
DSH4224,而漏掉一个非opt的输入是DSH4217。 - 函数名区分大小写(自 2.0.0 起):只靠大小写对上的调用仍能
解析,带 warning
DSH5275。没有重载解析 —— 一个名字只声明一样东西,对它再做一次 声明会在写的地方报DSH4210。 - 数学内建函数(
lerp、dot、pow、min、max、clamp、saturate、sin、frac、fmod……)先于用户函数解析;和它们同名的函数会在声明处报DSH6206。mix、fract和mod是它们的 GLSL 拼写,在 1.xGraph中被接受,带 warningDSH5277。
写成 .dss
// DShader/Materials/M_Calls.dss
#include "Shared/Common.dsh"
#pragma material(Domain = Surface, ShadingModel = Unlit, BlendMode = Translucent)
uniform float4 Source = float4(0.4, 0.6, 0.9, 0.75);
uniform float3 Tint = float3(1.0, 0.4, 0.1);
export void M_Calls(inout material m)
{
float3 Rgb;
float A;
SplitChannels(Source, Rgb, A);
float3 Tinted;
Common_ApplyTint(Rgb, Tint, Tinted);
float L = Common_Luma(Tinted);
float Soft;
Remap01(L, Soft);
m.EmissiveColor = Tinted * Soft;
m.Opacity = A;
}.dss 会把一次调用的每个 out 传给一个它先声明好的变量。对没有返回类型的函数做 1.x 值调用
即 Remap01(L),是一条旧规则;dsc migrate 会把它写成上面的语句形式。
UE.* 节点与通用 UE.Expression
UE.<Name>(…) 会创建一个 UMaterialExpression。<Name> 是节点目录中的一个类 —— 任何引擎表达式类
用其短名(Sine、CameraVectorWS),或者目录携带的 1.x 别名之一(TexCoord、
TransformVector……)。UE.Expression(Class = …) 则改为在参数里写类名;两者是同一条路径
(自 2.0.0 起)。
// DShader/Materials/M_UEBuiltins.dsm
Shader(Name="Materials/M_UEBuiltins")
{
Settings = {
Domain = "UI";
ShadingModel = "Unlit";
}
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
float2 uv = UE.TexCoord(Index = 0);
float t = UE.Time();
float3 view = UE.CameraVectorWS();
// Generic form: any MaterialExpression class, by name.
float pulse = UE.Expression(Class="Sine", OutputType="float1", Input=t);
// Class= may be omitted when the function name IS the class name.
float pulse2 = UE.Sine(OutputType="float1", Input=t * 0.5);
// Reflected literal properties are written by name.
float3 local = UE.TransformVector(Input = view, Source = "World", Destination = "Local");
Color = float3(uv.x, uv.y, pulse + pulse2 + local.z);
}
}Class="Sine"、"MaterialExpressionSine"、"UMaterialExpressionSine"(自 2.0.0 起)和"/Script/Engine.MaterialExpressionSine"都解析到同一个类。引号可省:Class=Sine和OutputType=float1效果一样。- 参数名先匹配类的输入 pin,再匹配它的一个属性;同名时 pin 优先。pin 接一个
表达式;属性接一个字面量(运行时才算出的值是
DSH4373)。 - 不带
Class=的UE.Expression(…)是DSH5218。 OutputType(别名ResultType)不再是必需的(自 2.0.0 起):legacy 前端会把它从 调用上拿掉,由目录给节点定类型。它仍然决定Custom节点的输出类型;在其他地方,一个数值 token 只 给出单输出节点值的宽度,其余的被丢弃。参见UE.Expression。
警告:
对 UE.* 目录里 1.x 的 27 个名字 —— TexCoord、Time、CameraVectorWS、
TransformVector…… —— legacy 前端保留 1.x 读的那些参数,其余的 —— 未知名字或
位置参数 —— 一律丢掉,带 warning DSH5254(自 2.0.0 起)(1.x 一声不吭地丢掉)。UE.Time(Bogus = 1)
和 UE.TexCoord(0) 照样构建,并说明原因。唯一保留的位置参数是 UE.TransformVector /
UE.TransformPosition 的第一个(→ Input)。在其他任何节点上,位置参数只在目录给了该类
参数顺序时才绑定,否则是 DSH5220。
写成 .dss
// DShader/Materials/M_UEBuiltins.dss
#pragma material(Domain = UI, ShadingModel = Unlit)
export void M_UEBuiltins(inout material m)
{
float2 uv = UE.TexCoord(Index = 0);
float t = UE.Time();
float3 view = UE.CameraVectorWS();
float pulse = UE.Expression(Class = "Sine", Input = t);
float pulse2 = UE.Sine(Input = t * 0.5);
float3 local = UE.TransformVector(Input = view, Source = "World", Destination = "Local");
m.EmissiveColor = float3(uv.x, uv.y, pulse + pulse2 + local.z);
}.dss 不受 legacy 过滤:节点没有的参数是 DSH5213,不是警告。
放在 .dsf 里的 ShaderFunction
.dsf 声明函数资产——ShaderFunction、ShaderLayer、ShaderLayerBlend——旁边可以放 helper 块
(Function、GraphFunction、Namespace、VirtualFunction)。只有 .dsh 能被 import,所以另一个文件里的材质触达生成的函数,
和触达任何已有资产一样:通过 VirtualFunction(自 2.0.0 起)。
和函数在同一个 .dsm 里的材质直接按名字调用它。
// DShader/Functions/F_Tint.dsf
ShaderFunction(Name="Functions/F_Tint")
{
Inputs = {
vec3 InColor;
opt float Strength = 1.0 [
Description = "Preview strength";
SortPriority = 10;
];
}
Outputs = {
vec3 OutColor [Description="Tinted colour"];
}
Settings = {
Description = "Tint helper";
ExposeToLibrary = true;
LibraryCategories = "Examples";
}
Graph = {
OutColor = InColor * Strength;
}
}从材质里调用:
// DShader/Materials/M_UsesTint.dsm
VirtualFunction(Name="F_Tint")
{
Options = { Asset = Path(Game, "Functions/F_Tint"); }
Inputs = {
vec3 InColor;
opt float Strength = 1.0;
}
Outputs = {
vec3 OutColor;
}
}
Shader(Name="Materials/M_UsesTint")
{
Properties = {
vec3 Albedo = vec3(0.6, 0.8, 1.0);
}
Settings = { Domain = "Surface"; ShadingModel = "Unlit"; }
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
// named form; the opt input may be omitted or passed `default`
Color = F_Tint(InColor = Albedo, Strength = 0.5);
}
}- (自 2.0.0 起)1.9.x 及以前,材质 import 函数文件——
import "Functions/F_Tint.dsf";——一次编译构建两个资产。这条 import 现在是DSH2252;VirtualFunction声明F_Tint.dsf构建的那个资产,两个文件各自构建自己的。 - 先构建函数。调用在构建材质时加载资产,找不到是
DSH8219。材质函数每次构建成功都存盘,commandlet 的-All先编译所有.dsf。 - 在它自己的
.dsf里,ShaderFunction按Name的最后一段(F_Tint)调用;在别处按VirtualFunction的Name调用。名字区分大小写(自 2.0.0 起);只靠大小写对上的调用仍能解析,带 warningDSH5275。 - 实参可以全按位置、全命名,或先位置后命名(自 2.0.0 起)(1.x 拒绝混用);命名参数之后再出现位置参数是
DSH2158。 调用里省略的输入必须是opt或有默认值,否则是DSH4217。 opt——或者(自 2.0.0 起)单独一个默认值——是让输入在生成的UMaterialFunction上变成可选的东西; 它的默认值驱动该输入的预览值。- 材质函数认
Description、ExposeToLibrary和LibraryCategories。(自 2.0.0 起)ExposeToLibrary只在LibraryCategories非空时生效,UserExposedCaption不被应用 (warningDSH3264),其他键被忽略并给出 warningDSH3263。而在Shader里,不是材质属性的键 会让构建失败。
写成 .dss
// DShader/Functions/F_Tint.dss
/// @desc Tint helper
/// @library Examples
/// @param Strength Preview strength
export void F_Tint(float3 InColor, float Strength = 1.0, out float3 OutColor)
{
OutColor = InColor * Strength;
}// DShader/Materials/M_UsesTint.dss
/// @asset /Game/Functions/F_Tint
extern void F_Tint(float3 InColor, float Strength = 1.0, out float3 OutColor);
#pragma material(Domain = Surface, ShadingModel = Unlit)
uniform float3 Albedo = float3(0.6, 0.8, 1.0);
export void M_UsesTint(inout material m)
{
float3 Tinted;
F_Tint(Albedo, Strength = 0.5, OutColor = Tinted);
m.EmissiveColor = Tinted;
}不是材质入口的 export 函数就是材质函数;默认参数让输入可选,out 参数是额外的输出,
/// @library 是库分类。材质通过一个由 /// @asset 绑定的 extern 原型调用这个资产——那就是 2.0 的
VirtualFunction。
ShaderLayer 与 ShaderLayerBlend
这两个块生成原生的 UMaterialFunctionMaterialLayer 和 UMaterialFunctionMaterialLayerBlend 资产
(自 1.3.0 起),接口形状由固定的签名规则约束。
// DShader/Layers/L_SimpleSurface.dsf
ShaderLayer(Name="Layers/L_SimpleSurface")
{
Properties = {
VectorParameter LayerColor = float4(0.8, 0.2, 0.1, 1.0) [Group="Layer"];
ScalarParameter LayerRough = 0.5 [Group="Layer"; Slider(0, 1)];
}
Outputs = {
MaterialAttributes Attrs;
}
Graph = {
Attrs.BaseColor = LayerColor.rgb;
Attrs.Roughness = LayerRough;
}
}
ShaderLayerBlend(Name="Layers/LB_Overlay")
{
Properties = {
ScalarParameter Alpha = 0.5 [Group="Blend"; Slider(0, 1)];
}
Inputs = {
MaterialAttributes Bottom;
MaterialAttributes Top;
}
Outputs = {
MaterialAttributes Attrs;
}
Graph = {
Attrs.BaseColor = lerp(Bottom.BaseColor, Top.BaseColor, Alpha);
Attrs.Roughness = lerp(Bottom.Roughness, Top.Roughness, Alpha);
}
}| 块 | 输入 | 输出 |
|---|---|---|
ShaderLayer | 没有,或一个 MaterialAttributes。(自 2.0.0 起)layer 总是恰好得到一个 MaterialAttributes 输入,以它的输出命名——也就是它所作用的那个材质 | 恰好一个 MaterialAttributes |
ShaderLayerBlend | 只能是 MaterialAttributes,至少一个(自 2.0.0 起)(1.x:恰好两个);引擎的图层栈混合两个 | 恰好一个 MaterialAttributes |
- 有其他输入或输出的 layer 是
DSH6204;没有MaterialAttributes输入、或有其他输出的 blendDSH6205;两者没有MaterialAttributes输出都是DSH3278。Layer 和 blend 的控制项放Properties,在那里它们成为生成函数的参数。 - layer 的输出从进来的那个材质开始,所以
L_SimpleSurface改的是它所作用材质的两个属性;blend 的输出从空开始。 - 在(自 UE 5.7 起)上,每个 blend 输入按名字得到一个
BlendInputRelevance:Top/TopLayer是Top,Bottom/BottomLayer/Base/BaseLayer是Bottom,其他名字第一个输入是Bottom、 后面的是Top。更早的引擎上没有这个属性。 - 两个块都不能从
Graph调用(自 2.0.0 起):DSH6208。它们的使用者是材质或材质实例的图层栈。
**自 1.3.0 起弃用。**请改用 ShaderLayer。
MaterialLayer(...) 和 MaterialLayerBlend(...) 仍然作为旧拼写解析,生成的资产也完全相同,
但会给出 warning DSH2251。
写成 .dss
// DShader/Layers/L_SimpleSurface.dss
/// @group Layer
uniform float4 LayerColor = float4(0.8, 0.2, 0.1, 1.0);
/// @group Layer @slider 0 1
uniform float LayerRough = 0.5;
/// @group Blend @slider 0 1
uniform float Alpha = 0.5;
/// @layer
export void L_SimpleSurface(inout material m)
{
m.BaseColor = LayerColor.rgb;
m.Roughness = LayerRough;
}
/// @layerblend
export void LB_Overlay(material Bottom, material Top, inout material Attrs)
{
Attrs.BaseColor = lerp(Bottom.BaseColor, Top.BaseColor, Alpha);
Attrs.Roughness = lerp(Bottom.Roughness, Top.Roughness, Alpha);
}一个文件、两个 export、两个资产:/// @layer 和 /// @layerblend 说明各自是哪一种。
VirtualFunction:包装已有资产
VirtualFunction 不生成任何东西。它声明一个已经存在的 UMaterialFunction 的接口,好让
Graph 块能带类型检查地调用它。
// DShader/VirtualFunctions/BufferWriter.dsh
VirtualFunction(Name="BufferWriter")
{
Options = {
Asset = Path(Game, "MaterialFunctions/F_BufferWriter");
Description = "Existing material function declared for Graph calls.";
}
Inputs = {
float3 Color;
opt float Alpha = 1.0;
}
Outputs = {
float3 Result;
}
}// DShader/Materials/M_Buffered.dsm
import "VirtualFunctions/BufferWriter.dsh";
Shader(Name="Materials/M_Buffered")
{
Properties = { vec3 Tint = vec3(1.0, 0.4, 0.1); }
Settings = { Domain = "Surface"; ShadingModel = "Unlit"; }
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
Color = BufferWriter(Color = Tint, Alpha = 0.5);
}
}提示:
这个例子引用的 /Game/MaterialFunctions/F_BufferWriter 是项目资产。请换成你项目里真实存在的路径——
或者让编辑器帮你写:UMaterialFunction 右键菜单里的 CreateVirtualFunction 会在 DShader/VirtualFunctions 下写一个
对应的 .dsh,Create extern Prototype (2.0) 写出 .dss include 用的 extern 形式。
见编辑器工具。
- 资产可以来自
Options = { Asset = … },也可以来自头部属性VirtualFunction(Name="…", Asset="…")。两者都没有是DSH6312。 - 仅在这个块内,
Settings是Options的别名,Properties是Inputs的别名。Graph或Codesection 是DSH2247。 - 至少要有一个输出(
DSH6313),Name必须是标识符(DSH6311)。 - 在有
Graph调用它之前,什么都不会对照资产检查。调用时,声明的输入和输出按名字匹配资产的 pin——先精确、 再忽略大小写——两者都落空就按位置;什么都匹配不上是DSH8220/DSH8221,资产加载不到是DSH8219。 Path的根:Game、Engine、Plugin.<Name>/Plugins.<Name>、完整对象路径,或者裸的带引号"/Game/…"。 见 Path 资产引用。
写成 .dss
// DShader/VirtualFunctions/BufferWriter.dsh
/// @asset /Game/MaterialFunctions/F_BufferWriter
/// @desc Existing material function declared for Graph calls.
extern float3 BufferWriter(float3 Color, float Alpha = 1.0);// DShader/Materials/M_Buffered.dss
#include "VirtualFunctions/BufferWriter.dsh"
#pragma material(Domain = Surface, ShadingModel = Unlit)
uniform float3 Tint = float3(1.0, 0.4, 0.1);
export void M_Buffered(inout material m)
{
m.EmissiveColor = BufferWriter(Tint, Alpha = 0.5);
}头文件是同一个声明的 2.0 写法——一个 extern 原型;名为 Result 的第一个输出就是它的返回值,
默认参数是调用可以省略的输入。.dsm import 这个头文件和 import 1.x 那个一样,
.dss 两个都能 include。
GraphFunction:把 UE.* 调用提升进 Custom 节点
GraphFunction 的体和 Function 一样是 HLSL,但里面每个 UE.* 调用都会被当作真正的材质节点求值,
并作为额外输入 pin 接到生成的 Custom 节点上。
// DShader/Shared/Wind.dsh
GraphFunction WindPulse(in float2 uv, out float pulse) {
float t = UE.Time();
pulse = sin(uv.x * 8.0 + t);
}// DShader/Materials/M_Wind.dsm
import "Shared/Wind.dsh";
Shader(Name="Materials/M_Wind")
{
Settings = { Domain = "Surface"; ShadingModel = "Unlit"; }
Outputs = {
vec3 Color;
Base.EmissiveColor = Color;
}
Graph = {
vec2 UV = UE.TexCoord(Index = 0);
float Pulse = WindPulse(UV); // value form: one out result
Color = vec3(Pulse, Pulse, Pulse);
}
}生成的 Custom 节点代码,大致形状:
float pulse = (float)0;
// Begin DreamShader source: <file>
// DreamShader custom: WindPulse line <N>
float t = _ds_WindPulse_UE0;
pulse = sin(uv.x * 8.0 + t);
// End DreamShader source: <file>
return pulse;- 生成的 pin 名为
_ds_<Function>_UE<N>:基础名__ds_<Function>_UE<N>经过标识符净化, 其开头的__收成一个_,与已声明的in参数重名的名字会得到_1、_2…… - (自 2.0.0 起) 被提升的调用在函数自己的作用域里绑定:它的参数(每次调用时换成那次调用的实参)和文件的声明。只有调用方
才有的名字是
DSH6325—— 1.x 读的是调用方的Graph作用域。 - 只提升
UE.前缀。Substrate.*调用留作 HLSL 文本,带 warningDSH6316;对另一个Function的调用被嵌进节点,和Function体里一样。 - 被提升的值可以是数字、bool,或者 (自 2.0.0 起) 纹理对象;
MaterialAttributes或 Substrate 值是DSH6329。 GraphFunction不接受SelfContained/Inline修饰符(DSH6307),也不允许递归(DSH6260,以及经由提升出来的调用 的DSH6330)。它接受命名参数 (自 2.0.0 起)。空函数体可以,它的输出是 0。- 语句形式同样可用:没有别处声明时,
WindPulse(UV, Pulse);会声明Pulse(infoDSH5283)。
写成 .dss
// DShader/Materials/M_Wind.dss
#pragma material(Domain = Surface, ShadingModel = Unlit)
/// @custom
void WindPulse(float2 uv, out float pulse)
{
float t = UE.Time();
pulse = sin(uv.x * 8.0 + t);
}
export void M_Wind(inout material m)
{
float2 UV = UE.TexCoord(Index = 0);
float Pulse;
WindPulse(UV, Pulse);
m.EmissiveColor = float3(Pulse, Pulse, Pulse);
}/// @custom 函数同时就是 GraphFunction 和 Function:它的体是原样的 HLSL,里面的 UE. 调用
同样被提升为 Custom 节点的输入。
Layout 与 #Region
Layout 把生成的节点钉在固定位置并画注释框。#Region / #EndRegion 在 Graph 块内
给语句分组;每个 region 在生成图里变成一个注释框。
// DShader/Materials/M_Laid.dsm
Shader(Name="Materials/M_Laid")
{
Properties = {
VectorParameter BaseColor = float4(0.8, 0.8, 0.8, 1.0) [Group="Surface"; SortPriority=10];
ScalarParameter Roughness = 0.55 [Group="Surface"; SortPriority=20];
Texture2D NoiseTex = Path(Engine, "EngineResources/WhiteSquareTexture");
}
Settings = {
Domain = "Surface";
ShadingModel = "DefaultLit";
BlendMode = "Opaque";
}
Outputs = {
float3 Color;
float Rough;
Base.BaseColor = Color;
Base.Roughness = Rough;
}
Graph = {
#Region "Sampling"
vec2 UV = UE.TexCoord(Index = 0);
vec4 Noise = SampleTexture2D(NoiseTex, UV);
#EndRegion
#Region "Surface"
Color = BaseColor.rgb * Noise.rgb;
Rough = Roughness;
#EndRegion
}
Layout = {
Comment(Name="Sampling", X=-1200, Y=-200, W=900, H=400, Color=float4(0.10, 0.16, 0.22, 0.35));
Comment(Name="Surface", X=-1200, Y=260, W=900, H=400);
Node(Var="UV", X=-1100, Y=-120);
Node(Var="Noise", X=-760, Y=-120);
}
}| 调用 | 必填参数 | 可选 |
|---|---|---|
Node | Var(文本)、X、Y(整数) | — |
Comment | Name(文本)、X、Y、W、H(整数) | Color(一个 float4 字面量) |
Var指的是一个Graph变量。Layoutsection 里除Node(…)和Comment(…)之外的东西是DSH3274;缺参数 或参数不是整数是DSH3275,Color写坏了是DSH3276。- 第二个
Layoutsection 会替换第一个,而不是追加,并给出 warningDSH2258—— 这和会追加的Properties、Inputs、Outputs不同。 #Region名可以带引号也可以裸写;指令不区分大小写,region 可以嵌套。没名字的#Region是DSH2216,没有打开的 region 却写了#EndRegion是DSH2217,一直没关的 region 是DSH2218。指令是一个 token,所以每条诊断都保留真实的行列。- region 指令只在
Graph体里读取;Function或GraphFunction体是 HLSL,原样传下去。 #Region名和Layout的Comment名互不相干:上面的Samplingregion 和Sampling注释 是两个框。- Export Decompiled Layout 项目设置开着时(默认开),反编译器会写回节点位置和独立的注释框 —— 写成
#pragma layout行, 或者 legacy 格式下的Layoutsection。
警告:
重新生成会清空目标图。没有被 Layout 钉住的节点位置、手加的节点、手改的节点属性,以及文本以
DreamShader: 开头的注释框,都会被销毁。只有不带这个前缀的注释框能保留。生成之后被手改过的
资产根本不会被重建(DSH8207);见
重新生成。
写成 .dss
// DShader/Materials/M_Laid.dss
#pragma material(Domain = Surface, ShadingModel = DefaultLit, BlendMode = Opaque)
/// @group Surface @sort 10
uniform float4 BaseColor = float4(0.8, 0.8, 0.8, 1.0);
/// @group Surface @sort 20
uniform float Roughness = 0.55;
/// @default /Engine/EngineResources/WhiteSquareTexture
uniform Texture2D NoiseTex;
#pragma layout(Comment, Name = "Sampling", X = -1200, Y = -200, W = 900, H = 400, Color = "0.10 0.16 0.22 0.35")
#pragma layout(Comment, Name = "Surface", X = -1200, Y = 260, W = 900, H = 400)
#pragma layout(Node, Var = "UV", X = -1100, Y = -120)
#pragma layout(Node, Var = "Noise", X = -760, Y = -120)
export void M_Laid(inout material m)
{
#pragma region Sampling
float2 UV = UE.TexCoord(Index = 0);
float4 Noise = NoiseTex.Sample(UV);
#pragma endregion
#pragma region Surface
m.BaseColor = BaseColor.rgb * Noise.rgb;
m.Roughness = Roughness;
#pragma endregion
}#pragma layout(…) 是 2.0 的 Layout 语句,#pragma region / #pragma endregion 是 2.0 的 #Region /
#EndRegion。
从 package 引入
package 就是 DShader/Packages 下的一个目录。它的头文件 import 起来和项目文件完全一样,因为
DShader/Packages 正是第三个解析根。
<Project>/DShader/
Materials/
M_Noisy.dsm
Packages/
@typedreammoon/
dream-noise/
Library/
Noise.dsh// DShader/Materials/M_Noisy.dsm
import "@typedreammoon/dream-noise/Library/Noise.dsh";.dss 写成 #include "@typedreammoon/dream-noise/Library/Noise.dsh",解析规则相同。说明符会依次尝试每个根:
specifier @typedreammoon/dream-noise/Library/Noise.dsh
candidate 1 <Project>/DShader/Materials/@typedreammoon/dream-noise/Library/Noise.dsh missing
candidate 2 <Project>/DShader/@typedreammoon/dream-noise/Library/Noise.dsh missing
candidate 3 <Project>/DShader/Packages/@typedreammoon/dream-noise/Library/Noise.dsh resolved这里同样可以省掉扩展名 —— import "@typedreammoon/dream-noise/Library/Noise"; 是同一条 import。哪个根都解析不到是
DSH8292。package 的编写方式、清单文件和编辑器工具见
Package。
接下来
- 完整示例 — 用这些形状拼出来的完整材质。
- Custom Pass 示例 — .dsp 管线,连同它们的材质和 shader。
- DreamShaderLang 2.0 — 每段第二种写法所用的 .dss 语言。
- dsc migrate — 把 1.x 源文件改写成 .dss,写盘之前先证明。
- 顶层块 — 全部七个块,以及各自生成什么。
- 调用 — 完整调用语法:形式、实参、输出选择。