| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- using System.Diagnostics.CodeAnalysis;
- using System.Numerics;
- using Robust.Shared.Random;
- using Robust.Shared.Utility;
- namespace Content.Server.Worldgen.Tools;
- /// <summary>
- /// An implementation of Poisson Disk Sampling, for evenly spreading points across a given area.
- /// </summary>
- public sealed class PoissonDiskSampler
- {
- public const int DefaultPointsPerIteration = 30;
- [Dependency] private readonly IRobustRandom _random = default!;
- /// <summary>
- /// Samples for points within the given circle.
- /// </summary>
- /// <param name="center">Center of the sample</param>
- /// <param name="radius">Radius of the sample</param>
- /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
- /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
- /// <returns>An enumerator of points</returns>
- public SampleEnumerator SampleCircle(Vector2 center, float radius, float minimumDistance,
- int pointsPerIteration = DefaultPointsPerIteration)
- {
- return Sample(center - new Vector2(radius, radius), center + new Vector2(radius, radius), radius,
- minimumDistance, pointsPerIteration);
- }
- /// <summary>
- /// Samples for points within the given rectangle.
- /// </summary>
- /// <param name="topLeft">The top left of the rectangle</param>
- /// <param name="lowerRight">The bottom right of the rectangle</param>
- /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
- /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
- /// <returns>An enumerator of points</returns>
- public SampleEnumerator SampleRectangle(Vector2 topLeft, Vector2 lowerRight, float minimumDistance,
- int pointsPerIteration = DefaultPointsPerIteration)
- {
- return Sample(topLeft, lowerRight, null, minimumDistance, pointsPerIteration);
- }
- /// <summary>
- /// Samples for points within the given rectangle, with an optional rejection distance.
- /// </summary>
- /// <param name="topLeft">The top left of the rectangle</param>
- /// <param name="lowerRight">The bottom right of the rectangle</param>
- /// <param name="rejectionDistance">The distance at which points will be discarded, if any</param>
- /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
- /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
- /// <returns>An enumerator of points</returns>
- public SampleEnumerator Sample(Vector2 topLeft, Vector2 lowerRight, float? rejectionDistance,
- float minimumDistance, int pointsPerIteration)
- {
- // This still doesn't guard against dangerously low but non-zero distances, but this will do for now.
- DebugTools.Assert(minimumDistance > 0, "Minimum distance must be above 0, or else an infinite number of points would be generated.");
- var settings = new SampleSettings
- {
- TopLeft = topLeft, LowerRight = lowerRight,
- Dimensions = lowerRight - topLeft,
- Center = (topLeft + lowerRight) / 2,
- CellSize = minimumDistance / (float) Math.Sqrt(2),
- MinimumDistance = minimumDistance,
- RejectionSqDistance = rejectionDistance * rejectionDistance
- };
- settings.GridWidth = (int) (settings.Dimensions.X / settings.CellSize) + 1;
- settings.GridHeight = (int) (settings.Dimensions.Y / settings.CellSize) + 1;
- var state = new State
- {
- Grid = new Vector2?[settings.GridWidth, settings.GridHeight],
- ActivePoints = new List<Vector2>()
- };
- return new SampleEnumerator(this, state, settings, pointsPerIteration);
- }
- private Vector2 AddFirstPoint(ref SampleSettings settings, ref State state)
- {
- while (true)
- {
- var d = _random.NextDouble();
- var xr = settings.TopLeft.X + settings.Dimensions.X * d;
- d = _random.NextDouble();
- var yr = settings.TopLeft.Y + settings.Dimensions.Y * d;
- var p = new Vector2((float) xr, (float) yr);
- if (settings.RejectionSqDistance != null &&
- (settings.Center - p).LengthSquared() > settings.RejectionSqDistance)
- continue;
- var index = Denormalize(p, settings.TopLeft, settings.CellSize);
- state.Grid[(int) index.X, (int) index.Y] = p;
- state.ActivePoints.Add(p);
- return p;
- }
- }
- private Vector2? AddNextPoint(Vector2 point, ref SampleSettings settings, ref State state)
- {
- var q = GenerateRandomAround(point, settings.MinimumDistance);
- if (q.X >= settings.TopLeft.X && q.X < settings.LowerRight.X &&
- q.Y > settings.TopLeft.Y && q.Y < settings.LowerRight.Y &&
- (settings.RejectionSqDistance == null ||
- (settings.Center - q).LengthSquared() <= settings.RejectionSqDistance))
- {
- var qIndex = Denormalize(q, settings.TopLeft, settings.CellSize);
- var tooClose = false;
- for (var i = (int) Math.Max(0, qIndex.X - 2);
- i < Math.Min(settings.GridWidth, qIndex.X + 3) && !tooClose;
- i++)
- for (var j = (int) Math.Max(0, qIndex.Y - 2);
- j < Math.Min(settings.GridHeight, qIndex.Y + 3) && !tooClose;
- j++)
- {
- if (state.Grid[i, j].HasValue && (state.Grid[i, j]!.Value - q).Length() < settings.MinimumDistance)
- tooClose = true;
- }
- if (!tooClose)
- {
- state.ActivePoints.Add(q);
- state.Grid[(int) qIndex.X, (int) qIndex.Y] = q;
- return q;
- }
- }
- return null;
- }
- private Vector2 GenerateRandomAround(Vector2 center, float minimumDistance)
- {
- var d = _random.NextDouble();
- var radius = minimumDistance + minimumDistance * d;
- d = _random.NextDouble();
- var angle = Math.PI * 2 * d;
- var newX = radius * Math.Sin(angle);
- var newY = radius * Math.Cos(angle);
- return new Vector2((float) (center.X + newX), (float) (center.Y + newY));
- }
- private static Vector2 Denormalize(Vector2 point, Vector2 origin, double cellSize)
- {
- return new Vector2((int) ((point.X - origin.X) / cellSize), (int) ((point.Y - origin.Y) / cellSize));
- }
- public struct SampleEnumerator
- {
- private PoissonDiskSampler _pds;
- private State _state;
- private SampleSettings _settings;
- // These variables make up the state machine.
- private bool _returnedFirstPoint;
- private int _pointsPerIteration;
- private int _iterationListIndex;
- private bool _iterationFound;
- private int _iterationPosition;
- // This has internal access because C# nested type access is being weird.
- internal SampleEnumerator(PoissonDiskSampler pds, State state, SampleSettings settings, int ppi)
- {
- _pds = pds;
- _state = state;
- _settings = settings;
- _pointsPerIteration = ppi;
- }
- public bool MoveNext([NotNullWhen(true)] out Vector2? point)
- {
- // First point is chosen via a very particular method.
- if (!_returnedFirstPoint)
- {
- _returnedFirstPoint = true;
- point = _pds.AddFirstPoint(ref _settings, ref _state);
- return true;
- }
- // Remaining points have to be fed out carefully.
- // We can be interrupted (by a successful point) mid-stream.
- while (_state.ActivePoints.Count != 0)
- {
- if (_iterationPosition == 0)
- {
- // First point of iteration.
- _iterationListIndex = _pds._random.Next(_state.ActivePoints.Count);
- _iterationFound = false;
- }
- var basePoint = _state.ActivePoints[_iterationListIndex];
- point = _pds.AddNextPoint(basePoint, ref _settings, ref _state);
- // Set this now, return later after processing is complete.
- _iterationFound |= point != null;
- // Iteration loop advance.
- _iterationPosition++;
- if (_iterationPosition == _pointsPerIteration)
- {
- // Reached end of this iteration.
- _iterationPosition = 0;
- if (!_iterationFound)
- _state.ActivePoints.RemoveAt(_iterationListIndex);
- }
- if (point != null)
- return true;
- }
- point = null;
- return false;
- }
- }
- internal struct State
- {
- public Vector2?[,] Grid;
- public List<Vector2> ActivePoints;
- }
- internal struct SampleSettings
- {
- public Vector2 TopLeft, LowerRight, Center;
- public Vector2 Dimensions;
- public float? RejectionSqDistance;
- public float MinimumDistance;
- public float CellSize;
- public int GridWidth, GridHeight;
- }
- }
|