PhytoEngine · Docs
How to find assets, what they cost in triangles and bytes, and how to put them in a scene. If you just want to see the catalog, start at explore; if you need a plant that does not exist yet, that is the studio.
Quickstart
Three steps, and they are a real sequence — you cannot instance a plan you have not resolved. Hit the API for individual assets, or use the zero-dependency scene module to lay out a whole deterministic stand.
Three ways in. /api/v1/at takes a latitude and longitude, works out what
is there and what grows there, and hands back the trees the catalog has — for clients
placing vegetation on real-world geography that know where but not what.
/api/v1/query parses species, stage, climate, season and style out of plain
text. Or filter precisely with /api/v1/assets. Every result is a permanently
cacheable static .glb with tri_count,
file_size_bytes and real height_meters.
# you know where you are, not what grows there
GET /api/v1/at?lat=46.4900&lon=-80.9900&count=3
# -> place: Greater Sudbury, Ontario, Canada — boreal forest
# -> assets: sugar-maple, birch-scan, pine-leafy
# -> gaps: trees that grow there and we cannot serve yet
# easiest: let the query parser do the work
GET /api/v1/query?q=northern+ontario+old+growth+birch
# or take exact control (all filters optional, combinable)
GET /api/v1/assets?species=birch&stage=mature&climate=boreal&lod=1The scene module is a plain ES module (browser or Node, no dependencies). Same
seed + biome always produce the byte-identical forest — so
every client and every server agree without shipping placement data.
import { generateForest, assetPlan } from
'https://phytoengine.com/js/forest-recipe.js';
// deterministic layout: trees, understory, ground cover, a suggested camera
const recipe = generateForest({ seed: 7, biome: 'boreal-mix', size: 140 });
// resolve what the scene needs against the live catalog
// (limit defaults to 100 — ask for the whole catalog, or the plan reports false gaps)
// has_more: true means you got a page, not the catalog — the gaps would be lies
const { assets, total, has_more } = await (await fetch('https://phytoengine.com/api/v1/assets?limit=5000')).json();
const plan = assetPlan(recipe, assets);
console.log(plan.totals); // { drawCalls, instancedTris, downloadBytes, ... }
console.log(plan.gaps); // anything the catalog couldn't fill + what it substitutedThe plan groups placements into buckets, one InstancedMesh each. Load each
bucket's chosen-LOD file once, then stamp its instances. Apply scaleFix so a
substituted species still reads at its real height.
const loader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder);
const dummy = new THREE.Object3D();
for (const b of plan.buckets) {
const gltf = await loader.loadAsync(b.url); // b.lod already picked per ring
const src = gltf.scene.getObjectByProperty('isMesh', true);
const mesh = new THREE.InstancedMesh(src.geometry, src.material, b.instances.length);
b.instances.forEach((it, i) => {
dummy.position.set(it.x, it.y, it.z); // recipe coords are world coords — Y is up
dummy.rotation.set(it.lean ?? 0, it.rotation, (it.lean ?? 0) * 0.7); // yaw + slight lean
dummy.scale.setScalar(it.scale * b.scaleFix); // real-height correction
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
});
scene.add(mesh);
}That's a full, seeded, instanced stand.
See it running →
The reference client (preview.html) does exactly this, plus terrain, wind and grass.
Built for low-memory phones
You download a species, not a forest. A handful of small base meshes get GPU-instanced into thousands of plants; the layout comes from a seed, not a scene file; distant trees collapse to ~4-triangle billboards. It streams in bite-sized, cacheable pieces — so a living stand renders on a simple phone without ever holding gigabytes in memory.
representative — open on the live server for exact, computed numbers
One small base mesh becomes a whole stand via GPU instancing — thousands of plants, one draw call per bucket, and no per-tree geometry to ship or store.
The same seed and biome produce the byte-identical forest everywhere, so a scene costs you an integer over the wire instead of a placement table.
Every asset ships a full decimation chain down to a near-billboard, so the trees you cannot make out stop costing what the ones in front of you do.
What you get
Everything is a static file and a plain ES module. No SDK, no runtime, no account — and nothing that assumes which engine you are in.
meshopt-compressed GLB with WebP textures. three.js, Babylon, Godot, Unity, Bevy — if it reads glTF, it reads these.
Every asset carries a measured height_meters and pivots at base centre, so a
sapling next to an old pine is the right size without you tuning anything.
Per-vertex colour holds the tint; its alpha holds wind stiffness. Drive foliage motion in a vertex shader with no bones and no extra attributes.
Shrubs, cacti and succulents, grasses, ferns, flowers, vines, and invented flora. Growth habit is a real column, so a cactus is never handed a trunk and a crown.
Sapling through old, plus damaged, fallen, chopped, stump-cut, lightning-struck and rotting conditions — the states a world actually needs.
Stylized, leaf-card and scan-grade are separately authored assets, each with its own LOD chain. Pick how a plant was made, independently of how far away it is.
In the catalog today
Conifers with full sapling→old age ramps, storybook species, deadwood and understory. Browse and search the whole set in the explorer.