Collision management for painted flora instances. It tracks nearby painted instances, spawns transient GameObject model colliders for those within a radius of the viewer, reuses and destroys colliders as the viewer or storage changes, and keeps a hidden collision root object.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.FloraTool;
/// <summary>
/// Collision for painted flora. Instances are drawn entirely on the GPU, so nothing exists for the
/// player to walk into - colliders are spawned only for instances near the viewer and recycled as it
/// moves, keeping the physics cost tied to what is reachable rather than to the whole painted set.
/// </summary>
public sealed partial class FloraRenderer
{
private readonly record struct CollisionKey( string ModelPath, int Index );
private readonly Dictionary<CollisionKey, GameObject> _colliders = [];
// A set rather than a list: SyncColliders tests every live collider against it, so a linear
// scan there would be quadratic once a few hundred are in range.
private readonly HashSet<CollisionKey> _wanted = [];
private readonly List<CollisionKey> _stale = [];
private GameObject _collisionRoot;
private Vector3 _lastCollisionOrigin;
private bool _hasCollisionOrigin;
private int _collisionRevision = -1;
/// <summary>
/// Rebuilding the set walks every painted instance, so it only happens once the viewer has moved
/// far enough for the answer to have changed.
/// </summary>
private const float CollisionRefreshDistance = 256.0f;
private void UpdateCollision( Vector3 origin )
{
if ( Storage is null || !Definition.IsValid() )
{
ReleaseCollision();
return;
}
var radius = Definition.CollisionRadius;
if ( radius <= 0.0f )
{
ReleaseCollision();
return;
}
// Painting invalidates the set regardless of whether the viewer moved.
var storageChanged = _collisionRevision != Storage.Revision;
if ( !storageChanged && _hasCollisionOrigin &&
origin.Distance( _lastCollisionOrigin ) < CollisionRefreshDistance )
return;
_collisionRevision = Storage.Revision;
_lastCollisionOrigin = origin;
_hasCollisionOrigin = true;
GatherWanted( origin, radius );
SyncColliders();
}
private void GatherWanted( Vector3 origin, float radius )
{
_wanted.Clear();
var radiusSquared = radius * radius;
foreach ( var (modelPath, instances) in Storage.Instances )
{
var entry = Definition.FindEntry( modelPath );
// No entry means the model is painted but no longer in the palette - leave it visual
// rather than guessing that it should be solid.
if ( entry?.EnablePhysics is not true )
continue;
for ( var i = 0; i < instances.Count; i++ )
{
if ( instances[i].Position.DistanceSquared( origin ) > radiusSquared )
continue;
_wanted.Add( new CollisionKey( modelPath, i ) );
}
}
}
private void SyncColliders()
{
// Drop what fell out of range first, so the objects are free to be reused this same frame.
_stale.Clear();
foreach ( var (key, gameObject) in _colliders )
{
if ( gameObject.IsValid() && _wanted.Contains( key ) )
continue;
_stale.Add( key );
}
foreach ( var key in _stale )
{
if ( _colliders.Remove( key, out var gameObject ) && gameObject.IsValid() )
gameObject.Destroy();
}
foreach ( var key in _wanted )
{
if ( _colliders.ContainsKey( key ) )
continue;
var gameObject = CreateCollider( key );
if ( gameObject.IsValid() )
_colliders[key] = gameObject;
}
}
private GameObject CreateCollider( CollisionKey key )
{
var instances = Storage.GetInstances( key.ModelPath );
if ( key.Index < 0 || key.Index >= instances.Count )
return null;
var entry = Definition.FindEntry( key.ModelPath );
var model = entry?.Model ?? Model.Load( key.ModelPath );
if ( !model.IsValid() )
return null;
EnsureCollisionRoot();
var instance = instances[key.Index];
var gameObject = new GameObject( true, "FloraCollider" )
{
Parent = _collisionRoot,
WorldTransform = instance.ToTransform(),
};
// Not saved with the scene and not shown in the hierarchy - these are transient physics
// proxies for geometry that only exists on the GPU.
gameObject.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden;
var collider = gameObject.Components.Create<ModelCollider>();
collider.Model = model;
collider.Static = true;
return gameObject;
}
private void EnsureCollisionRoot()
{
if ( _collisionRoot.IsValid() )
return;
_collisionRoot = new GameObject( true, "Flora Colliders" ) { Parent = GameObject };
_collisionRoot.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden;
}
private void ReleaseCollision()
{
foreach ( var (_, gameObject) in _colliders )
{
if ( gameObject.IsValid() )
gameObject.Destroy();
}
_colliders.Clear();
_wanted.Clear();
_stale.Clear();
if ( _collisionRoot.IsValid() )
_collisionRoot.Destroy();
_collisionRoot = null;
_hasCollisionOrigin = false;
_collisionRevision = -1;
}
}