Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.cs
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" ), Description("Click ``Reinitialize``, after changing properties."), 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 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( "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 bool[][] _allowedModelIndices;
private GameObject _cachedHitObject;
private bool _cachedIsObstruction;
private Sandbox.Utility.INoiseField[] _modelNoiseFields;
private float _totalWeight;
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 ChunkRenderData[] _renderCache = Array.Empty<ChunkRenderData>();
[ThreadStatic]
private static List<Transform>[] _lodBuckets;
protected override void OnEnabled()
{
if ( !TargetTerrain.IsValid() )
{
TargetTerrain = Scene.GetComponentInChildren<Terrain>();
}
CreateSceneObject();
InitializeSystem();
}
protected override void OnUpdate()
{
ManageChunks();
}
protected override void OnDisabled()
{
_sceneObject?.Delete();
_sceneObject = null;
}
private void CreateSceneObject()
{
_sceneObject?.Delete();
_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;
_totalWeight = 0f;
_maxRenderDistance = ChunkLoadDistance;
foreach ( var t in Models )
{
if ( t.Model != null )
{
_totalWeight += t.Weight;
if ( t.RenderDistance > _maxRenderDistance )
_maxRenderDistance = t.RenderDistance;
}
}
if ( _totalWeight <= 0f ) 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;
_allowedModelIndices = new bool[Models.Count][];
for ( int m = 0; m < Models.Count; m++ )
{
_allowedModelIndices[m] = new bool[32];
if ( TargetTerrain.Storage.Materials != null && Models[m].AllowedMaterials != null )
{
for ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )
{
if ( Models[m].AllowedMaterials.Contains( TargetTerrain.Storage.Materials[i] ) )
_allowedModelIndices[m][i] = true;
}
}
}
_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 );
}
RebuildRenderCache();
}
}