Skip to content

Repository files navigation

GenTree — procedural Roblox tree generator

CILatest releaseMIT license

GenTree

GenTree is a small procedural tree generator I made for Roblox. I wanted to see how far I could get with ordinary Parts, a seed, and a few rules instead of storing a finished tree model. The result builds the trunk, branches, and foliage entirely from Luau and returns one organized Model.

Note

Oh! Just to be clear, this is a random personal project I made for experimenting and learning. It is not an official Roblox product or a full production forestry system. I still keep the package, documentation, and releases tidy so it is pleasant to try.

Why I made this

I liked the idea of dropping one module into a project and asking it for a tree at any position. At that point I thought, “What if the same little system could also make a repeatable forest?” That is why GenTree has both single-tree and forest APIs, along with an optional seed.

The design has a few simple goals:

  • Generate everything from code—no hidden tree assets.
  • Return a complete model, not one that is still filling in later.
  • Keep every visual choice in one configuration table.
  • Make seeded results repeatable enough to save and rebuild a layout.
  • Stay small enough that someone can read the source and understand the whole idea.

Get GenTree

GitHub release

Download GenTree-0.1.1.rbxm from the latest release, place it under ServerScriptService, and require it from a server script.

Wally

The repository is packaged for Wally as devkyato/gentree. Once that version is available in the registry, add it to your server dependencies:

[server-dependencies]
GenTree = "devkyato/gentree@0.1.1"

Then run:

wally install

Map ServerPackages or Packages into ServerScriptService in your Rojo project:

localServerScriptService=game:GetService("ServerScriptService")
localGenTree=require(ServerScriptService.Packages.GenTree)

Make one tree

localServerScriptService=game:GetService("ServerScriptService")
localWorkspace=game:GetService("Workspace")
localGenTree=require(ServerScriptService.Packages.GenTree)
localgenerator=GenTree.new({
seed=42,
minHeight=24,
maxHeight=32,
branchesPerTier=6,
})
localtree=generator:Generate(Vector3.new(0, 0, 0), Workspace)
print(tree:GetAttribute("PartCount"))

For a quick tree with the defaults:

localtree=GenTree.generate(Vector3.new(0, 0, 0), workspace)

And for a forest:

localtrees=generator:GenerateForest(
Vector3.new(0, 0, 0),
25, -- tree count80, -- radiusworkspace
)

How I think about generation

I think of one tree as a bottom-up pipeline. The trunk gives me a set of reliable attachment points, the branch tiers turn those points into a canopy, and the leaves fill the outside of that canopy. Here is the full trip through Generator.lua:

flowchart TD
REQUEST["Generate(position, parent)"] --> CONFIG["Validate and freeze configuration"]
CONFIG --> SEED["Create Random from seed"]
SEED --> HEIGHT["Choose height between minHeight and maxHeight"]
HEIGHT --> TRUNK_LOOP{"More trunk segments?"}
TRUNK_LOOP -->|Yes| TRUNK["Move upward, add controlled lean,<br>and create the next cylinder"]
TRUNK --> TRUNK_LOOP
TRUNK_LOOP -->|No| TIER_LOOP{"More branch tiers?"}
TIER_LOOP -->|Yes| ORIGIN["Select a point on the upper 65% of the trunk"]
ORIGIN --> SPIRAL["Rotate around the trunk using the golden angle"]
SPIRAL --> BRANCH["Choose upward angle and varied branch length"]
BRANCH --> LEAVES["Place foliage clusters along the outer half"]
LEAVES --> TIER_LOOP
TIER_LOOP -->|No| CROWN["Add the crown foliage"]
CROWN --> METADATA["Set Seed, Height, and PartCount attributes"]
METADATA --> PARENT["Parent and return the complete Model"]
Loading

One detail I cared about here: I build every descendant before I parent the model. That means Generate returns a finished tree. yieldEveryParts can still pause a large build occasionally, but callers never receive a half-populated model.

Oh! On the trunk part

The trunk is a chain of cylinders. Each new point moves upward by one segment and takes a small seeded step sideways. I interpolate from trunkBaseRadius to trunkTopRadius, so the separate cylinders read as one leaning, tapered trunk.

Geometry.partBetween does the reusable part: it finds the midpoint between two positions, sizes a cylinder to that distance, and rotates its long axis toward the next point.

How the branches become a canopy

At this point, I thought too about how evenly spaced branches can look strangely artificial. I use the golden angle to keep rotating around the trunk without stacking every tier into the same few directions. Then I add a small seeded variation to the angle, lift, and length.

graph TD
CROWN["Crown foliage"]
T4["Upper trunk segment"]
T3["Upper branch tier"]
T2["Middle branch tier"]
T1["Lower branch tier"]
ROOT["Generation position"]
ROOT --> T1 --> T2 --> T3 --> T4 --> CROWN
T1 --> B11["Branch"]
T1 --> B12["Branch"]
B11 --> F11["Leaf clusters"]
B12 --> F12["Leaf clusters"]
T2 --> B21["Branch"]
T2 --> B22["Branch"]
B21 --> F21["Leaf clusters"]
B22 --> F22["Leaf clusters"]
T3 --> B31["Branch"]
T3 --> B32["Branch"]
B31 --> F31["Leaf clusters"]
B32 --> F32["Leaf clusters"]
Loading

The leaves are simple Ball Parts. I vary their size, color, rotation, and position inside each cluster. They stay anchored and non-collidable, and I tag them GenTreeLeaf by default so a separate wind or effects system can find them.

How the options change the result

I kept the controls grouped by what they affect. If I want a taller silhouette, I can stay in the trunk settings. If I want a denser tree, I can work only on canopy values. Config.lua owns the defaults and rejects unknown or invalid options before generation begins.

graph LR
CONFIG["GenTree options"]
CONFIG --> TRUNK["Trunk shape"]
CONFIG --> CANOPY["Canopy shape"]
CONFIG --> LOOK["Appearance"]
CONFIG --> RUNTIME["Runtime behavior"]
TRUNK --> HEIGHTS["minHeight / maxHeight"]
TRUNK --> SEGMENTS["trunkSegments / trunkLean"]
TRUNK --> TAPER["trunkBaseRadius / trunkTopRadius"]
CANOPY --> TIERS["branchTiers / branchesPerTier"]
CANOPY --> LENGTH["branchLength / branchUpwardAngle"]
CANOPY --> DENSITY["leafClustersPerBranch / leavesPerCluster"]
LOOK --> COLORS["trunkColor / leafColor"]
LOOK --> MATERIALS["trunkMaterial / leafMaterial"]
RUNTIME --> RANDOMNESS["seed"]
RUNTIME --> YIELDING["yieldEveryParts"]
RUNTIME --> TAGGING["tagLeaves"]
Loading

How I spread a forest

For forests, I sample a disk instead of a square. The angle chooses a direction around the center, while sqrt(random) * radius chooses the distance. The square root matters: without it, too many trees would crowd the middle.

flowchart TD
START["GenerateForest(center, count, radius)"] --> RNG["Create deterministic forest RNG"]
RNG --> LOOP{"Generated count trees?"}
LOOP -->|No| ANGLE["Choose angle from 0 to 2π"]
ANGLE --> DISTANCE["distance = sqrt(random) × radius"]
DISTANCE --> POSITION["position = center + radial offset"]
POSITION --> CHILD_SEED["Derive a unique seed for this tree"]
CHILD_SEED --> GENERATE["Run the complete single-tree pipeline"]
GENERATE --> COLLECT["Append Model to result array"]
COLLECT --> LOOP
LOOP -->|Yes| RETURN["Return all completed tree Models"]
Loading
 tree
tree tree
tree center tree
tree tree
tree tree
circular radius

The forest RNG derives a different seed for every tree. With the same original seed and configuration, I get the same positions and shapes again. Without a seed, GenTree starts with fresh randomness.

What comes back

Every generated tree stays easy to inspect in Explorer:

GenTree (Model)
├── Trunk (Folder)
├── Branches (Folder)
└── Foliage (Folder)

The model includes GenTree, Seed, Height, and PartCount attributes. I use those as a small receipt of what the generator produced.

Configuration reference

Pass any subset of these options to GenTree.new. The full read-only defaults are also available as GenTree.DefaultConfig.

OptionDefaultWhat I use it for
seedrandomRebuild the same result when set
minHeight / maxHeight22 / 30Trunk height range
trunkSegments9Number of trunk cylinders
trunkBaseRadius / trunkTopRadius2.2 / 0.65Trunk taper
trunkLean1.4Horizontal trunk wandering
branchTiers4Vertical canopy layers
branchesPerTier5Branches in each layer
branchLength11Average branch length
branchLengthVariation3Seeded length variation
branchUpwardAngle24Average branch lift in degrees
leafClustersPerBranch3Foliage groups on each branch
leavesPerCluster4Leaf Parts in each group
leafSizeVector3.new(3.6, 3.2, 3.6)Base leaf size
leafSpread3.2Cluster radius
yieldEveryParts80Yield interval; 0 disables yielding

Colors, materials, collision, and leaf tagging are configurable too. Set tagLeaves = false if you do not want CollectionService tags.

Where each idea lives

FileWhat I put there
src/init.luaThe small public API and current version
src/Generator.luaSingle-tree and forest generation
src/Geometry.luaCylinder alignment and random unit vectors
src/Config.luaDefaults, override checks, and validation
src/Types.luaPublic Luau types
examples/GenerateTree.server.luaA tiny tree-and-forest example

Working on it locally

aftman install
wally install
stylua --check src examples
selene src examples
rojo build package.project.json --output GenTree.rbxm

Serve demo.project.json with Rojo if you want to open the example place. I keep versions in wally.toml and src/init.lua together; a matching vX.Y.Z tag builds the .rbxm, creates a GitHub release, and publishes to Wally when WALLY_AUTH_TOKEN is configured.

If you want to change something, CONTRIBUTING.md has the short workflow. CHANGELOG.md keeps the release story.

About

I put together a procedural tree generator system in Luau as a small skill demo for HiddenDevs.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages