DreamShaderLang
The Graph Language

Control Flow

if / else in a Graph block — a build-time select that materialises both branches, and the truthy form that means != 0 rather than > 0.

if / else is the only control-flow statement in the Graph language, and it is not a jump. Both branches are executed at generation time, every node in both of them ends up in the finished material, and a UMaterialExpressionIf picks between the two results per pixel.

Synopsis

if ( <condition> )
{
    <graph-statement>
}
[ else
{
    <graph-statement>
} ]

<condition> := <expression> [ { >= | <= | == | != | > | < } <expression> ]

else if chains are legal and are captured as raw text, then re-parsed as a nested if inside the else body — an else if chain is a tree of nested statements, not a flat list.

Requirements

RuleDetail
Condition parenthesesrequiredif x > 0 { } fails with Graph if statement is missing a condition block.
Body bracesrequired on both branches — a braceless single-statement body fails with Graph if statement is missing a '{ ... }' body.
Statement terminatornot required — the extent is computed by brace matching, so no ; follows the closing }, and a ; inside a body never splits the statement. A stray ; after } is skipped as an empty statement.
Keyword caseif and else are case-sensitive. They are the only two keywords in the Graph language; every other name is case-insensitive.
Body contentsevery statement form is legal inside a branch, including a nested if

If (x) {} and ELSE {} are not control flow. Because the keyword match is case-sensitive, the text falls into the declaration classifier, and If (x) {} reports Failed to declare Graph variable '{}'. Unsupported Graph variable type 'If (x)'. — a message that never mentions if. See What Graph Is Not.

The condition

The condition text is split textually, before the expression parser runs. Comparison operators are not part of the expression grammar — this is the only place any of them is accepted.

OperatorMeaning
>left greater than right
<left less than right
>=left greater than or equal to right
<=left less than or equal to right
==left equal to right
!=left not equal to right
(none)the truthy form — see below

How the split is performed:

  1. The text is scanned left to right at nesting depth 0. ( ), { }, [ ] depth and string literals are tracked and skipped.
  2. At each position the operators are probed in the fixed order >=, <=, ==, !=, >, <. The first position that matches wins, and a two-character operator is always probed before the one-character operator that prefixes it, so a >= b splits on >= and never on >.
  3. If a match is found but either side is empty after trimming, the split is discarded and the whole text becomes a truthy condition instead.
  4. With no operator match, the whole text is the left operand and the operator is truthy.
  5. Each side goes to the ordinary expression parser. A failure is reported as In Graph if condition '{Condition}': {Error}.

Both operands must evaluate to a scalar — one component — and must not be a texture object, a MaterialAttributes value or a Substrate value.

Truthy semantics

if (x) is wired as x != 0, not as x > 0. A negative value is truthy: if (Height) takes the then-branch for Height = -1.0 exactly as it does for Height = 1.0.

// Written — the then-branch runs for -1.0 as well as for 1.0.
if (Height) { Color = Hot; } else { Color = Cold; }

// What that actually compiles to:
if (Height != 0) { Color = Hot; } else { Color = Cold; }

// If the sign is what you meant, say so:
if (Height > 0) { Color = Hot; } else { Color = Cold; }

This matches HLSL and C semantics, and the decompiler's != 0 convention. Write the comparison explicitly whenever the sign matters.

The right operand of a truthy condition is a synthesized Constant node holding 0.

A condition whose right side is empty, such as if (Mask >), does not error. The split is rejected because the right operand is blank, the whole text Mask > becomes a truthy condition, and the trailing > is discarded as end-of-input. The result is if (Mask != 0).

&& and || are silently dropped

& and | are not tokens. The expression parser treats them as end-of-input and accepts everything before them, so a compound condition quietly loses every conjunct after the first. No diagnostic is produced.

In if (a > 0 && b > 0) the first > splits the text into left a and right 0 && b > 0; the right-hand side parses successfully as 0; the compiled condition is exactly a > 0.

// Written — compiles, but the second test is discarded.
if (Mask > 0.5 && Alpha > 0.5) { Color = Hot; } else { Color = Cold; }

// Equivalent, and what the compiler actually built:
if (Mask > 0.5) { Color = Hot; } else { Color = Cold; }

// Write nested ifs instead:
if (Mask > 0.5) {
    if (Alpha > 0.5) { Color = Hot; } else { Color = Cold; }
} else {
    Color = Cold;
}

The same fate awaits %, ?:, [ ], ~, and the shift and bitwise operators anywhere in a condition. See What Graph Is Not.

Node wiring

One UMaterialExpressionIf node is created per merged variable. A receives the condition's left operand, B the right operand (or the synthesized zero), and the three result pins are wired from the branch values:

OperatorAGreaterThanBAEqualsBALessThanB
>thenelseelse
<elseelsethen
>=thenthenelse
<=elsethenthen
==elsethenelse
!=thenelsethen
truthythenelsethen

The result takes its component count and its MaterialAttributes flag from the then branch. It is never a texture object and never a Substrate value.

Both branches are always built

There is no dead-code elimination and no build-time constant folding of the condition. Executing an if does exactly this, in order:

  1. Copy the current variable map; run every then-statement against the copy.
  2. Copy the current variable map again; run every else-statement against that copy.
  3. Restore the enclosing map and merge.

What that means in practice:

  • Every node in both branches exists in the generated material. A branch that is never taken at runtime still costs shader instructions. if is a select, not a jump.
  • An error inside either branch fails the build, even a branch that a constant condition could never reach. Body errors are wrapped as In Graph if body: {Error} / In Graph else body: {Error}.
  • A variable declared inside a branch does not stay local — it participates in the merge.
  • Node reuse is not scoped to a branch. A subexpression first built in the then-branch is reused verbatim in the else-branch and after the merge.
  • The condition is evaluated once per merged variable. The operands go through the reuse cache, so there is one set of condition nodes — but N If nodes for N merged variables.

Parameter nodes are the exception to that last reuse note. A property is materialized into the current value map, and property names are deliberately excluded from the merge, so a parameter first read inside a branch does not survive the if. Reading it again elsewhere builds a second parameter node. The material behaves correctly — both nodes address the same parameter — but the graph carries duplicates.

// Two ScalarParameter nodes named 'Threshold'.
if (u > 0.5) { Color = Lit.rgb * Threshold; } else { Color = Dark.rgb * Threshold; }

// One.
float T = Threshold;
if (u > 0.5) { Color = Lit.rgb * T; } else { Color = Dark.rgb * T; }

The merge

A name is a branch output when it is present in a branch's map and is either absent from the enclosing map or bound to a different value there. Two values count as the same only when the node, output index, component count, all five channel-mask fields, the texture flag, the texture type, the MaterialAttributes flag and the Substrate flag all match.

StepRuleFailure
1names matching a declared property or parameter are skipped — materialising a parameter node is a read side effect, not an assignment
2the name must be present in both branch mapsGraph if statement could not resolve both branch values for '{Name}'.
3expected shape = the enclosing value's shape, if the name existed before the if
4otherwise, expected shape = the matching Outputs declaration's shape, if there is one
5otherwise, the two branch values must agree exactly on component count and all four kind flagsGraph if branches assign variable '{Name}' with inconsistent types (no trailing period)
6the expected shape must not be a texture objectGraph if statement cannot select texture value '{Name}'.
7the expected shape must not be a Substrate valueGraph if statement cannot select Substrate value '{Name}'.
8both branch values are coerced to the expected shapeGraph if branches assign incompatible values to '{Name}'. {Error}
9the If node is created and wiredGraph if statement failed to merge '{Name}'. {Error}

Step 1 is why reading a parameter inside a single branch is legal at all. Without it, if (m > T) { Color = Lit.rgb; } else { Color = Dark.rgb; } would fail, because Lit is materialised into the then-map only and Dark into the else-map only.

Branch-local declarations leak into the merge. A float3 Temp = …; declared only in the then-branch is a branch output, so the merge demands a Temp in the else-branch too. Declaring it in both branches with different widths fails as well.

// Fails: 'Blend' is float3 in one branch and float in the other.
if (Mask > 0.5) { float3 Blend = float3(1.0, 0.0, 0.0); Color = Blend; }
else            { float  Blend = 0.25;                  Color = float3(Blend, Blend, Blend); }

// Works: one declaration, one shape, assigned in both branches.
float3 Blend = float3(0.0, 0.0, 0.0);
if (Mask > 0.5) { Blend = float3(1.0, 0.0, 0.0); }
else            { Blend = float3(0.25, 0.25, 0.25); }
Color = Blend;

Because a merged name becomes an ordinary entry of the enclosing map, declaring that same name again after the if is a redeclaration error.

What cannot be selected

Value kindBehaviour
scalar / vector, 1–4 componentsselected normally
MaterialAttributesselected, provided both branches produce one
texture objectrejected — Graph if statement cannot select texture value '{Name}'.
Substraterejected — Graph if statement cannot select Substrate value '{Name}'.
mixed MaterialAttributes and numericrejected — Graph if branches cannot mix MaterialAttributes and numeric values.

To branch on textures, sample both and select the resulting numeric values, or move the choice into a StaticSwitchParameter on the sampled results. See Calls.

Diagnostics

MessageCauseFix
Graph if statement is missing a condition block.No ( after if.Parenthesise the condition: if (x) { ... }
Graph if statement has an unterminated condition block.The condition's ( is never closed.
Graph if statement is missing a '{ ... }' body.No { after the condition, including a braceless single-statement body.Braces are mandatory on both branches.
Graph if statement has an unterminated body block.The then-body's { is never closed.
Graph else statement is missing a '{ ... }' body.else is followed by neither { nor if.
Graph else statement has an unterminated body block.The else-body's { is never closed.
Graph if condition is empty.if (), or a condition that is only whitespace.
In Graph if condition '{Condition}': {Error}Either side of the condition failed to parse — commonly !x, a = b, or a leading comparison operator.Details
In Graph if body: {Error}A statement inside the then-body failed.Details
In Graph else body: {Error}A statement inside the else-body failed.Details
Graph if condition left side must evaluate to a scalar value.The left operand is a vector, a texture object, a MaterialAttributes value or a Substrate value.Swizzle it down to one channel, or compare a scalar derived from it.
Graph if condition right side must evaluate to a scalar value.The same, for the right operand.
Failed to evaluate Graph if condition. {Error}The left or right operand expression could not be evaluated.
Failed to create a zero literal for Graph if condition.The truthy form's Constant(0) node could not be created.
Graph if statement could not resolve both branch values for '{Name}'.The name is a branch output in one branch only — usually a declaration or assignment present in only one body.Declare or assign it in both branches, or hoist the declaration above the if.
Graph if branches assign variable '{Name}' with inconsistent typesNew in both branches with different shapes, and nothing outside fixes the expected shape. Emitted without a trailing period.
Graph if branches assign incompatible values to '{Name}'. {Error}A branch value could not be coerced to the expected shape.Details
Graph if statement cannot select texture value '{Name}'.A texture object is a branch output.
Graph if statement cannot select Substrate value '{Name}'.A Substrate value is a branch output.
Graph if branches cannot mix MaterialAttributes and numeric values.One branch produces an attribute value and the other a numeric value.
Graph if statement failed to merge '{Name}'. {Error}The If node could not be built for this name.
Unsupported Graph if comparison operator '{Operator}'.Defensive guard; the splitter can only produce the six operators and truthy.

Example

Shader(Name="Docs/M_IfElse")
{
    Properties {
        float Mask = 0.6;
        vec3  Tint = vec3(1.0, 0.2, 0.2);
    }

    Settings {
        Domain       = "UI";
        ShadingModel = "Unlit";
    }

    Outputs {
        vec3 Color;
        Base.EmissiveColor = Color;
    }

    Graph {
        if (Mask > 0.5) {
            Color = Tint;
        } else if (Mask > 0.25) {
            Color = Tint * 0.5;
        } else {
            Color = vec3(0.0, 0.0, 0.0);
        }
    }
}
VectorParameter  Tint
ScalarParameter  Mask
Constant         0.5                 (shared)
Constant         0.25
Multiply         Tint * 0.5
Constant3Vector  (0,0,0)             (folded literal)
If               A=Mask B=0.25  ->   inner else-if merge of 'Color'
If               A=Mask B=0.5   ->   outer merge of 'Color'

Both If nodes exist because the else if is a nested if, and every branch's nodes are present in the material regardless of the runtime value of Mask.

See also

  • Statements — the forms legal inside a branch, and declaration scope
  • Expressions and Conversions — the grammar the operands are parsed with, and the coercion applied at the merge
  • CallsStaticSwitchParameter, the compile-time alternative to if
  • What Graph Is Not — loops, switch, ?: and the silent-truncation catalogue

On this page