Pure function generator for flora instances in a chunk. It deterministically hashes chunk+cell data and definition to produce per-cell instance positions, rotations and scales without runtime RNG state.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.FloraTool;
/// <summary>
/// Turns painted coverage into concrete instances. Everything here is a pure function of the chunk
/// coordinate, the cell contents and the seed - no state, no RNG object - so a chunk regenerates
/// identically every run, on every machine, however many times it is streamed in and out.
/// </summary>
public static class FloraGenerator
{
public readonly record struct Instance(int EntryIndex, Vector3 Position, Rotation Rotation, float Scale)
{
public readonly Transform ToTransform() => new(Position, Rotation, Scale);
}
/// <summary>
/// Integer avalanche hash. Deterministic across runs and platforms, which the framework RNG is
/// not guaranteed to be, and cheap enough to call several times per instance.
/// </summary>
private static uint Hash(uint x)
{
x ^= x >> 16;
x *= 0x7feb352du;
x ^= x >> 15;
x *= 0x846ca68bu;
x ^= x >> 16;
return x;
}
private static float HashFloat(uint x) => Hash(x) * (1.0f / 4294967296.0f);
/// <summary>
/// Generates every instance for one chunk, appending into <paramref name="results"/>.
/// </summary>
public static void GenerateChunk(FloraStorage.ChunkCoord coord, FloraStorage.Cell[] cells,
FloraDefinition definition, int seed, List<Instance> results)
{
if (cells is null || definition is null)
return;
var origin = FloraStorage.ChunkOrigin(coord);
var maxPerCell = Math.Max(definition.MaxPerCell, 1);
// Mixing the chunk coordinate into the seed keeps neighbouring chunks from sharing a
// sequence, which would otherwise show up as a visible repeating pattern across the world.
var chunkSeed = Hash((uint)seed
^ Hash((uint)coord.X * 73856093u)
^ Hash((uint)coord.Y * 19349663u));
for (var cellIndex = 0; cellIndex < cells.Length; cellIndex++)
{
var cell = cells[cellIndex];
var density = cell.Density;
if (density <= 0.0f)
continue;
if (cell.Normal.z < definition.SlopeLimit)
continue;
var cellSeed = Hash(chunkSeed ^ Hash((uint)cellIndex * 0x9e3779b9u));
var cx = cellIndex % FloraStorage.ChunkResolution;
var cy = cellIndex / FloraStorage.ChunkResolution;
var cellMinX = origin.x + cx * FloraStorage.CellSize;
var cellMinY = origin.y + cy * FloraStorage.CellSize;
// Fractional counts are resolved by a hash rather than rounding, so density reads as a
// smooth thinning across a field instead of stepping between whole numbers per cell.
var exact = density * maxPerCell;
var count = (int)exact;
if (HashFloat(cellSeed ^ 0x1b56c4e9u) < exact - count)
count++;
for (var i = 0; i < count; i++)
{
var s = Hash(cellSeed + (uint)i * 0x85ebca6bu);
var entry = ResolveEntry(definition, cell.EntryIndex, s);
if (entry.Index < 0)
continue;
results.Add(BuildInstance(entry.Index, entry.Entry, cell, s, cellMinX, cellMinY));
}
}
}
/// <summary>
/// A cell either names its entry - painted deliberately with one species selected - or defers to
/// the definition's weights.
/// </summary>
private static (int Index, FloraEntry Entry) ResolveEntry(FloraDefinition definition, int cellEntryIndex, uint seed)
{
var entries = definition.Entries;
if (entries is null || entries.Count == 0)
return (-1, null);
if (cellEntryIndex < entries.Count)
{
var named = entries[cellEntryIndex];
return named?.HasModel is true ? (cellEntryIndex, named) : (-1, null);
}
var total = 0.0f;
for (var i = 0; i < entries.Count; i++)
{
if (entries[i]?.HasModel is true && entries[i].Weight > 0.0f)
total += entries[i].Weight;
}
if (total <= 0.0f)
return (-1, null);
var pick = HashFloat(seed ^ 0x3c6ef372u) * total;
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
if (entry?.HasModel is not true || entry.Weight <= 0.0f)
continue;
pick -= entry.Weight;
if (pick <= 0.0f)
return (i, entry);
}
return (-1, null);
}
private static Instance BuildInstance(int entryIndex, FloraEntry entry, FloraStorage.Cell cell,
uint seed, float cellMinX, float cellMinY)
{
var jitterX = HashFloat(seed ^ 0x68bc21ebu);
var jitterY = HashFloat(seed ^ 0x02e5be93u);
var x = cellMinX + jitterX * FloraStorage.CellSize;
var y = cellMinY + jitterY * FloraStorage.CellSize;
var normal = cell.Normal;
// The baked height is the cell centre's, so a slope needs the offset carried across to the
// jittered position or trunks float on the uphill side and sink on the downhill one.
var offsetX = x - (cellMinX + FloraStorage.CellSize * 0.5f);
var offsetY = y - (cellMinY + FloraStorage.CellSize * 0.5f);
var z = cell.Height - (normal.x * offsetX + normal.y * offsetY) / MathF.Max(normal.z, 0.1f);
var position = new Vector3(x, y, z);
if (entry.SinkDepth > 0.0f)
position -= normal * entry.SinkDepth;
var rotation = entry.RandomYaw
? Rotation.FromYaw(HashFloat(seed ^ 0x7f4a7c15u) * 360.0f)
: Rotation.Identity;
if (entry.AlignToNormal > 0.0f)
{
var aligned = Rotation.LookAt(normal) * Rotation.FromPitch(90.0f);
rotation = Rotation.Slerp(rotation, aligned * rotation, entry.AlignToNormal);
}
if (entry.RandomTilt > 0.0f)
{
var tiltAngle = HashFloat(seed ^ 0x165667b1u) * entry.RandomTilt;
var tiltDirection = HashFloat(seed ^ 0x27d4eb2fu) * 360.0f;
rotation *= Rotation.FromAxis(Rotation.FromYaw(tiltDirection).Forward, tiltAngle);
}
var scale = MathX.Lerp(entry.Scale.Min, entry.Scale.Max, HashFloat(seed ^ 0xd3a2646cu));
return new Instance(entryIndex, position, rotation, scale);
}
}