s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=kivin.performantterrainscatterer&take=20
Showing code results for query:
*
(14 total matches found)
Game
library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Performant Terrain Scatterer" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "performantterrainscatterer" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "kivin" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "kivin.performantterrainscatterer" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-23T11:51:31.4189525Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.119.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.119.0")]
kivin.performantterrainscatterer
/
PerformantTerrainScatterer/PerformantTerrainScattererManager.Chunks.cs
Game
library
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private void ManageChunks()
{
if ( Scene == null ) return;
var camera = Scene.Camera;
if ( !camera.IsValid() ) return;
Vector3 cameraPos = camera.WorldPosition;
if ( _activeChunks.Count > 0 && cameraPos.DistanceSquared( _lastCameraPos ) < CameraUpdateThresholdSq )
return;
_lastCameraPos = cameraPos;
int cameraCx = (int)MathF.Floor( cameraPos.x / ChunkSize );
int cameraCy = (int)MathF.Floor( cameraPos.y / ChunkSize );
int chunksInRadius = (int)MathF.Ceiling( ChunkLoadDistance / ChunkSize );
for ( int dx = -chunksInRadius; dx <= chunksInRadius; dx++ )
{
for ( int dy = -chunksInRadius; dy <= chunksInRadius; dy++ )
{
var chunkCoord = (cameraCx + dx, cameraCy + dy);
if ( !_activeChunks.ContainsKey( chunkCoord ) && _generatingChunks.Add( chunkCoord ) )
_ = GenerateChunkAsync( chunkCoord.Item1, chunkCoord.Item2 );
}
}
_chunkRemovalList.Clear();
foreach ( var key in _activeChunks.Keys )
{
if ( Math.Abs( key.Item1 - cameraCx ) > chunksInRadius ||
Math.Abs( key.Item2 - cameraCy ) > chunksInRadius )
_chunkRemovalList.Add( key );
}
foreach ( var t in _chunkRemovalList )
_activeChunks.Remove( t );
if ( _chunkRemovalList.Count > 0 )
RebuildRenderCache();
}
private async Task GenerateChunkAsync( int cx, int cy )
{
try
{
if ( !IsValid || Scene == null ) return;
Transform terrainTransform = TargetTerrain.IsValid() ? TargetTerrain.WorldTransform : new Transform();
var tentativeInstances =
await GameTask.RunInThreadAsync( () => CalculateChunkPlacements( cx, cy, terrainTransform ) );
if ( !IsValid || Scene == null ) return;
var modelEntries = ModelEntries;
if ( tentativeInstances == null || modelEntries == null ) return;
int cellsPerChunk = (int)MathF.Ceiling( ChunkSize / GridCellSize );
int gridPadding = ObstructionBuffer;
int gridWidth = cellsPerChunk + (gridPadding * 2) + 2;
int gridArea = gridWidth * gridWidth;
bool[] localBlockedGrid = ArrayPool<bool>.Shared.Rent( gridArea );
Array.Clear( localBlockedGrid, 0, gridArea );
try
{
int minGridX = (int)MathF.Floor( (cx * ChunkSize) / GridCellSize ) - gridPadding;
int minGridY = (int)MathF.Floor( (cy * ChunkSize) / GridCellSize ) - gridPadding;
int traceCounter = 0;
bool needsTrace = CheckObstructions || SnapToGround;
List<Transform>[] modelLists = new List<Transform>[modelEntries.Length];
float cxSum = 0f, cySum = 0f, czSum = 0f;
int placedCount = 0;
Vector3 traceUpOffset = Vector3.Up * TraceStartHeight;
foreach ( var inst in tentativeInstances )
{
if ( !IsValid || Scene == null ) return;
Vector3 finalPosition = inst.Position;
int gridX = (int)MathF.Floor( inst.Position.x / GridCellSize );
int gridY = (int)MathF.Floor( inst.Position.y / GridCellSize );
int localX = gridX - minGridX;
int localY = gridY - minGridY;
if ( CheckObstructions )
{
bool isBlocked = false;
for ( int dx = -ObstructionBuffer; dx <= ObstructionBuffer; dx++ )
{
int nx = localX + dx;
if ( nx < 0 || nx >= gridWidth ) continue;
for ( int dy = -ObstructionBuffer; dy <= ObstructionBuffer; dy++ )
{
int ny = localY + dy;
if ( ny < 0 || ny >= gridWidth ) continue;
if ( localBlockedGrid[nx + ny * gridWidth] )
{
isBlocked = true;
break;
}
}
if ( isBlocked ) break;
}
if ( isBlocked ) continue;
}
if ( needsTrace )
{
var traceStart = inst.Position + traceUpOffset;
var traceEnd = inst.Position - traceUpOffset;
var trace = Scene.Trace.Ray( traceStart, traceEnd )
.WithoutTags( TraceIgnoreTags )
.Run();
bool hitObstruction = trace.Hit && IsObstruction( trace.GameObject );
if ( CheckObstructions && (trace.StartedSolid || hitObstruction) )
{
if ( localX >= 0 && localX < gridWidth && localY >= 0 && localY < gridWidth )
localBlockedGrid[localX + localY * gridWidth] = true;
continue;
}
if ( SnapToGround && trace.Hit && !trace.StartedSolid && !hitObstruction )
finalPosition = trace.HitPosition;
traceCounter++;
if ( traceCounter % 100 == 0 )
{
await GameTask.Yield();
if ( !IsValid || Scene == null ) return;
}
}
if ( inst.ModelIndex < 0 || inst.ModelIndex >= modelEntries.Length ) continue;
finalPosition += Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;
var list = modelLists[inst.ModelIndex] ??= new List<Transform>();
list.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );
cxSum += finalPosition.x;
cySum += finalPosition.y;
czSum += finalPosition.z;
placedCount++;
}
if ( !IsValid ) return;
var chunk = new ClutterChunk();
chunk.Center = placedCount > 0
? new Vector3( cxSum / placedCount, cySum / placedCount, czSum / placedCount )
: new Vector3( cx * ChunkSize + ChunkSize * 0.5f, cy * ChunkSize + ChunkSize * 0.5f, 0f );
for ( int i = 0; i < modelLists.Length; i++ )
{
if ( modelLists[i] != null && modelLists[i].Count > 0 )
chunk.TransformsByModel[i] = modelLists[i].ToArray();
}
_activeChunks[(cx, cy)] = chunk;
RebuildRenderCache();
}
finally
{
ArrayPool<bool>.Shared.Return( localBlockedGrid );
}
}
catch ( Exception )
{
}
finally
{
_generatingChunks.Remove( (cx, cy) );
}
}
}
UnitTest
library
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}
Game
library
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();
}
}
kivin.performantterrainscatterer
/
PerformantTerrainScatterer/PerformantTerrainScattererManager.Data.cs
Game
library
using System.Collections.Generic;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
public enum ScattererNoiseType
{
None,
Perlin,
Simplex
}
public struct MaterialWeight
{
public MaterialWeight() { }
[Property] public TerrainMaterial Material { get; set; } = null;
[Property, Range( 0f, 100f )] public float Weight { get; set; } = 1f;
}
public struct MaterialInstanceOverride
{
public MaterialInstanceOverride() { }
[Property] public TerrainMaterial Material { get; set; } = null;
[Property] public int MaxInstances { get; set; } = 500;
}
public struct ScattererModelEntry
{
public ScattererModelEntry() { }
[Property, Category( "Base" )] public Model Model { get; set; } = null;
[Property, Category( "Base" )] public float ZOffset { get; set; } = 0f;
[Property, Category( "Noise Scattering" )]
public ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;
[Property, Category( "Noise Scattering" )]
public float NoiseScale { get; set; } = 0.002f;
[Property, Category( "Noise Scattering" ), Range( 0f, 1f )]
public float NoiseThreshold { get; set; } = 0.4f;
[Property, Category( "Performance" )] public float RenderDistance { get; set; } = 2500f;
[Property, Category( "Terrain" )] public List<MaterialWeight> MaterialWeights { get; set; } = new();
[Property, Category( "Randomization" )]
public bool RandomizeScale { get; set; } = false;
[Property, Category( "Randomization" ), ShowIf( nameof(RandomizeScale), true )]
public float MinScale { get; set; } = 0.8f;
[Property, Category( "Randomization" ), ShowIf( nameof(RandomizeScale), true )]
public float MaxScale { get; set; } = 1.2f;
[Property, Category( "LOD" ), Range( 0.1f, 4f )]
public float LodBias { get; set; } = 1.0f;
}
private struct ChunkRenderData
{
public Vector3 Center;
public ModelRenderBatch[] Batches;
}
private struct ModelRenderBatch
{
public Model Model;
public Transform[] Transforms;
public float RenderDistSq;
public float[] LodDistancesSq;
public int MaxLod;
}
private struct InstanceData
{
public Vector3 Position;
public Rotation Rotation;
public float Scale;
public int ModelIndex;
}
private class ClutterChunk
{
public Vector3 Center;
public readonly Dictionary<int, Transform[]> TransformsByModel = new();
}
}
kivin.performantterrainscatterer
/
PerformantTerrainScatterer/PerformantTerrainScattererManager.Rendering.cs
Game
library
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private void RebuildRenderCache()
{
var newCache = new ChunkRenderData[_activeChunks.Count];
int index = 0;
foreach ( var chunk in _activeChunks.Values )
{
var batches = new ModelRenderBatch[chunk.TransformsByModel.Count];
int bIndex = 0;
foreach ( var kvp in chunk.TransformsByModel )
{
int mIdx = kvp.Key;
batches[bIndex++] = new ModelRenderBatch
{
Model = ModelEntries[mIdx].Model,
Transforms = kvp.Value,
RenderDistSq = _modelRenderDistSq[mIdx],
LodDistancesSq = _modelLodDistancesSq[mIdx],
MaxLod = Math.Max( 0, _modelLodCounts[mIdx] - 1 )
};
}
newCache[index++] = new ChunkRenderData { Center = chunk.Center, Batches = batches };
}
_renderCache = newCache;
}
private void RenderClutter( SceneObject obj )
{
var cache = _renderCache;
if ( cache.Length == 0 ) return;
if ( Scene == null ) return;
var camera = Scene.Camera;
if ( !camera.IsValid() ) return;
Vector3 cameraPos = camera.WorldPosition;
Vector3 cameraForward = camera.WorldRotation.Forward;
float frustumMinDistSq = FrustumCullMinDistance * FrustumCullMinDistance;
float maxRenderDistSq = _maxRenderDistance * _maxRenderDistance;
bool useFrustum = UseFrustumCulling;
if ( _lodBuckets == null )
{
_lodBuckets = new List<Transform>[16];
for ( int i = 0; i < _lodBuckets.Length; i++ )
_lodBuckets[i] = new List<Transform>();
}
for ( int c = 0; c < cache.Length; c++ )
{
ref var chunk = ref cache[c];
Vector3 toChunk = chunk.Center - cameraPos;
float chunkDistSq = toChunk.LengthSquared;
if ( chunkDistSq > maxRenderDistSq ) continue;
if ( useFrustum && chunkDistSq > frustumMinDistSq )
{
float dot = Vector3.Dot( cameraForward, toChunk );
if ( dot < 0f && (dot * dot) > 0.04f * chunkDistSq )
continue;
}
var batches = chunk.Batches;
for ( int b = 0; b < batches.Length; b++ )
{
ref var batch = ref batches[b];
if ( chunkDistSq > batch.RenderDistSq ) continue;
int maxLod = batch.MaxLod;
for ( int i = 0; i <= maxLod; i++ )
_lodBuckets[i].Clear();
var transforms = batch.Transforms;
var lodDists = batch.LodDistancesSq;
for ( int i = 0; i < transforms.Length; i++ )
{
Vector3 instPos = transforms[i].Position;
float instDistSq = (instPos - cameraPos).LengthSquared;
if ( instDistSq > batch.RenderDistSq ) continue;
int lod = 0;
for ( int l = 0; l < lodDists.Length; l++ )
{
if ( instDistSq > lodDists[l] )
lod = l + 1;
else
break;
}
if ( lod > maxLod ) lod = maxLod;
_lodBuckets[lod].Add( transforms[i] );
}
for ( int l = 0; l <= maxLod; l++ )
{
var bucket = _lodBuckets[l];
if ( bucket.Count > 0 )
{
Graphics.DrawModelInstanced( batch.Model, CollectionsMarshal.AsSpan( bucket ), l );
}
}
}
}
}
}
Editor
library
using Editor;
public static class MyEditorMenu
{
[Menu( "Editor", "Performant Terrain Scatterer/My Menu Option" )]
public static void OpenMyMenu()
{
EditorUtility.DisplayDialog( "It worked!", "This is being called from your library's editor code!" );
}
}
kivin.performantterrainscatterer
/
Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Data.cs
Game
library
using System.Collections.Generic;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
public enum ScattererNoiseType
{
None,
Perlin,
Simplex
}
public struct MaterialWeight
{
public MaterialWeight() { }
[Property] public TerrainMaterial Material { get; set; } = null;
[Property, Range( 0f, 100f )] public float Weight { get; set; } = 1f;
}
public struct MaterialInstanceOverride
{
public MaterialInstanceOverride() { }
[Property] public TerrainMaterial Material { get; set; } = null;
[Property] public int MaxInstances { get; set; } = 500;
}
public struct ScattererModelEntry
{
public ScattererModelEntry() { }
[Property, Category( "Base" )] public Model Model { get; set; } = null;
[Property, Category( "Base" )] public float ZOffset { get; set; } = 0f;
[Property, Category( "Noise Scattering" )]
public ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;
[Property, Category( "Noise Scattering" )]
public float NoiseScale { get; set; } = 0.002f;
[Property, Category( "Noise Scattering" ), Range( 0f, 1f )]
public float NoiseThreshold { get; set; } = 0.4f;
[Property, Category( "Performance" )] public float RenderDistance { get; set; } = 2500f;
[Property, Category( "Terrain" )] public List<MaterialWeight> MaterialWeights { get; set; } = new();
[Property, Category( "Randomization" )]
public bool RandomizeScale { get; set; } = false;
[Property, Category( "Randomization" ), ShowIf( nameof(RandomizeScale), true )]
public float MinScale { get; set; } = 0.8f;
[Property, Category( "Randomization" ), ShowIf( nameof(RandomizeScale), true )]
public float MaxScale { get; set; } = 1.2f;
[Property, Category( "LOD" ), Range( 0.1f, 4f )]
public float LodBias { get; set; } = 1.0f;
}
private struct ChunkRenderData
{
public Vector3 Center;
public ModelRenderBatch[] Batches;
}
private struct ModelRenderBatch
{
public Model Model;
public Transform[] Transforms;
public float RenderDistSq;
public float[] LodDistancesSq;
public int MaxLod;
}
private struct InstanceData
{
public Vector3 Position;
public Rotation Rotation;
public float Scale;
public int ModelIndex;
}
private class ClutterChunk
{
public Vector3 Center;
public readonly Dictionary<int, Transform[]> TransformsByModel = new();
}
}
kivin.performantterrainscatterer
/
PerformantTerrainScatterer/PerformantTerrainScattererManager.Placement.cs
Game
library
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private List<InstanceData> CalculateChunkPlacements( int cx, int cy, Transform terrainTransform )
{
if ( _heightMapPixels == null || _controlMapPixels == null ) return null;
var modelEntries = ModelEntries;
if ( modelEntries == null || modelEntries.Length == 0 ) return null;
int loopCount = _maxInstancesLoopCount;
var instances = new List<InstanceData>( loopCount );
int chunkSeed = HashCombine( Seed, HashCombine( cx, cy ) );
var random = new Random( chunkSeed );
float textureEps = 1.0f / (_resolution - 1);
float chunkBaseX = cx * ChunkSize;
float chunkBaseY = cy * ChunkSize;
int modelsCount = modelEntries.Length;
int[] placedCountPerMaterial = new int[32];
for ( int i = 0; i < loopCount; i++ )
{
float localX = random.NextSingle() * ChunkSize;
float localY = random.NextSingle() * ChunkSize;
float worldX = chunkBaseX + localX;
float worldY = chunkBaseY + localY;
Vector3 worldAbsPoint = new Vector3( worldX, worldY, 0f );
Vector3 terrainLocal = terrainTransform.PointToLocal( worldAbsPoint );
float u = terrainLocal.x / _terrainSize;
float v = terrainLocal.y / _terrainSize;
if ( u < 0f || u > 1f || v < 0f || v > 1f ) continue;
int cX = (int)(u * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );
int cY = (int)(v * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );
int controlIndex = cX + cY * _resolution;
uint packedMap = _controlMapPixels[controlIndex];
var compactMat = new CompactTerrainMaterial( packedMap );
if ( compactMat.IsHole ) continue;
int dominantMaterialId = (compactMat.BlendFactor > 127)
? compactMat.OverlayTextureId
: compactMat.BaseTextureId;
if ( dominantMaterialId < 32 )
{
if ( placedCountPerMaterial[dominantMaterialId] >= _materialMaxInstances[dominantMaterialId] )
continue;
}
float materialTotalWeight = 0f;
for ( int m = 0; m < modelsCount; m++ )
{
if ( dominantMaterialId < _modelMaterialWeights[m].Length )
{
materialTotalWeight += _modelMaterialWeights[m][dominantMaterialId];
}
}
if ( materialTotalWeight <= 0f ) continue;
float roll = random.NextSingle() * materialTotalWeight;
float currentWeightSum = 0f;
int selectedModelIndex = -1;
for ( int m = 0; m < modelsCount; m++ )
{
if ( dominantMaterialId < _modelMaterialWeights[m].Length )
{
float w = _modelMaterialWeights[m][dominantMaterialId];
if ( w > 0f )
{
currentWeightSum += w;
if ( roll <= currentWeightSum )
{
selectedModelIndex = m;
break;
}
}
}
}
if ( selectedModelIndex == -1 ) continue;
ref var selectedModel = ref modelEntries[selectedModelIndex];
if ( ReduceDensityOnTransitions )
{
float blendNorm = compactMat.BlendFactor * 0.003921569f;
float purity = MathF.Abs( blendNorm - 0.5f ) * 2f;
float spawnChance = MathF.Pow( purity, TransitionThinningIntensity );
if ( random.NextSingle() > spawnChance ) continue;
}
if ( selectedModel.NoiseType != ScattererNoiseType.None )
{
var noiseField = _modelNoiseFields[selectedModelIndex];
float noiseVal = noiseField.Sample( worldX, worldY );
if ( noiseVal < selectedModel.NoiseThreshold ) continue;
}
Vector3 normal = Vector3.Up;
if ( UseTerrainNormal || MaxSlopeAngle < 90f )
{
float hL = SampleHeightBilinear( u - textureEps, v );
float hR = SampleHeightBilinear( u + textureEps, v );
float hD = SampleHeightBilinear( u, v - textureEps );
float hU = SampleHeightBilinear( u, v + textureEps );
Vector3 terrainTangentX =
new Vector3( 2.0f * textureEps * _terrainSize, 0f, (hR - hL) * _terrainHeight );
Vector3 terrainTangentY =
new Vector3( 0f, 2.0f * textureEps * _terrainSize, (hU - hD) * _terrainHeight );
Vector3 terrainNormalLocal = Vector3.Cross( terrainTangentX, terrainTangentY ).Normal;
normal = terrainTransform.Rotation * terrainNormalLocal;
float slopeAngle = Vector3.GetAngle( Vector3.Up, normal );
if ( slopeAngle > MaxSlopeAngle ) continue;
}
float z = SampleHeightBilinear( u, v ) * _terrainHeight;
Vector3 finalLocalPoint = new Vector3( terrainLocal.x, terrainLocal.y, z );
Vector3 worldTerrainPoint = terrainTransform.PointToWorld( finalLocalPoint );
Rotation randomYaw = RandomizeRotation
? Rotation.FromYaw( random.NextSingle() * 360f )
: Rotation.Identity;
Rotation finalRot = UseTerrainNormal
? Rotation.FromToRotation( Vector3.Up, normal ) * randomYaw
: randomYaw;
float scale = selectedModel.RandomizeScale
? (random.NextSingle() * (selectedModel.MaxScale - selectedModel.MinScale) + selectedModel.MinScale)
: 1.0f;
instances.Add( new InstanceData
{
Position = worldTerrainPoint, Rotation = finalRot, Scale = scale, ModelIndex = selectedModelIndex
} );
if ( dominantMaterialId < 32 )
{
placedCountPerMaterial[dominantMaterialId]++;
}
}
return instances;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private int HashCombine( int seed, int value )
=> seed ^ (value + -1640531527 + (seed << 6) + (seed >> 2));
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private bool IsObstruction( GameObject hitObject )
{
if ( hitObject == null ) return false;
if ( _cachedHitObject == hitObject ) return _cachedIsObstruction;
_cachedHitObject = hitObject;
_cachedIsObstruction = hitObject.Components.Get<Terrain>() == null;
return _cachedIsObstruction;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private float SampleHeightBilinear( float u, float v )
{
u = u.Clamp( 0f, 1f );
v = v.Clamp( 0f, 1f );
float gx = u * (_resolution - 1);
float gy = v * (_resolution - 1);
int ix = (int)gx;
int iy = (int)gy;
float fx = gx - ix;
float fy = gy - iy;
int ix1 = Math.Min( ix + 1, _resolution - 1 );
int iy1 = Math.Min( iy + 1, _resolution - 1 );
int row0 = iy * _resolution;
int row1 = iy1 * _resolution;
float h00 = _heightMapPixels[row0 + ix] * UshortMaxReciprocal;
float h10 = _heightMapPixels[row0 + ix1] * UshortMaxReciprocal;
float h01 = _heightMapPixels[row1 + ix] * UshortMaxReciprocal;
float h11 = _heightMapPixels[row1 + ix1] * UshortMaxReciprocal;
float h0 = h00 + (h10 - h00) * fx;
float h1 = h01 + (h11 - h01) * fx;
return h0 + (h1 - h0) * fy;
}
}
kivin.performantterrainscatterer
/
Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.cs
Game
library
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();
}
}
UnitTest
library
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
kivin.performantterrainscatterer
/
Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Chunks.cs
Game
library
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private void ManageChunks()
{
if ( Scene == null ) return;
var camera = Scene.Camera;
if ( !camera.IsValid() ) return;
Vector3 cameraPos = camera.WorldPosition;
if ( _activeChunks.Count > 0 && cameraPos.DistanceSquared( _lastCameraPos ) < CameraUpdateThresholdSq )
return;
_lastCameraPos = cameraPos;
int cameraCx = (int)MathF.Floor( cameraPos.x / ChunkSize );
int cameraCy = (int)MathF.Floor( cameraPos.y / ChunkSize );
int chunksInRadius = (int)MathF.Ceiling( ChunkLoadDistance / ChunkSize );
for ( int dx = -chunksInRadius; dx <= chunksInRadius; dx++ )
{
for ( int dy = -chunksInRadius; dy <= chunksInRadius; dy++ )
{
var chunkCoord = (cameraCx + dx, cameraCy + dy);
if ( !_activeChunks.ContainsKey( chunkCoord ) && _generatingChunks.Add( chunkCoord ) )
_ = GenerateChunkAsync( chunkCoord.Item1, chunkCoord.Item2 );
}
}
_chunkRemovalList.Clear();
foreach ( var key in _activeChunks.Keys )
{
if ( Math.Abs( key.Item1 - cameraCx ) > chunksInRadius ||
Math.Abs( key.Item2 - cameraCy ) > chunksInRadius )
_chunkRemovalList.Add( key );
}
foreach ( var t in _chunkRemovalList )
_activeChunks.Remove( t );
if ( _chunkRemovalList.Count > 0 )
RebuildRenderCache();
}
private async Task GenerateChunkAsync( int cx, int cy )
{
try
{
if ( !IsValid || Scene == null ) return;
Transform terrainTransform = TargetTerrain.IsValid() ? TargetTerrain.WorldTransform : new Transform();
var tentativeInstances =
await GameTask.RunInThreadAsync( () => CalculateChunkPlacements( cx, cy, terrainTransform ) );
if ( !IsValid || Scene == null ) return;
var modelEntries = ModelEntries;
if ( tentativeInstances == null || modelEntries == null ) return;
int cellsPerChunk = (int)MathF.Ceiling( ChunkSize / GridCellSize );
int gridPadding = ObstructionBuffer;
int gridWidth = cellsPerChunk + (gridPadding * 2) + 2;
int gridArea = gridWidth * gridWidth;
bool[] localBlockedGrid = ArrayPool<bool>.Shared.Rent( gridArea );
Array.Clear( localBlockedGrid, 0, gridArea );
try
{
int minGridX = (int)MathF.Floor( (cx * ChunkSize) / GridCellSize ) - gridPadding;
int minGridY = (int)MathF.Floor( (cy * ChunkSize) / GridCellSize ) - gridPadding;
int traceCounter = 0;
bool needsTrace = CheckObstructions || SnapToGround;
List<Transform>[] modelLists = new List<Transform>[modelEntries.Length];
float cxSum = 0f, cySum = 0f, czSum = 0f;
int placedCount = 0;
Vector3 traceUpOffset = Vector3.Up * TraceStartHeight;
foreach ( var inst in tentativeInstances )
{
if ( !IsValid || Scene == null ) return;
Vector3 finalPosition = inst.Position;
int gridX = (int)MathF.Floor( inst.Position.x / GridCellSize );
int gridY = (int)MathF.Floor( inst.Position.y / GridCellSize );
int localX = gridX - minGridX;
int localY = gridY - minGridY;
if ( CheckObstructions )
{
bool isBlocked = false;
for ( int dx = -ObstructionBuffer; dx <= ObstructionBuffer; dx++ )
{
int nx = localX + dx;
if ( nx < 0 || nx >= gridWidth ) continue;
for ( int dy = -ObstructionBuffer; dy <= ObstructionBuffer; dy++ )
{
int ny = localY + dy;
if ( ny < 0 || ny >= gridWidth ) continue;
if ( localBlockedGrid[nx + ny * gridWidth] )
{
isBlocked = true;
break;
}
}
if ( isBlocked ) break;
}
if ( isBlocked ) continue;
}
if ( needsTrace )
{
var traceStart = inst.Position + traceUpOffset;
var traceEnd = inst.Position - traceUpOffset;
var trace = Scene.Trace.Ray( traceStart, traceEnd )
.WithoutTags( TraceIgnoreTags )
.Run();
bool hitObstruction = trace.Hit && IsObstruction( trace.GameObject );
if ( CheckObstructions && (trace.StartedSolid || hitObstruction) )
{
if ( localX >= 0 && localX < gridWidth && localY >= 0 && localY < gridWidth )
localBlockedGrid[localX + localY * gridWidth] = true;
continue;
}
if ( SnapToGround && trace.Hit && !trace.StartedSolid && !hitObstruction )
finalPosition = trace.HitPosition;
traceCounter++;
if ( traceCounter % 100 == 0 )
{
await GameTask.Yield();
if ( !IsValid || Scene == null ) return;
}
}
if ( inst.ModelIndex < 0 || inst.ModelIndex >= modelEntries.Length ) continue;
finalPosition += Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;
var list = modelLists[inst.ModelIndex] ??= new List<Transform>();
list.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );
cxSum += finalPosition.x;
cySum += finalPosition.y;
czSum += finalPosition.z;
placedCount++;
}
if ( !IsValid ) return;
var chunk = new ClutterChunk();
chunk.Center = placedCount > 0
? new Vector3( cxSum / placedCount, cySum / placedCount, czSum / placedCount )
: new Vector3( cx * ChunkSize + ChunkSize * 0.5f, cy * ChunkSize + ChunkSize * 0.5f, 0f );
for ( int i = 0; i < modelLists.Length; i++ )
{
if ( modelLists[i] != null && modelLists[i].Count > 0 )
chunk.TransformsByModel[i] = modelLists[i].ToArray();
}
_activeChunks[(cx, cy)] = chunk;
RebuildRenderCache();
}
finally
{
ArrayPool<bool>.Shared.Return( localBlockedGrid );
}
}
catch ( Exception )
{
}
finally
{
_generatingChunks.Remove( (cx, cy) );
}
}
}
kivin.performantterrainscatterer
/
Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Rendering.cs
Game
library
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private void RebuildRenderCache()
{
var newCache = new ChunkRenderData[_activeChunks.Count];
int index = 0;
foreach ( var chunk in _activeChunks.Values )
{
var batches = new ModelRenderBatch[chunk.TransformsByModel.Count];
int bIndex = 0;
foreach ( var kvp in chunk.TransformsByModel )
{
int mIdx = kvp.Key;
batches[bIndex++] = new ModelRenderBatch
{
Model = ModelEntries[mIdx].Model,
Transforms = kvp.Value,
RenderDistSq = _modelRenderDistSq[mIdx],
LodDistancesSq = _modelLodDistancesSq[mIdx],
MaxLod = Math.Max( 0, _modelLodCounts[mIdx] - 1 )
};
}
newCache[index++] = new ChunkRenderData { Center = chunk.Center, Batches = batches };
}
_renderCache = newCache;
}
private void RenderClutter( SceneObject obj )
{
var cache = _renderCache;
if ( cache.Length == 0 ) return;
if ( Scene == null ) return;
var camera = Scene.Camera;
if ( !camera.IsValid() ) return;
Vector3 cameraPos = camera.WorldPosition;
Vector3 cameraForward = camera.WorldRotation.Forward;
float frustumMinDistSq = FrustumCullMinDistance * FrustumCullMinDistance;
float maxRenderDistSq = _maxRenderDistance * _maxRenderDistance;
bool useFrustum = UseFrustumCulling;
if ( _lodBuckets == null )
{
_lodBuckets = new List<Transform>[16];
for ( int i = 0; i < _lodBuckets.Length; i++ )
_lodBuckets[i] = new List<Transform>();
}
for ( int c = 0; c < cache.Length; c++ )
{
ref var chunk = ref cache[c];
Vector3 toChunk = chunk.Center - cameraPos;
float chunkDistSq = toChunk.LengthSquared;
if ( chunkDistSq > maxRenderDistSq ) continue;
if ( useFrustum && chunkDistSq > frustumMinDistSq )
{
float dot = Vector3.Dot( cameraForward, toChunk );
if ( dot < 0f && (dot * dot) > 0.04f * chunkDistSq )
continue;
}
var batches = chunk.Batches;
for ( int b = 0; b < batches.Length; b++ )
{
ref var batch = ref batches[b];
if ( chunkDistSq > batch.RenderDistSq ) continue;
int maxLod = batch.MaxLod;
for ( int i = 0; i <= maxLod; i++ )
_lodBuckets[i].Clear();
var transforms = batch.Transforms;
var lodDists = batch.LodDistancesSq;
for ( int i = 0; i < transforms.Length; i++ )
{
Vector3 instPos = transforms[i].Position;
float instDistSq = (instPos - cameraPos).LengthSquared;
if ( instDistSq > batch.RenderDistSq ) continue;
int lod = 0;
for ( int l = 0; l < lodDists.Length; l++ )
{
if ( instDistSq > lodDists[l] )
lod = l + 1;
else
break;
}
if ( lod > maxLod ) lod = maxLod;
_lodBuckets[lod].Add( transforms[i] );
}
for ( int l = 0; l <= maxLod; l++ )
{
var bucket = _lodBuckets[l];
if ( bucket.Count > 0 )
{
Graphics.DrawModelInstanced( batch.Model, CollectionsMarshal.AsSpan( bucket ), l );
}
}
}
}
}
}
kivin.performantterrainscatterer
/
Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Placement.cs
Game
library
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private List<InstanceData> CalculateChunkPlacements( int cx, int cy, Transform terrainTransform )
{
if ( _heightMapPixels == null || _controlMapPixels == null ) return null;
var modelEntries = ModelEntries;
if ( modelEntries == null || modelEntries.Length == 0 ) return null;
int loopCount = _maxInstancesLoopCount;
var instances = new List<InstanceData>( loopCount );
int chunkSeed = HashCombine( Seed, HashCombine( cx, cy ) );
var random = new Random( chunkSeed );
float textureEps = 1.0f / (_resolution - 1);
float chunkBaseX = cx * ChunkSize;
float chunkBaseY = cy * ChunkSize;
int modelsCount = modelEntries.Length;
int[] placedCountPerMaterial = new int[32];
for ( int i = 0; i < loopCount; i++ )
{
float localX = random.NextSingle() * ChunkSize;
float localY = random.NextSingle() * ChunkSize;
float worldX = chunkBaseX + localX;
float worldY = chunkBaseY + localY;
Vector3 worldAbsPoint = new Vector3( worldX, worldY, 0f );
Vector3 terrainLocal = terrainTransform.PointToLocal( worldAbsPoint );
float u = terrainLocal.x / _terrainSize;
float v = terrainLocal.y / _terrainSize;
if ( u < 0f || u > 1f || v < 0f || v > 1f ) continue;
int cX = (int)(u * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );
int cY = (int)(v * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );
int controlIndex = cX + cY * _resolution;
uint packedMap = _controlMapPixels[controlIndex];
var compactMat = new CompactTerrainMaterial( packedMap );
if ( compactMat.IsHole ) continue;
int dominantMaterialId = (compactMat.BlendFactor > 127)
? compactMat.OverlayTextureId
: compactMat.BaseTextureId;
if ( dominantMaterialId < 32 )
{
if ( placedCountPerMaterial[dominantMaterialId] >= _materialMaxInstances[dominantMaterialId] )
continue;
}
float materialTotalWeight = 0f;
for ( int m = 0; m < modelsCount; m++ )
{
if ( dominantMaterialId < _modelMaterialWeights[m].Length )
{
materialTotalWeight += _modelMaterialWeights[m][dominantMaterialId];
}
}
if ( materialTotalWeight <= 0f ) continue;
float roll = random.NextSingle() * materialTotalWeight;
float currentWeightSum = 0f;
int selectedModelIndex = -1;
for ( int m = 0; m < modelsCount; m++ )
{
if ( dominantMaterialId < _modelMaterialWeights[m].Length )
{
float w = _modelMaterialWeights[m][dominantMaterialId];
if ( w > 0f )
{
currentWeightSum += w;
if ( roll <= currentWeightSum )
{
selectedModelIndex = m;
break;
}
}
}
}
if ( selectedModelIndex == -1 ) continue;
ref var selectedModel = ref modelEntries[selectedModelIndex];
if ( ReduceDensityOnTransitions )
{
float blendNorm = compactMat.BlendFactor * 0.003921569f;
float purity = MathF.Abs( blendNorm - 0.5f ) * 2f;
float spawnChance = MathF.Pow( purity, TransitionThinningIntensity );
if ( random.NextSingle() > spawnChance ) continue;
}
if ( selectedModel.NoiseType != ScattererNoiseType.None )
{
var noiseField = _modelNoiseFields[selectedModelIndex];
float noiseVal = noiseField.Sample( worldX, worldY );
if ( noiseVal < selectedModel.NoiseThreshold ) continue;
}
Vector3 normal = Vector3.Up;
if ( UseTerrainNormal || MaxSlopeAngle < 90f )
{
float hL = SampleHeightBilinear( u - textureEps, v );
float hR = SampleHeightBilinear( u + textureEps, v );
float hD = SampleHeightBilinear( u, v - textureEps );
float hU = SampleHeightBilinear( u, v + textureEps );
Vector3 terrainTangentX =
new Vector3( 2.0f * textureEps * _terrainSize, 0f, (hR - hL) * _terrainHeight );
Vector3 terrainTangentY =
new Vector3( 0f, 2.0f * textureEps * _terrainSize, (hU - hD) * _terrainHeight );
Vector3 terrainNormalLocal = Vector3.Cross( terrainTangentX, terrainTangentY ).Normal;
normal = terrainTransform.Rotation * terrainNormalLocal;
float slopeAngle = Vector3.GetAngle( Vector3.Up, normal );
if ( slopeAngle > MaxSlopeAngle ) continue;
}
float z = SampleHeightBilinear( u, v ) * _terrainHeight;
Vector3 finalLocalPoint = new Vector3( terrainLocal.x, terrainLocal.y, z );
Vector3 worldTerrainPoint = terrainTransform.PointToWorld( finalLocalPoint );
Rotation randomYaw = RandomizeRotation
? Rotation.FromYaw( random.NextSingle() * 360f )
: Rotation.Identity;
Rotation finalRot = UseTerrainNormal
? Rotation.FromToRotation( Vector3.Up, normal ) * randomYaw
: randomYaw;
float scale = selectedModel.RandomizeScale
? (random.NextSingle() * (selectedModel.MaxScale - selectedModel.MinScale) + selectedModel.MinScale)
: 1.0f;
instances.Add( new InstanceData
{
Position = worldTerrainPoint, Rotation = finalRot, Scale = scale, ModelIndex = selectedModelIndex
} );
if ( dominantMaterialId < 32 )
{
placedCountPerMaterial[dominantMaterialId]++;
}
}
return instances;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private int HashCombine( int seed, int value )
=> seed ^ (value + -1640531527 + (seed << 6) + (seed >> 2));
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private bool IsObstruction( GameObject hitObject )
{
if ( hitObject == null ) return false;
if ( _cachedHitObject == hitObject ) return _cachedIsObstruction;
_cachedHitObject = hitObject;
_cachedIsObstruction = hitObject.Components.Get<Terrain>() == null;
return _cachedIsObstruction;
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private float SampleHeightBilinear( float u, float v )
{
u = u.Clamp( 0f, 1f );
v = v.Clamp( 0f, 1f );
float gx = u * (_resolution - 1);
float gy = v * (_resolution - 1);
int ix = (int)gx;
int iy = (int)gy;
float fx = gx - ix;
float fy = gy - iy;
int ix1 = Math.Min( ix + 1, _resolution - 1 );
int iy1 = Math.Min( iy + 1, _resolution - 1 );
int row0 = iy * _resolution;
int row1 = iy1 * _resolution;
float h00 = _heightMapPixels[row0 + ix] * UshortMaxReciprocal;
float h10 = _heightMapPixels[row0 + ix1] * UshortMaxReciprocal;
float h01 = _heightMapPixels[row1 + ix] * UshortMaxReciprocal;
float h11 = _heightMapPixels[row1 + ix1] * UshortMaxReciprocal;
float h0 = h00 + (h10 - h00) * fx;
float h1 = h01 + (h11 - h01) * fx;
return h0 + (h1 - h0) * fy;
}
}
Debug: View Raw JSON Response
{
"TotalCount": 14,
"Files": [
{
"Ident": "kivin.performantterrainscatterer",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Performant Terrain Scatterer\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"performantterrainscatterer\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"kivin\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"kivin.performantterrainscatterer\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"26\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-06-23T11:51:31.4189525Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.119.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.119.0\")]"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "PerformantTerrainScatterer/PerformantTerrainScattererManager.Chunks.cs",
"FileName": "PerformantTerrainScattererManager.Chunks.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Buffers;\r\nusing System.Collections.Generic;\r\nusing System.Threading.Tasks;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate void ManageChunks()\r\n\t{\r\n\t\tif ( Scene == null ) return;\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\t\tVector3 cameraPos = camera.WorldPosition;\r\n\r\n\t\tif ( _activeChunks.Count > 0 && cameraPos.DistanceSquared( _lastCameraPos ) < CameraUpdateThresholdSq )\r\n\t\t\treturn;\r\n\r\n\t\t_lastCameraPos = cameraPos;\r\n\t\tint cameraCx = (int)MathF.Floor( cameraPos.x / ChunkSize );\r\n\t\tint cameraCy = (int)MathF.Floor( cameraPos.y / ChunkSize );\r\n\t\tint chunksInRadius = (int)MathF.Ceiling( ChunkLoadDistance / ChunkSize );\r\n\r\n\t\tfor ( int dx = -chunksInRadius; dx <= chunksInRadius; dx++ )\r\n\t\t{\r\n\t\t\tfor ( int dy = -chunksInRadius; dy <= chunksInRadius; dy++ )\r\n\t\t\t{\r\n\t\t\t\tvar chunkCoord = (cameraCx + dx, cameraCy + dy);\r\n\t\t\t\tif ( !_activeChunks.ContainsKey( chunkCoord ) && _generatingChunks.Add( chunkCoord ) )\r\n\t\t\t\t\t_ = GenerateChunkAsync( chunkCoord.Item1, chunkCoord.Item2 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_chunkRemovalList.Clear();\r\n\t\tforeach ( var key in _activeChunks.Keys )\r\n\t\t{\r\n\t\t\tif ( Math.Abs( key.Item1 - cameraCx ) > chunksInRadius ||\r\n\t\t\t Math.Abs( key.Item2 - cameraCy ) > chunksInRadius )\r\n\t\t\t\t_chunkRemovalList.Add( key );\r\n\t\t}\r\n\r\n\t\tforeach ( var t in _chunkRemovalList )\r\n\t\t\t_activeChunks.Remove( t );\r\n\r\n\t\tif ( _chunkRemovalList.Count > 0 )\r\n\t\t\tRebuildRenderCache();\r\n\t}\r\n\r\n\tprivate async Task GenerateChunkAsync( int cx, int cy )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !IsValid || Scene == null ) return;\r\n\t\t\tTransform terrainTransform = TargetTerrain.IsValid() ? TargetTerrain.WorldTransform : new Transform();\r\n\r\n\t\t\tvar tentativeInstances =\r\n\t\t\t\tawait GameTask.RunInThreadAsync( () => CalculateChunkPlacements( cx, cy, terrainTransform ) );\r\n\r\n\t\t\tif ( !IsValid || Scene == null ) return;\r\n\r\n\t\t\tvar modelEntries = ModelEntries;\r\n\t\t\tif ( tentativeInstances == null || modelEntries == null ) return;\r\n\r\n\t\t\tint cellsPerChunk = (int)MathF.Ceiling( ChunkSize / GridCellSize );\r\n\t\t\tint gridPadding = ObstructionBuffer;\r\n\t\t\tint gridWidth = cellsPerChunk + (gridPadding * 2) + 2;\r\n\t\t\tint gridArea = gridWidth * gridWidth;\r\n\r\n\t\t\tbool[] localBlockedGrid = ArrayPool<bool>.Shared.Rent( gridArea );\r\n\t\t\tArray.Clear( localBlockedGrid, 0, gridArea );\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tint minGridX = (int)MathF.Floor( (cx * ChunkSize) / GridCellSize ) - gridPadding;\r\n\t\t\t\tint minGridY = (int)MathF.Floor( (cy * ChunkSize) / GridCellSize ) - gridPadding;\r\n\t\t\t\tint traceCounter = 0;\r\n\t\t\t\tbool needsTrace = CheckObstructions || SnapToGround;\r\n\t\t\t\tList<Transform>[] modelLists = new List<Transform>[modelEntries.Length];\r\n\t\t\t\tfloat cxSum = 0f, cySum = 0f, czSum = 0f;\r\n\t\t\t\tint placedCount = 0;\r\n\t\t\t\tVector3 traceUpOffset = Vector3.Up * TraceStartHeight;\r\n\r\n\t\t\t\tforeach ( var inst in tentativeInstances )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !IsValid || Scene == null ) return;\r\n\r\n\t\t\t\t\tVector3 finalPosition = inst.Position;\r\n\t\t\t\t\tint gridX = (int)MathF.Floor( inst.Position.x / GridCellSize );\r\n\t\t\t\t\tint gridY = (int)MathF.Floor( inst.Position.y / GridCellSize );\r\n\t\t\t\t\tint localX = gridX - minGridX;\r\n\t\t\t\t\tint localY = gridY - minGridY;\r\n\r\n\t\t\t\t\tif ( CheckObstructions )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbool isBlocked = false;\r\n\t\t\t\t\t\tfor ( int dx = -ObstructionBuffer; dx <= ObstructionBuffer; dx++ )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tint nx = localX + dx;\r\n\t\t\t\t\t\t\tif ( nx < 0 || nx >= gridWidth ) continue;\r\n\t\t\t\t\t\t\tfor ( int dy = -ObstructionBuffer; dy <= ObstructionBuffer; dy++ )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tint ny = localY + dy;\r\n\t\t\t\t\t\t\t\tif ( ny < 0 || ny >= gridWidth ) continue;\r\n\t\t\t\t\t\t\t\tif ( localBlockedGrid[nx + ny * gridWidth] )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tisBlocked = true;\r\n\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tif ( isBlocked ) break;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif ( isBlocked ) continue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( needsTrace )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar traceStart = inst.Position + traceUpOffset;\r\n\t\t\t\t\t\tvar traceEnd = inst.Position - traceUpOffset;\r\n\t\t\t\t\t\tvar trace = Scene.Trace.Ray( traceStart, traceEnd )\r\n\t\t\t\t\t\t\t.WithoutTags( TraceIgnoreTags )\r\n\t\t\t\t\t\t\t.Run();\r\n\r\n\t\t\t\t\t\tbool hitObstruction = trace.Hit && IsObstruction( trace.GameObject );\r\n\r\n\t\t\t\t\t\tif ( CheckObstructions && (trace.StartedSolid || hitObstruction) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( localX >= 0 && localX < gridWidth && localY >= 0 && localY < gridWidth )\r\n\t\t\t\t\t\t\t\tlocalBlockedGrid[localX + localY * gridWidth] = true;\r\n\t\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif ( SnapToGround && trace.Hit && !trace.StartedSolid && !hitObstruction )\r\n\t\t\t\t\t\t\tfinalPosition = trace.HitPosition;\r\n\r\n\t\t\t\t\t\ttraceCounter++;\r\n\t\t\t\t\t\tif ( traceCounter % 100 == 0 )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tawait GameTask.Yield();\r\n\t\t\t\t\t\t\tif ( !IsValid || Scene == null ) return;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( inst.ModelIndex < 0 || inst.ModelIndex >= modelEntries.Length ) continue;\r\n\r\n\t\t\t\t\tfinalPosition += Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;\r\n\t\t\t\t\tvar list = modelLists[inst.ModelIndex] ??= new List<Transform>();\r\n\t\t\t\t\tlist.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );\r\n\r\n\t\t\t\t\tcxSum += finalPosition.x;\r\n\t\t\t\t\tcySum += finalPosition.y;\r\n\t\t\t\t\tczSum += finalPosition.z;\r\n\t\t\t\t\tplacedCount++;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( !IsValid ) return;\r\n\r\n\t\t\t\tvar chunk = new ClutterChunk();\r\n\t\t\t\tchunk.Center = placedCount > 0\r\n\t\t\t\t\t? new Vector3( cxSum / placedCount, cySum / placedCount, czSum / placedCount )\r\n\t\t\t\t\t: new Vector3( cx * ChunkSize + ChunkSize * 0.5f, cy * ChunkSize + ChunkSize * 0.5f, 0f );\r\n\r\n\t\t\t\tfor ( int i = 0; i < modelLists.Length; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( modelLists[i] != null && modelLists[i].Count > 0 )\r\n\t\t\t\t\t\tchunk.TransformsByModel[i] = modelLists[i].ToArray();\r\n\t\t\t\t}\r\n\r\n\t\t\t\t_activeChunks[(cx, cy)] = chunk;\r\n\t\t\t\tRebuildRenderCache();\r\n\t\t\t}\r\n\t\t\tfinally\r\n\t\t\t{\r\n\t\t\t\tArrayPool<bool>.Shared.Return( localBlockedGrid );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception )\r\n\t\t{\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_generatingChunks.Remove( (cx, cy) );\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "UnitTests/UnitTest.cs",
"FileName": "UnitTest.cs",
"PackageType": "library",
"CodeKind": "UnitTest",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "global using Microsoft.VisualStudio.TestTools.UnitTesting;\r\n\r\n[TestClass]\r\npublic class TestInit\r\n{\r\n\tpublic static Sandbox.TestAppSystem AppSystem;\r\n\r\n\t[AssemblyInitialize]\r\n\tpublic static void AssemblyInitialize( TestContext context )\r\n\t{\r\n\t\tAppSystem = new Sandbox.TestAppSystem();\r\n\t\tAppSystem.Init();\r\n\t}\r\n\r\n\t[AssemblyCleanup]\r\n\tpublic static void AssemblyCleanup()\r\n\t{\r\n\t\tAppSystem.Shutdown();\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "PerformantTerrainScatterer/PerformantTerrainScattererManager.cs",
"FileName": "PerformantTerrainScattererManager.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer : Component, Component.ExecuteInEditor\r\n{\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic Terrain TargetTerrain { get; set; }\r\n\r\n\t[Property, Category( \"Models\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic List<ScattererModelEntry> Models { get; set; } = new();\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) ), Range( 0, 90 )]\r\n\tpublic float MaxSlopeAngle { get; set; } = 45f;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool ReduceDensityOnTransitions { get; set; } = false;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) ),\r\n\t ShowIf( nameof(ReduceDensityOnTransitions), true )]\r\n\tpublic float TransitionThinningIntensity { get; set; } = 1.0f;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool SnapToGround { get; set; } = true;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool CheckObstructions { get; set; } = true;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float TraceStartHeight { get; set; } = 200f;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float GridCellSize { get; set; } = 50f;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int ObstructionBuffer { get; set; } = 2;\r\n\r\n\t[Property, Category( \"Collision\" )]\r\n\tpublic string[] TraceIgnoreTags { get; set; } = { \"player\", \"trigger\", \"clutter\" };\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int MaxInstancesPerChunk { get; set; } = 500;\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic List<MaterialInstanceOverride> MaterialOverrides { get; set; } = new();\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkSize { get; set; } = 500f;\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkLoadDistance { get; set; } = 3000f;\r\n\r\n\t[Property, Category( \"Performance\" )] public bool UseFrustumCulling { get; set; } = true;\r\n\t[Property, Category( \"Performance\" )] public float FrustumCullMinDistance { get; set; } = 800f;\r\n\t[Property, Category( \"Performance\" )] public bool AutomaticallyInitializeSystem { get; set; } = true;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool UseTerrainNormal { get; set; } = true;\r\n\r\n\t[Property, Category( \"Seed\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int Seed { get; set; } = 12345;\r\n\r\n\t[Property, Category( \"Randomization\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool RandomizeRotation { get; set; } = true;\r\n\r\n\tprivate readonly Dictionary<(int, int), ClutterChunk> _activeChunks = new();\r\n\tprivate readonly HashSet<(int, int)> _generatingChunks = new();\r\n\tprivate readonly List<(int, int)> _chunkRemovalList = new();\r\n\r\n\tprivate ushort[] _heightMapPixels;\r\n\tprivate uint[] _controlMapPixels;\r\n\tprivate int _resolution;\r\n\tprivate float _terrainSize;\r\n\tprivate float _terrainHeight;\r\n\tprivate float _maxRenderDistance;\r\n\tprivate float[][] _modelMaterialWeights;\r\n\tprivate GameObject _cachedHitObject;\r\n\tprivate bool _cachedIsObstruction;\r\n\tprivate Sandbox.Utility.INoiseField[] _modelNoiseFields;\r\n\tprivate Vector3 _lastCameraPos;\r\n\tprivate const float CameraUpdateThresholdSq = 10000f;\r\n\tprivate const float UshortMaxReciprocal = 1.0f / ushort.MaxValue;\r\n\tprivate SceneCustomObject _sceneObject;\r\n\r\n\tprivate ScattererModelEntry[] ModelEntries { get; set; }\r\n\tprivate float[] _modelRenderDistSq;\r\n\tprivate int[] _modelLodCounts;\r\n\tprivate float[][] _modelLodDistancesSq;\r\n\r\n\tprivate int[] _materialMaxInstances;\r\n\tprivate int _maxInstancesLoopCount;\r\n\r\n\tprivate ChunkRenderData[] _renderCache = Array.Empty<ChunkRenderData>();\r\n\r\n\t[ThreadStatic] private static List<Transform>[] _lodBuckets;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !TargetTerrain.IsValid() )\r\n\t\t{\r\n\t\t\tTargetTerrain = Scene?.GetComponentInChildren<Terrain>();\r\n\t\t}\r\n\r\n\t\tCreateSceneObject();\r\n\t\tif ( AutomaticallyInitializeSystem )\r\n\t\t{\r\n\t\t\tInitializeSystem();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tManageChunks();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\t_sceneObject?.Delete();\r\n\t\t_sceneObject = null;\r\n\t}\r\n\r\n\tprivate void CreateSceneObject()\r\n\t{\r\n\t\t_sceneObject?.Delete();\r\n\t\tif ( Scene == null ) return;\r\n\r\n\t\t_sceneObject = new SceneCustomObject( Scene.SceneWorld )\r\n\t\t{\r\n\t\t\tRenderOverride = RenderClutter,\r\n\t\t\tTransform = new Transform( Vector3.Zero, Rotation.Identity, 1f ),\r\n\t\t\tFlags = { IsOpaque = true, IsTranslucent = false }\r\n\t\t};\r\n\t}\r\n\r\n\t[Button( \"Reinitialize\" )]\r\n\tpublic void InitializeSystem()\r\n\t{\r\n\t\t_activeChunks.Clear();\r\n\t\t_generatingChunks.Clear();\r\n\t\t_chunkRemovalList.Clear();\r\n\t\t_renderCache = Array.Empty<ChunkRenderData>();\r\n\t\tModelEntries = null;\r\n\t\t_modelRenderDistSq = null;\r\n\t\t_modelLodCounts = null;\r\n\t\t_modelLodDistancesSq = null;\r\n\r\n\t\tif ( Models == null || Models.Count == 0 ) return;\r\n\t\tif ( !TargetTerrain.IsValid() || TargetTerrain.Storage == null ) return;\r\n\r\n\t\t// --- CONFIGURATION VALIDATION WARNINGS ---\r\n\t\tvar seenModels = new HashSet<Model>();\r\n\t\tforeach ( var entry in Models )\r\n\t\t{\r\n\t\t\tif ( entry.Model != null && !seenModels.Add( entry.Model ) )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"[Scatterer] Duplicate model entry found for '{entry.Model.ResourceName}' in the Models configuration list.\" );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tvar seenOverrideMaterials = new HashSet<TerrainMaterial>();\r\n\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t{\r\n\t\t\t\tif ( ov.Material != null && !seenOverrideMaterials.Add( ov.Material ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Warning( $\"[Scatterer] Duplicate material override configuration found for material '{ov.Material.ResourceName}'.\" );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tbool hasValidModel = false;\r\n\t\t_maxRenderDistance = ChunkLoadDistance;\r\n\r\n\t\tforeach ( var t in Models )\r\n\t\t{\r\n\t\t\tif ( t.Model != null )\r\n\t\t\t{\r\n\t\t\t\tvar seenMaterials = new HashSet<TerrainMaterial>();\r\n\t\t\t\tbool hasWeight = false;\r\n\r\n\t\t\t\t// MISSING NULL CHECK FIXED HERE:\r\n\t\t\t\tif ( t.MaterialWeights != null )\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach ( var mw in t.MaterialWeights )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( mw.Material == null ) continue;\r\n\r\n\t\t\t\t\t\tif ( !seenMaterials.Add( mw.Material ) )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\"[Scatterer] Duplicate material entry '{mw.Material.ResourceName}' found for model '{t.Model.ResourceName}'.\" );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight <= 0f )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\"[Scatterer] Weight is set to 0 for material '{mw.Material.ResourceName}' on model '{t.Model.ResourceName}'.\" );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight > 0f ) hasWeight = true;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( hasWeight ) hasValidModel = true;\r\n\r\n\t\t\t\tif ( t.RenderDistance > _maxRenderDistance )\r\n\t\t\t\t\t_maxRenderDistance = t.RenderDistance;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !hasValidModel ) return;\r\n\r\n\t\tModelEntries = Models.ToArray();\r\n\t\t_modelRenderDistSq = new float[ModelEntries.Length];\r\n\t\t_modelLodCounts = new int[ModelEntries.Length];\r\n\t\t_modelLodDistancesSq = new float[ModelEntries.Length][];\r\n\r\n\t\tfor ( int m = 0; m < ModelEntries.Length; m++ )\r\n\t\t{\r\n\t\t\t_modelRenderDistSq[m] = ModelEntries[m].RenderDistance * ModelEntries[m].RenderDistance;\r\n\t\t\tvar mdl = ModelEntries[m].Model;\r\n\r\n\t\t\tif ( mdl != null )\r\n\t\t\t{\r\n\t\t\t\t_modelLodCounts[m] = Math.Max( 1, mdl.MeshInfo.LodCount );\r\n\t\t\t\tif ( mdl.MeshInfo.LodSwitchDistances != null && mdl.MeshInfo.LodSwitchDistances.Length > 0 &&\r\n\t\t\t\t mdl.MeshInfo.LodSwitchDistances[0] > 0f )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar switchDistances = mdl.MeshInfo.LodSwitchDistances;\r\n\t\t\t\t\t_modelLodDistancesSq[m] = new float[switchDistances.Length];\r\n\t\t\t\t\tfor ( int i = 0; i < switchDistances.Length; i++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = switchDistances[i] * ModelEntries[m].LodBias;\r\n\t\t\t\t\t\t_modelLodDistancesSq[m][i] = dist * dist;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse if ( _modelLodCounts[m] > 1 )\r\n\t\t\t\t{\r\n\t\t\t\t\tint switchCount = _modelLodCounts[m] - 1;\r\n\t\t\t\t\t_modelLodDistancesSq[m] = new float[switchCount];\r\n\t\t\t\t\tfloat step = ModelEntries[m].RenderDistance / _modelLodCounts[m];\r\n\t\t\t\t\tfor ( int i = 0; i < switchCount; i++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = (step * (i + 1)) * ModelEntries[m].LodBias;\r\n\t\t\t\t\t\t_modelLodDistancesSq[m][i] = dist * dist;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t_modelLodDistancesSq[m] = Array.Empty<float>();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_modelLodCounts[m] = 1;\r\n\t\t\t\t_modelLodDistancesSq[m] = Array.Empty<float>();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_heightMapPixels = TargetTerrain.Storage.HeightMap;\r\n\t\t_controlMapPixels = TargetTerrain.Storage.ControlMap;\r\n\t\t_resolution = TargetTerrain.Storage.Resolution;\r\n\t\t_terrainSize = TargetTerrain.TerrainSize;\r\n\t\t_terrainHeight = TargetTerrain.TerrainHeight;\r\n\r\n\t\t_modelMaterialWeights = new float[Models.Count][];\r\n\r\n\t\tfor ( int m = 0; m < Models.Count; m++ )\r\n\t\t{\r\n\t\t\t_modelMaterialWeights[m] = new float[32];\r\n\t\t\tif ( TargetTerrain.Storage.Materials != null && Models[m].MaterialWeights != null )\r\n\t\t\t{\r\n\t\t\t\tfor ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach ( var mw in Models[m].MaterialWeights )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( mw.Material == TargetTerrain.Storage.Materials[i] )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t_modelMaterialWeights[m][i] = mw.Weight;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_modelNoiseFields = new Sandbox.Utility.INoiseField[Models.Count];\r\n\t\tfor ( int m = 0; m < Models.Count; m++ )\r\n\t\t{\r\n\t\t\tvar modelEntry = Models[m];\r\n\t\t\tif ( modelEntry.NoiseType == ScattererNoiseType.None ) continue;\r\n\t\t\tvar parameters = new Sandbox.Utility.Noise.Parameters( Seed + m, modelEntry.NoiseScale );\r\n\r\n\t\t\tif ( modelEntry.NoiseType == ScattererNoiseType.Perlin )\r\n\t\t\t\t_modelNoiseFields[m] = Sandbox.Utility.Noise.PerlinField( parameters );\r\n\t\t\telse if ( modelEntry.NoiseType == ScattererNoiseType.Simplex )\r\n\t\t\t\t_modelNoiseFields[m] = Sandbox.Utility.Noise.SimplexField( parameters );\r\n\t\t}\r\n\r\n\t\t// --- MAPPING AND CEILING INITIALIZATION ---\r\n\t\t_materialMaxInstances = new int[32];\r\n\t\tArray.Fill( _materialMaxInstances, MaxInstancesPerChunk );\r\n\t\t_maxInstancesLoopCount = MaxInstancesPerChunk;\r\n\r\n\t\tif ( TargetTerrain.Storage.Materials != null && MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( ov.Material == TargetTerrain.Storage.Materials[i] )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t_materialMaxInstances[i] = ov.MaxInstances;\r\n\t\t\t\t\t\tif ( ov.MaxInstances > _maxInstancesLoopCount )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t_maxInstancesLoopCount = ov.MaxInstances;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tRebuildRenderCache();\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "PerformantTerrainScatterer/PerformantTerrainScattererManager.Data.cs",
"FileName": "PerformantTerrainScattererManager.Data.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tpublic enum ScattererNoiseType\r\n\t{\r\n\t\tNone,\r\n\t\tPerlin,\r\n\t\tSimplex\r\n\t}\r\n\r\n\tpublic struct MaterialWeight\r\n\t{\r\n\t\tpublic MaterialWeight() { }\r\n\t\t[Property] public TerrainMaterial Material { get; set; } = null;\r\n\t\t[Property, Range( 0f, 100f )] public float Weight { get; set; } = 1f;\r\n\t}\r\n\r\n\tpublic struct MaterialInstanceOverride\r\n\t{\r\n\t\tpublic MaterialInstanceOverride() { }\r\n\t\t[Property] public TerrainMaterial Material { get; set; } = null;\r\n\t\t[Property] public int MaxInstances { get; set; } = 500;\r\n\t}\r\n\r\n\tpublic struct ScattererModelEntry\r\n\t{\r\n\t\tpublic ScattererModelEntry() { }\r\n\t\t[Property, Category( \"Base\" )] public Model Model { get; set; } = null;\r\n\t\t[Property, Category( \"Base\" )] public float ZOffset { get; set; } = 0f;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" )]\r\n\t\tpublic ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" )]\r\n\t\tpublic float NoiseScale { get; set; } = 0.002f;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" ), Range( 0f, 1f )]\r\n\t\tpublic float NoiseThreshold { get; set; } = 0.4f;\r\n\r\n\t\t[Property, Category( \"Performance\" )] public float RenderDistance { get; set; } = 2500f;\r\n\t\t[Property, Category( \"Terrain\" )] public List<MaterialWeight> MaterialWeights { get; set; } = new();\r\n\r\n\t\t[Property, Category( \"Randomization\" )]\r\n\t\tpublic bool RandomizeScale { get; set; } = false;\r\n\r\n\t\t[Property, Category( \"Randomization\" ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MinScale { get; set; } = 0.8f;\r\n\r\n\t\t[Property, Category( \"Randomization\" ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MaxScale { get; set; } = 1.2f;\r\n\r\n\t\t[Property, Category( \"LOD\" ), Range( 0.1f, 4f )]\r\n\t\tpublic float LodBias { get; set; } = 1.0f;\r\n\t}\r\n\r\n\tprivate struct ChunkRenderData\r\n\t{\r\n\t\tpublic Vector3 Center;\r\n\t\tpublic ModelRenderBatch[] Batches;\r\n\t}\r\n\r\n\tprivate struct ModelRenderBatch\r\n\t{\r\n\t\tpublic Model Model;\r\n\t\tpublic Transform[] Transforms;\r\n\t\tpublic float RenderDistSq;\r\n\t\tpublic float[] LodDistancesSq;\r\n\t\tpublic int MaxLod;\r\n\t}\r\n\r\n\tprivate struct InstanceData\r\n\t{\r\n\t\tpublic Vector3 Position;\r\n\t\tpublic Rotation Rotation;\r\n\t\tpublic float Scale;\r\n\t\tpublic int ModelIndex;\r\n\t}\r\n\r\n\tprivate class ClutterChunk\r\n\t{\r\n\t\tpublic Vector3 Center;\r\n\t\tpublic readonly Dictionary<int, Transform[]> TransformsByModel = new();\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "PerformantTerrainScatterer/PerformantTerrainScattererManager.Rendering.cs",
"FileName": "PerformantTerrainScattererManager.Rendering.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Runtime.InteropServices;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate void RebuildRenderCache()\r\n\t{\r\n\t\tvar newCache = new ChunkRenderData[_activeChunks.Count];\r\n\t\tint index = 0;\r\n\r\n\t\tforeach ( var chunk in _activeChunks.Values )\r\n\t\t{\r\n\t\t\tvar batches = new ModelRenderBatch[chunk.TransformsByModel.Count];\r\n\t\t\tint bIndex = 0;\r\n\r\n\t\t\tforeach ( var kvp in chunk.TransformsByModel )\r\n\t\t\t{\r\n\t\t\t\tint mIdx = kvp.Key;\r\n\t\t\t\tbatches[bIndex++] = new ModelRenderBatch\r\n\t\t\t\t{\r\n\t\t\t\t\tModel = ModelEntries[mIdx].Model,\r\n\t\t\t\t\tTransforms = kvp.Value,\r\n\t\t\t\t\tRenderDistSq = _modelRenderDistSq[mIdx],\r\n\t\t\t\t\tLodDistancesSq = _modelLodDistancesSq[mIdx],\r\n\t\t\t\t\tMaxLod = Math.Max( 0, _modelLodCounts[mIdx] - 1 )\r\n\t\t\t\t};\r\n\t\t\t}\r\n\r\n\t\t\tnewCache[index++] = new ChunkRenderData { Center = chunk.Center, Batches = batches };\r\n\t\t}\r\n\r\n\t\t_renderCache = newCache;\r\n\t}\r\n\r\n\tprivate void RenderClutter( SceneObject obj )\r\n\t{\r\n\t\tvar cache = _renderCache;\r\n\t\tif ( cache.Length == 0 ) return;\r\n\r\n\t\tif ( Scene == null ) return;\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\r\n\t\tVector3 cameraPos = camera.WorldPosition;\r\n\t\tVector3 cameraForward = camera.WorldRotation.Forward;\r\n\r\n\t\tfloat frustumMinDistSq = FrustumCullMinDistance * FrustumCullMinDistance;\r\n\t\tfloat maxRenderDistSq = _maxRenderDistance * _maxRenderDistance;\r\n\t\tbool useFrustum = UseFrustumCulling;\r\n\r\n\t\tif ( _lodBuckets == null )\r\n\t\t{\r\n\t\t\t_lodBuckets = new List<Transform>[16];\r\n\t\t\tfor ( int i = 0; i < _lodBuckets.Length; i++ )\r\n\t\t\t\t_lodBuckets[i] = new List<Transform>();\r\n\t\t}\r\n\r\n\t\tfor ( int c = 0; c < cache.Length; c++ )\r\n\t\t{\r\n\t\t\tref var chunk = ref cache[c];\r\n\r\n\t\t\tVector3 toChunk = chunk.Center - cameraPos;\r\n\t\t\tfloat chunkDistSq = toChunk.LengthSquared;\r\n\r\n\t\t\tif ( chunkDistSq > maxRenderDistSq ) continue;\r\n\r\n\t\t\tif ( useFrustum && chunkDistSq > frustumMinDistSq )\r\n\t\t\t{\r\n\t\t\t\tfloat dot = Vector3.Dot( cameraForward, toChunk );\r\n\t\t\t\tif ( dot < 0f && (dot * dot) > 0.04f * chunkDistSq )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar batches = chunk.Batches;\r\n\t\t\tfor ( int b = 0; b < batches.Length; b++ )\r\n\t\t\t{\r\n\t\t\t\tref var batch = ref batches[b];\r\n\t\t\t\tif ( chunkDistSq > batch.RenderDistSq ) continue;\r\n\r\n\t\t\t\tint maxLod = batch.MaxLod;\r\n\r\n\t\t\t\tfor ( int i = 0; i <= maxLod; i++ )\r\n\t\t\t\t\t_lodBuckets[i].Clear();\r\n\r\n\t\t\t\tvar transforms = batch.Transforms;\r\n\t\t\t\tvar lodDists = batch.LodDistancesSq;\r\n\r\n\t\t\t\tfor ( int i = 0; i < transforms.Length; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 instPos = transforms[i].Position;\r\n\t\t\t\t\tfloat instDistSq = (instPos - cameraPos).LengthSquared;\r\n\r\n\t\t\t\t\tif ( instDistSq > batch.RenderDistSq ) continue;\r\n\r\n\t\t\t\t\tint lod = 0;\r\n\t\t\t\t\tfor ( int l = 0; l < lodDists.Length; l++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( instDistSq > lodDists[l] )\r\n\t\t\t\t\t\t\tlod = l + 1;\r\n\t\t\t\t\t\telse\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( lod > maxLod ) lod = maxLod;\r\n\r\n\t\t\t\t\t_lodBuckets[lod].Add( transforms[i] );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfor ( int l = 0; l <= maxLod; l++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar bucket = _lodBuckets[l];\r\n\t\t\t\t\tif ( bucket.Count > 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tGraphics.DrawModelInstanced( batch.Model, CollectionsMarshal.AsSpan( bucket ), l );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Editor/MyEditorMenu.cs",
"FileName": "MyEditorMenu.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using Editor;\r\n\r\npublic static class MyEditorMenu\r\n{\r\n\t[Menu( \"Editor\", \"Performant Terrain Scatterer/My Menu Option\" )]\r\n\tpublic static void OpenMyMenu()\r\n\t{\r\n\t\tEditorUtility.DisplayDialog( \"It worked!\", \"This is being called from your library's editor code!\" );\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Data.cs",
"FileName": "PerformantTerrainScattererManager.Data.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tpublic enum ScattererNoiseType\r\n\t{\r\n\t\tNone,\r\n\t\tPerlin,\r\n\t\tSimplex\r\n\t}\r\n\r\n\tpublic struct MaterialWeight\r\n\t{\r\n\t\tpublic MaterialWeight() { }\r\n\t\t[Property] public TerrainMaterial Material { get; set; } = null;\r\n\t\t[Property, Range( 0f, 100f )] public float Weight { get; set; } = 1f;\r\n\t}\r\n\r\n\tpublic struct MaterialInstanceOverride\r\n\t{\r\n\t\tpublic MaterialInstanceOverride() { }\r\n\t\t[Property] public TerrainMaterial Material { get; set; } = null;\r\n\t\t[Property] public int MaxInstances { get; set; } = 500;\r\n\t}\r\n\r\n\tpublic struct ScattererModelEntry\r\n\t{\r\n\t\tpublic ScattererModelEntry() { }\r\n\t\t[Property, Category( \"Base\" )] public Model Model { get; set; } = null;\r\n\t\t[Property, Category( \"Base\" )] public float ZOffset { get; set; } = 0f;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" )]\r\n\t\tpublic ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" )]\r\n\t\tpublic float NoiseScale { get; set; } = 0.002f;\r\n\r\n\t\t[Property, Category( \"Noise Scattering\" ), Range( 0f, 1f )]\r\n\t\tpublic float NoiseThreshold { get; set; } = 0.4f;\r\n\r\n\t\t[Property, Category( \"Performance\" )] public float RenderDistance { get; set; } = 2500f;\r\n\t\t[Property, Category( \"Terrain\" )] public List<MaterialWeight> MaterialWeights { get; set; } = new();\r\n\r\n\t\t[Property, Category( \"Randomization\" )]\r\n\t\tpublic bool RandomizeScale { get; set; } = false;\r\n\r\n\t\t[Property, Category( \"Randomization\" ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MinScale { get; set; } = 0.8f;\r\n\r\n\t\t[Property, Category( \"Randomization\" ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MaxScale { get; set; } = 1.2f;\r\n\r\n\t\t[Property, Category( \"LOD\" ), Range( 0.1f, 4f )]\r\n\t\tpublic float LodBias { get; set; } = 1.0f;\r\n\t}\r\n\r\n\tprivate struct ChunkRenderData\r\n\t{\r\n\t\tpublic Vector3 Center;\r\n\t\tpublic ModelRenderBatch[] Batches;\r\n\t}\r\n\r\n\tprivate struct ModelRenderBatch\r\n\t{\r\n\t\tpublic Model Model;\r\n\t\tpublic Transform[] Transforms;\r\n\t\tpublic float RenderDistSq;\r\n\t\tpublic float[] LodDistancesSq;\r\n\t\tpublic int MaxLod;\r\n\t}\r\n\r\n\tprivate struct InstanceData\r\n\t{\r\n\t\tpublic Vector3 Position;\r\n\t\tpublic Rotation Rotation;\r\n\t\tpublic float Scale;\r\n\t\tpublic int ModelIndex;\r\n\t}\r\n\r\n\tprivate class ClutterChunk\r\n\t{\r\n\t\tpublic Vector3 Center;\r\n\t\tpublic readonly Dictionary<int, Transform[]> TransformsByModel = new();\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "PerformantTerrainScatterer/PerformantTerrainScattererManager.Placement.cs",
"FileName": "PerformantTerrainScattererManager.Placement.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Runtime.CompilerServices;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate List<InstanceData> CalculateChunkPlacements( int cx, int cy, Transform terrainTransform )\r\n\t{\r\n\t\tif ( _heightMapPixels == null || _controlMapPixels == null ) return null;\r\n\t\tvar modelEntries = ModelEntries;\r\n\t\tif ( modelEntries == null || modelEntries.Length == 0 ) return null;\r\n\r\n\t\tint loopCount = _maxInstancesLoopCount;\r\n\t\tvar instances = new List<InstanceData>( loopCount );\r\n\t\tint chunkSeed = HashCombine( Seed, HashCombine( cx, cy ) );\r\n\t\tvar random = new Random( chunkSeed );\r\n\t\tfloat textureEps = 1.0f / (_resolution - 1);\r\n\t\tfloat chunkBaseX = cx * ChunkSize;\r\n\t\tfloat chunkBaseY = cy * ChunkSize;\r\n\t\tint modelsCount = modelEntries.Length;\r\n\r\n\t\tint[] placedCountPerMaterial = new int[32];\r\n\r\n\t\tfor ( int i = 0; i < loopCount; i++ )\r\n\t\t{\r\n\t\t\tfloat localX = random.NextSingle() * ChunkSize;\r\n\t\t\tfloat localY = random.NextSingle() * ChunkSize;\r\n\t\t\tfloat worldX = chunkBaseX + localX;\r\n\t\t\tfloat worldY = chunkBaseY + localY;\r\n\r\n\t\t\tVector3 worldAbsPoint = new Vector3( worldX, worldY, 0f );\r\n\t\t\tVector3 terrainLocal = terrainTransform.PointToLocal( worldAbsPoint );\r\n\t\t\tfloat u = terrainLocal.x / _terrainSize;\r\n\t\t\tfloat v = terrainLocal.y / _terrainSize;\r\n\r\n\t\t\tif ( u < 0f || u > 1f || v < 0f || v > 1f ) continue;\r\n\r\n\t\t\tint cX = (int)(u * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );\r\n\t\t\tint cY = (int)(v * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );\r\n\t\t\tint controlIndex = cX + cY * _resolution;\r\n\t\t\tuint packedMap = _controlMapPixels[controlIndex];\r\n\t\t\tvar compactMat = new CompactTerrainMaterial( packedMap );\r\n\r\n\t\t\tif ( compactMat.IsHole ) continue;\r\n\r\n\t\t\tint dominantMaterialId = (compactMat.BlendFactor > 127)\r\n\t\t\t\t? compactMat.OverlayTextureId\r\n\t\t\t\t: compactMat.BaseTextureId;\r\n\r\n\t\t\tif ( dominantMaterialId < 32 )\r\n\t\t\t{\r\n\t\t\t\tif ( placedCountPerMaterial[dominantMaterialId] >= _materialMaxInstances[dominantMaterialId] )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tfloat materialTotalWeight = 0f;\r\n\t\t\tfor ( int m = 0; m < modelsCount; m++ )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId < _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tmaterialTotalWeight += _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( materialTotalWeight <= 0f ) continue;\r\n\r\n\t\t\tfloat roll = random.NextSingle() * materialTotalWeight;\r\n\t\t\tfloat currentWeightSum = 0f;\r\n\t\t\tint selectedModelIndex = -1;\r\n\r\n\t\t\tfor ( int m = 0; m < modelsCount; m++ )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId < _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat w = _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t\tif ( w > 0f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tcurrentWeightSum += w;\r\n\t\t\t\t\t\tif ( roll <= currentWeightSum )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tselectedModelIndex = m;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( selectedModelIndex == -1 ) continue;\r\n\t\t\tref var selectedModel = ref modelEntries[selectedModelIndex];\r\n\r\n\t\t\tif ( ReduceDensityOnTransitions )\r\n\t\t\t{\r\n\t\t\t\tfloat blendNorm = compactMat.BlendFactor * 0.003921569f;\r\n\t\t\t\tfloat purity = MathF.Abs( blendNorm - 0.5f ) * 2f;\r\n\t\t\t\tfloat spawnChance = MathF.Pow( purity, TransitionThinningIntensity );\r\n\t\t\t\tif ( random.NextSingle() > spawnChance ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( selectedModel.NoiseType != ScattererNoiseType.None )\r\n\t\t\t{\r\n\t\t\t\tvar noiseField = _modelNoiseFields[selectedModelIndex];\r\n\t\t\t\tfloat noiseVal = noiseField.Sample( worldX, worldY );\r\n\t\t\t\tif ( noiseVal < selectedModel.NoiseThreshold ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tVector3 normal = Vector3.Up;\r\n\r\n\t\t\tif ( UseTerrainNormal || MaxSlopeAngle < 90f )\r\n\t\t\t{\r\n\t\t\t\tfloat hL = SampleHeightBilinear( u - textureEps, v );\r\n\t\t\t\tfloat hR = SampleHeightBilinear( u + textureEps, v );\r\n\t\t\t\tfloat hD = SampleHeightBilinear( u, v - textureEps );\r\n\t\t\t\tfloat hU = SampleHeightBilinear( u, v + textureEps );\r\n\t\t\t\tVector3 terrainTangentX =\r\n\t\t\t\t\tnew Vector3( 2.0f * textureEps * _terrainSize, 0f, (hR - hL) * _terrainHeight );\r\n\t\t\t\tVector3 terrainTangentY =\r\n\t\t\t\t\tnew Vector3( 0f, 2.0f * textureEps * _terrainSize, (hU - hD) * _terrainHeight );\r\n\t\t\t\tVector3 terrainNormalLocal = Vector3.Cross( terrainTangentX, terrainTangentY ).Normal;\r\n\t\t\t\tnormal = terrainTransform.Rotation * terrainNormalLocal;\r\n\t\t\t\tfloat slopeAngle = Vector3.GetAngle( Vector3.Up, normal );\r\n\t\t\t\tif ( slopeAngle > MaxSlopeAngle ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tfloat z = SampleHeightBilinear( u, v ) * _terrainHeight;\r\n\t\t\tVector3 finalLocalPoint = new Vector3( terrainLocal.x, terrainLocal.y, z );\r\n\t\t\tVector3 worldTerrainPoint = terrainTransform.PointToWorld( finalLocalPoint );\r\n\r\n\t\t\tRotation randomYaw = RandomizeRotation\r\n\t\t\t\t? Rotation.FromYaw( random.NextSingle() * 360f )\r\n\t\t\t\t: Rotation.Identity;\r\n\r\n\t\t\tRotation finalRot = UseTerrainNormal\r\n\t\t\t\t? Rotation.FromToRotation( Vector3.Up, normal ) * randomYaw\r\n\t\t\t\t: randomYaw;\r\n\r\n\t\t\tfloat scale = selectedModel.RandomizeScale\r\n\t\t\t\t? (random.NextSingle() * (selectedModel.MaxScale - selectedModel.MinScale) + selectedModel.MinScale)\r\n\t\t\t\t: 1.0f;\r\n\r\n\t\t\tinstances.Add( new InstanceData\r\n\t\t\t{\r\n\t\t\t\tPosition = worldTerrainPoint, Rotation = finalRot, Scale = scale, ModelIndex = selectedModelIndex\r\n\t\t\t} );\r\n\r\n\t\t\tif ( dominantMaterialId < 32 )\r\n\t\t\t{\r\n\t\t\t\tplacedCountPerMaterial[dominantMaterialId]++;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn instances;\r\n\t}\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate int HashCombine( int seed, int value )\r\n\t\t=> seed ^ (value + -1640531527 + (seed << 6) + (seed >> 2));\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate bool IsObstruction( GameObject hitObject )\r\n\t{\r\n\t\tif ( hitObject == null ) return false;\r\n\t\tif ( _cachedHitObject == hitObject ) return _cachedIsObstruction;\r\n\t\t_cachedHitObject = hitObject;\r\n\t\t_cachedIsObstruction = hitObject.Components.Get<Terrain>() == null;\r\n\t\treturn _cachedIsObstruction;\r\n\t}\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate float SampleHeightBilinear( float u, float v )\r\n\t{\r\n\t\tu = u.Clamp( 0f, 1f );\r\n\t\tv = v.Clamp( 0f, 1f );\r\n\t\tfloat gx = u * (_resolution - 1);\r\n\t\tfloat gy = v * (_resolution - 1);\r\n\t\tint ix = (int)gx;\r\n\t\tint iy = (int)gy;\r\n\t\tfloat fx = gx - ix;\r\n\t\tfloat fy = gy - iy;\r\n\t\tint ix1 = Math.Min( ix + 1, _resolution - 1 );\r\n\t\tint iy1 = Math.Min( iy + 1, _resolution - 1 );\r\n\t\tint row0 = iy * _resolution;\r\n\t\tint row1 = iy1 * _resolution;\r\n\t\tfloat h00 = _heightMapPixels[row0 + ix] * UshortMaxReciprocal;\r\n\t\tfloat h10 = _heightMapPixels[row0 + ix1] * UshortMaxReciprocal;\r\n\t\tfloat h01 = _heightMapPixels[row1 + ix] * UshortMaxReciprocal;\r\n\t\tfloat h11 = _heightMapPixels[row1 + ix1] * UshortMaxReciprocal;\r\n\t\tfloat h0 = h00 + (h10 - h00) * fx;\r\n\t\tfloat h1 = h01 + (h11 - h01) * fx;\r\n\t\treturn h0 + (h1 - h0) * fy;\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.cs",
"FileName": "PerformantTerrainScattererManager.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer : Component, Component.ExecuteInEditor\r\n{\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic Terrain TargetTerrain { get; set; }\r\n\r\n\t[Property, Category( \"Models\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic List<ScattererModelEntry> Models { get; set; } = new();\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) ), Range( 0, 90 )]\r\n\tpublic float MaxSlopeAngle { get; set; } = 45f;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool ReduceDensityOnTransitions { get; set; } = false;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) ),\r\n\t ShowIf( nameof(ReduceDensityOnTransitions), true )]\r\n\tpublic float TransitionThinningIntensity { get; set; } = 1.0f;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool SnapToGround { get; set; } = true;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool CheckObstructions { get; set; } = true;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float TraceStartHeight { get; set; } = 200f;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float GridCellSize { get; set; } = 50f;\r\n\r\n\t[Property, Category( \"Collision\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int ObstructionBuffer { get; set; } = 2;\r\n\r\n\t[Property, Category( \"Collision\" )]\r\n\tpublic string[] TraceIgnoreTags { get; set; } = { \"player\", \"trigger\", \"clutter\" };\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int MaxInstancesPerChunk { get; set; } = 500;\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic List<MaterialInstanceOverride> MaterialOverrides { get; set; } = new();\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkSize { get; set; } = 500f;\r\n\r\n\t[Property, Category( \"Performance\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkLoadDistance { get; set; } = 3000f;\r\n\r\n\t[Property, Category( \"Performance\" )] public bool UseFrustumCulling { get; set; } = true;\r\n\t[Property, Category( \"Performance\" )] public float FrustumCullMinDistance { get; set; } = 800f;\r\n\t[Property, Category( \"Performance\" )] public bool AutomaticallyInitializeSystem { get; set; } = true;\r\n\r\n\t[Property, Category( \"Terrain\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool UseTerrainNormal { get; set; } = true;\r\n\r\n\t[Property, Category( \"Seed\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic int Seed { get; set; } = 12345;\r\n\r\n\t[Property, Category( \"Randomization\" ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool RandomizeRotation { get; set; } = true;\r\n\r\n\tprivate readonly Dictionary<(int, int), ClutterChunk> _activeChunks = new();\r\n\tprivate readonly HashSet<(int, int)> _generatingChunks = new();\r\n\tprivate readonly List<(int, int)> _chunkRemovalList = new();\r\n\r\n\tprivate ushort[] _heightMapPixels;\r\n\tprivate uint[] _controlMapPixels;\r\n\tprivate int _resolution;\r\n\tprivate float _terrainSize;\r\n\tprivate float _terrainHeight;\r\n\tprivate float _maxRenderDistance;\r\n\tprivate float[][] _modelMaterialWeights;\r\n\tprivate GameObject _cachedHitObject;\r\n\tprivate bool _cachedIsObstruction;\r\n\tprivate Sandbox.Utility.INoiseField[] _modelNoiseFields;\r\n\tprivate Vector3 _lastCameraPos;\r\n\tprivate const float CameraUpdateThresholdSq = 10000f;\r\n\tprivate const float UshortMaxReciprocal = 1.0f / ushort.MaxValue;\r\n\tprivate SceneCustomObject _sceneObject;\r\n\r\n\tprivate ScattererModelEntry[] ModelEntries { get; set; }\r\n\tprivate float[] _modelRenderDistSq;\r\n\tprivate int[] _modelLodCounts;\r\n\tprivate float[][] _modelLodDistancesSq;\r\n\r\n\tprivate int[] _materialMaxInstances;\r\n\tprivate int _maxInstancesLoopCount;\r\n\r\n\tprivate ChunkRenderData[] _renderCache = Array.Empty<ChunkRenderData>();\r\n\r\n\t[ThreadStatic] private static List<Transform>[] _lodBuckets;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !TargetTerrain.IsValid() )\r\n\t\t{\r\n\t\t\tTargetTerrain = Scene?.GetComponentInChildren<Terrain>();\r\n\t\t}\r\n\r\n\t\tCreateSceneObject();\r\n\t\tif ( AutomaticallyInitializeSystem )\r\n\t\t{\r\n\t\t\tInitializeSystem();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tManageChunks();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\t_sceneObject?.Delete();\r\n\t\t_sceneObject = null;\r\n\t}\r\n\r\n\tprivate void CreateSceneObject()\r\n\t{\r\n\t\t_sceneObject?.Delete();\r\n\t\tif ( Scene == null ) return;\r\n\r\n\t\t_sceneObject = new SceneCustomObject( Scene.SceneWorld )\r\n\t\t{\r\n\t\t\tRenderOverride = RenderClutter,\r\n\t\t\tTransform = new Transform( Vector3.Zero, Rotation.Identity, 1f ),\r\n\t\t\tFlags = { IsOpaque = true, IsTranslucent = false }\r\n\t\t};\r\n\t}\r\n\r\n\t[Button( \"Reinitialize\" )]\r\n\tpublic void InitializeSystem()\r\n\t{\r\n\t\t_activeChunks.Clear();\r\n\t\t_generatingChunks.Clear();\r\n\t\t_chunkRemovalList.Clear();\r\n\t\t_renderCache = Array.Empty<ChunkRenderData>();\r\n\t\tModelEntries = null;\r\n\t\t_modelRenderDistSq = null;\r\n\t\t_modelLodCounts = null;\r\n\t\t_modelLodDistancesSq = null;\r\n\r\n\t\tif ( Models == null || Models.Count == 0 ) return;\r\n\t\tif ( !TargetTerrain.IsValid() || TargetTerrain.Storage == null ) return;\r\n\r\n\t\t// --- CONFIGURATION VALIDATION WARNINGS ---\r\n\t\tvar seenModels = new HashSet<Model>();\r\n\t\tforeach ( var entry in Models )\r\n\t\t{\r\n\t\t\tif ( entry.Model != null && !seenModels.Add( entry.Model ) )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"[Scatterer] Duplicate model entry found for '{entry.Model.ResourceName}' in the Models configuration list.\" );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tvar seenOverrideMaterials = new HashSet<TerrainMaterial>();\r\n\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t{\r\n\t\t\t\tif ( ov.Material != null && !seenOverrideMaterials.Add( ov.Material ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Warning( $\"[Scatterer] Duplicate material override configuration found for material '{ov.Material.ResourceName}'.\" );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tbool hasValidModel = false;\r\n\t\t_maxRenderDistance = ChunkLoadDistance;\r\n\r\n\t\tforeach ( var t in Models )\r\n\t\t{\r\n\t\t\tif ( t.Model != null )\r\n\t\t\t{\r\n\t\t\t\tvar seenMaterials = new HashSet<TerrainMaterial>();\r\n\t\t\t\tbool hasWeight = false;\r\n\r\n\t\t\t\t// MISSING NULL CHECK FIXED HERE:\r\n\t\t\t\tif ( t.MaterialWeights != null )\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach ( var mw in t.MaterialWeights )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( mw.Material == null ) continue;\r\n\r\n\t\t\t\t\t\tif ( !seenMaterials.Add( mw.Material ) )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\"[Scatterer] Duplicate material entry '{mw.Material.ResourceName}' found for model '{t.Model.ResourceName}'.\" );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight <= 0f )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\"[Scatterer] Weight is set to 0 for material '{mw.Material.ResourceName}' on model '{t.Model.ResourceName}'.\" );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight > 0f ) hasWeight = true;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( hasWeight ) hasValidModel = true;\r\n\r\n\t\t\t\tif ( t.RenderDistance > _maxRenderDistance )\r\n\t\t\t\t\t_maxRenderDistance = t.RenderDistance;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !hasValidModel ) return;\r\n\r\n\t\tModelEntries = Models.ToArray();\r\n\t\t_modelRenderDistSq = new float[ModelEntries.Length];\r\n\t\t_modelLodCounts = new int[ModelEntries.Length];\r\n\t\t_modelLodDistancesSq = new float[ModelEntries.Length][];\r\n\r\n\t\tfor ( int m = 0; m < ModelEntries.Length; m++ )\r\n\t\t{\r\n\t\t\t_modelRenderDistSq[m] = ModelEntries[m].RenderDistance * ModelEntries[m].RenderDistance;\r\n\t\t\tvar mdl = ModelEntries[m].Model;\r\n\r\n\t\t\tif ( mdl != null )\r\n\t\t\t{\r\n\t\t\t\t_modelLodCounts[m] = Math.Max( 1, mdl.MeshInfo.LodCount );\r\n\t\t\t\tif ( mdl.MeshInfo.LodSwitchDistances != null && mdl.MeshInfo.LodSwitchDistances.Length > 0 &&\r\n\t\t\t\t mdl.MeshInfo.LodSwitchDistances[0] > 0f )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar switchDistances = mdl.MeshInfo.LodSwitchDistances;\r\n\t\t\t\t\t_modelLodDistancesSq[m] = new float[switchDistances.Length];\r\n\t\t\t\t\tfor ( int i = 0; i < switchDistances.Length; i++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = switchDistances[i] * ModelEntries[m].LodBias;\r\n\t\t\t\t\t\t_modelLodDistancesSq[m][i] = dist * dist;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse if ( _modelLodCounts[m] > 1 )\r\n\t\t\t\t{\r\n\t\t\t\t\tint switchCount = _modelLodCounts[m] - 1;\r\n\t\t\t\t\t_modelLodDistancesSq[m] = new float[switchCount];\r\n\t\t\t\t\tfloat step = ModelEntries[m].RenderDistance / _modelLodCounts[m];\r\n\t\t\t\t\tfor ( int i = 0; i < switchCount; i++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = (step * (i + 1)) * ModelEntries[m].LodBias;\r\n\t\t\t\t\t\t_modelLodDistancesSq[m][i] = dist * dist;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t_modelLodDistancesSq[m] = Array.Empty<float>();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_modelLodCounts[m] = 1;\r\n\t\t\t\t_modelLodDistancesSq[m] = Array.Empty<float>();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_heightMapPixels = TargetTerrain.Storage.HeightMap;\r\n\t\t_controlMapPixels = TargetTerrain.Storage.ControlMap;\r\n\t\t_resolution = TargetTerrain.Storage.Resolution;\r\n\t\t_terrainSize = TargetTerrain.TerrainSize;\r\n\t\t_terrainHeight = TargetTerrain.TerrainHeight;\r\n\r\n\t\t_modelMaterialWeights = new float[Models.Count][];\r\n\r\n\t\tfor ( int m = 0; m < Models.Count; m++ )\r\n\t\t{\r\n\t\t\t_modelMaterialWeights[m] = new float[32];\r\n\t\t\tif ( TargetTerrain.Storage.Materials != null && Models[m].MaterialWeights != null )\r\n\t\t\t{\r\n\t\t\t\tfor ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach ( var mw in Models[m].MaterialWeights )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( mw.Material == TargetTerrain.Storage.Materials[i] )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t_modelMaterialWeights[m][i] = mw.Weight;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_modelNoiseFields = new Sandbox.Utility.INoiseField[Models.Count];\r\n\t\tfor ( int m = 0; m < Models.Count; m++ )\r\n\t\t{\r\n\t\t\tvar modelEntry = Models[m];\r\n\t\t\tif ( modelEntry.NoiseType == ScattererNoiseType.None ) continue;\r\n\t\t\tvar parameters = new Sandbox.Utility.Noise.Parameters( Seed + m, modelEntry.NoiseScale );\r\n\r\n\t\t\tif ( modelEntry.NoiseType == ScattererNoiseType.Perlin )\r\n\t\t\t\t_modelNoiseFields[m] = Sandbox.Utility.Noise.PerlinField( parameters );\r\n\t\t\telse if ( modelEntry.NoiseType == ScattererNoiseType.Simplex )\r\n\t\t\t\t_modelNoiseFields[m] = Sandbox.Utility.Noise.SimplexField( parameters );\r\n\t\t}\r\n\r\n\t\t// --- MAPPING AND CEILING INITIALIZATION ---\r\n\t\t_materialMaxInstances = new int[32];\r\n\t\tArray.Fill( _materialMaxInstances, MaxInstancesPerChunk );\r\n\t\t_maxInstancesLoopCount = MaxInstancesPerChunk;\r\n\r\n\t\tif ( TargetTerrain.Storage.Materials != null && MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i < TargetTerrain.Storage.Materials.Count; i++ )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( ov.Material == TargetTerrain.Storage.Materials[i] )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t_materialMaxInstances[i] = ov.MaxInstances;\r\n\t\t\t\t\t\tif ( ov.MaxInstances > _maxInstancesLoopCount )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t_maxInstancesLoopCount = ov.MaxInstances;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tRebuildRenderCache();\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "UnitTests/LibraryTest.cs",
"FileName": "LibraryTest.cs",
"PackageType": "library",
"CodeKind": "UnitTest",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\n[TestClass]\r\npublic partial class LibraryTests\r\n{\r\n\t[TestMethod]\r\n\tpublic void SceneTest()\r\n\t{\r\n\t\tvar scene = new Scene();\r\n\t\tusing ( scene.Push() )\r\n\t\t{\r\n\t\t\tvar go = new GameObject();\r\n\r\n\t\t\tAssert.AreEqual( 1, scene.Directory.GameObjectCount );\r\n\t\t}\r\n\t}\r\n\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Chunks.cs",
"FileName": "PerformantTerrainScattererManager.Chunks.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Buffers;\r\nusing System.Collections.Generic;\r\nusing System.Threading.Tasks;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate void ManageChunks()\r\n\t{\r\n\t\tif ( Scene == null ) return;\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\t\tVector3 cameraPos = camera.WorldPosition;\r\n\r\n\t\tif ( _activeChunks.Count > 0 && cameraPos.DistanceSquared( _lastCameraPos ) < CameraUpdateThresholdSq )\r\n\t\t\treturn;\r\n\r\n\t\t_lastCameraPos = cameraPos;\r\n\t\tint cameraCx = (int)MathF.Floor( cameraPos.x / ChunkSize );\r\n\t\tint cameraCy = (int)MathF.Floor( cameraPos.y / ChunkSize );\r\n\t\tint chunksInRadius = (int)MathF.Ceiling( ChunkLoadDistance / ChunkSize );\r\n\r\n\t\tfor ( int dx = -chunksInRadius; dx <= chunksInRadius; dx++ )\r\n\t\t{\r\n\t\t\tfor ( int dy = -chunksInRadius; dy <= chunksInRadius; dy++ )\r\n\t\t\t{\r\n\t\t\t\tvar chunkCoord = (cameraCx + dx, cameraCy + dy);\r\n\t\t\t\tif ( !_activeChunks.ContainsKey( chunkCoord ) && _generatingChunks.Add( chunkCoord ) )\r\n\t\t\t\t\t_ = GenerateChunkAsync( chunkCoord.Item1, chunkCoord.Item2 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_chunkRemovalList.Clear();\r\n\t\tforeach ( var key in _activeChunks.Keys )\r\n\t\t{\r\n\t\t\tif ( Math.Abs( key.Item1 - cameraCx ) > chunksInRadius ||\r\n\t\t\t Math.Abs( key.Item2 - cameraCy ) > chunksInRadius )\r\n\t\t\t\t_chunkRemovalList.Add( key );\r\n\t\t}\r\n\r\n\t\tforeach ( var t in _chunkRemovalList )\r\n\t\t\t_activeChunks.Remove( t );\r\n\r\n\t\tif ( _chunkRemovalList.Count > 0 )\r\n\t\t\tRebuildRenderCache();\r\n\t}\r\n\r\n\tprivate async Task GenerateChunkAsync( int cx, int cy )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !IsValid || Scene == null ) return;\r\n\t\t\tTransform terrainTransform = TargetTerrain.IsValid() ? TargetTerrain.WorldTransform : new Transform();\r\n\r\n\t\t\tvar tentativeInstances =\r\n\t\t\t\tawait GameTask.RunInThreadAsync( () => CalculateChunkPlacements( cx, cy, terrainTransform ) );\r\n\r\n\t\t\tif ( !IsValid || Scene == null ) return;\r\n\r\n\t\t\tvar modelEntries = ModelEntries;\r\n\t\t\tif ( tentativeInstances == null || modelEntries == null ) return;\r\n\r\n\t\t\tint cellsPerChunk = (int)MathF.Ceiling( ChunkSize / GridCellSize );\r\n\t\t\tint gridPadding = ObstructionBuffer;\r\n\t\t\tint gridWidth = cellsPerChunk + (gridPadding * 2) + 2;\r\n\t\t\tint gridArea = gridWidth * gridWidth;\r\n\r\n\t\t\tbool[] localBlockedGrid = ArrayPool<bool>.Shared.Rent( gridArea );\r\n\t\t\tArray.Clear( localBlockedGrid, 0, gridArea );\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tint minGridX = (int)MathF.Floor( (cx * ChunkSize) / GridCellSize ) - gridPadding;\r\n\t\t\t\tint minGridY = (int)MathF.Floor( (cy * ChunkSize) / GridCellSize ) - gridPadding;\r\n\t\t\t\tint traceCounter = 0;\r\n\t\t\t\tbool needsTrace = CheckObstructions || SnapToGround;\r\n\t\t\t\tList<Transform>[] modelLists = new List<Transform>[modelEntries.Length];\r\n\t\t\t\tfloat cxSum = 0f, cySum = 0f, czSum = 0f;\r\n\t\t\t\tint placedCount = 0;\r\n\t\t\t\tVector3 traceUpOffset = Vector3.Up * TraceStartHeight;\r\n\r\n\t\t\t\tforeach ( var inst in tentativeInstances )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !IsValid || Scene == null ) return;\r\n\r\n\t\t\t\t\tVector3 finalPosition = inst.Position;\r\n\t\t\t\t\tint gridX = (int)MathF.Floor( inst.Position.x / GridCellSize );\r\n\t\t\t\t\tint gridY = (int)MathF.Floor( inst.Position.y / GridCellSize );\r\n\t\t\t\t\tint localX = gridX - minGridX;\r\n\t\t\t\t\tint localY = gridY - minGridY;\r\n\r\n\t\t\t\t\tif ( CheckObstructions )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbool isBlocked = false;\r\n\t\t\t\t\t\tfor ( int dx = -ObstructionBuffer; dx <= ObstructionBuffer; dx++ )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tint nx = localX + dx;\r\n\t\t\t\t\t\t\tif ( nx < 0 || nx >= gridWidth ) continue;\r\n\t\t\t\t\t\t\tfor ( int dy = -ObstructionBuffer; dy <= ObstructionBuffer; dy++ )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tint ny = localY + dy;\r\n\t\t\t\t\t\t\t\tif ( ny < 0 || ny >= gridWidth ) continue;\r\n\t\t\t\t\t\t\t\tif ( localBlockedGrid[nx + ny * gridWidth] )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tisBlocked = true;\r\n\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tif ( isBlocked ) break;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif ( isBlocked ) continue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( needsTrace )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar traceStart = inst.Position + traceUpOffset;\r\n\t\t\t\t\t\tvar traceEnd = inst.Position - traceUpOffset;\r\n\t\t\t\t\t\tvar trace = Scene.Trace.Ray( traceStart, traceEnd )\r\n\t\t\t\t\t\t\t.WithoutTags( TraceIgnoreTags )\r\n\t\t\t\t\t\t\t.Run();\r\n\r\n\t\t\t\t\t\tbool hitObstruction = trace.Hit && IsObstruction( trace.GameObject );\r\n\r\n\t\t\t\t\t\tif ( CheckObstructions && (trace.StartedSolid || hitObstruction) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( localX >= 0 && localX < gridWidth && localY >= 0 && localY < gridWidth )\r\n\t\t\t\t\t\t\t\tlocalBlockedGrid[localX + localY * gridWidth] = true;\r\n\t\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif ( SnapToGround && trace.Hit && !trace.StartedSolid && !hitObstruction )\r\n\t\t\t\t\t\t\tfinalPosition = trace.HitPosition;\r\n\r\n\t\t\t\t\t\ttraceCounter++;\r\n\t\t\t\t\t\tif ( traceCounter % 100 == 0 )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tawait GameTask.Yield();\r\n\t\t\t\t\t\t\tif ( !IsValid || Scene == null ) return;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( inst.ModelIndex < 0 || inst.ModelIndex >= modelEntries.Length ) continue;\r\n\r\n\t\t\t\t\tfinalPosition += Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;\r\n\t\t\t\t\tvar list = modelLists[inst.ModelIndex] ??= new List<Transform>();\r\n\t\t\t\t\tlist.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );\r\n\r\n\t\t\t\t\tcxSum += finalPosition.x;\r\n\t\t\t\t\tcySum += finalPosition.y;\r\n\t\t\t\t\tczSum += finalPosition.z;\r\n\t\t\t\t\tplacedCount++;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( !IsValid ) return;\r\n\r\n\t\t\t\tvar chunk = new ClutterChunk();\r\n\t\t\t\tchunk.Center = placedCount > 0\r\n\t\t\t\t\t? new Vector3( cxSum / placedCount, cySum / placedCount, czSum / placedCount )\r\n\t\t\t\t\t: new Vector3( cx * ChunkSize + ChunkSize * 0.5f, cy * ChunkSize + ChunkSize * 0.5f, 0f );\r\n\r\n\t\t\t\tfor ( int i = 0; i < modelLists.Length; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( modelLists[i] != null && modelLists[i].Count > 0 )\r\n\t\t\t\t\t\tchunk.TransformsByModel[i] = modelLists[i].ToArray();\r\n\t\t\t\t}\r\n\r\n\t\t\t\t_activeChunks[(cx, cy)] = chunk;\r\n\t\t\t\tRebuildRenderCache();\r\n\t\t\t}\r\n\t\t\tfinally\r\n\t\t\t{\r\n\t\t\t\tArrayPool<bool>.Shared.Return( localBlockedGrid );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception )\r\n\t\t{\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_generatingChunks.Remove( (cx, cy) );\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Rendering.cs",
"FileName": "PerformantTerrainScattererManager.Rendering.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Runtime.InteropServices;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate void RebuildRenderCache()\r\n\t{\r\n\t\tvar newCache = new ChunkRenderData[_activeChunks.Count];\r\n\t\tint index = 0;\r\n\r\n\t\tforeach ( var chunk in _activeChunks.Values )\r\n\t\t{\r\n\t\t\tvar batches = new ModelRenderBatch[chunk.TransformsByModel.Count];\r\n\t\t\tint bIndex = 0;\r\n\r\n\t\t\tforeach ( var kvp in chunk.TransformsByModel )\r\n\t\t\t{\r\n\t\t\t\tint mIdx = kvp.Key;\r\n\t\t\t\tbatches[bIndex++] = new ModelRenderBatch\r\n\t\t\t\t{\r\n\t\t\t\t\tModel = ModelEntries[mIdx].Model,\r\n\t\t\t\t\tTransforms = kvp.Value,\r\n\t\t\t\t\tRenderDistSq = _modelRenderDistSq[mIdx],\r\n\t\t\t\t\tLodDistancesSq = _modelLodDistancesSq[mIdx],\r\n\t\t\t\t\tMaxLod = Math.Max( 0, _modelLodCounts[mIdx] - 1 )\r\n\t\t\t\t};\r\n\t\t\t}\r\n\r\n\t\t\tnewCache[index++] = new ChunkRenderData { Center = chunk.Center, Batches = batches };\r\n\t\t}\r\n\r\n\t\t_renderCache = newCache;\r\n\t}\r\n\r\n\tprivate void RenderClutter( SceneObject obj )\r\n\t{\r\n\t\tvar cache = _renderCache;\r\n\t\tif ( cache.Length == 0 ) return;\r\n\r\n\t\tif ( Scene == null ) return;\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\r\n\t\tVector3 cameraPos = camera.WorldPosition;\r\n\t\tVector3 cameraForward = camera.WorldRotation.Forward;\r\n\r\n\t\tfloat frustumMinDistSq = FrustumCullMinDistance * FrustumCullMinDistance;\r\n\t\tfloat maxRenderDistSq = _maxRenderDistance * _maxRenderDistance;\r\n\t\tbool useFrustum = UseFrustumCulling;\r\n\r\n\t\tif ( _lodBuckets == null )\r\n\t\t{\r\n\t\t\t_lodBuckets = new List<Transform>[16];\r\n\t\t\tfor ( int i = 0; i < _lodBuckets.Length; i++ )\r\n\t\t\t\t_lodBuckets[i] = new List<Transform>();\r\n\t\t}\r\n\r\n\t\tfor ( int c = 0; c < cache.Length; c++ )\r\n\t\t{\r\n\t\t\tref var chunk = ref cache[c];\r\n\r\n\t\t\tVector3 toChunk = chunk.Center - cameraPos;\r\n\t\t\tfloat chunkDistSq = toChunk.LengthSquared;\r\n\r\n\t\t\tif ( chunkDistSq > maxRenderDistSq ) continue;\r\n\r\n\t\t\tif ( useFrustum && chunkDistSq > frustumMinDistSq )\r\n\t\t\t{\r\n\t\t\t\tfloat dot = Vector3.Dot( cameraForward, toChunk );\r\n\t\t\t\tif ( dot < 0f && (dot * dot) > 0.04f * chunkDistSq )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar batches = chunk.Batches;\r\n\t\t\tfor ( int b = 0; b < batches.Length; b++ )\r\n\t\t\t{\r\n\t\t\t\tref var batch = ref batches[b];\r\n\t\t\t\tif ( chunkDistSq > batch.RenderDistSq ) continue;\r\n\r\n\t\t\t\tint maxLod = batch.MaxLod;\r\n\r\n\t\t\t\tfor ( int i = 0; i <= maxLod; i++ )\r\n\t\t\t\t\t_lodBuckets[i].Clear();\r\n\r\n\t\t\t\tvar transforms = batch.Transforms;\r\n\t\t\t\tvar lodDists = batch.LodDistancesSq;\r\n\r\n\t\t\t\tfor ( int i = 0; i < transforms.Length; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 instPos = transforms[i].Position;\r\n\t\t\t\t\tfloat instDistSq = (instPos - cameraPos).LengthSquared;\r\n\r\n\t\t\t\t\tif ( instDistSq > batch.RenderDistSq ) continue;\r\n\r\n\t\t\t\t\tint lod = 0;\r\n\t\t\t\t\tfor ( int l = 0; l < lodDists.Length; l++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( instDistSq > lodDists[l] )\r\n\t\t\t\t\t\t\tlod = l + 1;\r\n\t\t\t\t\t\telse\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( lod > maxLod ) lod = maxLod;\r\n\r\n\t\t\t\t\t_lodBuckets[lod].Add( transforms[i] );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfor ( int l = 0; l <= maxLod; l++ )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar bucket = _lodBuckets[l];\r\n\t\t\t\t\tif ( bucket.Count > 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tGraphics.DrawModelInstanced( batch.Model, CollectionsMarshal.AsSpan( bucket ), l );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "kivin.performantterrainscatterer",
"Path": "Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Placement.cs",
"FileName": "PerformantTerrainScattererManager.Placement.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 293458,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Runtime.CompilerServices;\r\nusing Sandbox;\r\n\r\npublic sealed partial class PerformantTerrainScatterer\r\n{\r\n\tprivate List<InstanceData> CalculateChunkPlacements( int cx, int cy, Transform terrainTransform )\r\n\t{\r\n\t\tif ( _heightMapPixels == null || _controlMapPixels == null ) return null;\r\n\t\tvar modelEntries = ModelEntries;\r\n\t\tif ( modelEntries == null || modelEntries.Length == 0 ) return null;\r\n\r\n\t\tint loopCount = _maxInstancesLoopCount;\r\n\t\tvar instances = new List<InstanceData>( loopCount );\r\n\t\tint chunkSeed = HashCombine( Seed, HashCombine( cx, cy ) );\r\n\t\tvar random = new Random( chunkSeed );\r\n\t\tfloat textureEps = 1.0f / (_resolution - 1);\r\n\t\tfloat chunkBaseX = cx * ChunkSize;\r\n\t\tfloat chunkBaseY = cy * ChunkSize;\r\n\t\tint modelsCount = modelEntries.Length;\r\n\r\n\t\tint[] placedCountPerMaterial = new int[32];\r\n\r\n\t\tfor ( int i = 0; i < loopCount; i++ )\r\n\t\t{\r\n\t\t\tfloat localX = random.NextSingle() * ChunkSize;\r\n\t\t\tfloat localY = random.NextSingle() * ChunkSize;\r\n\t\t\tfloat worldX = chunkBaseX + localX;\r\n\t\t\tfloat worldY = chunkBaseY + localY;\r\n\r\n\t\t\tVector3 worldAbsPoint = new Vector3( worldX, worldY, 0f );\r\n\t\t\tVector3 terrainLocal = terrainTransform.PointToLocal( worldAbsPoint );\r\n\t\t\tfloat u = terrainLocal.x / _terrainSize;\r\n\t\t\tfloat v = terrainLocal.y / _terrainSize;\r\n\r\n\t\t\tif ( u < 0f || u > 1f || v < 0f || v > 1f ) continue;\r\n\r\n\t\t\tint cX = (int)(u * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );\r\n\t\t\tint cY = (int)(v * (_resolution - 1f)).Clamp( 0f, _resolution - 1f );\r\n\t\t\tint controlIndex = cX + cY * _resolution;\r\n\t\t\tuint packedMap = _controlMapPixels[controlIndex];\r\n\t\t\tvar compactMat = new CompactTerrainMaterial( packedMap );\r\n\r\n\t\t\tif ( compactMat.IsHole ) continue;\r\n\r\n\t\t\tint dominantMaterialId = (compactMat.BlendFactor > 127)\r\n\t\t\t\t? compactMat.OverlayTextureId\r\n\t\t\t\t: compactMat.BaseTextureId;\r\n\r\n\t\t\tif ( dominantMaterialId < 32 )\r\n\t\t\t{\r\n\t\t\t\tif ( placedCountPerMaterial[dominantMaterialId] >= _materialMaxInstances[dominantMaterialId] )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tfloat materialTotalWeight = 0f;\r\n\t\t\tfor ( int m = 0; m < modelsCount; m++ )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId < _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tmaterialTotalWeight += _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( materialTotalWeight <= 0f ) continue;\r\n\r\n\t\t\tfloat roll = random.NextSingle() * materialTotalWeight;\r\n\t\t\tfloat currentWeightSum = 0f;\r\n\t\t\tint selectedModelIndex = -1;\r\n\r\n\t\t\tfor ( int m = 0; m < modelsCount; m++ )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId < _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat w = _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t\tif ( w > 0f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tcurrentWeightSum += w;\r\n\t\t\t\t\t\tif ( roll <= currentWeightSum )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tselectedModelIndex = m;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( selectedModelIndex == -1 ) continue;\r\n\t\t\tref var selectedModel = ref modelEntries[selectedModelIndex];\r\n\r\n\t\t\tif ( ReduceDensityOnTransitions )\r\n\t\t\t{\r\n\t\t\t\tfloat blendNorm = compactMat.BlendFactor * 0.003921569f;\r\n\t\t\t\tfloat purity = MathF.Abs( blendNorm - 0.5f ) * 2f;\r\n\t\t\t\tfloat spawnChance = MathF.Pow( purity, TransitionThinningIntensity );\r\n\t\t\t\tif ( random.NextSingle() > spawnChance ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( selectedModel.NoiseType != ScattererNoiseType.None )\r\n\t\t\t{\r\n\t\t\t\tvar noiseField = _modelNoiseFields[selectedModelIndex];\r\n\t\t\t\tfloat noiseVal = noiseField.Sample( worldX, worldY );\r\n\t\t\t\tif ( noiseVal < selectedModel.NoiseThreshold ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tVector3 normal = Vector3.Up;\r\n\r\n\t\t\tif ( UseTerrainNormal || MaxSlopeAngle < 90f )\r\n\t\t\t{\r\n\t\t\t\tfloat hL = SampleHeightBilinear( u - textureEps, v );\r\n\t\t\t\tfloat hR = SampleHeightBilinear( u + textureEps, v );\r\n\t\t\t\tfloat hD = SampleHeightBilinear( u, v - textureEps );\r\n\t\t\t\tfloat hU = SampleHeightBilinear( u, v + textureEps );\r\n\t\t\t\tVector3 terrainTangentX =\r\n\t\t\t\t\tnew Vector3( 2.0f * textureEps * _terrainSize, 0f, (hR - hL) * _terrainHeight );\r\n\t\t\t\tVector3 terrainTangentY =\r\n\t\t\t\t\tnew Vector3( 0f, 2.0f * textureEps * _terrainSize, (hU - hD) * _terrainHeight );\r\n\t\t\t\tVector3 terrainNormalLocal = Vector3.Cross( terrainTangentX, terrainTangentY ).Normal;\r\n\t\t\t\tnormal = terrainTransform.Rotation * terrainNormalLocal;\r\n\t\t\t\tfloat slopeAngle = Vector3.GetAngle( Vector3.Up, normal );\r\n\t\t\t\tif ( slopeAngle > MaxSlopeAngle ) continue;\r\n\t\t\t}\r\n\r\n\t\t\tfloat z = SampleHeightBilinear( u, v ) * _terrainHeight;\r\n\t\t\tVector3 finalLocalPoint = new Vector3( terrainLocal.x, terrainLocal.y, z );\r\n\t\t\tVector3 worldTerrainPoint = terrainTransform.PointToWorld( finalLocalPoint );\r\n\r\n\t\t\tRotation randomYaw = RandomizeRotation\r\n\t\t\t\t? Rotation.FromYaw( random.NextSingle() * 360f )\r\n\t\t\t\t: Rotation.Identity;\r\n\r\n\t\t\tRotation finalRot = UseTerrainNormal\r\n\t\t\t\t? Rotation.FromToRotation( Vector3.Up, normal ) * randomYaw\r\n\t\t\t\t: randomYaw;\r\n\r\n\t\t\tfloat scale = selectedModel.RandomizeScale\r\n\t\t\t\t? (random.NextSingle() * (selectedModel.MaxScale - selectedModel.MinScale) + selectedModel.MinScale)\r\n\t\t\t\t: 1.0f;\r\n\r\n\t\t\tinstances.Add( new InstanceData\r\n\t\t\t{\r\n\t\t\t\tPosition = worldTerrainPoint, Rotation = finalRot, Scale = scale, ModelIndex = selectedModelIndex\r\n\t\t\t} );\r\n\r\n\t\t\tif ( dominantMaterialId < 32 )\r\n\t\t\t{\r\n\t\t\t\tplacedCountPerMaterial[dominantMaterialId]++;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn instances;\r\n\t}\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate int HashCombine( int seed, int value )\r\n\t\t=> seed ^ (value + -1640531527 + (seed << 6) + (seed >> 2));\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate bool IsObstruction( GameObject hitObject )\r\n\t{\r\n\t\tif ( hitObject == null ) return false;\r\n\t\tif ( _cachedHitObject == hitObject ) return _cachedIsObstruction;\r\n\t\t_cachedHitObject = hitObject;\r\n\t\t_cachedIsObstruction = hitObject.Components.Get<Terrain>() == null;\r\n\t\treturn _cachedIsObstruction;\r\n\t}\r\n\r\n\t[MethodImpl( MethodImplOptions.AggressiveInlining )]\r\n\tprivate float SampleHeightBilinear( float u, float v )\r\n\t{\r\n\t\tu = u.Clamp( 0f, 1f );\r\n\t\tv = v.Clamp( 0f, 1f );\r\n\t\tfloat gx = u * (_resolution - 1);\r\n\t\tfloat gy = v * (_resolution - 1);\r\n\t\tint ix = (int)gx;\r\n\t\tint iy = (int)gy;\r\n\t\tfloat fx = gx - ix;\r\n\t\tfloat fy = gy - iy;\r\n\t\tint ix1 = Math.Min( ix + 1, _resolution - 1 );\r\n\t\tint iy1 = Math.Min( iy + 1, _resolution - 1 );\r\n\t\tint row0 = iy * _resolution;\r\n\t\tint row1 = iy1 * _resolution;\r\n\t\tfloat h00 = _heightMapPixels[row0 + ix] * UshortMaxReciprocal;\r\n\t\tfloat h10 = _heightMapPixels[row0 + ix1] * UshortMaxReciprocal;\r\n\t\tfloat h01 = _heightMapPixels[row1 + ix] * UshortMaxReciprocal;\r\n\t\tfloat h11 = _heightMapPixels[row1 + ix1] * UshortMaxReciprocal;\r\n\t\tfloat h0 = h00 + (h10 - h00) * fx;\r\n\t\tfloat h1 = h01 + (h11 - h01) * fx;\r\n\t\treturn h0 + (h1 - h0) * fy;\r\n\t}\r\n}\r\n"
}
]
}