DreamShaderLang
The Graph Language

Expressions and Conversions

The four operators, their precedence, literals, constructors, swizzles, and the coercion rules that decide when a value silently changes width.

The value-producing half of the Graph language: literals, identifiers, calls, member access and four arithmetic operators, evaluated into UMaterialExpression nodes. Everything on this page also applies inside an Outputs binding expression and an Outputs declaration default, which are single expressions in the same grammar.

Synopsis

<expression>     := <additive>
<additive>       := <multiplicative> [ { + | - } <multiplicative> ] …
<multiplicative> := <unary> [ { * | / } <unary> ] …
<unary>          := { + | - } <unary> | <postfix>
<postfix>        := <primary> [ <postfix-op> ] …
<postfix-op>     := . <identifier> | :: <identifier> | ( [ <argument> [ , <argument> ] … ] )
<primary>        := <identifier> | <number-literal> | <string-literal> | ( <additive> )
<argument>       := [ <identifier> = ] <additive>

That is the whole grammar. ( ) , . :: = + - * / are the only punctuation it knows.

Precedence and associativity

Highest binding first. The parser implements exactly these four levels — there is no fifth.

LevelOperatorsArityAssociativityNotes
1f(…) call, .member, ::namepostfixleftchains freely: A::F(x).rgb.b
2+ -unary, prefixrightrecursive, so --x parses and is legal
3* /binarylefta / b / c is (a / b) / c
4+ -binarylefta - b - c is (a - b) - c
( … ) groupingoverrides the levels above; generates no node

Operators that do not exist

CategorySpellings absent from the grammar
Modulo% — use the fmod / mod builtin
Comparison== != < > <= >= — legal only in an if condition
Logical&& || !
Bitwise / shift& | ^ ~ << >>
Conditional? :
Increment / decrement++ --
Compound assignment+= -= *= /=
Assignment inside an expression=, outside a named call argument
Indexing[ ] — use a swizzle
Comma operator,, outside a call argument list
Matrix types and operatorsnone exist anywhere in the language

Unknown characters terminate an expression silently. The tokenizer maps every character it does not recognise to the end-of-input token, and the parser accepts an expression followed by end-of-input. So a % b compiles as a, a && b as a, v[0] as v — with no diagnostic. Two positions do report it: a leading unknown character (!x) errors, and one inside parentheses ((a % b)) errors because the ) is never found. Wrapping a suspect expression in parentheses is the reliable way to make truncation visible. The full catalogue is on What Graph Is Not.

Operand rules

Both operands of + - * / are tested in this order.

#TestOutcome when it fails
1Neither operand is a texture objectArithmetic operators cannot be applied to texture values.
2Neither operand is a MaterialAttributes valueArithmetic operators cannot be applied to MaterialAttributes values.
3Neither operand is a Substrate valueArithmetic operators cannot be applied to Substrate values.
4Component counts are equal, or either operand is a scalarone rescue attempt (rule 5), then rule 6
5Rescue: if exactly one operand carries an authoritative component count, the other may be widened to itrescue skipped
6Compatibility re-testedOperator '{Op}' requires matching vector sizes or a scalar/vector pair, got {Left} and {Right} component(s).

Rule 5 in full: the widened operand must be the one without an authoritative count, the authoritative count must be greater than zero, and the other operand's count must be less than or equal to it. Narrowing is never attempted at an operator — dropping channels there is deliberately refused so the size error surfaces instead.

There is no separate scalar-promotion step. A scalar operand is passed to the material node as-is and Unreal replicates it, so A * K with A a vec3 and K a float produces a single Multiply node, not an AppendVector splat.

Result of a binary operator

PropertyValue
NodeAdd / Subtract / Multiply / Divide
Component countmax(left, right) after rule 5
Authoritative component countset when either operand had one
Input channel maskcleared — the result is a full-width value
Integer markernot propagated; the result of any operator is non-integer

Unary operators

FormLoweringNode costResult
+xidentitynonex unchanged, all flags preserved
-xMultiply(x, Constant(-1))one Multiply, plus one Constant shared with every other unary minus in the assetcomponent count of x

-2.0 is not folded into a negative literal by the expression evaluator: it becomes Constant(2.0) * Constant(-1). Inside a constructor argument it is folded, because constant folding accepts a leading + or - on a literal — vec3(-1.0, 0.0, 1.0) emits a single Constant3Vector.

Integer division

/ is the only operator with an extra type rule:

Integer division is not supported by the material graph; use float() or floor(a/b).

It fires when both operands carry the integer marker. That marker is set by exactly one thing: a direct call to an integer constructor (int, int2..4, ivec2..4, uint, uint2..4, uvec2..4). It is not set by literals, by variables, by suffixed literals such as 3u, or by the result of any operator.

ExpressionResult
int(7) / int(2)error
int(7) / 2allowed — 2 is a literal and carries no marker
7 / 2allowed — a float Divide producing 3.5
float(int(7)) / int(2)allowed — every constructor assigns the marker from its own name, so float(…) clears it
int a = int(7); int b = int(2); a / berror — the marker travels with the variable
int a = 7; int b = 2; a / ballowed — the int declaration type sets no marker; only an int(…) call does

The marker exists solely to reject this one case. int, uint, bool and half are otherwise indistinguishable from float in the generated graph — there is no integer arithmetic and no truncation.

Literals

Numeric literals

<number-literal> := { <digit> | . } … [ { e | E } [ { + | - } ] <digit> … ] [ <suffix> ]
<suffix>         := { f | F | h | H | u | U | l | L }

Every numeric literal is a 1-component floating-point value. There is no integer literal type.

WrittenValueResult
11.0Constant, 1 component
1.01.0the same Constant node as 1
.50.5Constant — a leading . is legal
1e-30.001Constant
2.5E+2250.0Constant
0.55f0.55Constant — the suffix is stripped
3u3.0Constantnot an integer value
0.5.50.5Constant — the conversion keeps the longest valid prefix and silently discards the rest
0x10error: Unexpected token 'x10' in Graph expression.
1.0fxerror: Unexpected token 'fx' in Graph expression.
3ulerror: Unexpected token 'ul' in Graph expression.

Lexing details:

  • Digits and . are consumed greedily into one token. Multiple . are accepted, and the numeric conversion is permissive: it takes the longest valid prefix and throws the remainder away without a diagnostic, so 0.5.5 is 0.5 and 0..5 is 0.
  • At most one exponent is consumed. A second e / E terminates the token and begins an identifier.
  • There is no hexadecimal, binary or octal notation, and no digit separators.
  • A leading - is not part of the literal; it is the unary minus operator.

The eight suffixes f F h H u U l L are consumed and excluded from the token text. They have no semantic effect at all — not on the type, not on the width, not on the integer marker. Exactly one suffix character is allowed, and it is consumed only when the next character is not alphanumeric and not _. That is what distinguishes 0.55f; from 1.0fx.

String literals

A string literal is tokenized with escapes decoded and never evaluates to a value. It is legal only where an argument handler reads the literal text rather than evaluating it: a named UE.* or Substrate.* argument, an Output= / OutputName= selector, or a Path(…) asset reference.

EscapeProduces
\nline feed
\rcarriage return
\ttab
\""
\\\
\ + any other characterthat character, backslash dropped
vec4 Scene = UE.SceneTexture(Id = "PostProcessInput0");   // fine
float x    = "0.5";   // String literals can only be used in named UE builtin arguments.

An unterminated string literal is not diagnosed. The lexer consumes to end of input and emits what it collected; the failure surfaces later as a missing ) or a truncated statement.

Boolean literals

true and false (unreleased) lex as ordinary identifiers and resolve late — after Graph variables and after declared properties. They produce a UMaterialExpressionStaticBool node with one component, matched case-insensitively, and are the only way to feed a StaticSwitch input or a StaticBool function input from a Graph block.

Because they resolve after variables and properties, a Graph variable or a declared property named True or False shadows the literal, silently. Avoid those names.

Identifier resolution

A bare identifier is looked up on three surfaces, in order:

OrderSurfaceCaseResult
1an existing Graph value — declared, assigned, or a parameter already materialized in this scopeinsensitivethe stored value
2a declared property or parameter — function-local Properties first, then the enclosing block'sinsensitivethe parameter node, created on first read and cached under the property's declared name
3true / falseinsensitivea StaticBool node
4no matchUnknown Graph identifier '{Name}'.

Step 1 compares the exact spelling first, then falls back to a case-insensitive scan. Step 2 is where a parameter becomes a node: it is created lazily on first read and inserted into the value map, so every later read is served by step 1.

A StaticSwitchParameter property deliberately does not resolve as a bare identifier — it needs its two branches. Reading one by name fails with Unknown Graph identifier '{Name}'.; use the call form. See Calls.

Graph variable lookup is case-insensitive, but the value map is keyed by the spelling used at the assignment. float3 Color = …; followed by color = …; writes a second entry; later reads take whichever the exact-match test finds, and fall back to the first case-insensitive hit otherwise. Keep one spelling per variable.

Constructors

<constructor-call> := <constructor-name> ( <expression> [ , <expression> ] … )

34 names, all matched case-insensitively. Component count is decided purely by the last character of the name: 2 → 2, 3 → 3, 4 → 4, anything else → 1.

NameComponentsInteger-markedFamily
float1noHLSL float
float11noHLSL float
float22noHLSL float
float33noHLSL float
float44noHLSL float
half1noHLSL half — identical behaviour to float
half11noHLSL half
half22noHLSL half
half33noHLSL half
half44noHLSL half
vec22noGLSL float vector
vec33noGLSL float vector
vec44noGLSL float vector
int1yesHLSL signed integer
int22yesHLSL signed integer
int33yesHLSL signed integer
int44yesHLSL signed integer
ivec22yesGLSL signed integer vector
ivec33yesGLSL signed integer vector
ivec44yesGLSL signed integer vector
uint1yesHLSL unsigned integer
uint22yesHLSL unsigned integer
uint33yesHLSL unsigned integer
uint44yesHLSL unsigned integer
uvec22yesGLSL unsigned integer vector
uvec33yesGLSL unsigned integer vector
uvec44yesGLSL unsigned integer vector
bool1noHLSL boolean
bool22noHLSL boolean
bool33noHLSL boolean
bool44noHLSL boolean
bvec22noGLSL boolean vector
bvec33noGLSL boolean vector
bvec44noGLSL boolean vector

Names that look like they should exist and do not: vec1, ivec1, uvec1, bvec1 (the GLSL families start at 2); int1, uint1, bool1 (only float1 and half1 have a 1 spelling); hvec2..4 (no GLSL half family); double, dvec2..4; float2x2, float3x3, float4x4, mat2, mat3, mat4 (there are no matrix types anywhere in the language); and the removed aliases Scalar, Vector, Color. Each of those lexes as an ordinary identifier, falls through to the user-function lookup, and reports Unknown Graph function '{Name}'.

Constructor names shadow everything. The constructor test is the first step of call resolution, before UE.*, Substrate.*, math builtins, parameters and every user declaration. A Function, GraphFunction, ShaderFunction or VirtualFunction named float3, int, bool4 or vec2 can be declared without a diagnostic and can never be called — every call site builds a constructor instead. Rename it.

Argument rules

Arguments are evaluated left to right and screened before any arity check.

#ConditionBehaviour
1any argument is named, as in float3(x = 1.0)error: Constructor '{Name}' does not accept named arguments.
2any argument is a texture objecterror: Constructor '{Name}' cannot use Texture2D arguments.
3any argument is a MaterialAttributes valueerror: Constructor '{Name}' cannot use MaterialAttributes arguments.
4any argument is a Substrate valueerror: Constructor '{Name}' cannot use Substrate arguments.

Then, with N the constructor's component count:

#CaseBehaviourNodes
5N == 1, exactly one argument, that argument has 1 componentreturned unchanged, then the integer marker is set from the namenone
6N == 1, anything elseerror: Constructor '{Name}' expects a single scalar input.
7N > 1, exactly one scalar argumentsplat: replicated N timesN−1 AppendVector
8N > 1, exactly one argument that already has N componentsreturned unchangednone
9N > 1, otherwisethe arguments' component counts must sum to exactly NN−1 AppendVector
10N > 1, sum ≠ Nerror: Constructor '{Name}' expects {N} total components but got {Total}.

Rule 9 is a straight left-to-right concatenation of channels; mixed widths are fine as long as the total is exact.

CallTotalValidResult
float4(rgb, 1.0)3 + 1yesfloat4
float4(uv, uv)2 + 2yesfloat4
float3(x, yz)1 + 2yesfloat3
vec3(0.5)scalar splatyes(0.5, 0.5, 0.5)
float3(rgba)4noConstructor 'float3' expects 3 total components but got 4.
float3(uv)2noConstructor 'float3' expects 3 total components but got 2.
float(rgb)3noConstructor 'float' expects a single scalar input.
float3()0noConstructor 'float3' expects 3 total components but got 0.

A constructor never narrows and never zero-fills. float3(rgba) is an error, not a truncation — write rgba.rgb. float3(uv) is an error too — write float3(uv, 0.0). Widening happens only from a scalar. Narrowing happens only at the coercion sites listed under Conversions.

Constant folding

When N >= 2, the name is not an integer constructor, and every argument is unnamed and a numeric literal (optionally with a leading unary + or -), the whole call collapses to one constant-vector node.

WrittenFolded to
vec2(0.5, 1.0)Constant2Vector(0.5, 1.0)
vec3(0.5)Constant3Vector(0.5, 0.5, 0.5) — the single literal is replicated first
float4(1.0, 2.0, 3.0, -1.0)Constant4Vector(1, 2, 3, -1)
int3(1, 2, 3)not folded — integer constructors are excluded so the marker survives
vec3(K, 0.0, 0.0)not folded — K is not a literal

The folded node is the only constructor result that carries an authoritative component count. Folded vectors dedupe by value, so vec3(0.5) written five times is one Constant3Vector.

Swizzles

<swizzle>  := <expression> . <channels>
<channels> := <channel> [ <channel> ] [ <channel> ] [ <channel> ]
<channel>  := { x | y | z | w | r | g | b | a }
CharactersChannel index
x, X, r, R0
y, Y, g, G1
z, Z, b, B2
w, W, a, A3

Exactly two sets exist: xyzw and rgba. There is no stpq set and no uv set — neither u nor v resolves, so .uv fails. Channel characters are case-insensitive, and mixing the two sets in one swizzle is accepted: .xg is channels 0 and 1, exactly like .xy.

Length and bounds

RuleViolation
One to four channel characters5 or more: Unsupported swizzle '{Channels}'.
Every character must resolve to a channel indexSwizzle '{Channels}' is invalid for a value with {Count} components.
Every channel index must be less than the base value's component countthe same message

A swizzle can only narrow or rearrange; it can never read past the base width.

BaseSwizzleResult
float4.rgbfloat3
float3.aerror — channel 3 is not less than 3
float2.xyzerror
float.x, .rthe base value, unchanged
float.xx, .rrrsplat to float2 / float3
float.y, .z, .w, .g, .b, .aerror

A scalar does not splat through an out-of-range channel. Roughness.yz on a scalar parameter is a hard error, not a two-component broadcast: Swizzle 'yz' is invalid for a value with 1 components. To broadcast a scalar, repeat channel 0 (Roughness.xx), use a constructor (float2(Roughness)), or rely on scalar widening at an assignment.

Lowering

Three strategies, tried in this order.

#Applies whenNodes createdResult
1the base has an expression node and the channel indices are strictly increasing with no repeatsnonethe selection is recorded as a channel mask and applied when the value is connected to a pin
2base has 2–4 components and the mask is reordered or repeatedN−1 AppendVectoreach channel becomes its own single-channel masked value, then the pieces are concatenated
3base has 1 component and every channel is in rangeN−1 AppendVectorthe scalar is replicated N times
SwizzleChannelsStrategyNode cost
.r / .x010
.a310
.rg / .xy0,110
.rgb / .xyz0,1,210
.ga1,31 — increasing, gaps allowed0
.xg0,11 — mixed sets are still increasing0
.rgba0,1,2,310
.gr1,021
.bgr2,1,02 since 1.3.32
.xxx0,0,022
.rrgg0,0,1,123
.rr on a float0,031

"Strictly increasing" is judged on the source channel of the underlying node, not on the letters written.

Composition

A swizzle of an already-masked value re-maps through the existing mask, so the numbering is always relative to the value being swizzled, not to the original node.

WrittenMeaning
v.rgb.bchannel 2 of v.rgb selects 0,1,2, then .b picks entry 2
v.ga.rchannel 1 of v.ga selects 1,3, then .r picks entry 0
v.ga.gchannel 3 of v
v.bgr.rchannel 2 of v

Composition folds into a single mask wherever the result is still sequential, so v.rgb.rg costs no nodes at all. Selecting an entry that does not exist in the outer list is a bounds failure against the masked width: v.ga.b reports Swizzle 'b' is invalid for a value with 2 components.

Because . is an ordinary postfix operator, any call result can be swizzled directly — no temporary is required:

float u  = UE.TexCoord().x;
vec3  c  = SampleTexture2D(Albedo, uv).rgb;
vec2  yx = vec4(1.0, 2.0, 3.0, 4.0).yx;
float m  = MyFunction(A, B).r;

Non-swizzlable bases

Base valueBehaviour
MaterialAttributesnot a swizzle — the member name resolves as a material attribute and a BreakMaterialAttributes read is generated
Texture objecterror: Texture values do not support swizzle/member access in Code.
Substrateerror: Substrate values do not support swizzle/member access in Graph.

The texture message says "in Code" while the Substrate one says "in Graph". Both come from the same builder; the wording predates the CodeGraph section rename.

Conversions

Coercion is never written by the author. It is applied automatically wherever a value meets an expected shape, and it is the mechanism behind most silent width changes in a generated material.

Where it applies

#SiteExpected shape taken from
1declaration with an initializer — float3 c = A;the declared type token, unless the authoritative escape hatch fires
2assignment to an existing Graph variable — c = A;the existing value's shape
3assignment to a name matching an Outputs declaration — Color = A;the output's declared type
4MaterialAttributes member write — Attrs.BaseColor = A;the attribute's component count
5Function / GraphFunction / ShaderFunction / VirtualFunction input argumentthe input's declared type
6if branch mergethe pre-branch value, else the Outputs declaration, else the two branches must already agree
7binary-operator rescuethe authoritative operand's count — widening only
8Outputs binding expressionthe bound output's declared type

Assignment to a name that is neither an existing variable nor an Outputs declaration performs no conversion at all: the new variable takes the value's own shape.

Rule order

#ConditionBehaviourMessage on failure
1expected is MaterialAttributesinput must be one; passes through unchangedExpected a MaterialAttributes value.
2expected is Substrateinput must be one; passes through unchangedExpected a Substrate value.
3expected is a textureinput must be a texture objectExpected a texture object value.
3aboth are texturesthe dimension must be identicalExpected a texture object value with a matching texture type.
4input is MaterialAttributes, expected numericrejectedMaterialAttributes values cannot be assigned to numeric outputs.
5input is Substrate, expected numericrejectedSubstrate values cannot be assigned to numeric outputs.
6input is a texture object, expected numericrejectedTexture objects cannot be assigned to numeric outputs.
7component counts are equalpasses through unchanged, every flag preserved
8narrowing — input count is greater than expecteda leading sequential swizzle (r, rg, rgb) is applied; silent, no node
9widening from a scalar — expected is greater than 1 and input is 1replicated through AppendVector
10anything elserejectedExpected {Expected} component(s) but got {Actual}.

Input width down the side, expected width across the top:

input \ expected1234
1unchangedsplat, 1 AppendVectorsplat, 2 AppendVectorsplat, 3 AppendVector
2.r, no nodeunchangederrorerror
3.r, no node.rg, no nodeunchangederror
4.r, no node.rg, no node.rgb, no nodeunchanged

And across kinds:

expected \ inputnumericMaterialAttributesSubstratetexture object
MaterialAttributeserrorpasserrorerror
Substrateerrorerrorpasserror
Texture(T)errorerrorerrorpass if the dimension is T
Numeric(N)the matrix aboveerrorerrorerror

The four texture dimensions are distinct expected shapes. Texture2D and SamplerState both mean Texture2D; Texture3D and VolumeTexture both mean VolumeTexture; TextureCube and Texture2DArray stand alone. A TextureCube never converts to a Texture2D.

Narrowing is silent. There is no warning and no node cost, because it is implemented as a leading sequential swizzle that lives on the connection.

vec4   Src = vec4(0.1, 0.2, 0.3, 0.4);
float3 Rgb = Src;      // silently becomes Src.rgb — channel A is dropped
float  R   = Src;      // silently becomes Src.r

If a channel disappears from a generated material, check every assignment whose right-hand side is wider than its target. Writing the swizzle explicitly documents the intent and behaves identically.

Narrowing is deliberately not applied at binary-operator operands (the size-mismatch error fires instead), at constructor arguments, or at swizzle bounds.

The only widening rule is scalar splat. There is no zero-fill and no partial widening: float3 v = SomeFloat2; is Expected 3 component(s) but got 2. Use a constructor to say what the extra channels contain.

Float, int and bool

There are no numeric-representation conversions in the Graph language at all.

Token familyWhat it means for conversion
float, float1..4, half, half1..4, vec2..41 / 2 / 3 / 4 components
int, int2..4, ivec2..4the same 1 / 2 / 3 / 4 components — no truncation, no rounding
uint, uint2..4, uvec2..4the same — no range clamping, no sign handling
bool, bool2..4, bvec2..4the same — no normalisation to 0 or 1
StaticBool, StaticBoolParameter1 component; carries no marker of its own

So int x = 7.9; stores 7.9 — use floor(…) if truncation is wanted — and bool b = 0.5; stores 0.5. The only observable difference between an integer and a float value is the integer marker, and its only effect is the integer-division rejection.

Authoritative component counts

A value carries an authoritative component count when its width is known from the engine rather than inferred from a declaration.

SourceAuthoritative
a constant-folded constructor — vec3(0.5), float4(1,0,0,1)yes — the only path where a constructor originates the flag
a UE.* builtin whose node class is in the known-width table belowyes
the dot math builtinyes, 1 component
any other math builtininherits — lerp/min/max take the logical OR of their two value operands, the rest inherit from their first operand
a bare numeric literalno
a declared Properties parameter of any widthno
a non-folded constructor — vec3(K), float4(rgb, A)inherits, as the OR of its arguments
a swizzleinherits from the base
the result of + - * /inherits, as the OR of the two operands
a narrowed or widened valueinherits from the input
a variablewhatever the value assigned to it carried

Known-width builtin node classes:

ComponentsNode classes
1PixelDepth, TwoSidedSign, Arctangent2Fast, Length, MaterialXLuminance
2TextureCoordinate, Panner, ScreenPosition, Rotator, SceneTexelSize
3WorldPosition, ObjectPositionWS, CameraVectorWS, VertexNormalWS, VertexTangentWS, Transform, TransformPosition, SkyAtmosphereLightDirection, PixelNormalWS, CrossProduct

Every other node's output width is unknown to the generator.

The flag changes two things.

Effect 1 — the operator rescue. When + - * / receives two incompatible widths, one operand may be widened to the authoritative one, but only upward. This is why UE.CameraVectorWS() * Tint fails when Tint is a 4-component VectorParameter: the builtin is an authoritative 3, the parameter a non-authoritative 4, and 4 is greater than 3, so the rescue is blocked and the size error is raised.

Effect 2 — a declared width can be ignored.

When a declaration's initializer carries an authoritative component count that differs from the declared type's width, the value is stored as-is, uncoerced, with no diagnostic. The declared width is effectively ignored.

float2 dir = UE.CameraVectorWS();   // stored as 3 components; float2 is ignored, no error
Color = dir * Tint;                 // fails HERE if Tint is 4 components

The mismatch is not lost — it resurfaces at the first place the value is used with a width that matters, where the message names the real widths rather than the declared one. Silently truncating an engine-known width is considered worse than reporting one step later. The escape hatch applies to declarations only; assignment to an existing variable or an output name always coerces, and so narrows silently even for an authoritative value.

Effect 3 — branch merging. The authoritative flag and the integer flag are not compared when deciding whether an if branch changed a value, so two values differing only in those flags are treated as identical and the name is not merged.

Node reuse

Textually identical subexpressions over identical operand values collapse to one node. A + B written twice yields one Add; every occurrence of 0.5 in one asset maps to one Constant; the Constant(-1) behind unary minus is shared by every unary minus. The cache is keyed on a value identity token — node, output index, component count, all five mask fields, and the texture, attribute, Substrate, authoritative and integer flags — so int(2) and float(2) never collide, and re-assigning a variable between two textually identical calls correctly produces two nodes.

F_Tint(a, b) and F_Tint(b, a) are different keys because positional arguments are keyed by index; F_Tint(Color = a) and F_Tint(color = a) are the same key because argument names are normalised. Which call kinds dedupe at all is covered on Calls.

Diagnostics

MessageCauseFix
Unexpected token '{Text}' in Graph expression.A non-primary token in primary position (!x, = b), or a real token left over after a complete expression (a == b). Never emitted for an unknown character in trailing position.Details
Expected token type {Code} in Graph expression near '{Text}'.A ) or , was required and not found — usually an unknown character inside parentheses or an argument list.
Expected member name after '.'.A . not followed by an identifier.
Empty Graph expression.The expression text is empty after trimming.
Operator '{Op}' requires matching vector sizes or a scalar/vector pair, got {Left} and {Right} component(s).Operand widths differ, neither is a scalar, and the authoritative rescue did not apply.Swizzle the wider operand explicitly, or build the narrower one with a constructor.
Integer division is not supported by the material graph; use float() or floor(a/b).Both operands of / came from integer constructors.
Arithmetic operators cannot be applied to texture values.A texture object used as an operand of + - * /.
Arithmetic operators cannot be applied to MaterialAttributes values.A MaterialAttributes value used as an operand.
Arithmetic operators cannot be applied to Substrate values.A Substrate value used as an operand.
Expected {Expected} component(s) but got {Actual}.Widening a 2- or 3-component value to a wider target.Use a constructor: float3(v, 0.0).
Constructor '{Name}' expects {N} total components but got {Total}.The argument widths do not sum to the constructor's width.
Constructor '{Name}' expects a single scalar input.A 1-component constructor called with zero arguments, several arguments, or one wider than 1 component.
Constructor '{Name}' does not accept named arguments.Any name = value argument in a constructor call.
Swizzle '{Channels}' is invalid for a value with {Count} components.A channel character does not resolve, or its index is not less than the base width.
Unsupported swizzle '{Channels}'.More than four channel characters.
Unknown Graph identifier '{Name}'.A name that is neither a Graph variable, nor a declared property, nor true/false — including a StaticSwitchParameter read by name.Details
String literals can only be used in named UE builtin arguments.A string literal evaluated as a value.
Invalid numeric literal '{Text}'.A number token that converts to zero while containing a character no spelling of zero can contain — in practice an underflowing exponent such as 1e-9999.

Example

Shader(Name="Docs/M_Expressions")
{
    Properties {
        vec3 A = vec3(1.0, 0.5, 0.2);
        vec3 B = vec3(0.1, 0.2, 0.3);
        float K = 2.0;
    }
    Settings { Domain = "UI"; ShadingModel = "Unlit"; }
    Outputs { vec3 Color; Base.EmissiveColor = Color; }
    Graph {
        vec3 Sum    = A + B;
        vec3 Diff   = A - B;
        vec3 Scaled = A * K;
        vec3 Ratio  = A / K;
        vec3 Neg    = -Scaled;
        Color = (Sum + Diff - Scaled + Ratio) * 0.25 + Neg;
    }
}
VectorParameter A, VectorParameter B, ScalarParameter K   (property nodes)
Add(A, B)                                        -> Sum
Subtract(A, B)                                   -> Diff
Multiply(A, K)                                   -> Scaled
Divide(A, K)                                     -> Ratio
Multiply(Scaled, Constant(-1))                   -> Neg
Add / Subtract / Add chain, Multiply(..., 0.25), Add(..., Neg)

See also

  • Statements — the statement forms an expression appears in
  • Calls — call syntax, named arguments, output selection
  • Math Builtinsfmod, pow, min, max, lerp, saturate
  • UE.* Nodes — which builtins have a known output width
  • What Graph Is Not — every rejected and every silently-truncated construct

On this page