Skip to content

Scatter

Scatter / placement (engine PLM-246 / ADR 0085) — part of Block SPL (splines / geometry / scatter), and the last of the trio. Scatter is a collection of static factories that fill an InstancedMesh with thousands of seeded, varied placements — grass, rocks, trees, debris — either across a surface (projected onto the scene’s ground) or along a Spline. Each factory returns the number of instances placed.

The placement engine is deliberately neutral: it does the geometry (where each instance goes, its scale, its yaw, its slope-alignment) and nothing else. The look and policy — density presets, biome masks, which prototype where — are a content-pack concern layered on top.

import "engine" for Scene, Graphics, Scatter, Spline, Physics
// A blade prototype (a Mesh), one InstancedMesh to fill.
var blade = Graphics.newMesh(bladeVerts(), "triangles")
var foliage = _scene.createInstancedMesh("foliage")
foliage.setMesh(blade)
foliage.useGpuInstancing = true
// Scatter 8000 blades across a 36×36 patch, projected onto the scene's ground.
var placed = Scatter.onSurface(foliage, _scene, -18, -18, 18, 18, 8000, 1234)
System.print("placed %(placed) blades") // ≤ 8000 — misses (off the collider) drop

Scatter writes the instances into the InstancedMesh in C++ — it never returns a 10,000-item Wren List and never runs a per-instance Wren callback. Each factory calls the mesh’s capacity + addInstance path natively, so filling a large field is a single native loop, not thousands of foreign-call round-trips.

  • The scale is baked into each instance transform (and mirrored in the instance’s params.x). A shader must therefore not re-apply params.x as a second scale — read scale from the transform, and use the params slots for effects like wind. (The scatter_demo scatter_grass shader reads scale from the transform and uses params.y only for wind sway.)
  • The base colour is left at the mesh default, so the prototype mesh’s own vertex colour / material shows through. Per-instance tint is a caller concern (colour the prototype, or use addInstanceColored yourself), not scatter policy.
  • Deterministic. Placement is driven by a portable PCG32 seeded via SplitMix64 — the same RNG family as the Wren Random class — so the same seed (and the same inputs) produces the same field every run, with no std distributions.

Scatter is never instantiated — call the statics directly. Each returns a Num (the count placed).

Scatter.onSurface(mesh, scene, minX, minZ, maxX, maxZ, count, seed)

Section titled “Scatter.onSurface(mesh, scene, minX, minZ, maxX, maxZ, count, seed)”

Returns: Num — the number of instances actually placed (≤ count; a candidate whose ray-down misses the ground is dropped, so a sparse or holed collider yields fewer than count).

Scatter count attempts uniformly across the world-XZ rectangle [minX, maxX] × [minZ, maxZ], projecting each onto the scene’s ground and adding an instance there — with natural defaults (scale 0.8–1.2, a slight slope-align, random yaw). The one-call path from “an empty InstancedMesh + a terrain collider” to “a filled foliage patch”.

  • mesh (InstancedMesh) — the mesh to fill; set its prototype with setMesh first.
  • scene (Scene) — the scene whose physics ground the candidates project onto.
  • minX, minZ, maxX, maxZ (Num) — the world-XZ placement rectangle.
  • count (Num) — how many candidates to attempt.
  • seed (Num) — RNG seed; the same seed reproduces the same field.

Scatter.onSurfaceFull(mesh, scene, minX, minZ, maxX, maxZ, count, minScale, maxScale, slopeMaxDegrees, alignToNormal, seed)

Section titled “Scatter.onSurfaceFull(mesh, scene, minX, minZ, maxX, maxZ, count, minScale, maxScale, slopeMaxDegrees, alignToNormal, seed)”

Returns: Num — instances placed (≤ count).

The full-knob form of onSurface: same rectangle + ground projection, with explicit control over scale range, slope rejection, and slope alignment.

  • mesh, scene, minX, minZ, maxX, maxZ, count — as onSurface.
  • minScale, maxScale (Num) — per-instance uniform scale is picked in [minScale, maxScale] and baked into the transform.
  • slopeMaxDegrees (Num) — reject any candidate where the ground is steeper than this (in degrees from horizontal), so foliage doesn’t stand on cliffs. A rejected candidate is a miss (it drops, lowering the returned count).
  • alignToNormal (Num) — 0 = every instance stands upright (world +Y); 1 = each instance tilts fully onto the ground normal (lies along the slope); values between lerp the two.
  • seed (Num) — RNG seed.
// Rocks that sit flat on the slope, but never on ground steeper than 40°.
var n = Scatter.onSurfaceFull(rocks, _scene, -20, -20, 20, 20, 1500,
0.5, 1.4, // scale 0.5–1.4
40, // reject slopes steeper than 40°
1.0, // fully align to the ground normal
99)

Scatter.alongSpline(mesh, spline, count, widthJitter, minScale, maxScale, seed)

Section titled “Scatter.alongSpline(mesh, spline, count, widthJitter, minScale, maxScale, seed)”

Returns: Num — instances placed (here always count — spline placement never misses).

Scatter count instances along a Spline, evenly spaced by arc length and jittered ±widthJitter sideways across the ribbon (using the spline’s rotation-minimizing frames), so a path, hedge, fence line, or roadside strip fills naturally without hand-placing.

  • mesh (InstancedMesh) — the mesh to fill.
  • spline (Spline) — the curve to line instances along.
  • count (Num) — how many instances.
  • widthJitter (Num) — maximum sideways offset (world units) across the spline’s binormal; 0 keeps every instance exactly on the centre line.
  • minScale, maxScale (Num) — per-instance scale range, baked into the transform.
  • seed (Num) — RNG seed.
// A denser strip of the same foliage along a terrain-following path.
var path = Spline.new()
for (p in points) {
var g = Physics.groundPoint(_scene, p[0], p[1]) // snap each control point to the ground
path.addPoint(p[0], (g == null ? 0 : g[1]) + 0.02, p[1])
}
var onPath = Scatter.alongSpline(foliage, path, 2600, 3.0, 0.8, 1.5, 77)
  • One GPU-instanced mesh per frame. Enhanced instancing writes one instance SSBO at offset 0 per frame, so all of a scene’s scattered instances should share one InstancedMesh (fill it from several factories, as scatter_demo does — onSurface then alongSpline into the same mesh). Multiple GPU-instanced meshes in a single frame is a documented instancing follow-up.
  • Scatter is a frozen public class (ADR 0085) — its signatures are a contract games depend on.
  • The core is device-free (modules/geometry/scatter); it takes an injected ray-down callback and returns a plain instance list that the Wren binding streams into the InstancedMesh, so the module links neither the renderer nor physics.

See InstancedMesh for the per-instance colour / params / culling surface a scattered field draws through, Spline for the curve alongSpline follows, Physics — Surface & ground queries for the ground projection onSurface uses, and the Scatter Demo for ~10,600 instances placed both ways.