Skip to content
Edit on GitHub

Author ground flora species and procedural scattering

verifiedAgainst e41a048 · verifiedOn 2026-09-11.

In Voxamine, open landscapes are not bare geometric voxel steps. Ground flora—such as wild clovers, delicate alpine lichens, arid sagebrush, shore reeds, and floating lily pads—decorates the world to give each biome its tactile identity (MASTERPLAN.md §13.1, §14.5).

Rather than allocating heavy GameObjects or writing decorative entries into chunk voxel memory, ground flora is purely presentational:

  • It is drawn using GPU instancing (Graphics.DrawMeshInstanced) in batches up to 1023 instances per draw call.
  • It streams within a bounded 3-chunk horizontal presentation radius around the active camera.
  • It strictly obeys voxel geometry: each prop requires an allowed solid support block (FloraSupport.cs), tests for vertical headroom (ClearanceHeight), and despawns the instant a player mines the ground or places a roof overhead.
  • It sways harmoniously under atmospheric wind currents (AtmosphericPressureField.PrevailingWind).

You will author a data-driven species definition in SurfaceFloraCatalog.cs, configure support and clearance legality gates in VegetationPlacementRules.cs, inspect deterministic PRNG scattering in VegetationPlacementMath.cs, and verify streaming presentation in SurfaceVegetationRuntime.cs.

// Example: Authoring a hardy alpine wildflower in SurfaceFloraCatalog:
new SurfaceFloraSpecies(
SurfaceFloraKind.AlpineWildflower, "Alpine Wildflower",
BiomeType.Mountains | BiomeType.Tundra,
new[] { FloraSupport.Grass, FloraSupport.Snow },
clearanceHeight: 1, density: 0.04f, maxPerChunk: 24,
FloraMeshStyle.CrossQuad,
new Color(0.95f, 0.95f, 0.88f), new Color(0.96f, 0.82f, 0.32f),
new Vector3(0.14f, 0.34f, 0.14f), swayAmount: 0.5f)

Ground flora sits cleanly between procedural terrain streaming and GPU presentation:

  • Biome & Terrain Engine (BiomeClassifier & ChunkStreamingRuntime) determines local biome classification and provides world block lookups.
  • Flora Catalogue (SurfaceFloraCatalog & SurfaceFloraSpecies) acts as the immutable code-defined catalogue of all flora types, their biome masks, and support requirements.
  • Placement Rules (VegetationPlacementRules) validates that candidate coordinates satisfy physical support, vertical clearance, and environmental adjacency.
  • Deterministic Math (VegetationPlacementMath) computes seeded hash scattering and candidate priority without storing persistent state.
  • Presentation Runtime (SurfaceVegetationRuntime) monitors active chunk transitions, organizes per-chunk candidate patches, evaluates wind sway from AtmosphericPressureField, and issues Graphics.DrawMeshInstanced calls.
flowchart TD
  subgraph TERRAIN["Terrain Generation & Streaming"]
    BIOME["BiomeClassifier.Classify(x, z)\n(BiomeType, Temperature, Moisture)"]
    CHUNK["ChunkStreamingRuntime\n(Voxel Block State & Palette)"]
  end

  subgraph CATALOG["Authoring & Rules"]
    CATALOGUE["SurfaceFloraCatalog.All\n(26 immutable Species)"]
    RULES["VegetationPlacementRules.CanPlace\n(Support, Clearance, Water Adjacency)"]
    MATH["VegetationPlacementMath\n(Seeded Hash Scattering & Offset/Yaw Style)"]
  end

  subgraph RUNTIME["Presentation Runtime"]
    STREAM["SurfaceVegetationRuntime\n(3-chunk radius patch streaming)"]
    SWAY["AtmosphericPressureField.PrevailingWind\n(Wind angle & phase modulation)"]
    REBASE["world.OriginBlockOffset\n(Floating-origin coordinate reprojection)"]
  end

  subgraph GPU["GPU Instancing"]
    DRAW["Graphics.DrawMeshInstanced\n(FloraMeshLibrary meshes, up to 1023/batch)"]
  end

  BIOME --> STREAM
  CHUNK --> RULES
  CATALOGUE --> STREAM
  CATALOGUE --> RULES
  MATH --> STREAM
  RULES --> STREAM
  SWAY --> STREAM
  REBASE --> STREAM
  STREAM --> DRAW

Read MASTERPLAN.md §13.1 (“World presentation”) and §14.5 (“Flora & vegetation”), then inspect:

Keep the core architectural distinction clear:

Category Typical Examples Representation Persistence Collision
Voxel Blocks Stone, Dirt, Wood, Cutout Foliage Block Voxel byte in chunk array (BlockId) Persisted in world save Solid box or raycast hit
Ground Flora Wildflowers, Grass clumps, Ferns, Lichen GPU-instanced procedural mesh clump Not persisted (procedurally derived from seed + voxels) None (purely visual)

In SurfaceFloraSpecies.cs, append your new species kind to the end of SurfaceFloraKind:

public enum SurfaceFloraKind
{
GrassClump = 0,
Wildflower = 1,
Cactus = 2,
// ...
Glasswort = 22,
SilicaHorsetail = 23,
SulfurLichen = 24,
SphagnumMoss = 25
}

[!IMPORTANT] Never reorder existing enum members. The integer ordinal directly salts the deterministic PRNG hash (salt = KindSeedSalt + (int)kind * KindSeedStride). Reordering members silently scrambles the flora layout of existing worlds.

In SurfaceFloraCatalog.cs, instantiate the immutable SurfaceFloraSpecies in Build():

new SurfaceFloraSpecies(
SurfaceFloraKind.Glasswort, "Glasswort",
BiomeType.Desert | BiomeType.Badlands,
sand, clearanceHeight: 2, density: 0.045f, maxPerChunk: 18,
FloraMeshStyle.SaltwortCard,
new Color(0.52f, 0.60f, 0.20f), new Color(0.92f, 0.22f, 0.20f),
new Vector3(0.72f, 0.96f, 0.72f), swayAmount: 0.25f,
chemicalRole: FloraChemicalRole.SodiumAshPrecursor,
textureResourcePath: "Flora/Glasswort")

Every species specifies:

  • biomes: Flags mask (BiomeType.Plains | BiomeType.Forest). Every classified biome must have at least two distinct flora species.
  • supports: One or more FloraSupport categories (Grass, Sand, Snow, Stone, WaterSurface).
  • clearanceHeight: Number of continuous air blocks required above the root.
  • density: Scatter probability in $(0, 1]$ evaluated on qualifying surfaces.
  • maxPerChunk: Hard instance cap per chunk preventing performance spikes.
  • meshStyle: The procedural silhouette drawn by FloraMeshLibrary (CrossQuad, BushCluster, LowCrust, LilyPadDisc, etc.).
  • swayAmount: Wind reactivity ($0.0$ for rigid lichen and cacti, $>0.8$ for flexible tall grasses).
  • chemicalRole: Clue indicating future chemistry harvest roles (e.g. SodiumAshPrecursor, BiogenicSilica).

In VegetationPlacementRules.CanPlace, incoming coordinates are validated against the active palette and world voxels:

public static bool CanPlace(
SurfaceFloraSpecies species,
BiomeType biome,
GenerationBlockPalette palette,
BlockId support,
System.Func<int, BlockId> cellAbove,
bool hasWaterNeighbour)
{
if (species == null || palette == null || cellAbove == null) return false;
if ((species.Biomes & biome) == BiomeType.None) return false;
if (!species.SupportsBlock(support, palette)) return false;
if (species.RequiresWaterAdjacency && !hasWaterNeighbour) return false;
if (species.IsWaterSurface)
{
return support == palette.Water && cellAbove(1).IsAir;
}
for (int height = 1; height <= species.ClearanceHeight; height++)
{
if (!cellAbove(height).IsAir)
{
return false;
}
}
return true;
}

Support blocks are resolved dynamically via palette (FloraSupport.Grass $\to$ palette.Grass), ensuring flora authoring is completely independent of specific block IDs.

In VegetationPlacementMath, evaluate deterministic placement without state:

// 1. Fair rotation of species candidate order to prevent the first species from consuming all spots:
int startIndex = VegetationPlacementMath.GetSelectionStartIndex(worldX, worldZ, worldSeed, candidateCount);
// 2. Independent per-species scatter probability:
bool place = VegetationPlacementMath.ShouldPlace(worldX, worldZ, worldSeed, candidate.Kind, density);
// 3. Jitter local offset, yaw rotation, and scale:
VegetationVisualStyle style = VegetationPlacementMath.GetStyle(worldX, worldZ, worldSeed);

Because each species salts the seed differently, species scatter independently across biomes, creating natural clusters and varied ground tapestries.

SurfaceVegetationRuntime manages active chunks:

  1. As chunks activate within a 3-chunk Chebyshev radius around the camera, it computes candidate instances into VegetationPatch.
  2. In LateUpdate, it checks still-valid legality and builds transformation matrices.
  3. It offsets coordinates by world.OriginBlockOffset to maintain floating-origin invariance.
  4. It modulates rotation by atmospheric wind:
    Vector2 wind = AtmosphericPressureField.PrevailingWind;
    float windAngle = Mathf.Lerp(3f, 12f, clampedStorm) * species.SwayAmount;
    float sway = Mathf.Sin(Time.time * swaySpeed + instance.Style.WindPhase) * windAngle;
    Quaternion rotation = Quaternion.Euler(-wind.y * sway, instance.Style.YawDegrees, wind.x * sway);
  5. It issues Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, null, ShadowCastingMode.Off, true).

Run the dedicated flora test suites:

Terminal window
/opt/unity/Editor/Unity -batchmode -nographics \
-projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \
-runTests -testPlatform EditMode \
-testFilter VoxelSandbox.Tests.EditMode.World.SurfaceFloraCatalogTests
Terminal window
/opt/unity/Editor/Unity -batchmode -nographics \
-projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \
-runTests -testPlatform EditMode \
-testFilter VoxelSandbox.Tests.EditMode.World.VegetationPlacementRulesTests

Verify that all key assertions pass:

  • CatalogExposesAtLeastFourteenDistinctSpecies: guarantees coverage across all species.
  • EveryClassifiedBiomeHasAtLeastTwoGroundFloraSpecies: verifies no barren biomes.
  • EverySpeciesHonoursItsOwnInvariants: checks valid densities in $(0, 1]$, non-empty display names, and positive heights.
  • ScatterIsDeterministicPerKindAndKindsScatterIndependently: ensures repeatable, non-colliding scattering.
  • ChemistryFacingFloraUsesDistinctHonestBiomassOrIndicatorRoles: confirms valid 256x256 clamp textures for chemistry-facing species.
  1. Enter Play Mode in Gameplay.unity.
  2. Teleport to various biomes (/tp or fly):
    • Plains: Grass Clumps, Wildflowers, Clover, Tall Grass.
    • Desert: Saguaro Cacti, Barrel Cacti, Sagebrush, Dead Bush, Glasswort.
    • Tundra & Mountains: Lichen, Alpine Wildflower, Snow Shrub, Hardy Tuft, Sulfur Lichen.
    • Jungle & Swamps: Ferns, Giant Ferns, Bromeliads, Reeds, Lily Pads, Sphagnum Moss.
  3. Break a dirt block beneath a wildflower: observe that the flower instantly vanishes on the next frame because its support block was destroyed.
  4. Build a stone overhang 1 block above a reed: observe that it despawns due to clearance violation.

To author a new regional species (e.g. Cave Phosphor Fungi or Coastal Mangrove Tuft):

  1. Add Enum: Add CavePhosphorFungi to SurfaceFloraKind.
  2. Add Catalogue Entry: Construct the SurfaceFloraSpecies in SurfaceFloraCatalog.Build().
  3. Select Mesh Style: Choose from FloraMeshStyle or add a new procedural silhouette in FloraMeshLibrary.cs.
  4. Assign Textures or Colors: Set primary/secondary colors or add an alpha-clipped 256x256 card texture under Resources/Flora/.
  5. Add Test Fixtures: Verify biome coverage and placement assertions in SurfaceFloraCatalogTests.cs.
Symptom Cause Fix
Flora layout shifts across the entire world after an update. SurfaceFloraKind enum was reordered or an item inserted into the middle. Always append new species at the end of SurfaceFloraKind. Never change existing enum values.
Flora appears floating in mid-air or clipping through ceilings. ClearanceHeight was set to $0$ or support was not checked against palette. Ensure ClearanceHeight >= 1 and all vertical cells are verified using cellAbove(h).IsAir.
Lily pads fail to spawn on water surfaces. Support was set to FloraSupport.Grass instead of FloraSupport.WaterSurface. Use FloraSupport.WaterSurface and ensure IsWaterSurface handles palette.Water checks.
High frame-time hitching when turning the camera quickly. Flora runtime created new Material or Mesh instances inside LateUpdate. Cache all procedural meshes in meshCache and materials in materialCache by FloraMeshStyle and SurfaceFloraKind.
Ground flora fails to align with camera during origin rebase. World positions were computed without subtracting world.OriginBlockOffset. Always evaluate instance transformation matrices relative to world.OriginBlockOffset.

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…