Portable vegetation
The objects/vegetation folder is independently copyable. Its ES modules import only other files in that folder. All Three.js and engine services arrive through constructor injection. Geometry and tree textures are generated locally; one optional project-generated meadow albedo is shipped inside the package. There are no remote runtime assets or runtime AI calls. The current objects are a foundation for graphics approaching photorealism; asset input targets describe further improvements to source and integrate without requiring user photographs.
GROUND_TEXTURES.grass exports the local meadow albedo URL using import.meta.url, so it remains valid when this folder is copied. The application may pass it to the engine's ground material and await material.userData.ready. The engine does not import vegetation. The asset's provenance records its checksum, visually tested blend/scale settings, and measured close-view cost.
import * as api from './engine/index.js';
import {Tree, Forest, GrassPatch} from './objects/vegetation/index.js';
const wind = new api.WindField({strength: .8});
const forest = new Forest(api, {
landscape, wind, count: 2200, seed: 731,
avoid: (x, z) => applicationStructureFootprintsContain(x, z)
});
const grass = new GrassPatch(api, {landscape, wind});
scene.add(forest.root, grass.root);
// The application advances the shared field once each frame.
wind.update(time);
forest.update(dt, time, {cameraPosition: camera.position, quality: 'high'});
grass.update(dt, time, {cameraPosition: camera.position, quality: 'high'});
// On application teardown:
forest.dispose();
grass.dispose();
The injected API requires OBJECT_PROTOCOL === 1, ObjectState, THREE, disposeTree, addWindData, and the engine's signed applyInstanceFade contract. Negative fade -a retains noise values above 1-a; positive a retains values below a. This complementary coverage prevents holes in shared trunks during detail transitions. Wind is optional; supplied fields implement apply(mesh). The field remains caller-owned, and object disposal does not dispose it.
All three classes extend the local PortableObject. They expose root, properties, transform accessors, setPosition(x,y,z), getBounds(), serialize(), restore(data), update(dt,time,context), and idempotent dispose(). Dimensions use metres; times use seconds. Terrain queries and forest placements use the object's local coordinate frame. Normally terrain and vegetation roots share the identity transform. Camera positions supplied to updates are in world coordinates and are converted into the object's frame.
Tree
new Tree(api, {species='oak', height=15, width=1, seed=731, wind=null, foliage=1, name='Tree'})
species:oak,birch, orconifer; unknown values throw.height: positive vertical model envelope in metres. The local geometry is normalized before scaling.width: positive horizontal multiplier, allowing narrow or spreading crowns.seed: deterministic branching, leaf orientation, color, and variant selection.foliage: coverage from 0 to 1; exposed as a read-only accessor and changed withsetFoliage(amount).setSpecies(species),setDimensions({height,width}), andsetWind(wind)rebuild and dispose owned resources. They are occasional editing operations, not per-frame controls.getStats()includes species counts, submitted near/mid/far instances, discrete leaf count, model count, height, and foliage coverage.
Oak has three major low forks, secondary branching, and overlapping irregular crown lobes containing 12,600 small six-sided, four-triangle leaves. Birch has slim pale stems, restrained dark bark markings, drooping branchlets, and 13,960 smaller leaves forming an airy connected crown. Conifer has an irregular spiral of branches and 5,632 overlapping sprays whose locally generated alpha texture contains individual needle shapes. The conifer uses frond geometry, not a separate mesh for every needle. Every family has three seeded forest prototypes, with additional per-instance height, width, heading, and tint variation. Midrange foliage uses small textured leaf sprays constrained to the actual crown envelope. Far geometry uses three crossed side views, a horizontal aerial crown view, and two narrow stem cards. Constant stem UVs retain an opaque bark sample when mipmaps would otherwise erase a thin trunk. Aerial cards fade out at shallow viewing elevations, preventing horizontal skyline bars; color and depth passes use the same coverage. All views are generated locally from the model.
Forest
new Forest(api, {landscape=null, terrain=landscape?.terrain, wind=null, count=2200, seed=731, avoid=null, world=null, name='Forest'})
terrain requires size, minX, minZ, and heightAt(x,z). Its optional sample(x,z) provides height and normal; this enables slope filtering. landscape optionally adds biomeAt, seaLevel, lake, riverX, riverLevel, riverWidth, beach, and airfield using the engine landscape's shapes. An optional treeLine (or snowLine) sets the upper woodland limit.
Placement samples a continuous seeded density field with warped broad patches and finer openings. It uses the same smoothly interpolated lattice-noise construction as the terrain, implemented locally to preserve package independence. There are no radial grove centres or shared-radius boundaries. At the 8 km world scale, the broad noise wavelength is approximately 727 m, with additional variation at approximately 269 m and 93 m; coordinate warping makes the patch boundaries irregular.
Suitable slopes and uplands receive greater relative density, while flat lowlands stay more open. The slope preference rises as the normal's Y component falls from .982 to .89; upland preference rises from 90 to 300 m above the supplied sea level. These preferences do not relax the hard slope cutoff of .86 or any excluded biome. Meadow and riverbank density factors are .40 and .45 relative to forest habitat. Conifer probability increases with elevation; oak and birch remain mixed in lower woodland. The existing tree prototypes, spacing limits, total requested count, and rendering/LOD budgets are unchanged.
Height and slope queries match the heightfield. Trees remain excluded from water surfaces, beach and runway clearance, steep slopes, and beach/rock/alpine/farmland biomes. Density fades to zero over the final 150 m below treeLine or snowLine; the default upper limit is 650 m when a biome provider is present. Terrain-only consumers have no implicit altitude limit. The caller's avoid(x,z) applies before placement and must cover structures, activity zones, or other application-specific clearances. Trees do not infer arbitrary scene meshes. Requested counts may be unattainable with restrictive terrain/exclusions; getStats().placementShortfall reports this explicitly.
placements: actual records containingx,y,z,height,radius,species,variant,rotation,width,tint,seed, plus renderer bookkeeping. Read these for integration; direct record mutation does not rebuild cached transforms.radiusis a conservative horizontal canopy radius derived from the prototype. Roots are embedded by 0.08 m to prevent tiny floating gaps.getPlacements(): clean copied records with no renderer bookkeeping.getTreeAt(x,z,maxDistance=Infinity): nearest tree record, ornull.count: number actually placed.serialize()includes placements, preserving explicit distribution;restore()can restore them.worldis retained as a caller reference. The package does not register collisions automatically. Applications may use placements to construct their preferred collision primitives.
Rendering uses one instanced batch per species, prototype, layer, and material. A 160 m spatial grid finds only nearby detail candidates; trees leaving the candidate region remain active until their outgoing fade settles. Near/middle slots update transforms and colors only when membership changes. Far transforms/colors stay fixed, with distance opacity evaluated in matching color/depth shaders. Settled stationary views skip the forest update work entirely. Distant trees do not submit nearby leaf geometry. Smooth local detail weights are damped over time and use complementary alpha-tested dithering. Crossfades may submit a tree in two layers; layer counts therefore need not sum to the placement count.
getStats() includes activeCandidates, updateWork, and farSubmitted. Its far count describes local LOD coverage before the GPU distance mask; farSubmitted is the fixed submitted instance count. distanceVisibility:'shader' makes that distinction explicit.
| Quality | Near-to-mid blend | Mid-to-far blend | Distance fade ends |
|---|---|---|---|
| high | 45–78 m | 300–440 m | 7400 m |
| medium | 30–58 m | 210–330 m | 6600 m |
| low | 17–34 m | 115–210 m | 5600 m |
Vertical camera separation contributes to distance. In addition, near foliage smoothly disappears between 35 and 70 m above each tree's base, preventing detailed nearby crowns beneath an elevated camera from dominating flight rendering. Geometry wind data drives trunk bending and foliage flutter; the same shared field patches custom depth materials. The application must update the shared wind separately.
GrassPatch
new GrassPatch(api, {landscape=null, terrain=landscape?.terrain, wind=null, avoid=null, seed=997, count=7000, radius=43, center=null, followCamera=true, name='Grass and ground cover'})
The patch uses instanced tufts of fourteen bent three-triangle blades. Tufts are terrain-aware and share the same exclusion logic as trees, with a slightly larger allowed slope. Coordinate hashing makes a tuft's location, scale, heading, tint, and thinning priority stable as the camera moves. Forest ground cover is sparser than meadow cover.
The patch regenerates only after the camera moves 12% of its radius. Grass fades to zero before the regenerated patch edge enters view. It fades away between 35 and 80 m above local ground; after the fade settles, it submits no blades and performs no regeneration while remaining at flight altitude. Returning near ground creates the needed patch. followCamera:false keeps a fixed patch at center. Quality changes and setDensity(amount) thin smoothly using stable per-tuft priorities.
getStats() reports visible tuft and blade counts, available placements, radius, altitude fade, generation counter, and detail (blades or dormant). Grass restoration currently restores common transforms; constructor generation parameters should be supplied when reconstructing a saved patch.
Verification and limits
Open tests/vegetation-object.html for the close, individual-species, near/middle transition, middle/far transition, and flight views, shared wind control, quality controls, and contract assertions. Add ?world=1 for the actual 8 km terrain with 24,000 trees, current ground material, rock inspection, and view-local profiling. window.vegetationFixture exposes the objects, renderer, assertions, and setView(name). The fixture verifies requested counts, species, terrain alignment, exclusions, and nearby foliage elements. The compatibility statistic discreteLeaves counts broadleaf leaf forms or conifer sprays, not individual texture needles.
The refinement was inspected in the user-confirmed Chrome instance in both isolated and actual-world views. Corrected issues include inward bark winding, open branch ends, sparse side-facing oak coverage, middle cards extending beyond the crown, and thin far stems disappearing in alpha mipmaps. The broadleaf crowns remain discrete leaves; an opaque canopy-core experiment was rejected after visible faceting. The aerial crown view reduces crossed-card star footprints without making the far representation volumetric. No shader/WebGL errors were reported in the inspected views.
| Species | Near triangles per prototype | Middle triangles | Far triangles |
|---|---|---|---|
| Oak | 53,685 | 1,389 | 12 |
| Birch | 57,530 | 1,499 | 12 |
| Conifer | 25,463 | 5,134 | 12 |
These counts were measured for a seeded prototype. The visual pass increases near triangles by at most about 1% compared with the preceding refined models; leaf counts and draw-batch counts remain unchanged. Checks pass for finite geometry, outward bark winding, all-discrete broadleaf crowns, opaque constant-UV far stems, local import closure, complementary detail weights, altitude suppression, disposal, and terrain/biome placement. A 24,000-tree, 130-frame comparison against the original full-scan LOD calculation produced zero weight difference after the spatial optimization. Full-scene profiling and shader/POI checks are recorded through the flight-integration fixture.
The visual4 evidence records three passing focused view checks, including all six locations and paused woodland detail. Its 24,000-tree flight profile retains nine forest draws, no dirty far attributes, and forest-update medians below 0.1 ms stationary and approximately 0.1 ms guided. Full-scene GPU timings are also recorded without attributing unrelated scene changes to vegetation.
The later placement-only refinement passes four focused checks in tests/vegetation-placement.test.mjs, without constructing tree geometry or a renderer. On the 8 km seed-731 terrain with 24,000 placements and forest seed 894, flat-lowland trees decrease from 8,187 to 5,180 while suitable upland-slope trees increase from 5,550 to 7,307. Equal-area habitat sampling gives slope density 3.72 times flat-lowland density. This measures distribution, not a new rendering performance claim; forest-placement-continuous.json records the definitions and limits. Existing saved placement arrays still restore their exact previous distribution.
The geometry is procedural, with nine reusable forest prototypes. It does not reproduce scanned botanical assets. Far crossed views can reveal their plane arrangement at steep viewing angles, and alpha dithering can be visible during transitions. Sparse forests over a very large map require an appropriately increased count or deliberately placed groves. Individual leaf flutter is approximate, and wind does not simulate branch mechanics. Far setFoliage(0) removes the whole silhouette, including its rasterized branches; close and middle branch geometry remains. The package does not guarantee photorealism, automatic structure detection, or registered collisions.