PerformantTerrainScatterer/PerformantTerrainScattererManager.cs

Component that manages a terrain scatterer system for s&box, it configures model entries, LODs, noise fields, material weight maps and prepares chunk render/cache data. It initializes state from a Terrain component, validates configuration, computes distances and prepares internal arrays used for runtime chunk generation and rendering.

Native Interop
using System;
using System.Collections.Generic;
using Sandbox;

public sealed partial class PerformantTerrainScatterer : Component, Component.ExecuteInEditor
{
	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) )]
	public Terrain TargetTerrain { get; set; }

	[Property, Category( "Models" ), Change( nameof(InitializeSystem) )]
	public List<ScattererModelEntry> Models { get; set; } = new();

	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) ), Range( 0, 90 )]
	public float MaxSlopeAngle { get; set; } = 45f;

	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) )]
	public bool ReduceDensityOnTransitions { get; set; } = false;

	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) ),
	 ShowIf( nameof(ReduceDensityOnTransitions), true )]
	public float TransitionThinningIntensity { get; set; } = 1.0f;

	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) )]
	public bool SnapToGround { get; set; } = true;

	[Property, Category( "Collision" ), Change( nameof(InitializeSystem) )]
	public bool CheckObstructions { get; set; } = true;

	[Property, Category( "Collision" ), Change( nameof(InitializeSystem) )]
	public float TraceStartHeight { get; set; } = 200f;

	[Property, Category( "Collision" ), Change( nameof(InitializeSystem) )]
	public float GridCellSize { get; set; } = 50f;

	[Property, Category( "Collision" ), Change( nameof(InitializeSystem) )]
	public int ObstructionBuffer { get; set; } = 2;

	[Property, Category( "Collision" )]
	public string[] TraceIgnoreTags { get; set; } = { "player", "trigger", "clutter" };

	[Property, Category( "Performance" ), Change( nameof(InitializeSystem) )]
	public int MaxInstancesPerChunk { get; set; } = 500;

	[Property, Category( "Performance" ), Change( nameof(InitializeSystem) )]
	public List<MaterialInstanceOverride> MaterialOverrides { get; set; } = new();

	[Property, Category( "Performance" ), Change( nameof(InitializeSystem) )]
	public float ChunkSize { get; set; } = 500f;

	[Property, Category( "Performance" ), Change( nameof(InitializeSystem) )]
	public float ChunkLoadDistance { get; set; } = 3000f;

	[Property, Category( "Performance" )] public bool UseFrustumCulling { get; set; } = true;
	[Property, Category( "Performance" )] public float FrustumCullMinDistance { get; set; } = 800f;
	[Property, Category( "Performance" )] public bool AutomaticallyInitializeSystem { get; set; } = true;

	[Property, Category( "Terrain" ), Change( nameof(InitializeSystem) )]
	public bool UseTerrainNormal { get; set; } = true;

	[Property, Category( "Seed" ), Change( nameof(InitializeSystem) )]
	public int Seed { get; set; } = 12345;

	[Property, Category( "Randomization" ), Change( nameof(InitializeSystem) )]
	public bool RandomizeRotation { get; set; } = true;

	private readonly Dictionary<(int, int), ClutterChunk> _activeChunks = new();
	private readonly HashSet<(int, int)> _generatingChunks = new();
	private readonly List<(int, int)> _chunkRemovalList = new();

	private ushort[] _heightMapPixels;
	private uint[] _controlMapPixels;
	private int _resolution;
	private float _terrainSize;
	private float _terrainHeight;
	private float _maxRenderDistance;
	private float[][] _modelMaterialWeights;
	private GameObject _cachedHitObject;
	private bool _cachedIsObstruction;
	private Sandbox.Utility.INoiseField[] _modelNoiseFields;
	private Vector3 _lastCameraPos;
	private const float CameraUpdateThresholdSq = 10000f;
	private const float UshortMaxReciprocal = 1.0f / ushort.MaxValue;
	private SceneCustomObject _sceneObject;

	private ScattererModelEntry[] ModelEntries { get; set; }
	private float[] _modelRenderDistSq;
	private int[] _modelLodCounts;
	private float[][] _modelLodDistancesSq;

	private int[] _materialMaxInstances;
	private int _maxInstancesLoopCount;

	private ChunkRenderData[] _renderCache = Array.Empty<ChunkRenderData>();

	[ThreadStatic] private static List<Transform>[] _lodBuckets;

	protected override void OnEnabled()
	{
		if ( !TargetTerrain.IsValid() )
		{
			TargetTerrain = Scene?.GetComponentInChildren<Terrain>();
		}

		CreateSceneObject();
		if ( AutomaticallyInitializeSystem )
		{
			InitializeSystem();
		}
	}


	protected override void OnUpdate()
	{
		ManageChunks();
	}

	protected override void OnDisabled()
	{
		_sceneObject?.Delete();
		_sceneObject = null;
	}

	private void CreateSceneObject()
	{
		_sceneObject?.Delete();
		if ( Scene == null ) return;

		_sceneObject = new SceneCustomObject( Scene.SceneWorld )
		{
			RenderOverride = RenderClutter,
			Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f ),
			Flags = { IsOpaque = true, IsTranslucent = false }
		};
	}

	[Button( "Reinitialize" )]
	public void InitializeSystem()
	{
		_activeChunks.Clear();
		_generatingChunks.Clear();
		_chunkRemovalList.Clear();
		_renderCache = Array.Empty<ChunkRenderData>();
		ModelEntries = null;
		_modelRenderDistSq = null;
		_modelLodCounts = null;
		_modelLodDistancesSq = null;

		if ( Models == null || Models.Count == 0 ) return;
		if ( !TargetTerrain.IsValid() || TargetTerrain.Storage == null ) return;

		// --- CONFIGURATION VALIDATION WARNINGS ---
		var seenModels = new HashSet<Model>();
		foreach ( var entry in Models )
		{
			if ( entry.Model != null && !seenModels.Add( entry.Model ) )
			{
				Log.Warning( $"[Scatterer] Duplicate model entry found for '{entry.Model.ResourceName}' in the Models configuration list." );
			}
		}

		if ( MaterialOverrides != null )
		{
			var seenOverrideMaterials = new HashSet<TerrainMaterial>();
			foreach ( var ov in MaterialOverrides )
			{
				if ( ov.Material != null && !seenOverrideMaterials.Add( ov.Material ) )
				{
					Log.Warning( $"[Scatterer] Duplicate material override configuration found for material '{ov.Material.ResourceName}'." );
				}
			}
		}

		bool hasValidModel = false;
		_maxRenderDistance = ChunkLoadDistance;

		foreach ( var t in Models )
		{
			if ( t.Model != null )
			{
				var seenMaterials = new HashSet<TerrainMaterial>();
				bool hasWeight = false;

				// MISSING NULL CHECK FIXED HERE:
				if ( t.MaterialWeights != null )
				{
					foreach ( var mw in t.MaterialWeights )
					{
						if ( mw.Material == null ) continue;

						if ( !seenMaterials.Add( mw.Material ) )
							Log.Warning(
								$"[Scatterer] Duplicate material entry '{mw.Material.ResourceName}' found for model '{t.Model.ResourceName}'." );

						if ( mw.Weight <= 0f )
							Log.Warning(
								$"[Scatterer] Weight is set to 0 for material '{mw.Material.ResourceName}' on model '{t.Model.ResourceName}'." );

						if ( mw.Weight > 0f ) hasWeight = true;
					}
				}

				if ( hasWeight ) hasValidModel = true;

				if ( t.RenderDistance > _maxRenderDistance )
					_maxRenderDistance = t.RenderDistance;
			}
		}

		if ( !hasValidModel ) return;

		ModelEntries = Models.ToArray();
		_modelRenderDistSq = new float[ModelEntries.Length];
		_modelLodCounts = new int[ModelEntries.Length];
		_modelLodDistancesSq = new float[ModelEntries.Length][];

		for ( int m = 0; m < ModelEntries.Length; m++ )
		{
			_modelRenderDistSq[m] = ModelEntries[m].RenderDistance * ModelEntries[m].RenderDistance;
			var mdl = ModelEntries[m].Model;

			if ( mdl != null )
			{
				_modelLodCounts[m] = Math.Max( 1, mdl.MeshInfo.LodCount );
				if ( mdl.MeshInfo.LodSwitchDistances != null && mdl.MeshInfo.LodSwitchDistances.Length > 0 &&
				     mdl.MeshInfo.LodSwitchDistances[0] > 0f )
				{
					var switchDistances = mdl.MeshInfo.LodSwitchDistances;
					_modelLodDistancesSq[m] = new float[switchDistances.Length];
					for ( int i = 0; i < switchDistances.Length; i++ )
					{
						float dist = switchDistances[i] * ModelEntries[m].LodBias;
						_modelLodDistancesSq[m][i] = dist * dist;
					}
				}
				else if ( _modelLodCounts[m] > 1 )
				{
					int switchCount = _modelLodCounts[m] - 1;
					_modelLodDistancesSq[m] = new float[switchCount];
					float step = ModelEntries[m].RenderDistance / _modelLodCounts[m];
					for ( int i = 0; i < switchCount; i++ )
					{
						float dist = (step * (i + 1)) * ModelEntries[m].LodBias;
						_modelLodDistancesSq[m][i] = dist * dist;
					}
				}
				else
				{
					_modelLodDistancesSq[m] = Array.Empty<float>();
				}
			}
			else
			{
				_modelLodCounts[m] = 1;
				_modelLodDistancesSq[m] = Array.Empty<float>();
			}
		}

		_heightMapPixels = TargetTerrain.Storage.HeightMap;
		_controlMapPixels = TargetTerrain.Storage.ControlMap;
		_resolution = TargetTerrain.Storage.Resolution;
		_terrainSize = TargetTerrain.TerrainSize;
		_terrainHeight = TargetTerrain.TerrainHeight;

		_modelMaterialWeights = new float[Models.Count][];

		for ( int m = 0; m < Models.Count; m++ )
		{
			_modelMaterialWeights[m] = new float[32];
			if ( TargetTerrain.Storage.Materials != null && Models[m].MaterialWeights != null )
			{
				for ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )
				{
					foreach ( var mw in Models[m].MaterialWeights )
					{
						if ( mw.Material == TargetTerrain.Storage.Materials[i] )
						{
							_modelMaterialWeights[m][i] = mw.Weight;
							break;
						}
					}
				}
			}
		}

		_modelNoiseFields = new Sandbox.Utility.INoiseField[Models.Count];
		for ( int m = 0; m < Models.Count; m++ )
		{
			var modelEntry = Models[m];
			if ( modelEntry.NoiseType == ScattererNoiseType.None ) continue;
			var parameters = new Sandbox.Utility.Noise.Parameters( Seed + m, modelEntry.NoiseScale );

			if ( modelEntry.NoiseType == ScattererNoiseType.Perlin )
				_modelNoiseFields[m] = Sandbox.Utility.Noise.PerlinField( parameters );
			else if ( modelEntry.NoiseType == ScattererNoiseType.Simplex )
				_modelNoiseFields[m] = Sandbox.Utility.Noise.SimplexField( parameters );
		}

		// --- MAPPING AND CEILING INITIALIZATION ---
		_materialMaxInstances = new int[32];
		Array.Fill( _materialMaxInstances, MaxInstancesPerChunk );
		_maxInstancesLoopCount = MaxInstancesPerChunk;

		if ( TargetTerrain.Storage.Materials != null && MaterialOverrides != null )
		{
			for ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )
			{
				foreach ( var ov in MaterialOverrides )
				{
					if ( ov.Material == TargetTerrain.Storage.Materials[i] )
					{
						_materialMaxInstances[i] = ov.MaxInstances;
						if ( ov.MaxInstances > _maxInstancesLoopCount )
						{
							_maxInstancesLoopCount = ov.MaxInstances;
						}
						break;
					}
				}
			}
		}

		RebuildRenderCache();
	}
}