Data types for flora painting. FloraEntry holds a model plus tuning (weight, scale, yaw, tilt, sink, physics and rendering flags). FloraDefinition is a GameResource asset that stores a list of entries and painting/physics parameters and can pick or lookup entries by weight or model path.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.FloraTool;
/// <summary>
/// One kind of flora the brush can plant. Weight decides how often it comes up relative to the
/// other entries in the definition.
/// </summary>
public sealed class FloraEntry
{
[Property]
public Model Model { get; set; }
/// <summary>Relative chance of this entry being picked. Zero excludes it without deleting it.</summary>
[Property, Range( 0, 10 )]
public float Weight { get; set; } = 1.0f;
[Property]
public RangedFloat Scale { get; set; } = new( 0.85f, 1.25f );
/// <summary>Random spin about the vertical axis, so repeated instances don't read as clones.</summary>
[Property]
public bool RandomYaw { get; set; } = true;
/// <summary>
/// Tilts the instance toward the surface normal. Right for rocks and bushes, usually wrong for
/// trees - a trunk growing perpendicular to a hillside looks broken.
/// </summary>
[Property, Range( 0, 1 )]
public float AlignToNormal { get; set; } = 0.0f;
/// <summary>Random lean away from vertical, in degrees. A little goes a long way on trees.</summary>
[Property, Range( 0, 45 )]
public float RandomTilt { get; set; } = 0.0f;
/// <summary>Sinks the instance into the ground, hiding the seam where the base meets the surface.</summary>
[Property, Range( 0, 64 )]
public float SinkDepth { get; set; } = 0.0f;
/// <summary>
/// Gives this entry real collision. Colliders are only created near the player, so this is about
/// whether the flora is solid at all - not about paying for every painted instance at once.
/// </summary>
[Property, Group( "Physics" )]
public bool EnablePhysics { get; set; } = true;
[Property, Group( "Rendering" )]
public bool CastShadows { get; set; } = true;
public bool HasModel => Model is not null && !string.IsNullOrEmpty( Model.ResourcePath );
}
/// <summary>
/// A palette of flora plus the rules used when painting it. Shared by every
/// <see cref="FloraRenderer"/> that references it, so a whole world can be retuned from one asset.
/// </summary>
[AssetType( Name = "Flora Definition", Extension = "floradef", Category = "Flora" )]
public sealed class FloraDefinition : GameResource
{
[Property]
public List<FloraEntry> Entries { get; set; } = [];
/// <summary>Minimum gap between instances. Stops the brush stacking trees on one spot.</summary>
[Property, Group( "Painting" ), Range( 8, 1024 )]
public float Spacing { get; set; } = 128.0f;
/// <summary>Instances attempted per brush stroke. Spacing still governs how many actually land.</summary>
[Property, Group( "Painting" ), Range( 1, 64 )]
public int PerStroke { get; set; } = 8;
/// <summary>Minimum ground normal Z. Steeper than this and nothing plants, so cliffs stay bare.</summary>
[Property, Group( "Painting" ), Range( 0, 1 )]
public float SlopeLimit { get; set; } = 0.6f;
/// <summary>
/// Radius around the viewer within which entries flagged <see cref="FloraEntry.EnablePhysics"/>
/// get real colliders. Keep it just past where the player can reach.
/// </summary>
[Property, Group( "Physics" ), Range( 256, 20000 )]
public float CollisionRadius { get; set; } = 4000.0f;
/// <summary>Picks an entry at random, biased by weight. Null when nothing is usable.</summary>
public FloraEntry PickEntry( Random random )
{
if ( Entries is null || Entries.Count == 0 )
return null;
var total = 0.0f;
foreach ( var entry in Entries )
{
if ( entry?.HasModel is true && entry.Weight > 0.0f )
total += entry.Weight;
}
if ( total <= 0.0f )
return null;
var pick = random.NextSingle() * total;
foreach ( var entry in Entries )
{
if ( entry?.HasModel is not true || entry.Weight <= 0.0f )
continue;
pick -= entry.Weight;
if ( pick <= 0.0f )
return entry;
}
return null;
}
/// <summary>
/// The entry that owns a model path. Instances are stored per model, so this is how the renderer
/// recovers the authored settings - shadows, physics, LOD distances - for a batch.
/// </summary>
public FloraEntry FindEntry( string modelPath )
{
if ( Entries is null || string.IsNullOrEmpty( modelPath ) )
return null;
foreach ( var entry in Entries )
{
if ( entry?.HasModel is true && entry.Model.ResourcePath == modelPath )
return entry;
}
return null;
}
}