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
| Rule | Detail |
|---|---|
| Condition parentheses | required — if x > 0 { } fails with Graph if statement is missing a condition block. |
| Body braces | required on both branches — a braceless single-statement body fails with Graph if statement is missing a '{ ... }' body. |
| Statement terminator | not 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 case | if and else are case-sensitive. They are the only two keywords in the Graph language; every other name is case-insensitive. |
| Body contents | every 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.
| Operator | Meaning |
|---|---|
> | 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:
- The text is scanned left to right at nesting depth 0.
( ),{ },[ ]depth and string literals are tracked and skipped. - 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, soa >= bsplits on>=and never on>. - 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.
- With no operator match, the whole text is the left operand and the operator is truthy.
- 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:
| Operator | AGreaterThanB | AEqualsB | ALessThanB |
|---|---|---|---|
> | then | else | else |
< | else | else | then |
>= | then | then | else |
<= | else | then | then |
== | else | then | else |
!= | then | else | then |
| truthy | then | else | then |
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:
- Copy the current variable map; run every then-statement against the copy.
- Copy the current variable map again; run every else-statement against that copy.
- 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.
ifis 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
Ifnodes 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.
| Step | Rule | Failure |
|---|---|---|
| 1 | names matching a declared property or parameter are skipped — materialising a parameter node is a read side effect, not an assignment | — |
| 2 | the name must be present in both branch maps | Graph if statement could not resolve both branch values for '{Name}'. |
| 3 | expected shape = the enclosing value's shape, if the name existed before the if | — |
| 4 | otherwise, expected shape = the matching Outputs declaration's shape, if there is one | — |
| 5 | otherwise, the two branch values must agree exactly on component count and all four kind flags | Graph if branches assign variable '{Name}' with inconsistent types (no trailing period) |
| 6 | the expected shape must not be a texture object | Graph if statement cannot select texture value '{Name}'. |
| 7 | the expected shape must not be a Substrate value | Graph if statement cannot select Substrate value '{Name}'. |
| 8 | both branch values are coerced to the expected shape | Graph if branches assign incompatible values to '{Name}'. {Error} |
| 9 | the If node is created and wired | Graph 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 kind | Behaviour |
|---|---|
| scalar / vector, 1–4 components | selected normally |
MaterialAttributes | selected, provided both branches produce one |
| texture object | rejected — Graph if statement cannot select texture value '{Name}'. |
Substrate | rejected — Graph if statement cannot select Substrate value '{Name}'. |
mixed MaterialAttributes and numeric | rejected — 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
| Message | Cause | Fix |
|---|---|---|
| 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 types | New 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
- Calls —
StaticSwitchParameter, the compile-time alternative toif - What Graph Is Not — loops,
switch,?:and the silent-truncation catalogue
Expressions and Conversions
The four operators, their precedence, literals, constructors, swizzles, and the coercion rules that decide when a value silently changes width.
Calls
Calling Function, GraphFunction, ShaderFunction, VirtualFunction and parameter pins from a Graph block — value form, statement form, arguments and output selection.