The complete SKILL.md as loaded by agents — verbatim, rendered.
Procedural Vegetation
Use this skill for vegetation that has botanical structure and scale discipline: dense grass, authored meadows, deciduous trees, recursive branches, roots, canopies, leaf cards, trellises, deterministic species variation, and rooted wind. Start every implementation with $threejs-choose-skills when the scene also needs atmosphere, terrain, water, shadows, post, or camera doctrine.
The taught renderer path is pinned Three.js r185 WebGPURenderer from three/webgpu, TSL from three/tsl, NodeMaterial subclasses, node render pipelines, and compute/storage only when selected by workload. Legacy WebGL implementation (deprecated, do not extend): examples/stylized-meadow-grass/grass-system.js, examples/gpu-computed-grass/gpu-grass-system.js.
Canonical WebGPU dense-grass example: examples/webgpu-dense-grass/.
Select Architecture From Population And Projected Error
Choose the representation by visual error and population before choosing
compute:
| Workload |
Default |
Reason |
| One/few close trees |
CPU growth compile into indexed typed arrays; optional generated LODs |
Topology changes rarely; compute adds runtime state without saving frame work |
| Repeated trees/shrubs |
Species mesh/cluster variants instanced in spatial pages, with geometry LOD then impostors |
Reuses topology and preserves chunk culling |
| Dense grass/ground cover |
Shared strip/clump geometry plus the cheapest proven placement representation: implicit seed reconstruction, clump records, compact CPU attributes, or storage |
Avoids object traversal without assuming per-blade hot storage is free |
| Interactive growth or massive regenerated placement |
Compute only the changing topology/placement/state, then compact visible records |
Compute earns its dispatch through changed data volume |
Do not confuse GPU generation with GPU efficiency. Static vegetation generated
once on the CPU can be the lowest-runtime-cost result.
For dense grass and meadow vegetation, use this architecture before considering any API detail:
- Partition the field into deterministic chunks with fixed tile seeds, conservative per-patch bounds, and a visible debug overlay for bounds, density, and impostor state.
- A/B placement storage: reconstruct invariants from
{chunkSeed,instanceIndex,shared density/terrain field}, store clump-level
records, upload compact worker-generated attributes, or compute-fill
per-blade storage. Record the actual r185-aligned stride/resident bytes;
vec3 may pad to vec4 and narrow integers may widen.
- Consume only irreducible per-instance data from a
MeshStandardNodeMaterial/MeshPhysicalNodeMaterial; derive the rest from
shared fields and stable integer hashes.
- Keep wind, touch response, camera-facing yaw, seasonal wetness, and trampling as dynamic fields. Update only those fields per frame, or evaluate them in the vertex node when it is cheaper than a dispatch.
- Cull and LOD by patch before draw submission: frustum culling from expanded bounds, distance density tiers, near geometric blades, mid reduced instance density, far clump/impostor cards, and optional compute compaction for high-overlap fields.
- Use node post only after the geometry budget is under control:
RenderPipeline, pass(), conditional mrt() only for consumed signals, GTAONode for contact, full-scene BloomNode when HDR vegetation emission/lighting requires glare, and TRAANode when alpha shimmer needs temporal stabilization. Selective emissive MRT remains a separately proven exception.
The replaced sub-best path is full-field per-frame parameter regeneration. Static blade and clump data are immutable after spawn; only dynamic fields should be refreshed.
Terrain And Coastal Ecology Contract
Vegetation placement consumes the shared terrain/coast field provider; it does
not synthesize private height, shoreline, slope, moisture, or salt noise. The
provider declares coordinate frame, units, generation revision, filtering,
chunk halo, and invalidation for:
terrain height and geometric normal/slope
signed coast distance and coast tangent/normal
substrate/semantic region, cliff/beach/water and authored exclusions
drainage, cavity, soil moisture, disturbance, and canopy/light exposure
prevailing wind, directional terrain shelter, and salt/spray exposure
run-up/inundation envelope and persistent wetness when supplied
Keep hard eligibility separate from ecological preference. Water, active
run-up, unstable cliff, built footprint, path, and authored clearance volumes
are hard exclusions when the species contract says so. Moisture, slope,
salinity, wind exposure, sun, substrate, and elevation feed smooth species
response curves. A stable hashed priority or blue-noise/variable-radius Poisson
process turns suitability into candidates. Independent color noise and density
noise are forbidden.
Windward/leeward distribution is directional. Derive static shelter from the
terrain/obstacle field along the prevailing-wind direction, then combine it
with coast exposure and elevation. Distance from water alone cannot distinguish
an exposed windward headland from a protected leeward hollow. Recompute this
coarse field only when terrain, obstacles, or climate policy changes; the
per-frame wind deformation field is a separate dynamic consumer.
Each plant asset carries a root/ground frame, conservative crown and wind-swept
bounds, clearance radius or footprint, material slots, shadow proxy,
species-response table, growth/age variant, LOD/impostor representations, and
stable semantic ID. Flowers, grass clumps, palms, shrubs, and trees are selected
by species identity first and varied within that identity second. Placement
around ruins, paths, rocks, docks, or instruments consumes their exclusion
volumes and optional colonization sockets; it never infers clearance from the
render mesh at runtime.
Read
references/coastal-ecology-and-placement.md
for suitability equations, windward/leeward and salt fields, order-independent
placement, chunk seams, succession/asset manifests, LOD population invariants,
and coastal diagnostics.
Shared Environmental And Interaction Boundary
When vegetation participates in coupled weather, contacts, loading, or water,
first read the router's
physics-domain and interaction contract.
Consume its PhysicsContext, EnvironmentForcingSnapshot, receiver-state
signal sample, and InteractionRecord; do not author private clocks,
wind/current fields, or contact schemas.
Environmental wind is an air-velocity provider in meters per second with a
declared frame, altitude/support domain, exact sampleInstant: PhysicsInstant,
cadence, interpolation policy, requested/actual oriented crown/patch footprint
and spatial/temporal filter or band, and per-channel error. Vegetation owns only
the structural response: modal trunk and
branch state, leaf flutter, damping, root constraints, bounds, and LOD
projection. Water current and water-surface velocity are different providers.
Static prevailing-wind exposure used by ecology is a climatological reduction
sampled at an exact PhysicsInstant with its own revision; it is not the
instantaneous deformation sample.
Consume trampling, sweep, support-load, and impact events as typed
InteractionRecords. Use stable source/receiver IDs, an
applicationInterval: PhysicsTimeInterval, SI-physics-frame footprint/impulse
or load units, deterministic ordering keys, canonical source/target ownership,
SurfaceExchange.mode, reaction-group identity, and batch/partition identity.
Keep sequence ranges, consumer cursors, overflow, and lost/deferred commodities
in the canonical immutable InteractionBatchLedger; coalescing is a producer
transformation with provenance. Bin sparse records into affected
patches, update the contact/touch state after physics contact resolution, then
evaluate structural wind/contact deformation and commit domain state over the
declared PhysicsGraphStage.executionInterval: PhysicsTimeInterval.
Contribute each stable deformation binding as a PresentedStatePair to one
view-independent PhysicsPresentationCandidate at
requestedPresentationInstant: PhysicsInstant; the candidate owns the
PresentationResourceLease records and consumer-scoped event sequence ranges.
Each pair's previousPresented.provenance and
currentPresented.provenance are independent
PresentationSampleProvenance records;
previousPresented.presentedInstant: PhysicsInstant and
currentPresented.presentedInstant: PhysicsInstant remain distinct, as do
their state handles and global spatial bindings. A shared pair-level provenance
shorthand is forbidden.
The candidate contains no camera or render transform. The camera owner publishes
one CameraViewPublication per target/view with
previousRenderSampleInstant: PhysicsInstant and
currentRenderSampleInstant: PhysicsInstant plus
globalToRenderPrevious/globalToRenderCurrent, view/projection matrices,
jitter, viewport, and depth state; visibility, acceleration, shadow, cache,
reactive, and reset owners then publish
ViewPreparationPublication with visibilityPublicationRefs,
accelerationPublicationRefs, shadowViewPublicationRefs,
cachePublicationRefs, reactiveEpochs, reactivePublications,
resetDependencies, full resourceLeases for newly created camera-dependent
generations, and resourceLeaseRefs. The sealed
PhysicsPresentationSnapshot names the candidate, camera/preparation
publications through candidateId, cameraPublicationId, and
viewPreparationId, plus presentationTargetId, viewId, and sealVersion;
its state/resource payload is only presentedStatePairRefs and
resourceLeaseRefs, accompanied by scoped eventSequenceRanges, never copied
pairs or transforms. Never mutate immutable placement or publication records. One-way
visual deformation uses a one-way exchange and emits no reaction record;
two-way coupling leaves the matched reaction to the authoritative physics
solver.
Wetness, run-up, snow load, and melt are consumed from the route-selected
receiver-state owner at an exact PhysicsInstant after it integrates all
exchanges. Vegetation and its materials do not create independent accumulation
fields. On budgeted/mobile tiers retain analytic vertex wind, compact sparse
interaction records, bounded dirty patches, lower-rate receiver fields, and
explicit stream-loss/per-channel-error accounting.
A tier change that alters physics-facing state, cadence, represented support or
band/filter, error bounds, inventories, stable IDs/RNG streams, or event and
exact-once application-ledger cursors requires the shared QualityTransition:
request it, conservatively map and atomically commit it at a safe graph-step
boundary, then retire the old state through its completion join. A change may
remain vegetation-local only when it is render-only and leaves every provider,
PhysicsGraph ownership, interaction, committed state, ID, cursor, and physical
error contract unchanged.
Capability Gate And Tiers
Initialize the renderer before allocating storage resources:
import { WebGPURenderer } from "three/webgpu";
const renderer = new WebGPURenderer({ antialias: false });
await renderer.init();
if (renderer.backend.isWebGPUBackend !== true) {
throw new Error('WebGPU backend unavailable for the canonical vegetation path.');
}
Quality tiers are product tiers, not alternate shader stacks:
| Tier |
Capability |
Vegetation path |
Acceptance axis |
| Full |
Native WebGPU |
selected CPU/implicit/storage placement, dynamic wind/contact fields, spatial culling, density LOD, impostors |
close silhouette, interaction, and motion-error gates all pass |
| Balanced |
Native WebGPU with lower bandwidth |
implicit/compact static placement, vertex-node wind, reduced post and density |
projected blade/canopy and motion error pass |
| Budgeted |
Native WebGPU |
packed/coarser storage, fewer dynamic fields, larger pages and earlier impostors |
whole-frame, memory, alpha-overdraw, and transition gates pass |
| Minimum |
Native WebGPU minimal graph |
authored tree meshes, sparse grass cards, impostor rings, static wind phase |
primary silhouette/species identity remains above the declared error floor |
If the user explicitly asks how to apply fallback when WebGPU is unavailable,
route that teaching to ../threejs-compatibility-fallbacks/ instead of adding
a second path here.
Dense Grass Build Order
- Choose patch/page size from the measured culling-versus-submission
crossover, not texture dimensions or a copied meter/blade count. Budget
submitted work as
visibleDraws = drawObjectsPerPage * visiblePages, then
include rejected-but-submitted vertices and alpha fragments.
- Select implicit/clump/compact-CPU/per-blade-storage placement by paired A/B.
If storage wins, derive the real post-init stride, allocator bytes, duplicate
LOD records, and hot vertex reads. Packing is explicit and capability-tested;
it is not implied by authoring a narrow JS typed array.
- Deterministic placement uses one specified u32 mixer with wraparound/bit
semantics and CPU test vectors shared by CPU/TSL. Float
sin hashes are only
same-adapter visual variation. Compute initialization is optional and never a
GPU-completion claim from computeAsync().
- Use the same terrain field, density mask, path mask, and clump field for placement, material variation, and LOD thresholds. Do not let visual color clumps drift from density clumps.
- Render blades with a shared low-vertex strip or clump mesh. Fold the blade in the vertex node with Bezier or circular-arc math from root to tip; keep the root anchored in both color and shadow passes.
- Update dynamic fields on a fixed budget: wind vector texture or storage buffer, gust-front phase, contact/interactor history, wetness, snow weight, and seasonal color scalar.
- Submit only visible chunks. Each chunk owns expanded bounds for maximum blade height, terrain displacement, and wind bend. Never make one monolithic uncullable grass object the production default.
- Transition by patch under the
shared physical-pixel error contract.
Trees use world-up-constrained multi-azimuth/octahedral impostors, adding
depth/normal for relighting/parallax when required; one free-facing sprite
is insufficient for structured crowns. Use dither/hysteresis/dwell and a
matched shadow proxy.
Use queued renderer.compute() only when compute placement/state wins the A/B.
In r185 computeAsync() merely awaits renderer initialization before enqueue;
it is not a GPU-completion fence.
The dense-grass fixture allocates one matrix-free instanced blade mesh and one
impostor-card mesh per patch, but mutual exclusion submits at most one
representation per visible patch. Its Derived worst vegetation submission
is therefore visiblePatchesWorst; allocation count is still
2 * patchCount. Those fixture counts are structural evidence for that
example, not a recommended page size, blade density, or submission ceiling.
Per-patch draws are a culling baseline, not an invariant. If worst-case visible
patch draws exceed the target's measured CPU submission ceiling, aggregate
adjacent patches into larger spatial pages or use compute compaction with an
indirect count. A vertex-stage visibility mask that still processes every
blade is not culling. Select page size from the measured trade between
overdraw/over-submission and culling granularity.
Creature Composition
Vegetation and creatures consume the same EnvironmentForcingSnapshot air
velocity but own different structural response models. Creature stance/contact
resolution publishes typed InteractionRecords; vegetation bins completed
records into its dynamic touch texture or compact patch storage before blade
deformation. The current WebGPU dense-grass example exposes only wind uniforms
and static density storage, so it is not evidence that contact ingestion is
implemented. A production route must instantiate the shared record adapter,
dynamic touch resource, scheduler edge, overflow diagnostic, and immutable
static-placement invariant.
r185 Add-On Import Paths
Use these exact add-on paths when vegetation needs built-in post or shadow helpers:
| Helper |
Import path |
GTAONode / ao |
three/addons/tsl/display/GTAONode.js |
BloomNode / bloom |
three/addons/tsl/display/BloomNode.js |
TRAANode / traa |
three/addons/tsl/display/TRAANode.js |
CSMShadowNode |
three/addons/csm/CSMShadowNode.js |
TileShadowNode |
three/addons/tsl/shadows/TileShadowNode.js |
Trees And Plant Growth
Represent a plant as a growth hierarchy plus rendering adaptations. Do not model it as randomly scattered cylinders.
- Define a per-level species table: length, radius, taper, child count, emergence range, angle, twist, gnarliness, sections, radial segments.
- Grow branches iteratively from a queue so recursion depth and budgets remain inspectable.
- Emit each branch as oriented rings with an intentional UV seam, or route generic ring writing to
$threejs-procedural-geometry.
- Update section orientation from inherited direction, stochastic curvature, tropism or external force, and optional attraction constraints.
- Spawn children with stratified longitudinal slots and independently permuted angular slots.
- Generate leaves only after branch topology is stable.
- Build foliage normals from card orientation plus local crown volume; for high-density leaf fields, store per-leaf root, card basis, bend phase, and alpha cutoff in storage attributes.
- Choose wind scope explicitly. Leaf-root deformation, branch hierarchy deformation, trunk sway, and whole-tree motion are separate systems and must share the same visible and shadow deformation.
- Production branches use rotation-minimizing quaternion frames plus explicit
twist, arc-length/feature-scale curvature limits, and a species-calibrated
pipe/allometry law
r_parent^p >= r_cont^p + sum(r_child^p). Hero junctions
cut at the parent surface and stitch a collar/zipper patch or use load-time
implicit extraction; mid/far overlap removes hidden caps and gates the seam.
- Production wind is a reduced-order hierarchy of damped trunk/branch modes
driven by the shared field, with higher-band leaf flutter. Collapse modes by
LOD and use identical display/shadow deformation.
Read references/structured-ash-growth-system.md before tuning Ash-like deciduous trees. Preserve its preset, continuation, child-placement, leaf, material, wind, composition, diagnostic, and numeric contracts.
Materials, Alpha, And Output
- Use
MeshStandardNodeMaterial for grass and bark and
MeshPhysicalNodeMaterial when leaf translucency/wax response matters.
SpriteNodeMaterial is acceptable only inside a validated multi-view far
impostor representation, not as a single rotating tree card.
- Use
alphaTest or alphaHash for leaves and grass. Avoid sorted transparency for dense vegetation unless the asset truly needs partial glass-like blending. Use forceSinglePass for double-sided flat vegetation when it removes redundant back-side work without visible loss.
- Deform shadow casters with the same node math as visible geometry. If the wind root is at
uv.y = 0, both color and shadow geometry must keep that root fixed.
- LDR bark, leaf, flower, and albedo atlases encoded as sRGB use
SRGBColorSpace; HDR/EXR radiance remains loader-declared linear. Data maps
such as normal, roughness, density, alpha, path, clump, wind, LUT, and weather
masks use NoColorSpace/linear data. Disable mip generation only when compute
writes all mip levels or the map is sampled without minification.
- Keep HDR buffers as
HalfFloatType until tone mapping. The app has one tone-map owner and one output conversion owner through RenderPipeline.outputColorTransform or an explicit renderOutput() node.
- Start from the cheapest ordinary directional shadow that meets coverage and
texel-error gates. Route CSM, tiled arrays, or custom cached clipmaps through
$threejs-scalable-real-time-shadows; TileShadowNode is not a generic
large-scene or tile-GPU optimization.
Performance Contract
State the workload before implementation: visible spatial pages/patches,
instances per representation, blade/card pixels, mean and p95 alpha layers,
shadowed instances, dynamic-field texels, dirty interactions, draw objects,
storage stride, and post attachments. Instance count without projected alpha
coverage is not a vegetation cost model.
For terrain/coastal placement also record accepted/rejected candidates by
cause, species suitability distributions, overlap/clearance violations, chunk
halo work, coast/slope/moisture/salt field samples, static shelter-bake cost,
LOD population change, and resident bytes for species/asset manifests. These
are compile/update costs unless the placement actually changes every frame.
Storage budget targets:
- Static grass attributes: derive
instanceCount * alignedStrideBytes from the
actual packed layout and include duplicate LOD/impostor records.
- Dynamic fields: sum exact wind/contact texture or storage extents, formats,
history slots, dirty update traffic, and cadence per active region.
- Tree geometry: compile once into typed buffers; do not allocate vector objects in runtime wind or per-frame LOD loops.
- Node post: one scene pass with
mrt() when normals/depth/emissive are reused, reduced-resolution AO/bloom via setResolutionScale(), and no duplicate scene re-render for diagnostics unless explicitly requested.
Allocate vegetation cost from the whole frame and accept with contemporaneous
full-frame and paired-marginal p50/p95 GPU time, CPU submission, visible/culled
instances, alpha fragments, hot traffic, peak resident/transient bytes, and
thermal behavior. On low-power/tile targets first reduce invisible submission,
alpha coverage, shadow representation, and dynamic-field bandwidth while
preserving the primary silhouette/species contract.
Diagnostics And Failure Conditions
Capture diagnostics as first-class views: patch bounds, density LOD, impostor transitions, static versus dynamic storage fields, clump ids, terrain height/normal fit, signed coast distance, hard exclusions, species suitability, wind exposure/shelter, salt exposure, moisture, accepted candidate priority, clearance conflicts, wind displacement magnitude, alpha coverage, shadow parity, leaf origins, branch-level colors, bark UV checker, and final composition.
Visual failures:
- branches form visible helices;
- dense grass ignores terrain height, path masks, or clump-level variation;
- static blade parameters regenerate every frame;
- chunks outside the frustum still dominate submitted draw work;
- every child emerges at the same relative height;
- bark texture scale changes unintentionally with branch radius;
- leaves reveal flat card normals under rotation;
- leaf or grass wind moves roots instead of keeping them anchored;
- visible vegetation and shadow vegetation deform differently;
- different seeds change species identity rather than controlled variation;
- geometry cost grows without per-level, per-chunk, and named-target evidence.
- coast-distance rings replace a real ecology response, windward and leeward
sides receive identical populations despite a directional exposure contract,
or vegetation occupies active run-up/unstable cliff/building exclusions;
- chunk generation order changes accepted plants, neighboring chunks disagree
at seams, or density LOD causes the apparent biome boundary to migrate;
Routing Boundary
Use $threejs-procedural-geometry for generic branch-ring emission without a growth model. Use $threejs-procedural-fields for shared terrain, coast, exposure, density, weather, and biome fields. Use $threejs-procedural-materials for authored bark/leaf/grass PBR identities. Use $threejs-water-optics for run-up/inundation/shore signals when water is their causal owner; vegetation consumes those signals but does not solve water. Use $threejs-scalable-real-time-shadows for custom shadow clipmaps beyond CSMShadowNode or TileShadowNode. Use $threejs-image-pipeline when the vegetation scene owns the final HDR, AO, bloom, temporal AA, and output-transform stack.
This skill owns species tables, topology, child placement, foliage, grass fields,
roots, chunked dense vegetation, static/dynamic storage separation, and
hierarchical/rooted structural response. The routed environment owner supplies
air velocity; the authoritative contact solver supplies interaction records.