Log inGet started

population

Updated October 10, 2026

The asset is the recipe — which .mesh and material each variant draws with, plus the instance matrices as bytes. Its instantiate method is what turns that recipe back into a live draw, so a scene records ONE entity for a population of a million instances instead of a million entities.

Layout

<name>.population/
  population.json  { version, total, variants: [ { mesh, material | materialKey, count, offset } ] }
  transforms.bin   total × 64 B — little-endian f32, column-major mat4 per instance

offset counts INSTANCES, not bytes: variant k's matrices start at offset × 64 bytes of transforms.bin and run for count × 64. Each matrix is column-major, which puts its translation at floats 13/14/15 of the block — the layout the engine reads a transform buffer as array<mat4x4<f32>>. The matrices are taken in the space of the entity drawing the population, so it draws where the recipe placed it relative to that entity and moves with it.

mesh and material are asset references ({ "__ref": "<guid>" }), so the geometry and material a population draws travel with it in the dependency graph. A variant drawing with a material the renderer's registry holds but no asset backs names it in materialKey instead.

Putting one in a scene

local pop = asset.resolve("forest", "population")
pop:instantiate(nil, { position = { 0, 0, 0 } })     -- one entity, one draw call

That spawns an entity carrying the Population component, which owns the live GPU buffers and draw registrations for as long as the entity lives — a reload replaces them rather than stacking a second set. The same contract is what Asset { source = <population> } drives, which is how a baked procgen bundle carries a population: a root record plus one child holding the reference.

Building one

proc's population.write sink persists a Population value:

local pop = proc.registry.get("instances.draw").eval({}, { instances = iset }, {}).population
proc.registry.get("population.write").eval({}, { population = pop }, { name = "forest" })

Or through asset.create directly, with each variant's matrices as a flat count × 16 column-major float array:

asset.create("population", "forest", {
    variants = { { mesh = meshGuid, material = materialGuid, transforms = flat } },
})

Reading and drawing one by hand

local recipe = pop:spec()        -- { version, total, variants }
local live   = pop:draw()        -- allocate buffers + register draws
print(live:count(), live:drawCalls())
live:destroy()                   -- drop the registrations, free the buffers

The value :draw() returns OWNS those resources. Hold it and release it — a registration nobody holds can be neither enumerated nor dropped afterwards.

In the Inspector

Selecting a population opens its Population section: how many instances it places, across how many variants, and a table of each variant's mesh, material, instance count and offset, with a link to each variant's mesh.

  • asset-type
  • reference