Paint-aware PCG graphs

PCG recipes: two ways to read paint inside a graph.

The bridge ships two paths. Drop a custom TerraInk Sample Paint node into your graph (easy, recommended), or read the underlying render target as a user parameter and sample it with stock PCG nodes (advanced). Both are documented here.

Custom nodes User parameters 5 layers UE 5.6 / 5.7 / 5.8

Two ways to read paint

Pick by taste. Custom nodes are simpler; raw RT is more flexible.

1
Custom nodes (easy, recommended)
Drop TerraInk Sample Paint (single layer) or TerraInk Sample All Layers into your graph. They take points in, write a float attribute per point with the layer intensity at that point's world position. No UV math, no texture binding, no Vector4 dot product.
2
Raw RT user parameters (advanced)
The bridge also exposes the paint render target plus arena bounds and channel mask as graph user parameters. Use this if you want to compose paint with custom expressions, sample at non-point locations, or ride a non-standard path.

The custom nodes call UTerraInkSubsystem::SamplePaintAt directly, so they read the same paint state the rest of the plugin uses. The user parameters surface the underlying RT and let you sample it manually with stock PCG nodes.

User Parameters vs input pins. User Parameters are graph-level values declared in Graph Editor » User Parameters (sometimes labeled just "Parameters" in the graph details view). They live on the UPCGGraphInstance as a property bag and act as external configuration knobs for that graph instance. The bridge sets them via FPCGGraphParameterExtension::SetGraphParameter. Inside the graph, read them with Get Graph Parameter nodes or reference them from any node property that supports parameter binding.

Input node pins (PCGPinConstants::DefaultInputLabel) carry data flow – points and spatial data passed in by upstream. The bridge does not use those at all. It spawns an APCGVolume and lets the graph's Get Spatial Data / Surface Sampler operate on the volume bounds.

So in your graph: PaintRT, ArenaMin, ArenaSize, and ChannelMask go in the User Parameters panel. The point flow (Surface Sampler → Filter → Spawner) is unrelated to those.

The easy path: custom nodes

Both nodes live under TerraInk in the PCG node search.

TerraInk Sample Paint

Per input point, samples one paint layer at the point's world position and writes the result as a float metadata attribute on each output point.

Property Default What it does
Layer PaintLayer_R Which paint layer to sample. Pick from the same enum the rest of TerraInk uses.
OutputAttributeName Intensity Name of the float attribute written on each output point.

Both properties are PCG-overridable, so you can drive Layer from upstream attributes if you need a graph that reads different layers per dispatch.

TerraInk Sample All Layers

Same node, but samples all five paint layers (R, G, B, A, Secondary) per point in one pass and writes five float attributes.

Property Default What it does
AttributeNamePrefix Paint_ Prefix for the five output attributes. With the default you get Paint_R, Paint_G, Paint_B, Paint_A, Paint_Secondary.

Use this for territory-control or biome graphs where each point needs to know about every layer (e.g., spawn a contested-zone mesh where R and B both exceed a threshold).

Recipes

Four patterns covering the common cases. Each is composable with the others.

Recipe: density-driven scatter (with the node)

Goal: sample paint per point, keep only points whose layer intensity is above a threshold, spawn meshes there.

[Get Spatial Data Bounds]
       │
       ▼
[Surface Sampler]
   Iterations / density configured to taste
       │
       ▼
[TerraInk Sample Paint]
   Layer:               PaintLayer_R   (or whichever)
   OutputAttributeName: Intensity
       │
       ▼
[Filter Points by Attribute]
   keep where Intensity >= 0.1   (your gameplay threshold)
       │
       ▼
[Static Mesh Spawner]

Three nodes plus the spawner. No UV math.

Recipe: density gradient

Goal: scatter density falls off from a "core" of high paint intensity to sparse at the edges.

Same as the previous recipe, but instead of filtering hard:

  • After TerraInk Sample Paint, copy the Intensity attribute onto the point's Density attribute (one Attribute Transfer or Attribute Copy node).
  • Use a Density Filter node before the spawner. PCG drops points stochastically based on density – higher intensity gives denser scatter, lower intensity gives sparser.

Smooth gradient out of the box, no math.

Recipe: layer-routed meshes (with the multi-layer node)

Goal: one graph, multiple layers, route each point to the mesh set for whichever layer dominates that point.

[Surface Sampler] -> [TerraInk Sample All Layers]
                            │
                            ▼
              [branch on max(Paint_R, Paint_G, Paint_B, Paint_A, Paint_Secondary)]
                            │
              ┌────────────┬────────────┐
              ▼             ▼             ▼
         [SMSpawner: R] [SMSpawner: G] [SMSpawner: B] ...

PCG doesn't ship a one-shot "argmax" node, but the standard approach works: chain attribute math nodes that compare each layer attribute and tag the point with a winner enum or int. Filter into one branch per layer and spawn from the right mesh set.

Recipe: tier-aware density

The bridge passes LayerIndex, TierIndex, and Seed via the dispatch context. Seed is wired into UPCGComponent::Seed automatically and graph nodes that randomize honor it. LayerIndex and TierIndex are not exposed as graph parameters today. Three options:

1
Configure per-tier graphs
Each tier in FTerraInkScatterTier::PCG.Graphs references its own UPCGGraph asset, and you author tier-specific behavior at the asset level. No runtime tier check needed inside the graph.
2
Use Seed
Already deterministic per cell; nothing to do.
3
Plumb LayerIndex / TierIndex as user parameters
Easy add on the engineering side; ask if you need it.

Advanced: raw RT user parameters

Skip this section if the custom nodes cover your case.

When a cell promotes to a tier with PCG graphs configured, the bridge:

1
Spawn an APCGVolume
At the promoted cell's center.
2
Assign your graph
The volume's UPCGComponent is bound to your configured graph asset.
3
Set four user parameters
PaintRT, ArenaMin, ArenaSize, ChannelMask are pushed into the component's graph-instance property bag.
4
Schedule runtime Generate()
Via UPCGSubsystem; the graph runs against the parameters and writes points / meshes.

Declare these in your PCG graph via Graph Editor » User Parameters with the exact names and types below.

Parameter name Type What it carries
PaintRT Object Reference (UObject*) Zone-map render target for this layer. ZoneMapRT1 (RGBA8) for primary R/G/B/A layers; ZoneMapRT2 (R8) for the secondary layer.
ArenaMin Vector World-space minimum corner of the paintable arena.
ArenaSize Vector World-space extent of the arena (max minus min).
ChannelMask Vector4 RGBA mask isolating this layer's channel. (1,0,0,0) for R-layer, (0,1,0,0) for G, and so on. Dot-product against the sampled color to recover the layer's intensity as one scalar, regardless of channel.
Names are exact and case-sensitive. Misnamed parameters are silently skipped at dispatch time. Your graph will run with default values and you'll wonder why nothing reads paint. Match PaintRT (capital P, capital RT), not paintRT or paintRt.

Recipe: density-driven scatter (raw RT path)

Same goal as the easy path, but sampling the RT directly.

[Get Spatial Data Bounds]
       │
       ▼
[Surface Sampler]
       │
       ▼
[Add Attribute From Property]
   New attribute: UV (Vector2)
   Per point:  uv.x = (position.x - ArenaMin.x) / ArenaSize.x
               uv.y = (position.y - ArenaMin.y) / ArenaSize.y
       │
       ▼
[PCG Texture Sampler]
   Texture: PaintRT
   UV input: UV attribute from the previous node
   Output:   SampledColor (RGBA per point)
       │
       ▼
[Add Attribute From Property]
   New attribute: Intensity (float)
   Per point:    intensity = dot(SampledColor, ChannelMask)
       │
       ▼
[Filter Points by Attribute]
   keep where Intensity >= 0.1
       │
       ▼
[Static Mesh Spawner]

The Add Attribute From Property nodes do the math. In practice you wire a Get Property node plus a small expression chain.

Common gotchas

Five things that bite people the first time through.

  • Wrong parameter name.

    paintRT (lowercase p) won't bind. Match case exactly.

  • Wrong parameter type.

    PaintRT declared as Texture2D won't bind to the bridge's UTextureRenderTarget2D*; both are UObject derivatives, but PCG's property bag uses concrete typing. Declare as Object Reference (UObject*) and cast inside the graph.

  • Sampling a render target.

    UTextureRenderTarget2D works with PCGTextureSampler, but make sure your sampler's source is set to the parameter, not a hardcoded asset.

  • UV math.

    If your ArenaSize is wrong (e.g., zero on one axis), UVs go to infinity and the texture sampler returns garbage. The bridge initializes ArenaSize = ArenaBounds.GetSize() which is always positive for a valid FBox.

  • Layer mismatch.

    The dispatch fires per layer. If your graph reads ChannelMask and the dispatch was for layer R, you'll only get R-channel intensity. To compose multiple layers in one graph, use TerraInk Sample All Layers instead, or dispatch a separate combined graph and plumb additional channel masks.

Tier-promotion lifecycle

How the bridge tears down what it spawned.

The PCG dispatch fires on cell tier promotion. When the cell's tier later changes (demote, exit, or override by another layer), the bridge cleans up the spawned volume via FTerraInkPCGHooks::Cleanup. Your graph doesn't need to handle teardown; it runs once per promotion and the bridge tears down the spawn on demote.

Cross-layer override: if layer B paints over layer A, A's tier demotes only when A's ChannelMask intensity drops in the zone-map RT, which requires B's brush material to honor SubtractRate (default 0.5). Without subtraction, A's PCG content stays alive even though B's paint is now visible on the surface. See the Paint Config notes in the main documentation for the SubtractRate knob.

Where the magic lives

C++ side, for the curious.

  • FTerraInkPCGDispatchContext (TerraInkPCGHooks.h) – the dispatch payload (cell index, layer, tier, owner actor, paint RT, arena bounds, channel mask).
  • UTerraInkScatterManager::ScatterPaintLayerTierInCell – populates the context and fires FTerraInkPCGHooks::Dispatch().Execute(Ctx).
  • TerraInkPCGBridgeModule::HandleDispatch – receives the context, spawns the volume, calls FPCGGraphParameterExtension::SetGraphParameter against the graph instance's user-parameter property bag for each of the four parameters, then schedules Generate().
  • PCGTerraInkSampleNode.cpp – the two custom nodes (FPCGTerraInkSamplePaintElement, FPCGTerraInkSampleAllLayersElement); each walks input points, calls UTerraInkSubsystem::SamplePaintAt(WorldPos, Layer), writes a float metadata attribute.
Silent failure mode: If a user parameter doesn't show up in your graph, the bridge logs nothing. The SetGraphParameter call returns EPropertyBagResult::PropertyNotFound and continues. Verify the parameter name and type in Graph Editor » User Parameters.