Skip to content

Foliage Pack Demo

App: apps/foliage_pack_demo/

Demonstrates the Grass / Foliage / Trees content pack (engine PLM-266): stylized, instanced grass, foliage and trees with wind sway and back-light translucency (leaves glow when the sun is behind them), built with zero engine source changes. It is the fourth content pack of the Environment & Stylization program (Toon → Sky → Weather → Foliage → Water). Like the Toon, Sky and Weather packs, it is pure content — the whole look is one instanced .slang shader riding capabilities the engine already ships.

Terminal window
% ./plume3d foliage_pack_demo

A wind-swaying grass field plus a few procedural trees (a lit trunk with a canopy of instanced leaf cards) render under the foliage shader; the sun behind the leaves makes them glow. The demo captures a screenshot (foliage_pack.png) at frame 70 and exits at frame 90.

The Blender-authored workflow (the production path)

Section titled “The Blender-authored workflow (the production path)”

Grass, foliage and trees are authored in Blender and imported. This pack ships the look — foliage.slang — that the Blender-imported vegetation renders with. The demo’s procedural field only exists to exercise that shader in isolation; a real game builds its vegetation like this:

  1. Author in Blender — model a grass blade / leaf card / tree (trunk + canopy) and scatter it across the terrain with geometry-nodes, particle hair, or collection instances.
  2. Bake Scatter — the plume3d_wren Blender add-on’s Bake Scatter operator (engine PLM-250) bakes the depsgraph scatter down to a baked instance table (plume3d_scatter_proto + plume3d_scatter_xforms, ADR 0087). Where things scatter is controlled by a mask (shipped, PLM-268) — a geometry-nodes density mask the bake captures automatically, or the add-on’s top-down grayscale Scatter mask image (white = spawn, black = clear, gray = sparse), resolved at bake time.
  3. Import — Scene.loadBlendScene(path) (or BlendResult.instantiateScatter(scene) directly) emits one GPU-instanced draw per prototype — the same enhanced-instancing path this demo fills by hand, but authored in Blender. See the Blend Scatter Demo and Foliage + Terrain Demo.
  4. Render with this pack — load shaders/foliage as the active shader before drawing, so the imported instanced vegetation gets wind + translucency.
Graphics.loadShader("shaders/foliage") // FIRST → the active instanced foliage shader
var blend = _scene.loadBlendScene("Models/Forest.blend") // terrain + baked grass/tree scatter
// ...the baked scatter renders as instanced foliage under the active shader

An instanced prototype must render with the active shader — a per-mesh newMesh(.., shader) on an instanced prototype currently breaks the instance SSBO (engine PLM-264), so the foliage look is a set-active-shader, not a per-object assignment, until that is fixed. The Weather pack shares the same instanced descriptor discipline.

Trees, specifically. A tree is authored in Blender as a prototype (trunk + leaf canopy) and scattered into a forest like any other prototype. The tree-specific authoring needs are roadmap, not shipped in v1:

NeedStatus
Multi-part prototype (trunk mesh + canopy)PLM-266 follow-up (authored-tree bake)
Colliders so you can’t walk through trunksPLM-267 shipped for authored objects — tag a tree/rock/prop plume3d_collider (box/sphere/capsule/convex/mesh) and instantiateUnits builds a static Jolt collider. Per-instance colliders for GPU-scattered foliage remain a follow-up (ADR 0090 non-goal — thousands of bodies).
Spawn masking — control where grass/trees appearPLM-268 shipped — a geometry-nodes density mask (captured by the bake) or the add-on’s top-down grayscale Scatter mask image (white = spawn / black = clear / gray = sparse), a bake-time Blender input the engine never sees.
Distance LOD / billboard imposters for far treesPLM-266 follow-up (needs the PLM-024 LOD system)

What’s in the pack (all content — no engine change)

Section titled “What’s in the pack (all content — no engine change)”
FileKindWhat it is
shaders/src/foliage.slanginstanced shaderWind sway (the A#1 frame time + global wind + a per-instance phase so blades don’t move in lockstep) + a height gradient (darker at the base, brighter at the tip) + two-sided leaf lighting (thin leaves lit from both faces) + back-light translucency (leaves glow warm-green when the sun is behind them). Hand-declares its minimal descriptor set (set-0 view/proj + frame block, set-2 instance SSBO) — it must not #include "plume3d.slang".
main.wrenexampleA grass field plus a few procedural trees (a lit trunk with a canopy of instanced leaf cards), all foliage in one InstancedMesh, swaying in the wind.
shaders/src/{scene,sky}.slangreusedThe stock lit ground/trunk shader and a gradient sky.

1 — Load the foliage shader FIRST so it is the active shader for the instanced foliage, then the lit ground/trunk shader and the sky:

Graphics.loadShader("shaders/foliage") // FIRST → the active instanced foliage shader
_sceneShader = Graphics.loadShader("shaders/scene")
Graphics.loadShader("shaders/sky")

2 — Set the sun and atmosphere with the neutral A#6 knobs and a global wind the foliage bends with:

Graphics.setAmbient(0.34, 0.40, 0.46)
Graphics.setFog(0.62, 0.76, 0.92, 0.0, 42.0, 130.0)
Graphics.setSky("shaders/sky", 0, [/* horizon */ 0.55, 0.70, 0.92, 0.0, /* zenith */ 0.25, 0.45, 0.82, 0.0])
Graphics.setWind(1.0, 0.0, 0.45, 1.1) // the foliage sways along this

3 — Fill one InstancedMesh with a blade/leaf-card prototype — the grass field and every tree’s leaf canopy are all instances in the same mesh, each carrying a per-instance colour and a per-instance wind phase via enhanced instancing:

_foliage = _scene.createInstancedMesh("foliage")
_foliage.setMesh(_blade) // a crossed-quad blade/leaf card; no per-mesh shader → active foliage shader
_foliage.useGpuInstancing = true
var idx = _foliage.addInstanceColored(x, 0.0, z, scale, r, g, b)
_foliage.setInstanceParams(idx, 1.0, phase, 0.0) // params: speed, windPhase, mode

Each tree is a lit trunk (MeshGen.cylinder) drawn with the scene shader, with ~60 leaf-card instances added to the shared foliage mesh around its crown — so a whole forest’s foliage is still one instanced draw.

The engine capabilities it stands on (all already shipped)

Section titled “The engine capabilities it stands on (all already shipped)”

Nothing the pack touches is new engine surface:

  • Wind — Graphics.setWind; the foliage bends with it. A gust curve is Weather-pack policy.
  • Rain / wetness — a game sets a wetness the foliage shader can darken toward (rain-density-aware; a PLM-266 follow-up).
  • Colliders — shipped (PLM-267) for authored objects: an authored tree/prop blocks movement via a Blender-authored, configurable Jolt collider (plume3d_collider); per-instance colliders for GPU-scattered foliage remain a follow-up.
  • Masking — shipped (PLM-268): control where grass/trees spawn from a geometry-nodes density mask or the Bake-Scatter add-on’s grayscale mask image, resolved at bake time.
  • Audio — wind-in-foliage ambience via the shared audio hooks (PLM-272).
  • LOD / billboards — distant trees swap to imposters (a PLM-266 follow-up; needs the PLM-024 LOD system).
  • Content pack, not an engine feature. The whole look is one foliage.slang on the instanced path — the engine gains nothing. Packaging and licensing for a shippable pack are owner/business decisions; this in-repo demo is the exercised, gate-covered reference for the shader.
  • The demo trees are procedural placeholders. A trunk cylinder with a leaf-card canopy — enough to exercise the shader. Production grass and tree art is Blender art flowing through the workflow above.
  • Foliage is alpha-cutout-ready but the demo blades are opaque cards — no leaf texture yet. A leaf cutout is a set-5 custom texture a pack drops in (see the Custom Material Pack).
  • Fixed key-light direction. The shader lights the leaves from one fixed sun direction; a pack could pass the direction in. Wiring a scene light into the pack shader is left to the consuming game.
  • Roadmap, not shipped: multi-part authored-tree prototypes and distance LOD / billboards (needs PLM-024) are tracked as their own tickets — this v1 ships the foliage shader and the authoring path. (Both spawn masking — PLM-268 — and authored-object colliders — PLM-267 — now ship; per-scattered-instance colliders stay a follow-up.)