DreamShaderLang
The Graph Language

Statements

Every statement form a Graph body can contain — declarations, assignments, member writes, calls — plus splitting, classification and scope.

A Graph body is a flat list of statements. There are ten forms, they are separated by ;, and the parser decides which form a piece of text is by a fixed sequence of textual tests — not by keywords. Understanding that classification order explains most of the surprising diagnostics in the language.

Synopsis

graph-statement :=
      [ <type> ] <name> [ = <expression> ] ;                      // declaration / assignment
    | <type> <name> [ = <init> ] , <name> [ = <init> ] … ;        // comma declarators
    | <name> = { <expression> , … } ;                             // brace initializer
    | <name> . <member> = { <expression> | { <expression> , … } } ;
    | <call-expression> ;                                         // statement-form call
    | if ( <condition> ) { <graph-statement> … }
      [ else { <graph-statement> … } | else if ( … ) { … } … ]
NotationMeaningExample
<x>Placeholder — substitute a real value; the angle brackets are not typed.Name = <string>
[ x ]Optional — the whole group may be left out.[, Root = <string>]
{ a | b }Choice — take exactly one of the alternatives separated by |.{ Node( … ) | Comment( … ) }
Repetition — the preceding item may appear any number of times.<property-declaration> …

The ten forms

#FormExampleTerminator
1Declaration, no initializerfloat3 c;;
2Declaration + expression initializerfloat3 c = A * K;;
3Declaration + brace initializervec4 v = {rgb, 1.0};;
4Assignment to an existing or output variableColor = Tint;;
5Assignment + brace initializerColor = {r, g, b};;
6MaterialAttributes member writeAttrs.BaseColor = Tint;;
7Member write + brace initializerAttrs.BaseColor = {1, 0, 0};;
8Comma-separated declaratorsfloat a = 1, b, c = 3;;
9Statement-form callF_Split(Src, OutA, OutB);;
10if / else / else ifif (m > .5) { … } else { … }none — brace matched

Form 8 expands to one statement per declarator. All of them share the type token of the first declarator, and all of them report the same source line and column.

How a body is split

RuleBehaviour
Separator; at top level only. Parenthesis, brace, bracket depth and string state are tracked, so a ; inside ( ), { }, [ ] or "…" does not split.
Repeated ;runs of ; collapse; empty statements are dropped without a diagnostic
Trailing ;the final statement of a body may omit its ;
if statementsdetected before the ; scan and delimited by brace matching, so an if needs no ; and a ; inside its body does not split it
Leading whitespaceskipped; every statement records a 1-based line and column relative to the block
Nested bodiesif / else bodies go through the same splitter, so every form above is legal inside a branch, including a nested if
Graph = {
    float a = 1.0;;;             // the empty statements between ; are dropped
    if (a > 0.5) { a = 0.0; }    // no ; needed; the inner ; does not split the if
    float b = a * 2.0            // final statement, ; omitted
}

Classification order

Given one statement's text, the parser tests these rules in order. The first that matches wins.

OrderTestResult
1starts with the keyword if (case-sensitive, identifier-bounded)form 10
2splitting on top-level , yields more than one segment and the first segment, minus its = …, splits into a type and a nameform 8
3no top-level =, and the text splits into a type and a nameform 1
4no top-level =, and it does notform 9
5has a top-level =, and the left side splits into a type and a nameform 2, or form 3 when the right side is { … }
6has a top-level =, and the left side does notform 4/5, or form 6/7 when the target contains a .

"Splits into a type and a name" means: split at the last top-level whitespace character, tracking ( ) depth and string state; both halves must be non-empty. That is why UE.Panner(Speed = 0.1) P; is a declaration — the space inside the argument list is not at top level.

The keyword probe in rule 1 is case-sensitive and is the only case-sensitive construct in the statement grammar. If (x) { … } is not an if statement: it falls through to rule 3, splits into type If (x) and name { … }, and fails with Unsupported Graph variable type 'If (x)'. — a message that never mentions if. This is also why almost every unsupported construct is reported as an unresolvable variable type; see What Graph Is Not.

The name half of rules 3 and 5 is only required to be non-empty — it is not validated as an identifier. float3 A.B = x; therefore declares a variable literally named A.B; it is not a member write. Member writes are reachable only through rule 6, which requires the left side not to split into a type and a name. Declarators after the first in form 8 are identifier-checked.

Declarations

<type> <name> [ = { <expression> | <brace-initializer> } ]
     [ , <name> [ = { <expression> | <brace-initializer> } ] ] … ;

brace-initializer := { [ <expression> [ , <expression> ] … ] }

Accepted type tokens

Token(s)ComponentsNotes
float float1 half half1 int uint bool1
float2 half2 vec2 int2 uint2 bool2 ivec2 uvec2 bvec22
float3 half3 vec3 int3 uint3 bool3 ivec3 uvec3 bvec33
float4 half4 vec4 int4 uint4 bool4 ivec4 uvec4 bvec44
MaterialAttributes0see below
Substrate0since UE 5.4 requires an initializer
StaticBool StaticBoolParameter1unreleased
Texture2D SamplerState0texture object of dimension Texture2D; requires an initializer
TextureCube0requires an initializer
Texture2DArray0requires an initializer
Texture3D VolumeTexture0dimension VolumeTexture; requires an initializer

Comparison is case-insensitive. For the numeric rows, MaterialAttributes and Substrate, all internal spaces are removed before matching, so float 3 resolves as float3 and Material Attributes as MaterialAttributes. The texture rows and StaticBool are matched on the token as written, so Texture 2D does not resolve.

int, uint, bool and half are alternative spellings of the same float widths — the token fixes the component count and nothing else. The full per-context catalogue is on Types and Values.

Declarations without an initializer

Declared typeResult
scalar (1 component)one Constant node with R = 0
vector (2–4 components)the same zero Constant appended N times through AppendVector — 1 constant node and N−1 append nodes
MaterialAttributesa MakeMaterialAttributes node, component count 0
Substrateerror — Graph variable type '{Type}' requires an explicit initializer.
any texture type, SamplerStatethe same error
unresolvable tokenerror — Unsupported Graph variable type '{Type}'.
float3 c;              // Constant(0) -> AppendVector -> AppendVector
MaterialAttributes A;  // MakeMaterialAttributes, ready for member writes
Texture2D T;           // error: requires an explicit initializer

Declarations with an initializer

The initializer is evaluated first, then the declaration is typed:

OrderStep
1the type token must resolve, else Unsupported Graph variable type '{Type}' for '{Name}'.
2if the value carries an authoritative component count, both sides are plain numeric, and the counts differ — the value is stored as-is, unchanged, with no diagnostic
3otherwise the value is coerced to the declared type; failure gives Graph variable '{Name}' is declared as '{Type}' but assigned an incompatible value. {Detail}

Step 2 means a declared width can be ignored. float2 dir = UE.CameraVectorWS(); stores a 3-component value and reports nothing; the mismatch resurfaces at the first operator or binding that cannot reconcile it. Add an explicit swizzle (UE.CameraVectorWS().xy) when a narrower value is actually wanted. The full rule, and the list of values that are authoritative, is on Expressions and Conversions.

Coercion at step 3 silently narrows a wider value by prefixing an r / rg / rgb mask, and splats a scalar up to the declared width. It never widens 2 components to 3.

Comma declarators

float a = 1, b, c = 3;
RuleBehaviour
Recognitionapplies only when splitting on top-level , yields more than one segment and the first segment, minus its = …, splits into a type and a name
Shared typeevery declarator uses the type token of the first declarator; a type token on a later declarator is not accepted
Declarator namesmust be bare identifiers — [A-Za-z_][A-Za-z0-9_]*
Initializerseach declarator may carry its own = <expression> or = { … }, or none
Source locationall resulting statements report the same line and column as the whole list

The example declares three 1-component values: a initialized to 1, b defaulted to 0, c initialized to 3. A declarator that is not a bare identifier fails with In Graph statement '{Text}': '{Declarator}' is not a valid declarator in a comma-separated declaration.

Brace initializers

A right-hand side whose trimmed text is at least two characters long, starts with { and ends with } is a brace initializer. It is re-serialised as <TargetType>( <inner> ) and evaluated as an ordinary constructor call, so it inherits every constructor rule: positional arguments only, a single scalar splats to all channels, multiple arguments must sum to exactly the target width. See Constructors.

{} is special-cased: it produces the target type's default value, exactly as if the declaration had no initializer.

Target-type resolution, in order:

OrderSituationTarget type
1the statement is a declarationthe declared type token
2the target is a MaterialAttributes memberthe attribute's own type
3the target is an existing variablederived from its component count: 0 → MaterialAttributes, 1 → float, 2 → float2, 3 → float3, 4 → float4
4the target matches an Outputs declarationthe same component-count mapping
5none of the aboveerror — Brace initializer assignment for '{Name}' requires a declared scalar or vector target type.

Texture and Substrate targets are rejected at steps 3 and 4.

Brace initializers do not nest. float4 m = {{1,2},{3,4}}; re-serialises to float4({1,2},{3,4}), and { is not a token the expression lexer knows, so it fails with Invalid brace initializer for type 'float4'. Unexpected token '{' in Graph expression. Write float4(float2(1,2), float2(3,4)) instead.

Graph = {
    float r = 1.0;
    float g = 0.5;
    float b = 0.2;
    vec3 rgb  = {r, g, b};      // -> vec3(r, g, b)
    vec4 rgba = {rgb, 1.0};     // -> vec4(rgb, 1.0), mixed-width packing
    Color = rgba.rgb;
}

Redeclaration

A declaration whose name is already bound fails with Graph variable '{Name}' is declared more than once. The lookup tries the exact spelling first, then falls back to a case-insensitive scan, so float a = 1; float A = 2; is a redeclaration error — while A and a refer to the same value everywhere else. To rebind an existing name, omit the type token and write an assignment.

Assignment

Forms 4 and 5. The target is the left side of the top-level =, taken verbatim after trimming.

<target> = <expression> ;
<target> = { <expression> , … } ;

The target is resolved in this order:

OrderTargetBehaviour
1a name containing a . that splits into two non-empty halvesMaterialAttributes member write
2an existing Graph valuethe value is coerced to the existing binding's shape: component count, texture flag, texture type, Substrate flag
3an Outputs declaration of the enclosing blockthe value is coerced to the declared type
4nothing yeta new variable is created carrying the value's own shape — no type token needed

Lookup in rules 2 and 3 is case-insensitive.

Rule 4 means an undeclared name on the left of = is not an error:

Graph = {
    vec3 Base = Tint * 2.0;
    Scratch   = Base.rgb;    // legal: creates 'Scratch' as a 3-component value
    Color     = Scratch;     // 'Color' is an Outputs declaration -> coerced to its type
}

Coercion at rules 2 and 3 silently narrows. Assigning a float4 to a float3 target drops the alpha channel with no warning and no node cost. If a channel disappears from a generated material, check every assignment whose right-hand side is wider than its target. See Conversions.

Expression statements

Form 9. A statement that is neither a declaration nor an assignment is evaluated as an expression and accepted only when it is a call:

RequirementDiagnostic when unmet
the expression is a callGraph expression statements currently support only Function calls with explicit out arguments.
the callee flattens to a nameGraph expression statements must call a named Function.
the name resolves to exactly one Function, GraphFunction, ShaderFunction, ShaderLayer, ShaderLayerBlend or VirtualFunctionGraph expression statement '{Text}' is unsupported. Only DreamShader Function, GraphFunction, ShaderFunction, ShaderLayer, ShaderLayerBlend, or VirtualFunction calls may use statement syntax.
exactly one, not severalGraph expression statement '{Text}' is ambiguous because multiple callable definitions exist.

In statement form the arguments are the declared inputs followed by one plain variable name per output, in declaration order. Statement calls are positional-only for every callee kind. Full argument rules are on Calls.

MaterialAttributes member writes

Forms 6 and 7 write one attribute of a MaterialAttributes value. The type is a struct-like Graph value with component count 0 since 1.2.5; it carries a whole material's attribute set as one connection.

Outputs = { MaterialAttributes Attrs; Base.MaterialAttributes = Attrs; }
Graph   = {
    MaterialAttributes Attrs;      // the Outputs declaration does NOT create it
    Attrs.BaseColor = Tint;
    Attrs.Roughness = {0.35};
    float Rough = Attrs.Roughness; // a read: BreakMaterialAttributes
}

An Outputs declaration does not create the Graph variable. Only output declarations that carry an initializer become synthesized statements. Writing a member of a name that exists only as Outputs { MaterialAttributes Attrs; } fails with Unknown MaterialAttributes variable 'Attrs'. Declare it in the Graph block as well — the two declarations do not collide, because the redeclaration guard only looks at Graph variables.

Each member write chains a new SetMaterialAttributes node onto the variable's current value and rebinds the variable to it; N writes produce N nodes. Each member read creates a new BreakMaterialAttributes node — reads are not deduplicated.

Members

Member names are matched case-insensitively on both the read and the write side. Aliases are exact synonyms.

MemberAliasesComponentsLegacy Break index
BaseColor30
Metallic11
Specular12
Roughness13
Anisotropy14
EmissiveColorEmissive35
Opacity16
OpacityMask17
Normal38
Tangent39
WorldPositionOffsetWPO310
SubsurfaceColor311
CustomData0ClearCoat112
CustomData1ClearCoatRoughness113
AmbientOcclusionAO114
Refraction315
CustomizedUV0CustomizedUVs0216
CustomizedUV1CustomizedUVs1217
CustomizedUV2CustomizedUVs2218
CustomizedUV3CustomizedUVs3219
CustomizedUV4CustomizedUVs4220
CustomizedUV5CustomizedUVs5221
CustomizedUV6CustomizedUVs6222
CustomizedUV7CustomizedUVs7223
PixelDepthOffsetPDO124
Displacement126
DiffuseColor3none
SpecularColor3none
SurfaceThickness1none
FrontMaterial since UE 5.41none

MaterialAttributes and its alias Attributes resolve as material properties but are explicitly rejected as members. Any unrecognised name fails with Unsupported MaterialAttributes member '{Member}'.

A read picks the Break output by name first — the attribute's display name is compared case-insensitively against every output on the node — and falls back to the legacy index only when no name matches.

The last four members have no legacy index, so they are readable only when the engine build's BreakMaterialAttributes node exposes a matching output name. Otherwise the read fails with BreakMaterialAttributes does not expose member '{Member}'. SurfaceThickness in particular is not exposed on every build. Writes are unaffected — only reads go through BreakMaterialAttributes.

Member write rules

RuleFailure
The target splits on its first .; both halves must be non-emptyInvalid MaterialAttributes member assignment target '{Target}'.
The base name must already be a Graph variableUnknown MaterialAttributes variable '{Base}'.
The base variable must hold a MaterialAttributes valueGraph variable '{Base}' is not a MaterialAttributes value.
The member must resolve and must not be MaterialAttributesUnsupported MaterialAttributes member '{Member}'.
The value must coerce to the member's component countMaterialAttributes member '{Member}' expects {Count} component(s). {Detail}
The statement must have a right-hand sideMaterialAttributes member assignment '{Target}' requires a value.

Because the split is on the first ., Attrs.BaseColor.r = 1.0; is read as base Attrs, member BaseColor.r and fails with Unsupported MaterialAttributes member 'BaseColor.r'. There is no per-channel attribute write — build the full value first. Attribute values also reject arithmetic, constructors and swizzles; .member on one is always an attribute read, never a channel mask.

Scope

There is no block scope. One value map exists per builder — one per Shader, one per generated material function — and every declaration writes into it.

Branches are executed against copies of the enclosing map, and the copies are then merged:

CaseOutcome
the name is declared in both branches with the same shapemerged through a UMaterialExpressionIf and written into the enclosing map — visible after the if
the name is declared in both branches with different shapeserror — Graph if branches assign variable '{Name}' with inconsistent types
the name is declared in only one brancherror — Graph if statement could not resolve both branch values for '{Name}'.
the name is a texture or Substrate valueerror — Graph if statement cannot select texture value '{Name}'. / … cannot select Substrate value '{Name}'.

A branch-local temporary is not local. Declaring a helper in one branch only is an error, not a discarded name. Declare it above the if, or declare it in both branches with the same width. And because a merged name becomes an ordinary entry of the enclosing map, declaring that same name again after the if is a redeclaration error. The full merge algorithm is on Control Flow.

Synthesized statements

Every Outputs declaration of the enclosing block that carries a default value is turned into a declaration statement and prepended before the first statement of the Graph body. These statements have no source location, so their diagnostics carry no usable line number.

MessageCause
Output declaration initializer requires a type and name.the synthesized declaration had a blank type or name
Output declaration '{Name}' has an empty initializer.the Outputs default value text was empty

This is also what makes Graph = { } legal for a Shader: the initialized output declaration is the body. See Output Bindings.

Diagnostics

MessageCauseFix
In Graph statement '{Text}': {Detail}Any parse failure inside a statement; {Detail} is the inner message.
In Graph statement '{Text}': '{Declarator}' is not a valid declarator in a comma-separated declaration.A declarator after the first is not a bare identifier.Use one type token and bare names: float a = 1, b, c;
Encountered an invalid empty Graph statement.A statement with no expression, no declaration and no brace initializer.
Encountered a Graph assignment without a target variable.The left side of = was empty, e.g. = 5;
Failed to evaluate Graph assignment for '{Name}'. {Detail}The right-hand side failed to evaluate.
Failed to assign Graph member '{Name}'. {Detail}A MaterialAttributes member write failed.Details
Graph variable '{Name}' is declared more than once.Redeclaration; the check is case-insensitive.Drop the type token to reassign instead of redeclaring.
Graph variable '{Name}' was previously assigned an incompatible value. {Detail}Assignment to an existing variable could not be coerced to its shape.Details
Graph output variable '{Name}' was assigned an incompatible value. {Detail}Assignment to an Outputs name could not be coerced to its declared type.Details
Unsupported Graph variable type '{Type}'.An uninitialized declaration whose type token does not resolve — also what almost every unsupported construct reports.Details
Unsupported Graph variable type '{Type}' for '{Name}'.An initialized declaration whose type token does not resolve.Details
Graph variable type '{Type}' requires an explicit initializer.A texture, SamplerState or Substrate declaration with no =.
Graph variable '{Name}' uses Substrate, which requires Unreal Engine 5.4 or newer.Substrate declared on UE 5.3.Details
Graph builder is not initialized.Internal guard; the builder ran without a target material.

While the statements run, the progress text reads Building DreamShader graph nodes ({N} statements)... and Evaluating DreamShader graph statement {I} of {N}.... For bodies with more than 512 statements, only every 64th statement updates the text.

The complete cross-stage list is in the diagnostics index.

Example

Every statement form in one body:

ShaderFunction(Name="Functions/F_AllForms")
{
    Properties = { vec3 Tint = vec3(1.0, 0.4, 0.1); float K = 2.0; }
    Inputs     = { vec2 UV; }
    Outputs    = { MaterialAttributes Attrs; vec3 Debug; }

    Graph = {
        MaterialAttributes Attrs;              //    the Outputs declaration does not create it
        float3 c;                              // 1  declaration, no initializer
        float3 scaled = Tint * K;              // 2  declaration + expression
        vec4   packed = {scaled, 1.0};         // 3  declaration + brace initializer
        c = packed.rgb;                        // 4  assignment
        Debug = {0.0, 0.0, 0.0};               // 5  assignment + brace initializer
        Attrs.BaseColor = c;                   // 6  member write
        Attrs.Roughness = {0.35};              // 7  member write + brace initializer
        float a = 1, b, d = 3;                 // 8  comma declarators

        if (K > 1.0) {                         // 10 if / else
            Debug = c * a;
        } else {
            Debug = c * b;
        }
    }
}

Form 9, the statement-form call, is the one shape missing here; it needs a callee, and it has its own page.

See also

On this page