{"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( \u0022AddonTitle\u0022, \u0022Performant Terrain Scatterer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022performantterrainscatterer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022kivin\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022kivin.performantterrainscatterer\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002226\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-06-23T11:51:31.4189525Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.119.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.119.0\u0022)]"},{"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 \u003E 0 \u0026\u0026 cameraPos.DistanceSquared( _lastCameraPos ) \u003C 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 \u003C= chunksInRadius; dx\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( int dy = -chunksInRadius; dy \u003C= chunksInRadius; dy\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar chunkCoord = (cameraCx \u002B dx, cameraCy \u002B dy);\r\n\t\t\t\tif ( !_activeChunks.ContainsKey( chunkCoord ) \u0026\u0026 _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 ) \u003E chunksInRadius ||\r\n\t\t\t     Math.Abs( key.Item2 - cameraCy ) \u003E 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 \u003E 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( () =\u003E 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 \u002B (gridPadding * 2) \u002B 2;\r\n\t\t\tint gridArea = gridWidth * gridWidth;\r\n\r\n\t\t\tbool[] localBlockedGrid = ArrayPool\u003Cbool\u003E.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\u003CTransform\u003E[] modelLists = new List\u003CTransform\u003E[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 \u003C= ObstructionBuffer; dx\u002B\u002B )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tint nx = localX \u002B dx;\r\n\t\t\t\t\t\t\tif ( nx \u003C 0 || nx \u003E= gridWidth ) continue;\r\n\t\t\t\t\t\t\tfor ( int dy = -ObstructionBuffer; dy \u003C= ObstructionBuffer; dy\u002B\u002B )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tint ny = localY \u002B dy;\r\n\t\t\t\t\t\t\t\tif ( ny \u003C 0 || ny \u003E= gridWidth ) continue;\r\n\t\t\t\t\t\t\t\tif ( localBlockedGrid[nx \u002B 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 \u002B 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 \u0026\u0026 IsObstruction( trace.GameObject );\r\n\r\n\t\t\t\t\t\tif ( CheckObstructions \u0026\u0026 (trace.StartedSolid || hitObstruction) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( localX \u003E= 0 \u0026\u0026 localX \u003C gridWidth \u0026\u0026 localY \u003E= 0 \u0026\u0026 localY \u003C gridWidth )\r\n\t\t\t\t\t\t\t\tlocalBlockedGrid[localX \u002B 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 \u0026\u0026 trace.Hit \u0026\u0026 !trace.StartedSolid \u0026\u0026 !hitObstruction )\r\n\t\t\t\t\t\t\tfinalPosition = trace.HitPosition;\r\n\r\n\t\t\t\t\t\ttraceCounter\u002B\u002B;\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 \u003C 0 || inst.ModelIndex \u003E= modelEntries.Length ) continue;\r\n\r\n\t\t\t\t\tfinalPosition \u002B= Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;\r\n\t\t\t\t\tvar list = modelLists[inst.ModelIndex] ??= new List\u003CTransform\u003E();\r\n\t\t\t\t\tlist.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );\r\n\r\n\t\t\t\t\tcxSum \u002B= finalPosition.x;\r\n\t\t\t\t\tcySum \u002B= finalPosition.y;\r\n\t\t\t\t\tczSum \u002B= finalPosition.z;\r\n\t\t\t\t\tplacedCount\u002B\u002B;\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 \u003E 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 \u002B ChunkSize * 0.5f, cy * ChunkSize \u002B ChunkSize * 0.5f, 0f );\r\n\r\n\t\t\t\tfor ( int i = 0; i \u003C modelLists.Length; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( modelLists[i] != null \u0026\u0026 modelLists[i].Count \u003E 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\u003Cbool\u003E.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( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic Terrain TargetTerrain { get; set; }\r\n\r\n\t[Property, Category( \u0022Models\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic List\u003CScattererModelEntry\u003E Models { get; set; } = new();\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) ), Range( 0, 90 )]\r\n\tpublic float MaxSlopeAngle { get; set; } = 45f;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool ReduceDensityOnTransitions { get; set; } = false;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), 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( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool SnapToGround { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool CheckObstructions { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float TraceStartHeight { get; set; } = 200f;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float GridCellSize { get; set; } = 50f;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int ObstructionBuffer { get; set; } = 2;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 )]\r\n\tpublic string[] TraceIgnoreTags { get; set; } = { \u0022player\u0022, \u0022trigger\u0022, \u0022clutter\u0022 };\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int MaxInstancesPerChunk { get; set; } = 500;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic List\u003CMaterialInstanceOverride\u003E MaterialOverrides { get; set; } = new();\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkSize { get; set; } = 500f;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkLoadDistance { get; set; } = 3000f;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 )] public bool UseFrustumCulling { get; set; } = true;\r\n\t[Property, Category( \u0022Performance\u0022 )] public float FrustumCullMinDistance { get; set; } = 800f;\r\n\t[Property, Category( \u0022Performance\u0022 )] public bool AutomaticallyInitializeSystem { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool UseTerrainNormal { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Seed\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int Seed { get; set; } = 12345;\r\n\r\n\t[Property, Category( \u0022Randomization\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool RandomizeRotation { get; set; } = true;\r\n\r\n\tprivate readonly Dictionary\u003C(int, int), ClutterChunk\u003E _activeChunks = new();\r\n\tprivate readonly HashSet\u003C(int, int)\u003E _generatingChunks = new();\r\n\tprivate readonly List\u003C(int, int)\u003E _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\u003CChunkRenderData\u003E();\r\n\r\n\t[ThreadStatic] private static List\u003CTransform\u003E[] _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\u003CTerrain\u003E();\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( \u0022Reinitialize\u0022 )]\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\u003CChunkRenderData\u003E();\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\u003CModel\u003E();\r\n\t\tforeach ( var entry in Models )\r\n\t\t{\r\n\t\t\tif ( entry.Model != null \u0026\u0026 !seenModels.Add( entry.Model ) )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[Scatterer] Duplicate model entry found for \u0027{entry.Model.ResourceName}\u0027 in the Models configuration list.\u0022 );\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\u003CTerrainMaterial\u003E();\r\n\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t{\r\n\t\t\t\tif ( ov.Material != null \u0026\u0026 !seenOverrideMaterials.Add( ov.Material ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Warning( $\u0022[Scatterer] Duplicate material override configuration found for material \u0027{ov.Material.ResourceName}\u0027.\u0022 );\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\u003CTerrainMaterial\u003E();\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$\u0022[Scatterer] Duplicate material entry \u0027{mw.Material.ResourceName}\u0027 found for model \u0027{t.Model.ResourceName}\u0027.\u0022 );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight \u003C= 0f )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\u0022[Scatterer] Weight is set to 0 for material \u0027{mw.Material.ResourceName}\u0027 on model \u0027{t.Model.ResourceName}\u0027.\u0022 );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight \u003E 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 \u003E _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 \u003C ModelEntries.Length; m\u002B\u002B )\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 \u0026\u0026 mdl.MeshInfo.LodSwitchDistances.Length \u003E 0 \u0026\u0026\r\n\t\t\t\t     mdl.MeshInfo.LodSwitchDistances[0] \u003E 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 \u003C switchDistances.Length; i\u002B\u002B )\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] \u003E 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 \u003C switchCount; i\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = (step * (i \u002B 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\u003Cfloat\u003E();\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\u003Cfloat\u003E();\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 \u003C Models.Count; m\u002B\u002B )\r\n\t\t{\r\n\t\t\t_modelMaterialWeights[m] = new float[32];\r\n\t\t\tif ( TargetTerrain.Storage.Materials != null \u0026\u0026 Models[m].MaterialWeights != null )\r\n\t\t\t{\r\n\t\t\t\tfor ( int i = 0; i \u003C TargetTerrain.Storage.Materials.Count; i\u002B\u002B )\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 \u003C Models.Count; m\u002B\u002B )\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 \u002B 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 \u0026\u0026 MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i \u003C TargetTerrain.Storage.Materials.Count; i\u002B\u002B )\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 \u003E _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( \u0022Base\u0022 )] public Model Model { get; set; } = null;\r\n\t\t[Property, Category( \u0022Base\u0022 )] public float ZOffset { get; set; } = 0f;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 )]\r\n\t\tpublic ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 )]\r\n\t\tpublic float NoiseScale { get; set; } = 0.002f;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 ), Range( 0f, 1f )]\r\n\t\tpublic float NoiseThreshold { get; set; } = 0.4f;\r\n\r\n\t\t[Property, Category( \u0022Performance\u0022 )] public float RenderDistance { get; set; } = 2500f;\r\n\t\t[Property, Category( \u0022Terrain\u0022 )] public List\u003CMaterialWeight\u003E MaterialWeights { get; set; } = new();\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 )]\r\n\t\tpublic bool RandomizeScale { get; set; } = false;\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MinScale { get; set; } = 0.8f;\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MaxScale { get; set; } = 1.2f;\r\n\r\n\t\t[Property, Category( \u0022LOD\u0022 ), 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\u003Cint, Transform[]\u003E 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\u002B\u002B] = 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\u002B\u002B] = 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\u003CTransform\u003E[16];\r\n\t\t\tfor ( int i = 0; i \u003C _lodBuckets.Length; i\u002B\u002B )\r\n\t\t\t\t_lodBuckets[i] = new List\u003CTransform\u003E();\r\n\t\t}\r\n\r\n\t\tfor ( int c = 0; c \u003C cache.Length; c\u002B\u002B )\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 \u003E maxRenderDistSq ) continue;\r\n\r\n\t\t\tif ( useFrustum \u0026\u0026 chunkDistSq \u003E frustumMinDistSq )\r\n\t\t\t{\r\n\t\t\t\tfloat dot = Vector3.Dot( cameraForward, toChunk );\r\n\t\t\t\tif ( dot \u003C 0f \u0026\u0026 (dot * dot) \u003E 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 \u003C batches.Length; b\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tref var batch = ref batches[b];\r\n\t\t\t\tif ( chunkDistSq \u003E 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 \u003C= maxLod; i\u002B\u002B )\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 \u003C transforms.Length; i\u002B\u002B )\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 \u003E 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 \u003C lodDists.Length; l\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( instDistSq \u003E lodDists[l] )\r\n\t\t\t\t\t\t\tlod = l \u002B 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 \u003E 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 \u003C= maxLod; l\u002B\u002B )\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 \u003E 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( \u0022Editor\u0022, \u0022Performant Terrain Scatterer/My Menu Option\u0022 )]\r\n\tpublic static void OpenMyMenu()\r\n\t{\r\n\t\tEditorUtility.DisplayDialog( \u0022It worked!\u0022, \u0022This is being called from your library\u0027s editor code!\u0022 );\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( \u0022Base\u0022 )] public Model Model { get; set; } = null;\r\n\t\t[Property, Category( \u0022Base\u0022 )] public float ZOffset { get; set; } = 0f;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 )]\r\n\t\tpublic ScattererNoiseType NoiseType { get; set; } = ScattererNoiseType.None;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 )]\r\n\t\tpublic float NoiseScale { get; set; } = 0.002f;\r\n\r\n\t\t[Property, Category( \u0022Noise Scattering\u0022 ), Range( 0f, 1f )]\r\n\t\tpublic float NoiseThreshold { get; set; } = 0.4f;\r\n\r\n\t\t[Property, Category( \u0022Performance\u0022 )] public float RenderDistance { get; set; } = 2500f;\r\n\t\t[Property, Category( \u0022Terrain\u0022 )] public List\u003CMaterialWeight\u003E MaterialWeights { get; set; } = new();\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 )]\r\n\t\tpublic bool RandomizeScale { get; set; } = false;\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MinScale { get; set; } = 0.8f;\r\n\r\n\t\t[Property, Category( \u0022Randomization\u0022 ), ShowIf( nameof(RandomizeScale), true )]\r\n\t\tpublic float MaxScale { get; set; } = 1.2f;\r\n\r\n\t\t[Property, Category( \u0022LOD\u0022 ), 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\u003Cint, Transform[]\u003E 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\u003CInstanceData\u003E 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\u003CInstanceData\u003E( 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 \u003C loopCount; i\u002B\u002B )\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 \u002B localX;\r\n\t\t\tfloat worldY = chunkBaseY \u002B 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 \u003C 0f || u \u003E 1f || v \u003C 0f || v \u003E 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 \u002B 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 \u003E 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 \u003C 32 )\r\n\t\t\t{\r\n\t\t\t\tif ( placedCountPerMaterial[dominantMaterialId] \u003E= _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 \u003C modelsCount; m\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId \u003C _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tmaterialTotalWeight \u002B= _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( materialTotalWeight \u003C= 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 \u003C modelsCount; m\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId \u003C _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 \u003E 0f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tcurrentWeightSum \u002B= w;\r\n\t\t\t\t\t\tif ( roll \u003C= 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() \u003E 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 \u003C 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 \u003C 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 \u002B textureEps, v );\r\n\t\t\t\tfloat hD = SampleHeightBilinear( u, v - textureEps );\r\n\t\t\t\tfloat hU = SampleHeightBilinear( u, v \u002B 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 \u003E 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) \u002B 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 \u003C 32 )\r\n\t\t\t{\r\n\t\t\t\tplacedCountPerMaterial[dominantMaterialId]\u002B\u002B;\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=\u003E seed ^ (value \u002B -1640531527 \u002B (seed \u003C\u003C 6) \u002B (seed \u003E\u003E 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\u003CTerrain\u003E() == 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 \u002B 1, _resolution - 1 );\r\n\t\tint iy1 = Math.Min( iy \u002B 1, _resolution - 1 );\r\n\t\tint row0 = iy * _resolution;\r\n\t\tint row1 = iy1 * _resolution;\r\n\t\tfloat h00 = _heightMapPixels[row0 \u002B ix] * UshortMaxReciprocal;\r\n\t\tfloat h10 = _heightMapPixels[row0 \u002B ix1] * UshortMaxReciprocal;\r\n\t\tfloat h01 = _heightMapPixels[row1 \u002B ix] * UshortMaxReciprocal;\r\n\t\tfloat h11 = _heightMapPixels[row1 \u002B ix1] * UshortMaxReciprocal;\r\n\t\tfloat h0 = h00 \u002B (h10 - h00) * fx;\r\n\t\tfloat h1 = h01 \u002B (h11 - h01) * fx;\r\n\t\treturn h0 \u002B (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( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic Terrain TargetTerrain { get; set; }\r\n\r\n\t[Property, Category( \u0022Models\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic List\u003CScattererModelEntry\u003E Models { get; set; } = new();\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) ), Range( 0, 90 )]\r\n\tpublic float MaxSlopeAngle { get; set; } = 45f;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool ReduceDensityOnTransitions { get; set; } = false;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), 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( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool SnapToGround { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool CheckObstructions { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float TraceStartHeight { get; set; } = 200f;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float GridCellSize { get; set; } = 50f;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int ObstructionBuffer { get; set; } = 2;\r\n\r\n\t[Property, Category( \u0022Collision\u0022 )]\r\n\tpublic string[] TraceIgnoreTags { get; set; } = { \u0022player\u0022, \u0022trigger\u0022, \u0022clutter\u0022 };\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int MaxInstancesPerChunk { get; set; } = 500;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic List\u003CMaterialInstanceOverride\u003E MaterialOverrides { get; set; } = new();\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkSize { get; set; } = 500f;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic float ChunkLoadDistance { get; set; } = 3000f;\r\n\r\n\t[Property, Category( \u0022Performance\u0022 )] public bool UseFrustumCulling { get; set; } = true;\r\n\t[Property, Category( \u0022Performance\u0022 )] public float FrustumCullMinDistance { get; set; } = 800f;\r\n\t[Property, Category( \u0022Performance\u0022 )] public bool AutomaticallyInitializeSystem { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Terrain\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool UseTerrainNormal { get; set; } = true;\r\n\r\n\t[Property, Category( \u0022Seed\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic int Seed { get; set; } = 12345;\r\n\r\n\t[Property, Category( \u0022Randomization\u0022 ), Change( nameof(InitializeSystem) )]\r\n\tpublic bool RandomizeRotation { get; set; } = true;\r\n\r\n\tprivate readonly Dictionary\u003C(int, int), ClutterChunk\u003E _activeChunks = new();\r\n\tprivate readonly HashSet\u003C(int, int)\u003E _generatingChunks = new();\r\n\tprivate readonly List\u003C(int, int)\u003E _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\u003CChunkRenderData\u003E();\r\n\r\n\t[ThreadStatic] private static List\u003CTransform\u003E[] _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\u003CTerrain\u003E();\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( \u0022Reinitialize\u0022 )]\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\u003CChunkRenderData\u003E();\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\u003CModel\u003E();\r\n\t\tforeach ( var entry in Models )\r\n\t\t{\r\n\t\t\tif ( entry.Model != null \u0026\u0026 !seenModels.Add( entry.Model ) )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[Scatterer] Duplicate model entry found for \u0027{entry.Model.ResourceName}\u0027 in the Models configuration list.\u0022 );\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\u003CTerrainMaterial\u003E();\r\n\t\t\tforeach ( var ov in MaterialOverrides )\r\n\t\t\t{\r\n\t\t\t\tif ( ov.Material != null \u0026\u0026 !seenOverrideMaterials.Add( ov.Material ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Warning( $\u0022[Scatterer] Duplicate material override configuration found for material \u0027{ov.Material.ResourceName}\u0027.\u0022 );\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\u003CTerrainMaterial\u003E();\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$\u0022[Scatterer] Duplicate material entry \u0027{mw.Material.ResourceName}\u0027 found for model \u0027{t.Model.ResourceName}\u0027.\u0022 );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight \u003C= 0f )\r\n\t\t\t\t\t\t\tLog.Warning(\r\n\t\t\t\t\t\t\t\t$\u0022[Scatterer] Weight is set to 0 for material \u0027{mw.Material.ResourceName}\u0027 on model \u0027{t.Model.ResourceName}\u0027.\u0022 );\r\n\r\n\t\t\t\t\t\tif ( mw.Weight \u003E 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 \u003E _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 \u003C ModelEntries.Length; m\u002B\u002B )\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 \u0026\u0026 mdl.MeshInfo.LodSwitchDistances.Length \u003E 0 \u0026\u0026\r\n\t\t\t\t     mdl.MeshInfo.LodSwitchDistances[0] \u003E 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 \u003C switchDistances.Length; i\u002B\u002B )\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] \u003E 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 \u003C switchCount; i\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat dist = (step * (i \u002B 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\u003Cfloat\u003E();\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\u003Cfloat\u003E();\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 \u003C Models.Count; m\u002B\u002B )\r\n\t\t{\r\n\t\t\t_modelMaterialWeights[m] = new float[32];\r\n\t\t\tif ( TargetTerrain.Storage.Materials != null \u0026\u0026 Models[m].MaterialWeights != null )\r\n\t\t\t{\r\n\t\t\t\tfor ( int i = 0; i \u003C TargetTerrain.Storage.Materials.Count; i\u002B\u002B )\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 \u003C Models.Count; m\u002B\u002B )\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 \u002B 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 \u0026\u0026 MaterialOverrides != null )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i \u003C TargetTerrain.Storage.Materials.Count; i\u002B\u002B )\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 \u003E _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 \u003E 0 \u0026\u0026 cameraPos.DistanceSquared( _lastCameraPos ) \u003C 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 \u003C= chunksInRadius; dx\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( int dy = -chunksInRadius; dy \u003C= chunksInRadius; dy\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar chunkCoord = (cameraCx \u002B dx, cameraCy \u002B dy);\r\n\t\t\t\tif ( !_activeChunks.ContainsKey( chunkCoord ) \u0026\u0026 _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 ) \u003E chunksInRadius ||\r\n\t\t\t     Math.Abs( key.Item2 - cameraCy ) \u003E 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 \u003E 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( () =\u003E 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 \u002B (gridPadding * 2) \u002B 2;\r\n\t\t\tint gridArea = gridWidth * gridWidth;\r\n\r\n\t\t\tbool[] localBlockedGrid = ArrayPool\u003Cbool\u003E.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\u003CTransform\u003E[] modelLists = new List\u003CTransform\u003E[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 \u003C= ObstructionBuffer; dx\u002B\u002B )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tint nx = localX \u002B dx;\r\n\t\t\t\t\t\t\tif ( nx \u003C 0 || nx \u003E= gridWidth ) continue;\r\n\t\t\t\t\t\t\tfor ( int dy = -ObstructionBuffer; dy \u003C= ObstructionBuffer; dy\u002B\u002B )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tint ny = localY \u002B dy;\r\n\t\t\t\t\t\t\t\tif ( ny \u003C 0 || ny \u003E= gridWidth ) continue;\r\n\t\t\t\t\t\t\t\tif ( localBlockedGrid[nx \u002B 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 \u002B 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 \u0026\u0026 IsObstruction( trace.GameObject );\r\n\r\n\t\t\t\t\t\tif ( CheckObstructions \u0026\u0026 (trace.StartedSolid || hitObstruction) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( localX \u003E= 0 \u0026\u0026 localX \u003C gridWidth \u0026\u0026 localY \u003E= 0 \u0026\u0026 localY \u003C gridWidth )\r\n\t\t\t\t\t\t\t\tlocalBlockedGrid[localX \u002B 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 \u0026\u0026 trace.Hit \u0026\u0026 !trace.StartedSolid \u0026\u0026 !hitObstruction )\r\n\t\t\t\t\t\t\tfinalPosition = trace.HitPosition;\r\n\r\n\t\t\t\t\t\ttraceCounter\u002B\u002B;\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 \u003C 0 || inst.ModelIndex \u003E= modelEntries.Length ) continue;\r\n\r\n\t\t\t\t\tfinalPosition \u002B= Vector3.Up * modelEntries[inst.ModelIndex].ZOffset;\r\n\t\t\t\t\tvar list = modelLists[inst.ModelIndex] ??= new List\u003CTransform\u003E();\r\n\t\t\t\t\tlist.Add( new Transform( finalPosition, inst.Rotation, inst.Scale ) );\r\n\r\n\t\t\t\t\tcxSum \u002B= finalPosition.x;\r\n\t\t\t\t\tcySum \u002B= finalPosition.y;\r\n\t\t\t\t\tczSum \u002B= finalPosition.z;\r\n\t\t\t\t\tplacedCount\u002B\u002B;\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 \u003E 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 \u002B ChunkSize * 0.5f, cy * ChunkSize \u002B ChunkSize * 0.5f, 0f );\r\n\r\n\t\t\t\tfor ( int i = 0; i \u003C modelLists.Length; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( modelLists[i] != null \u0026\u0026 modelLists[i].Count \u003E 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\u003Cbool\u003E.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\u002B\u002B] = 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\u002B\u002B] = 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\u003CTransform\u003E[16];\r\n\t\t\tfor ( int i = 0; i \u003C _lodBuckets.Length; i\u002B\u002B )\r\n\t\t\t\t_lodBuckets[i] = new List\u003CTransform\u003E();\r\n\t\t}\r\n\r\n\t\tfor ( int c = 0; c \u003C cache.Length; c\u002B\u002B )\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 \u003E maxRenderDistSq ) continue;\r\n\r\n\t\t\tif ( useFrustum \u0026\u0026 chunkDistSq \u003E frustumMinDistSq )\r\n\t\t\t{\r\n\t\t\t\tfloat dot = Vector3.Dot( cameraForward, toChunk );\r\n\t\t\t\tif ( dot \u003C 0f \u0026\u0026 (dot * dot) \u003E 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 \u003C batches.Length; b\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tref var batch = ref batches[b];\r\n\t\t\t\tif ( chunkDistSq \u003E 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 \u003C= maxLod; i\u002B\u002B )\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 \u003C transforms.Length; i\u002B\u002B )\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 \u003E 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 \u003C lodDists.Length; l\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( instDistSq \u003E lodDists[l] )\r\n\t\t\t\t\t\t\tlod = l \u002B 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 \u003E 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 \u003C= maxLod; l\u002B\u002B )\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 \u003E 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\u003CInstanceData\u003E 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\u003CInstanceData\u003E( 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 \u003C loopCount; i\u002B\u002B )\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 \u002B localX;\r\n\t\t\tfloat worldY = chunkBaseY \u002B 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 \u003C 0f || u \u003E 1f || v \u003C 0f || v \u003E 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 \u002B 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 \u003E 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 \u003C 32 )\r\n\t\t\t{\r\n\t\t\t\tif ( placedCountPerMaterial[dominantMaterialId] \u003E= _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 \u003C modelsCount; m\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId \u003C _modelMaterialWeights[m].Length )\r\n\t\t\t\t{\r\n\t\t\t\t\tmaterialTotalWeight \u002B= _modelMaterialWeights[m][dominantMaterialId];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( materialTotalWeight \u003C= 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 \u003C modelsCount; m\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( dominantMaterialId \u003C _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 \u003E 0f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tcurrentWeightSum \u002B= w;\r\n\t\t\t\t\t\tif ( roll \u003C= 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() \u003E 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 \u003C 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 \u003C 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 \u002B textureEps, v );\r\n\t\t\t\tfloat hD = SampleHeightBilinear( u, v - textureEps );\r\n\t\t\t\tfloat hU = SampleHeightBilinear( u, v \u002B 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 \u003E 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) \u002B 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 \u003C 32 )\r\n\t\t\t{\r\n\t\t\t\tplacedCountPerMaterial[dominantMaterialId]\u002B\u002B;\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=\u003E seed ^ (value \u002B -1640531527 \u002B (seed \u003C\u003C 6) \u002B (seed \u003E\u003E 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\u003CTerrain\u003E() == 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 \u002B 1, _resolution - 1 );\r\n\t\tint iy1 = Math.Min( iy \u002B 1, _resolution - 1 );\r\n\t\tint row0 = iy * _resolution;\r\n\t\tint row1 = iy1 * _resolution;\r\n\t\tfloat h00 = _heightMapPixels[row0 \u002B ix] * UshortMaxReciprocal;\r\n\t\tfloat h10 = _heightMapPixels[row0 \u002B ix1] * UshortMaxReciprocal;\r\n\t\tfloat h01 = _heightMapPixels[row1 \u002B ix] * UshortMaxReciprocal;\r\n\t\tfloat h11 = _heightMapPixels[row1 \u002B ix1] * UshortMaxReciprocal;\r\n\t\tfloat h0 = h00 \u002B (h10 - h00) * fx;\r\n\t\tfloat h1 = h01 \u002B (h11 - h01) * fx;\r\n\t\treturn h0 \u002B (h1 - h0) * fy;\r\n\t}\r\n}\r\n"}]}