DreamShaderLang
示例

常用写法

DreamShaderLang 的文件级构件 —— 最小材质、参数、纹理采样、共享头文件、函数、Layer、GraphFunction 与 Layout,每一段都是完整源文件。

本页的每一段都是一个完整文件或一个完整块,短到可以直接粘贴改名。顺序从"能产出资产的最小文件"开始, 一直排到需要 .dsh 头文件、.dsf 函数文件或特定引擎版本的写法。

适用于DreamShaderLang 1.5.0
引擎UE 5.3 – 5.8;有版本门槛的都在正文里标出
假定源码根目录<Project>/DShader —— 即 SourceDirectory 项目设置
生成产物默认只在内存里 —— 见内存材质

每段开头注释里的路径就是该文件假定所在的位置,import 说明符按这个布局解析。用 Path(Engine, …) 引用的资产随引擎自带,所以那几个例子照抄就能加载。

本页讲的是形状。想要能跑出效果的完整材质,见完整示例

最小材质

能产出资产的最小文件:一个参数、一条绑定、一次赋值。

// 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
  • Shader 区分大小写;section 名 PropertiesSettingsOutputsGraph 不区分。见 词法与大小写
  • section 名后的 = 是可选糖 since 1.5.0Properties { … } 解析结果完全一样。section 体内最后一个 ; 同样可省。
  • Root 默认是 /Game,所以头部写成 Shader(Name="Materials/M_Minimal", Root="Game") 含义不变。 完整规则见资产路径
  • 一个 Shader 必须有 Graph 块,或者至少有一个带初始化式的输出声明。两者都没有时解析失败: Shader must provide a Graph block.

参数、分组与元数据

一个文件里覆盖所有参数族,带 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紧凑写法;ScalarParameterVectorParameterTextureSampleParameter2D 则直接点名 Unreal 节点类。两套写法都收录在 Properties 类型
  • Group("…") { … } 作用域 since 1.5.0 会把组名盖到里面每个参数上,并从整个块共用的一个 计数器0, 10, 20, … 分配 SortPriority。显式写的 SortPriority 优先,且不占用槽位 —— 所以上面的 UseDetail 拿到的是下一个自动值,而不是 100。嵌套的组用 | 拼接(Outer|Inner)。
  • const 声明的是 Constant 节点而不是参数,只能用在普通标量、向量或纹理类型上。

裸读一个 StaticSwitchParameter 不是值。 Color = UseDetail; 会失败于 Unknown Graph identifier 'UseDetail'. —— 这个参数必须被调用,带 True=False= (或 A= / B=,或按位置传),并且两个分支的分量数必须一致。

采样纹理的两种写法

上面那两个纹理声明的采样方式不同,而且这个区别不是写法偏好问题。

声明生成的节点采样方式
Texture2D NoiseTex = Path(…);TextureObjectParameter —— 纹理对象,没有输入 pinSampleTexture2D(NoiseTex, UV)
TextureSampleParameter2D DetailMap = Path(…);TextureSampleParameter2D —— 自带 Coordinates pinDetailMap(Coordinates = UV)

pin 调用形式 Name(Pin = …) 只属于十个参数 token —— ChannelMaskParameterStaticComponentMaskParameter,以及八个 *SampleParameter* token。纹理对象参数 (紧凑的 Texture2D,或 TextureObjectParameter)根本没有 pin,所以 NoiseTex(Coordinates = UV) 会失败于 Unknown Graph function 'NoiseTex'.

采样纹理对象请改用保留写法 SampleTexture2D(textureObject, uv)。它区分大小写,并且只接受两个 位置参数。

共享 .dsh 头文件

一个 .dsh 里只能有 FunctionGraphFunctionNamespaceVirtualFunction 块和 import 指令 —— 别的都不行。它自己不产出任何资产,内容会被内联进 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。所以引入 .dsf.dsm 必须显式写扩展名。
  • 说明符按三个根依次解析:import 它的文件所在目录、源码根(DShader)、DShader/Packages。 逃出所属根的候选会被拒绝。
  • ; 可省,'单引号' 也接受,行尾允许跟一个 // 注释 —— 但一条 import 必须独占一行
  • Function 体是 HLSL,不是 Graph 语句:上面的 dotsaturatefloat3(…) 都是 HLSL 内建函数。GLSL 写法会在这些函数体内被改写 —— vec3float3mixlerpfractfracmodfmod
  • InlineSelfContained 的完全别名。修饰符写在 Function 关键字之后,两种写法都不能用在 GraphFunction 上。

写在 /* … */ 块注释里的 import照样会被处理 —— import 扫描器只认 // 前缀。要注释掉一条 import,请用 //,不要用块注释。

写在另一个 FunctionGraphFunction 体内部的命名空间限定调用会被拍平成 Common_ApplyTint,并且永远不会改写成生成出来的符号名,于是输出的 HLSL 引用了一个不存在的函数。 请像上面那样从 Graph 块里调用带命名空间的 helper,或者把函数体复制到调用方里。

调用函数:值形式与语句形式

只有一个输出的函数可以当作值调用。两个及以上输出必须用语句形式,尾部的实参是接收结果的普通变量名。

// 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 = {
        // 语句形式:两个 out 结果,两个目标名
        SplitChannels(Source, Rgb, A);

        // 语句形式:一个 out 结果
        Common::ApplyTint(Rgb, Tint, Tinted);

        // 值形式:单输出函数
        float L    = Common::Luma(Tinted);
        float Soft = Remap01(L);

        Color = Tinted * Soft;
        Alpha = A;
    }
}
被调用者值形式语句形式命名参数
Function可以 —— 恰好一个输出时可以不支持
GraphFunction可以 —— 恰好一个输出时可以不支持
ShaderFunction / ShaderLayer / ShaderLayerBlend可以,输出个数不限可以仅值形式
VirtualFunction可以,输出个数不限可以仅值形式
  • out 目标不需要预先声明 —— 调用本身会创建它们。它们必须是裸标识符,且同一次调用里两个结果不能写进 同一个名字。
  • 参数个数必须精确:值形式是输入个数,语句形式是输入个数加结果个数。
  • 函数名不区分大小写,且没有重载解析。同一个名字解析到多种声明种类时调用失败: Graph call '…' is ambiguous because multiple definitions use that name: ….
  • 数学内置在用户函数之前匹配,永远无法被遮蔽。命名为 lerpdotFunctionGraph 块里不可达,而且没有任何诊断。

放在 .dsf 里的 ShaderFunction

一个 .dsf 可以声明 ShaderFunctionShaderLayerShaderLayerBlendFunctionGraphFunctionNamespaceVirtualFunction —— 除了顶层 Shader 之外都行。

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

    Graph = {
        OutColor = InColor * Strength;
    }
}

从材质里调用:

// DShader/Materials/M_UsesTint.dsm
import "Functions/F_Tint.dsf";

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 = {
        // 命名形式;opt 输入可以省略,也可以传 `default`
        Color = F_Tint(InColor = Albedo, Strength = 0.5);
    }
}
  • 这条 import 必须显式带 .dsf 扩展名。
  • ShaderFunction 可以用完整 NameFunctions/F_Tint)查找,也可以用最后一段 / 分隔的名字(F_Tint),都不区分大小写。
  • 实参要么全按位置、要么全命名;混用会失败于 ShaderFunction 'F_Tint' input arguments cannot mix positional and named forms.
  • 让输入在生成的 UMaterialFunction 上变成可选的是 opt;它的默认值驱动该输入的 Preview pin。 省略一个非 opt 输入会失败于 ShaderFunction 'F_Tint' is missing required input 'InColor'.
  • 编译这个 .dsm 会顺带生成被 import 的 ShaderFunction 资产 —— 材质函数总是先于调用它的材质写出。
  • 材质函数认得的 Settings 键只有四个:DescriptionUserExposedCaptionExposeToLibraryLibraryCategories。其他键在这里被静默忽略;这一点和 Shader 不同,在 Shader 里未知键是硬错误。

函数声明了多个输出时,在调用点用 Output="Name"(或 OutputName= / OutputIndex=)挑一个; 位置参数可以用 default 跳过:

vec3 tinted = F_Tint(Albedo, default, Output="OutColor");

ShaderLayerShaderLayerBlend

这两个块生成原生的 UMaterialFunctionMaterialLayerUMaterialFunctionMaterialLayerBlend 资产 since 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恰好一个 MaterialAttributes
ShaderLayerBlend恰好两个,都是 MaterialAttributes恰好一个 MaterialAttributes
  • Layer 的控制项Properties,绝不放 Inputs —— 那两条诊断 (… Use Properties for layer controls.… Use Properties for blend controls.)说的就是这件事。
  • since UE 5.7 上,名为 Top / TopLayer,或 Bottom / BottomLayer / Base / BaseLayer 的 blend 输入会被打上对应的 BlendInputRelevance。更早的引擎上这些名字只是普通名字, 只有顺序有意义。

已弃用 1.3.0

请改用 ShaderLayer

MaterialLayer(...)MaterialLayerBlend(...) 仍然作为兼容别名解析,生成的资产也完全相同, 但各自会发一条警告:MaterialLayer is deprecated; use ShaderLayer instead.MaterialLayerBlend is deprecated; use ShaderLayerBlend instead. 之后的所有诊断都报现代写法。

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 执行 DreamShader ▸ Create Virtual Function, 会在 DShader/VirtualFunctions 下生成对应的 .dsh

  • 资产可以来自 Options = { Asset = … },也可以来自头部属性 VirtualFunction(Name="…", Asset="…")。 两者都没有时解析失败:VirtualFunction 'BufferWriter' must provide Options = { Asset = Path(...); }.
  • 仅在这个块内,SettingsOptions 的别名,PropertiesInputs 的别名。GraphCode section 直接被拒绝。
  • 至少要有一个输出。
  • 声明的输入/输出名会不区分大小写地去匹配资产上的 pin 名,匹配不上再按序号回退。两者都落空时失败于 VirtualFunction 'BufferWriter' output 'Result' does not exist on MaterialFunction asset '…'.
  • Path 的根:GameEnginePlugin.<Name> / Plugins.<Name>、完整对象路径,或者裸的带引号 "/Game/…"。见 Path 资产引用

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);          // 值形式:一个 out 结果
        Color = vec3(Pulse, Pulse, Pulse);
    }
}

生成的 Custom 节点代码,大致形状:

float pulse = (float)0;
float t = __ds_WindPulse_UE0;
pulse = sin(uv.x * 8.0 + t);
return pulse;
  • 生成的 pin 名为 __ds_<Function>_UE<N>,会与已声明的参数名做去重。
  • 只扫描 UE. 前缀。GraphFunction 体里的 Substrate.* 调用、数学内置和用户函数调用都原样留作 HLSL 文本。
  • 被提升的值不能是纹理对象、MaterialAttributesSubstrate —— 这些穿不过 Custom 节点的输入 pin。
  • GraphFunction since 1.3.1 不接受 SelfContained / Inline 修饰符、不接受命名实参、 不允许递归(GraphFunction cycle detected: …)。空函数体是错误。
  • 语句形式同样可用:WindPulse(UV, Pulse); 会创建 Pulse

Layout#Region

Layout 把生成的节点钉在固定位置并画注释框。#Region / #EndRegionGraph 块内给语句分组, 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);
    }
}
调用必填参数可选
NodeVar(文本)、XY(整数)
CommentName(文本)、XYWH(整数)Color,一个 float4 字面量
  • Var 指的是一个 Graph 变量。除此之外没有别的合法 Layout 语句: Unknown Layout statement '…'.
  • 没有这个 section 时,注释框默认 W=420H=240Color = (0.10, 0.16, 0.22, 0.35)
  • 第二个 Layout section 会替换第一个,而不是追加 —— 这和会追加的 PropertiesInputsOutputs 不同。
  • #Region 的名字可以带引号也可以裸写;指令不区分大小写,并且会被替换成等长的空格串,所以诊断的 行号列号都不受影响。region 可以嵌套。
  • region 指令只在 Graph 块里处理 —— FunctionGraphFunction 体内都不处理。

重新生成会清空目标图。没有被 Layout 钉住的节点位置、手加的节点、手改的节点属性,以及文本以 DreamShader: 开头的注释框,都会被销毁。只有不带这个前缀的注释框能保留。见 重新生成

从 package 引入

package 就是 DShader/Packages 下的一个目录。它的头文件 import 起来和项目文件完全一样,因为 DShader/Packages 正是第三个 import 解析根。

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

说明符会依次尝试每个根:

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。 package 的编写方式、清单文件和编辑器工具见 Package

接下来

本页目录