Code/PerformantTerrainScatterer/PerformantTerrainScattererManager.Chunks.cs
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;
public sealed partial class PerformantTerrainScatterer
{
private void ManageChunks()
{
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 )
{
Transform terrainTransform = TargetTerrain.IsValid() ? TargetTerrain.WorldTransform : new Transform();
var tentativeInstances = await GameTask.RunInThreadAsync( () => CalculateChunkPlacements( cx, cy, terrainTransform ) );
var modelEntries = ModelEntries;
if ( tentativeInstances == null || modelEntries == null )
{
_generatingChunks.Remove( (cx, cy) );
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 );
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 )
{
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 ( 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++;
}
ArrayPool<bool>.Shared.Return( localBlockedGrid );
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;
_generatingChunks.Remove( (cx, cy) );
RebuildRenderCache();
}
}