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.
Two ways to read paint
Pick by taste. Custom nodes are simpler; raw RT is more flexible.
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.
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.
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 theIntensityattribute onto the point'sDensityattribute (oneAttribute TransferorAttribute Copynode). - 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:
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.
SeedLayerIndex / TierIndex as user parametersAdvanced: 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:
APCGVolumeUPCGComponent is bound to your configured graph asset.PaintRT, ArenaMin, ArenaSize, ChannelMask
are pushed into the component's graph-instance property bag.
Generate()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. |
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(lowercasep) won't bind. Match case exactly. -
Wrong parameter type.
PaintRTdeclared asTexture2Dwon't bind to the bridge'sUTextureRenderTarget2D*; both areUObjectderivatives, but PCG's property bag uses concrete typing. Declare as Object Reference (UObject*) and cast inside the graph. -
Sampling a render target.
UTextureRenderTarget2Dworks withPCGTextureSampler, but make sure your sampler's source is set to the parameter, not a hardcoded asset. -
UV math.
If your
ArenaSizeis wrong (e.g., zero on one axis), UVs go to infinity and the texture sampler returns garbage. The bridge initializesArenaSize = ArenaBounds.GetSize()which is always positive for a validFBox. -
Layer mismatch.
The dispatch fires per layer. If your graph reads
ChannelMaskand the dispatch was for layer R, you'll only get R-channel intensity. To compose multiple layers in one graph, useTerraInk Sample All Layersinstead, 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 firesFTerraInkPCGHooks::Dispatch().Execute(Ctx).TerraInkPCGBridgeModule::HandleDispatch– receives the context, spawns the volume, callsFPCGGraphParameterExtension::SetGraphParameteragainst the graph instance's user-parameter property bag for each of the four parameters, then schedulesGenerate().PCGTerraInkSampleNode.cpp– the two custom nodes (FPCGTerraInkSamplePaintElement,FPCGTerraInkSampleAllLayersElement); each walks input points, callsUTerraInkSubsystem::SamplePaintAt(WorldPos, Layer), writes a float metadata attribute.
SetGraphParameter call returns EPropertyBagResult::PropertyNotFound
and continues. Verify the parameter name and type in Graph Editor » User
Parameters.