Code/FloraGenerator.cs
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 );
}
}