Skip to content

Foliage + Terrain Demo

App: apps/foliage_terrain_demo/

Demonstrates the Block BLD Blender import end to end (engine PLM-249/251/252/253/254/267): FoliageTerrain.blend carries a sculpted terrain mesh tagged plume3d_terrain, a hidden grass-blade prototype, a Plume_Scatter Empty baking 256 blades seated on the terrain surface, and two static rock props that each carry a Blender-authored collider (rock_a tagged plume3d_collider = "sphere", rock_b tagged "box", engine PLM-267). This whole level comes in with one call:

_blend = _scene.loadBlendScene("Models/FoliageTerrain.blend")

Scene.loadBlendScene composes the four BlendResult primitives — it instantiates the terrain (a render node and an automatic static concave triangle-mesh Jolt collider, creating the scene’s physics world if absent, via instantiateTerrain), the baked scatter (one GPU-instanced grass draw, via instantiateScatter), and the static rock props (via instantiateUnits) — and returns the BlendResult. Coordinates convert automatically (Blender Z-up → engine Y-up). Call the primitives individually when you want finer control (as this demo did before loadBlendScene existed).

Terminal window
% ./plume3d foliage_terrain_demo

The demo drops three orange physics spheres from above; they settle on the hills at varying heights (printed, and drawn as boxes at their rest positions). It also drops a green probe straight onto each rock — proving the two rock props’ Blender-authored colliders. It captures a screenshot at frame 180 and exits at frame 200.

  • Loads a grass shader (active for the instanced foliage) and a lit shader, then loads the entire level with a single loadBlendScene call — terrain (render + collider), grass (one instanced draw), and the two rock props — printing the blend’s node count.

  • Drops physics probe spheres from just above the hilltops to prove the terrain collider:

    for (p in [[-3.0, -2.0], [0.5, 1.5], [3.0, -1.0]]) {
    var n = _scene.addNode("probe")
    n.setPosition(p[0], 3.5, p[1]) // start above the hills
    Physics.addDynamicSphere(_scene, n, 0.35, 1.0)
    _probes.add(n)
    }

    At frame 180 each probe’s rest height is printed — they settle at the terrain’s varying surface height rather than falling through, which is the collider working.

  • Proves the rock props’ Blender-authored colliders (engine PLM-267): each rock prop carries a plume3d_collider tag, so instantiateUnits builds a static Jolt collider sized from its bounds. The demo looks each rock up by node_id, drops a green probe straight down onto it, and at frame 180 prints how far the probe rests above the rock centre — > 0 means it landed on the rock (a sphere collider on rock_a, a box on rock_b) instead of falling past:

    for (rp in [["rock_a", "sphere"], ["rock_b", "box"]]) {
    var rock = _scene.findNodeById(rp[0])
    var pos = rock.getPosition()
    var n = _scene.addNode("rockprobe")
    n.setPosition(pos[0], pos[1] + 4.0, pos[2]) // straight above the rock
    Physics.addDynamicSphere(_scene, n, 0.3, 1.0)
    }
  • Sets a global wind (Graphics.setWind) and draws the scene (terrain + grass + rock props) plus the probe boxes at their physics-updated positions.

  • What this demo proves: one loadBlendScene call brings in a Blender-authored level — the terrain surface and grass and rock props all render, instantiateTerrain builds an automatic physics collider from the tagged mesh (proven by the orange probe spheres resting on the hilly surface at varying heights), the grass follows the terrain contour, and the two rock props are the instantiateUnits witness — and, tagged with plume3d_collider, each also gets a configurable static collider so a green probe rests on the rock (a sphere on rock_a, a box on rock_b), the movement-blocking half of Block BLD.
  • class_name-scripted units stay explicit. loadBlendScene returns the BlendResult so a caller instantiates characters/cameras/scripted props by hand with blend.instantiate(name, _scene) — they are intentionally not auto-instantiated. Give any such unit a class_name (not just a node_id), or instantiate-by-node_id after the whole-scene load would draw it twice (see the caveat).

See Scene.loadBlendScene and BlendResult for the full blend surface, the Blend Scatter Demo for the baked-scatter path on its own, and Physics Mesh Terrain for a script-built triangle-mesh collider.