DreamShaderLang
Examples

Patterns

The file-shaped building blocks of DreamShaderLang — minimal material, parameters, texture sampling, shared headers, functions, layers, GraphFunction and Layout — each one a complete source file.

Every snippet on this page is a whole file or a whole block, small enough to paste and rename. They run from the smallest thing that produces an asset to the constructs that need a .dsh header, a .dsf function file or a particular engine version.

Applies toDreamShaderLang 1.5.0
EnginesUE 5.3 – 5.8; anything version-gated is marked inline
Assumed source root<Project>/DShader — the SourceDirectory project setting
Generated outputin memory by default — see In-memory Materials

The path in each leading comment is where the file is assumed to live; import specifiers resolve against that layout. Assets referenced with Path(Engine, …) ship with the engine, so those examples load as written.

This page is about shapes. For complete materials that do something, see Recipes.

A minimal material

The smallest file that produces an asset: one parameter, one binding, one assignment.

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

Generated asset:

package     /Game/Materials/M_Minimal
object path /Game/Materials/M_Minimal.M_Minimal
  • Shader is matched case-sensitively; the section names Properties, Settings, Outputs and Graph are not. See Lexical Elements.
  • The = after a section name is optional sugar since 1.5.0: Properties { … } parses identically. The final ; inside a section body is optional too.
  • Root defaults to /Game, so the header could equally read Shader(Name="Materials/M_Minimal", Root="Game") with no change in meaning. Full rules: Asset Paths.
  • A Shader needs either a Graph block or at least one initialized output declaration. With neither, the parse fails with Shader must provide a Graph block.

Parameters, groups and metadata

Every parameter family in one file, with a Group("…") scope, a [ … ] metadata block and the Slider(min, max) shorthand.

// 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 are the compact spellings; ScalarParameter, VectorParameter and TextureSampleParameter2D name the Unreal node explicitly. Both sets are catalogued on Property Types.
  • A Group("…") { … } scope since 1.5.0 stamps its name onto every parameter inside and assigns SortPriority 0, 10, 20, … from one counter shared by all groups in the block. An explicit SortPriority wins and does not consume a slot — which is why UseDetail here takes the next automatic value rather than 100. Nested groups compose with | (Outer|Inner).
  • const declares a Constant node instead of a parameter. It is legal only with a plain scalar, vector or texture type.

A bare read of a StaticSwitchParameter is not a value. Color = UseDetail; fails with Unknown Graph identifier 'UseDetail'. — the parameter has to be called, with True= and False= (or A= / B=, or positionally), and both branches must have the same component count.

Two ways to sample a texture

The two texture declarations above are sampled differently, and the difference is not cosmetic.

DeclarationNode generatedHow to sample it
Texture2D NoiseTex = Path(…);TextureObjectParameter — a texture object, no input pinsSampleTexture2D(NoiseTex, UV)
TextureSampleParameter2D DetailMap = Path(…);TextureSampleParameter2D — owns a Coordinates pinDetailMap(Coordinates = UV)

The pin call form Name(Pin = …) belongs to exactly ten parameter tokensChannelMaskParameter, StaticComponentMaskParameter, and the eight *SampleParameter* tokens. A texture object parameter (a compact Texture2D, or TextureObjectParameter) has no pins at all, so NoiseTex(Coordinates = UV) fails with Unknown Graph function 'NoiseTex'.

Sample a texture object with the reserved SampleTexture2D(textureObject, uv) form instead. It is matched case-sensitively and takes exactly two positional arguments.

A shared .dsh header

A .dsh may contain Function, GraphFunction, Namespace and VirtualFunction blocks plus import directives — nothing else. It generates no asset of its own; its contents are inlined into whatever imports it.

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

Importing it:

// 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" is equivalent: when the specifier carries no extension at all, .dsh is appended. Importing a .dsf or .dsm therefore requires the explicit extension.
  • The specifier resolves against three roots, in order: the importing file's own directory, the source root (DShader), then DShader/Packages. A candidate that escapes its root is rejected.
  • The ; is optional, 'single quotes' are accepted, and a trailing // comment is allowed — but an import must be alone on its line.
  • Function bodies are HLSL, not Graph statements: dot, saturate and float3(…) above are HLSL intrinsics. GLSL spellings are rewritten inside those bodies — vec3float3, mixlerp, fractfrac, modfmod.
  • Inline is an exact alias of SelfContained. The modifier goes after the Function keyword, and neither spelling is accepted on a GraphFunction.

An import line inside a /* … */ block comment is still processed — the import scanner only recognises the // prefix. Comment an import out with //, never with a block comment.

A namespace-qualified call written inside another Function or GraphFunction body is flattened to Common_ApplyTint and never rewritten to the generated symbol, so the emitted HLSL references an undefined function. Call namespaced helpers from a Graph block, as above, or duplicate the body into the calling function.

Calling functions: value form and statement form

A function with exactly one output may be called as a value. Two or more outputs require the statement form, whose trailing arguments are plain variable names that receive the results.

// 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: single-output functions
        float L    = Common::Luma(Tinted);
        float Soft = Remap01(L);

        Color = Tinted * Soft;
        Alpha = A;
    }
}
Callee kindValue formStatement formNamed arguments
Functionyes — with exactly one outputyesno
GraphFunctionyes — with exactly one outputyesno
ShaderFunction / ShaderLayer / ShaderLayerBlendyes, any output countyesvalue form only
VirtualFunctionyes, any output countyesvalue form only
  • Out targets need not be declared first — the call creates them. They must be bare identifiers, and two results may not be written into the same name in one call.
  • Argument count is exact: the input count for the value form, inputs plus results for the statement form.
  • Function names are matched case-insensitively and there is no overload resolution. If one name resolves to more than one declaration kind, the call fails with Graph call '…' is ambiguous because multiple definitions use that name: ….
  • The math builtins are matched before user functions and can never be shadowed. A Function named lerp or dot is unreachable from a Graph block, with no diagnostic.

A ShaderFunction in a .dsf

A .dsf may declare ShaderFunction, ShaderLayer, ShaderLayerBlend, Function, GraphFunction, Namespace and VirtualFunction blocks — everything except a top-level 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;
    }
}

Calling it from a material:

// 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 = {
        // named form; the opt input may be omitted or passed `default`
        Color = F_Tint(InColor = Albedo, Strength = 0.5);
    }
}
  • The import needs the explicit .dsf extension.
  • A ShaderFunction is looked up by its full Name (Functions/F_Tint) or by the last /-separated segment (F_Tint), case-insensitively.
  • Arguments are either all positional or all named; mixing fails with ShaderFunction 'F_Tint' input arguments cannot mix positional and named forms.
  • opt is what makes an input optional on the generated UMaterialFunction; its default value drives the input's Preview pin. A non-opt input that is omitted fails with ShaderFunction 'F_Tint' is missing required input 'InColor'.
  • Compiling the .dsm also generates the imported ShaderFunction asset — material functions are written before the material that calls them.
  • The four Settings keys a material function honours are Description, UserExposedCaption, ExposeToLibrary and LibraryCategories. Other keys are silently ignored here, unlike a Shader, where an unknown key is a hard error.

When a function declares several outputs, select one at the call site with Output="Name" (or OutputName= / OutputIndex=); positional inputs may be skipped with default:

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

ShaderLayer and ShaderLayerBlend

These generate native UMaterialFunctionMaterialLayer and UMaterialFunctionMaterialLayerBlend assets since 1.3.0, and their interfaces are fixed by arity rules.

// 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);
    }
}
BlockInputsOutput
ShaderLayerat most one, and it must be MaterialAttributesexactly one MaterialAttributes
ShaderLayerBlendexactly two, both MaterialAttributesexactly one MaterialAttributes
  • Layer controls go in Properties, never in Inputs — that is what the two diagnostics (… Use Properties for layer controls. and … Use Properties for blend controls.) are telling you.
  • On since UE 5.7 a blend input named Top / TopLayer, or Bottom / BottomLayer / Base / BaseLayer, is tagged with the matching BlendInputRelevance. On earlier engines the names are ordinary and only the order matters.

Deprecated since 1.3.0

Use ShaderLayer instead.

MaterialLayer(...) and MaterialLayerBlend(...) still parse as compatibility aliases and generate the same assets, but each emits a warning: MaterialLayer is deprecated; use ShaderLayer instead. and MaterialLayerBlend is deprecated; use ShaderLayerBlend instead. Every later diagnostic reports the modern spelling.

VirtualFunction: wrapping an existing asset

A VirtualFunction generates nothing. It declares the interface of a UMaterialFunction that already exists, so Graph blocks can call it with type checking.

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

This example references /Game/MaterialFunctions/F_BufferWriter, a project asset. Substitute a path that exists in your project — or let the editor write the declaration for you: the DreamShader ▸ Create Virtual Function action on a UMaterialFunction emits a matching .dsh under DShader/VirtualFunctions.

  • The asset may come from Options = { Asset = … } or from the header attribute VirtualFunction(Name="…", Asset="…"). Without one, the parse fails with VirtualFunction 'BufferWriter' must provide Options = { Asset = Path(...); }.
  • Settings is an accepted alias for Options, and Properties is an accepted alias for Inputs inside this block only. Graph and Code sections are rejected outright.
  • At least one output is required.
  • Declared input and output names are matched against the asset's pin names case-insensitively, with an ordinal fallback. A name that resolves to neither fails with VirtualFunction 'BufferWriter' output 'Result' does not exist on MaterialFunction asset '…'.
  • Path roots: Game, Engine, Plugin.<Name> / Plugins.<Name>, a full object path, or a bare quoted "/Game/…". See Asset References.

GraphFunction: hoisting a UE.* call into a Custom node

A GraphFunction body is HLSL like a Function, but every UE.* call inside it is evaluated as a real material node and wired into the generated Custom node as an extra input pin.

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

Generated Custom-node code, in outline:

float pulse = (float)0;
float t = __ds_WindPulse_UE0;
pulse = sin(uv.x * 8.0 + t);
return pulse;
  • The generated pin is named __ds_<Function>_UE<N>, uniquified against the declared parameter names.
  • Only the UE. prefix is scanned. Substrate.* calls, math builtins and user function calls in a GraphFunction body are left as plain HLSL text.
  • A hoisted value may not be a texture object, MaterialAttributes or Substrate — those cannot cross a Custom-node input pin.
  • GraphFunction since 1.3.1 accepts no SelfContained / Inline modifier, no named call arguments and no recursion (GraphFunction cycle detected: …). An empty body is an error.
  • The statement form works too: WindPulse(UV, Pulse); creates Pulse.

Layout and #Region

Layout pins generated nodes to fixed positions and draws comment boxes. #Region / #EndRegion group statements inside a Graph block, and the region names become comment boxes in the generated graph.

// 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);
    }
}
CallRequired argumentsOptional
NodeVar (text), X, Y (integers)
CommentName (text), X, Y, W, H (integers)Color, a float4 literal
  • Var names a Graph variable. Nothing else is a valid Layout statement: Unknown Layout statement '…'.
  • When the section is absent, comment boxes default to W=420, H=240 and Color = (0.10, 0.16, 0.22, 0.35).
  • A second Layout section replaces the first rather than appending — unlike Properties, Inputs and Outputs, which append.
  • #Region names may be quoted or bare; the directives are matched case-insensitively and are replaced by equal-length runs of spaces, so diagnostic line and column numbers are unaffected. Regions nest.
  • Region directives are processed only inside a Graph block — never inside a Function or GraphFunction body.

Regeneration clears the target graph. Node positions not pinned by Layout, hand-added nodes, node property tweaks and comment boxes whose text begins with DreamShader: are destroyed. Only comment boxes without that prefix survive. See Regeneration.

Importing from a package

A package is a directory under DShader/Packages. Its headers import exactly like project files, because DShader/Packages is the third import resolution root.

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

The specifier is tried against each root in turn:

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

Dropping the extension works here too — import "@typedreammoon/dream-noise/Library/Noise"; is the same import. Package authoring, the manifest and the editor tooling are covered on Packages.

Where to next

On this page