Back to Tech Blog

Flattening UMG Hierarchies with Materials

UMG optimization — widget hierarchy before and after flattening

UMG is powerful and fast to work with — until it isn't. Once your UI starts getting complex, nested widget hierarchies become a real problem: each Border, Overlay, Image, and Canvas Panel adds a node to Slate's widget tree. More nodes means more work during layout passes, more invalidation overhead when anything changes, and more draw batches being generated every frame. The widget tree doesn't have to be enormous before you start feeling it — especially on scenes where multiple complex widgets are active at once.

On Desert Revenant and Desert Revenant 2 this showed up across three distinct UI systems. In each case the root cause was the same: a group of purely decorative Image widgets stacked on top of each other, each existing as a separate node in the hierarchy for no functional reason other than "that's the obvious way to build it."

The General Fix

The underlying principle is simple: if a set of stacked Image widgets is purely decorative — no independent interaction, no per-widget animation, no dynamic data that needs individual invalidation — there is no reason for them to exist as separate widgets. You can composite all of those layers into a single UMG-compatible material and render the entire thing with one Image widget. One node in the tree. One Slate batch entry — instead of six separate widgets each breaking Slate's batching with a different texture.

In the material graph, each visual layer becomes a Texture Sample node. You composite them manually using Multiply, Lerp, and Add nodes — the same operations you'd use in Photoshop, expressed in the material graph instead. Anything that needs to vary at runtime — a rarity tint, an active/inactive state, a selection highlight — becomes a Material Parameter that you set from Blueprint. The material instance handles all the visual variation; the widget itself stays exactly one Image node.

The shift in thinking is going from "each visual element is a widget" to "each visual element is a layer in a material." Below I'll walk through how this played out specifically across the spell cards, enchantment slots, and the Revenant selection widget in Desert Revenant, and what the different states looked like in practice.

Case Study: Spell Cards

The most visible instance of the problem was the spell cards. Each card in a player's hand was composed of roughly six to eight separate Image widgets stacked on top of each other — a base background, a rarity-coloured frame, a gem overlay, a glow effect, a foil highlight, and a mask cutout for the card art. All of them existed as independent sibling nodes in the widget tree, and every time a card was added to or removed from the hand, all of those nodes had to be created, laid out, and invalidated.

Spell card widget hierarchy before consolidation
Before — multiple Image nodes per card.
Spell card widget hierarchy after consolidation
After — single Image node with a composited material.

Consolidating those into a single material dropped the widget count for a full hand of eight cards from roughly 80 nodes down to around 20. But the spell card material ended up exposing significantly more parameters than the other widgets — because a card has a lot of visual states to cover, all from one Image node:

  • Frame image — Texture Parameter for the card border, swapped per card type
  • Spell image — Texture Parameter for the spell art, unique per card
  • Rarity colour — vector parameter tinting the frame to the card's rarity tier
  • Mana image colour — vector parameter for the mana cost icon tint
  • Ephemeral / Conserve effects — scalar parameters that enable the keyword effect overlays
  • Disabled state — scalar that desaturates the card when it can't be played
  • Glow on/off and glow colour — scalar toggle plus vector tint for the hover and selection glow
  • Card flip — scalar driving the flip-to-back-face transition
  • Card burn transition — scalar controlling a burn/dissolve effect
SpellMaterial — all nine parameters in action
All nine parameters in action — one material instance handles every visual state a spell card can be in.

Case Study: Enchantment Widget

The enchantment slots had the same layered structure. Each slot displayed a background, an active or inactive state indicator, a frame overlay, and a locked state overlay when the slot was unavailable — four Image widgets per slot, multiplied across all visible slots on screen simultaneously.

Enchantment widget hierarchy before consolidation
Before — four Image nodes per slot.
Enchantment widget hierarchy after consolidation
After — single Image node per slot.

What makes the enchantment material particularly interesting is that it goes beyond swapping textures — the slot shape itself is procedurally generated using a Signed Distance Field written in HLSL, meaning the shape is a parameter too. A single material instance handles all of this:

  • SDF shape — continuous scalar that morphs the slot between a circle and a rounded square, defined entirely in HLSL
  • Frame colour — vector parameter for the slot frame tint
  • Glow colour — vector parameter for the ambient glow around the slot
  • Hovered frame colour — vector parameter that changes the frame tint when the player hovers over the slot
  • Enchantment colour — vector parameter tinting the enchantment icon or art
  • Effect — parameter enabling an overlay effect on the enchantment
  • Effect colour — vector parameter tinting the effect overlay
  • Disabled state — scalar that greys out the slot when it's unavailable
  • Shape switch — quick toggle that snaps between circular and rounded square without manually adjusting the SDF scalar
EnchantmentMaterial — all parameters in action
All parameters in action — shape, colour, hover state, effect, and disabled state, all from a single HLSL SDF material instance.

Case Study: Revenant Selection Widget

The character selection screen used a dedicated widget for each selectable Revenant — and each of those widgets was built from several stacked Image nodes forming the portrait frame, background glow, selection highlight ring, and state indicators for locked or unlocked characters. Here's what the widget looked like before the consolidation:

Revenant selection widget hierarchy before consolidation
Before — multiple Image nodes per widget.
Revenant selection widget hierarchy after consolidation
After — single Image node per widget.

Each of those visual layers — background, frame, glow, state indicator — existed as its own Image widget in the hierarchy. The selection highlight and locked state were handled by toggling widget visibility rather than driving a material parameter, which meant Slate had to re-invalidate the layout on every state change. Collapsing everything into a single material eliminated those per-layer invalidations entirely. One widget node per Revenant card instead of five or six.

What makes the material genuinely flexible beyond the hierarchy reduction is what it exposes as parameters. A single material instance handles all of this:

  • Revenant portrait — a Texture Parameter, so every card in the selection screen uses the same material; only the portrait texture differs between characters
  • Locked state — a scalar that controls the locked overlay, set from Blueprint when a character is unavailable to the player
  • Hover glow — a scalar that fades in the selection glow when the player hovers over the card
  • Glow colour — a vector parameter allowing the glow to be tinted or adjusted without touching the material graph
Swapping the Revenant portrait via material Texture Parameter
Swapping the portrait Texture Parameter — the same material instance drives every card.
Selection glow toggling on and off via scalar parameter
The hover glow scalar fading in and out — no visibility toggling, no layout invalidation.
Locked state overlay controlled via scalar parameter
The locked state overlay driven by a scalar — set once from Blueprint when availability changes.
Glow colour changing via vector parameter
Glow colour as a vector parameter — adjustable per character without touching the material graph.

What to Watch Out For

This technique works best for static or semi-static decoration. If a layer needs to respond to input, animate independently, or bind to a changing value every frame, it's usually better to keep it as its own widget — the overhead of a per-frame material parameter update is fine, but you lose the ability to invalidate individual pieces cleanly.

A few things to keep in mind when you go down this route:

  • Make sure your UMG material has Is Opaque or Masked set correctly — UI materials default to translucent which forces them into a separate pass
  • Use material instances, not the base material, so you can swap tints and parameters per card without triggering a shader recompile
  • Keep an eye on texture sampler count — UMG materials share the same 16-sampler limit as any other material, and it's easy to blow past it when consolidating several layers
  • Profile before and after with Slate's widget stats (SlateDebugger.Start in console) to confirm you're actually moving the needle

The Result

On Desert Revenant, consolidating the decorative layers across spell cards, enchantment slots, and the Revenant selection widget reduced the overall widget count across those screens substantially — a full hand of cards alone went from roughly 80 widgets down to around 20. Draw batches dropped noticeably, and iteration time on each widget's visuals improved significantly because there was one material to edit instead of several assets scattered across a deep hierarchy.

On Desert Revenant 2 we carried the technique forward from day one, applying it proactively to new UI systems as they were built rather than retrofitting it after the fact. The result was a cleaner widget tree from the start and far less time spent diagnosing Slate performance issues late in development. Lesson learned: it's much easier to build flat hierarchies upfront than to flatten them after an artist has spent a week nesting widgets.


Found this useful? Share it:


Giuseppe Boschiero

Giuseppe Boschiero

Principal Technical Artist with 8 years across computer graphics, 3D art, and tech art — specialising in Unreal Engine 4 and 5. Shaders, VFX, tools, and optimization.