# EZ-Tree: seed a tree, call generate, and the geometry is deterministic

> EZ-Tree is a Three.js procedural tree generator published as an ES module, with dozens of tunable options, a THREE.LOD path for tree-heavy scenes and a raw geometry escape hatch for anyone with their own instancing system. The friction to plan for is that every parameter change requires an explicit generate() call, and the Docker port in the repository does not match the port the image serves.

**dgreenheck/ez-tree** — Procedural tree generator written with JavaScript and Three.js

- Repository: https://github.com/dgreenheck/ez-tree
- Website: https://www.eztree.dev
- Stars: 1,749 · Forks: 228
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/dgreenheck-ez-tree

## Install is one line, usage is four

EZ-Tree is published to npm as @dgreenheck/ez-tree and installs like any other package.

```js
npm i @dgreenheck/ez-tree
```

The usage example is short enough to hold in your head. You construct a Tree, set options on it, call generate, and add the result to your Three.js scene, because the tree object is itself the thing you add.

```js
// Create new instance
const tree = new Tree();

// Set parameters
tree.options.seed = 12345;
tree.options.trunk.length = 20;
tree.options.branch.levels = 3;

// Generate tree and add to your Three.js scene
tree.generate();
scene.add(tree);
```

That shape matters. The options object is live and mutable rather than a constructor argument, so the pattern is: mutate, regenerate, read the geometry. There is no reactive wiring, and there is one rule you have to hold onto, stated in the README as a requirement rather than a suggestion: any time the tree parameters are changed, you must call generate() to regenerate the geometry. Set trunk.length and do nothing and you have an unchanged mesh.

There is also a presets shortcut, used in the LOD example as tree.loadPreset('Ash Medium'), which is the fastest way to get something non-default without reading the parameter list.

On packaging, the package is ESM-first. The exports map offers an import path to an ES build and a require path to a UMD build, with TypeScript declarations shipped alongside, and the published files list includes both build and src, so you can read the implementation from the package itself. Three is a peer dependency rather than a direct one, which is the correct arrangement: your application's Three.js instance is the one the tree attaches to.

## TreeOptions is grouped, and most groups are per level

The parameter surface is organised into three groups plus a couple of general ones, and understanding the grouping tells you where the fine control is.

General properties are seed, which fixes the random stream so the same seed produces the same tree, and type, which selects from the TreeType enumeration with TreeType.Deciduous given as the example.

The bark group is about appearance rather than structure. type selects a BarkType such as BarkType.Oak, tint is a hexadecimal colour with 0xffffff for white, flatShading toggles flat versus smooth shading, textured toggles whether a texture is applied at all, and textureScale is an object with x and y factors so bark grain can be stretched independently on each axis.

The branch group is the real control surface, and most of its members are arrays indexed by level. levels is the recursion depth, where zero produces only a trunk. angle is the child branch angle in degrees relative to the parent, specified separately per level. children is the number of child branches at each level, with the index representing the level. gnarliness controls how twisted each level is, also per level. length and radius are per-level objects. sections is the number of segments along a branch's length, which sets mesh resolution along the branch. segments is the radial segment count, where higher means a smoother cylinder. start is the fraction along the parent where children begin forming. taper is a value between zero and one defining radius reduction from base to tip, and twist is the twisting applied per level.

force is the odd one out: it is an external directional influence defined by a direction vector of { x, y, z } and a numeric strength, which is how you make a whole stand of trees lean the same way.

The leaves group mirrors that shape. type selects a LeafType, billboard chooses Single or perpendicular Double-sided rendering from the Billboard enumeration, angle is the leaf angle relative to the parent branch in degrees, count is how many to generate, start is where along the branch they begin, size is the leaf size, and sizeVariance covers the randomness in it.

## LOD levels share one skeleton, which is why the silhouette holds

For a scene with many trees, generateLODs() builds the tree at several detail levels inside a THREE.LOD object placed within the tree group, and the renderer switches automatically based on camera distance.

The interesting design choice is that all levels are meshed from the same skeleton. The README's point is that the tree's silhouette stays consistent across switches, because distant levels use fewer ring segments and fewer but larger leaves rather than a different tree. That is a different approach from regenerating a lower-detail tree, and it is what prevents the popping you get when a low-poly stand-in has a different shape.

The default levels, exposed as Tree.defaultLODLevels, switch at 100 and 250 units and reduce to roughly 40% and 20% of the full triangle count. You can pass your own array instead:

```js
tree.generateLODs([
  { distance: 0, detail: {} }, // full detail
  {
    distance: 80,
    hysteresis: 0.05,
    detail: {
      sectionStride: 3,    // sample every 3rd ring along each branch
      segmentFactor: 0.75, // reduce radial segments to 75% (min 3)
      leafStride: 2,       // keep every 2nd leaf...
      leafScale: 1.4,      // ...enlarged to preserve canopy coverage
      billboard: 'single', // drop the second crossed leaf quad
    },
  },
]);
```

Read that block as a budget dial rather than a mystery. sectionStride samples every third ring along a branch, segmentFactor trims radial segments to 75% with a floor of 3, and the leaf pair of settings halves the leaf count while enlarging each leaf by 1.4 to hold canopy coverage, with billboard set to single dropping the second crossed quad entirely. The hysteresis value of 0.05 is there so a camera hovering at the boundary does not oscillate between levels.

Two consequences are worth planning for. All levels share one bark material and one leaf material, so a single tree.update(time) call animates wind across every level, which is the efficient arrangement. And calling generate() afterwards tears the LOD down and restores the single full-detail mesh pair, so an LOD tree and a generate tree are not interchangeable states.

## GLB export carries every level, and there is a raw geometry exit

Two escape hatches exist, and they exist for different reasons.

The first is that exporting a tree generated with generateLODs() to GLB includes every level. The README states this as a caveat rather than an option. So if your pipeline is build the LOD tree, then export it for a viewer that has no runtime LOD logic, the exported file carries the redundant geometry with it. If file size matters, export after generate() instead.

The second is tree.createGeometry(detail), which returns raw BufferGeometry pairs as { branches, leaves } at any detail level without touching the tree's own meshes. This is for the case where you already have an LOD system or an instancing system and want EZ-Tree to be a geometry source rather than a scene participant.

That distinction is the whole architecture in one line. The normal path hands you meshes, materials and a group that manages wind animation. The createGeometry path hands you geometry and nothing else, which means material assignment, instancing and animation are entirely your problem from that point.

For getting assets out rather than code, the standalone web app at eztree.dev is the route, and the README describes it as creating trees in the browser and exporting as .PNG or .GLB files. Those two formats are what the app offers. There is no documented export to FBX, OBJ or a game-engine specific format, which matters if your target is a pipeline expecting meshes rather than images.

## The Docker port and the Dockerfile port disagree

The repository offers two local routes for the standalone app. The plain one is npm install followed by npm run app, and the README explains why there are two commands: you must build the library before running the app, so the app script itself chains a library build into the dev server.

```bash
npm install
npm run app
```

The Docker route is meant to avoid local setup, but the two files do not agree with each other. The compose file builds from the repository Dockerfile, publishes a port, mounts src and public as volumes, and runs the app script with a host flag:

```yaml
services:
  tree-gen-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '5173:5173'
    volumes:
      - ./src:/usr/src/app/src
      - ./public:/usr/src/app/public
    environment:
      - NODE_ENV=development
    command: npm run app -- --host 0.0.0.0
```

The Dockerfile it points at is a production static build. It uses a node:24 base, installs dependencies, runs npm run build:app, installs http-server globally, exposes port 8080, and starts http-server on the dist directory.

So the image is built to serve static content on 8080, while the compose service publishes 5173 and overrides the command to run the Vite dev server instead. Both halves work, but they are two different applications: the Dockerfile CMD gives you a built static site on 8080, and the compose override gives you a live dev server on 5173 with your source mounted. Expect to try one and be confused by the other at least once.

One more thing to know before you start: the library is a peer dependency on three at >=0.167, and the development version is three at about 0.167 with matching types. If your application is on a newer Three.js major, the declared peer range will warn you.

## Conclusion

Adopt EZ-Tree if you need deterministic, parameter-driven trees inside an existing Three.js scene, where the seeded generator and the createGeometry escape hatch are worth more to you than hand-authored assets. Do not adopt it as a general-purpose asset pipeline or as a replacement for imported tree models, since every parameter change forces a regeneration and the export formats are limited to PNG and GLB from the browser app. Verify first which port your setup actually serves on, because the repository's docker-compose.yml publishes 5173 while the Dockerfile exposes 8080 and starts http-server on dist, so a containerised run will not answer where you expect until you reconcile the two.

## FAQ

### How do I install and use EZ-Tree?

Install it with npm i @dgreenheck/ez-tree, then create a new Tree, set values on tree.options such as seed, trunk.length and branch.levels, call tree.generate(), and add the tree to your Three.js scene with scene.add(tree).

### Do I need to call generate() every time I change a tree parameter in EZ-Tree?

Yes. The README states that any time the tree parameters are changed you must call generate() to regenerate the geometry, because the options object is mutable and does not rebuild the mesh on its own.

### How do I reduce the triangle count of EZ-Tree trees in a large scene?

Call generateLODs() instead of generate(), optionally passing your own level array with sectionStride, segmentFactor, leafStride, leafScale and billboard. The defaults switch at 100 and 250 units, and every level is meshed from the same skeleton so the silhouette stays consistent.

## Sources

- [dgreenheck/ez-tree on GitHub](https://github.com/dgreenheck/ez-tree)
- [License: MIT](https://github.com/dgreenheck/ez-tree/blob/main/LICENSE)
- [Project website](https://www.eztree.dev)
- [README](https://github.com/dgreenheck/ez-tree/blob/main/README.md)
- [Releases](https://github.com/dgreenheck/ez-tree/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dgreenheck-ez-tree
