Code/Spawners/TextureSpawner.cs
using System;
using System.Collections.Generic;
using Sandbox.Mask;

namespace Sandbox.Spawners;

public class TextureSpawner: BaseSpawner
{
	
	[Property] public List<SpawnLayer> TextureLayers { get; set; } = new();

	public override void OnBeforeGenerate()
	{
		_isFinished = false;
		base.OnBeforeGenerate();
	}

	public override void Generate()
	{
		base.Generate();
		GenerateTextureLayers();
	}
	
	[Button]
	public void GenerateTextureLayers()
	{
		var terrainInBounds = Manager.GetWorldCache().GetTerrainsInBounds( GenerateSpawnerBounds() );
		foreach ( var currentTerrain in terrainInBounds )
		{
			if ( currentTerrain?.Storage == null )
				return;

			var terrain = currentTerrain;
			var storage = terrain.Storage;

			int resolution = storage.Resolution;

			// Generate all masks first
			var generated = new List<(SpawnLayer layer, MaskField mask)>();

			foreach ( var layer in TextureLayers )
			{
				var mask = layer.GenerateMask(
					terrain,
					resolution	//dont use MaskResolution here
				);

				generated.Add( (layer, mask) );
			}

			BBox bounds = GenerateSpawnerBounds();

			Rect rect = ApexWorldUtils.GetTerrainRectFromBounds(
				bounds,
				terrain
			);

			// Sequential paint compositing
			for ( int y = (int)rect.Top; y <= (int)rect.Bottom; y++ )
			{
				for ( int x = (int)rect.Left; x <= (int)rect.Right; x++ )
				{
					int index = y * resolution + x;//dont use MaskResolution here

					int baseTex = 0;
					int overlayTex = 0;
					float blend = 0f;

					bool hasBase = false;

					foreach ( var pair in generated )
					{
						float value = pair.mask.Get( x, y );

						if ( value <= 0.001f )
							continue;

						if ( !hasBase )
						{
							baseTex = pair.layer.TextureId;
							hasBase = true;
						}
						else
						{
							overlayTex = pair.layer.TextureId;
							blend = value;
						}
					}

					// Skip untouched pixels
					if ( !hasBase )
						continue;

					var material = new CompactTerrainMaterial
					{
						BaseTextureId = (byte)Math.Clamp( baseTex, 0, 63 ),
						OverlayTextureId = (byte)Math.Clamp( overlayTex, 0, 63 ),
						BlendFactor = (byte)(Math.Clamp( blend, 0f, 1f ) * 255f),
						IsHole = false
					};

					storage.ControlMap[index] = material.Packed;
				}
			}

			terrain.SyncGPUTexture();
		}

		_isFinished = true;
	}
}