Generated concept thumbnail, not a real editor screenshot.

Making Vertex-Painted Terrain Work in Unity URP (Without Breaking Your Sampler Budget)


Unity URP has a sampler limit that becomes a problem quickly: you only get 16 texture samplers total. Before the terrain material even starts doing anything interesting, URP has already taken a chunk of that budget for lighting, shadows, and other pipeline work.

This became an issue when I needed a regular mesh to behave more like Unity terrain, where different materials can be painted through vertex colors. The idea sounds simple, but the sampler budget makes the naive version fall apart fast.

The Reality Check

For each terrain material, I usually want:

  • Base Color texture
  • Normal map
  • Metallic/Smoothness map
  • Ambient Occlusion map

That is 4 samplers per material. With only about 8 usable samplers left after URP takes its share, that means maybe 2 materials before hitting the limit. That was not enough for the kind of terrain blend I wanted.

My First Attempt: Texture Arrays

My first idea was Texture2DArray.

float4 albedo = SAMPLE_TEXTURE2D_ARRAY(_BaseColorArray, sampler_BaseColorArray, uv, slice);

The idea is technically useful: one sampler per array, no matter how many slices. The catch is that each type of data still needs its own array:

  • _BaseColorArray
  • _NormalArray
  • _MetallicSmoothnessArray
  • _AOArray

Each array still consumes a sampler. So the material was still heading straight into the same 16-sampler ceiling.

The Useful Fix: Channel Packing

The better route was channel packing.

The useful detail is that normal maps store their main information in the R and G channels. The Z component can be reconstructed in the shader, which frees up the B and A channels for other data.

I ended up using what I call an NSAO texture: Normal, Smoothness, and Ambient Occlusion packed into one RGBA texture.

ChannelData
RNormal X
GNormal Y
BSmoothness
AAmbient Occlusion

The Workflow

  1. Gather the source textures for each material: Normal, Metallic/Smoothness, and AO.
  2. Split or remap the channels in Substance Designer, Photoshop, or another texture tool.
  3. Pack them into the NSAO texture using the channel layout above.
  4. Export as TGA/PNG.
  5. Disable sRGB on import, because this is non-color data.
  6. Reconstruct normal Z in the shader with sqrt(1 - (R^2 + G^2)).

Creating the merged NSAO texture

That cuts sampler usage down. Instead of needing 4 samplers per material, each material needs 2: one Base Color texture and one packed NSAO texture.

The Shader Setup

The Shader Graph setup becomes much more manageable:

Shader Graph overview

  1. Use Vertex Color RGB as blend weights.
  2. Derive the fourth weight as 1 - sum.
  3. Sample the Base Color textures for each material.
  4. Sample the NSAO textures for each material.
  5. Lerp everything together based on vertex colors.

Lerp Example Mat01 and Mat02

Then:

  1. Unpack the NSAO data.
  2. Reconstruct the normal Z.
  3. Normalize the result.

Normals reconstruction

Finally, connect the result to the Fragment inputs.

Final connections

Working With Polybrush

A few practical notes from using Unity Polybrush:

  • Stick with version 1.1.6 if you are on Unity 2022+. Version 1.1.8 had gizmo issues in my setup.
  • UVs are still required even though the blend comes from vertex colors. A quick auto-unwrap is fine.
  • Watch UV seams because paint transitions can reveal splits if seams run across visible faces.

When Metallic Is Needed

If the project needs metallic values too, the packing can be adjusted:

  • Move Ambient Occlusion to the alpha channel of the Base Color texture, because terrain usually does not need transparency.
  • Store Metallic in the B channel of the packed texture.
  • Use a BAO / NMS style split instead.

I usually do not need metallic for terrain, so NSAO was the cleaner fit for this case.

Practical Takeaway

The important part is not the exact name of the packed texture. The important part is thinking in terms of data usage instead of one texture per concept.

Channel packing made the terrain material usable inside URP’s sampler limits. It is less elegant than having unlimited samplers, but it works reliably and keeps the material flexible enough for multi-material vertex painting.

Practical note: Check the sampler count in the Frame Debugger while iterating. It is easy to accidentally exceed the limit when the material gets more complex.

Have a question about this note?

Share the note where it fits. X opens with a question draft; LinkedIn shares the article link.

Ask on X Share on LinkedIn