---
title: "Per-entity shader data"
description: "Part of the rendering guide (guides { path = \"topics/rendering\" }), which lists every part."
section: "Topics"
slug: "topics-rendering-instance-data"
canonical: "https://origozero.ai/docs/topics-rendering-instance-data"
updated: "2026-10-15T11:08:40.169647147+00:00"
tags: ["documentation", "guide", "rendering"]
---

# Per-entity shader data

### Per-entity shader data — a per-instance property override, without a material each

A material per entity is what a dissolve, a hit flash or an effect at its own age
would otherwise cost: an authored material each or a runtime `renderer.material.create`
clone each, a separate pipeline binding and a separate row in the material table for
every one of them, and a `setProperty` call per entity per frame to drive it. The
per-instance channel is the way out: every drawn object carries a block of four
`vec4` lanes of its own, a surface shader reads lane `i` as `input.shader_data[i]`,
and the engine attaches no meaning to what is in them.

```lua
local DISSOLVE_LANE = 0
renderer.instanceData.laneCount()                    --> 4   lanes per entity
renderer.instanceData.set(subject, DISSOLVE_LANE, progress)
renderer.instanceData.clear(subject)                 -- back to zero lanes
```

```wgsl
fn fragment(input: FragmentData) -> vec4<f32> {
    let progress = input.shader_data[0].x;
    if progress > fract(sin(dot(input.uv, vec2<f32>(12.9898, 78.233))) * 43758.5) {
        discard;
    }
    return material.base_color;
}
```

Twenty subjects sharing that one material each dissolve at their own rate, and a
write reaches the frame after it. Reach for a material per entity when what
differs is the LOOK — a different shader, texture or blend — and for a lane when
what differs is a number the shader reads. A feature picks the lane indices it owns and
names them where it writes them, so its writer and its shader read the block the
same way — the baked-lightmap section of `topics/rendering-lighting` is that pattern in the engine's own
code, holding lane 0 for the atlas rect and lane 1 for the layer and enable flag.

A lane an entity was never given reads zero, so a shader tells "not set" from
"set to zero" by keeping a flag in one of the components rather than by testing
the value it wants.

**Which material an instance is drawing with** is a number, because a shader
cannot read a name. Every renderable's per-instance record carries a material
index, and `renderer.materialIdentity()` is the table those indices are drawn
from — assigned the first time the renderer draws with that material and stable
while the renderer holds it.

```lua
local id = renderer.materialIdentity()
id.slots["@builtin::materials.plastic_red"]  --> 4     the index that material took
id.renderables[1]                            --> { entity = "ent_…", slot = 4, index = 4, material = "…" }
id.populations[1]                            --> { slot = 96, count = 4096, index = 5, material = "…" }
renderer.materialIndex("@builtin::materials.plastic_red")  --> 4
```

A surface shader reads its own instance's index as `input.material_index`, so a
shader drawn by several materials can branch on which one it is under, and a
compute pass that traces the scene resolves the same index through
`zeroMaterial()`. Two renderables that differ only in their material read
different indices; one renderable reads the same index frame after frame, and a
material swap moves it on the next frame. A GPU-driven population is one row in
`populations` rather than one per member: every slot of its reserved run carries
the single material `renderer.mesh.drawInstanced` was given, so a member reads
that index wherever the population moves.

A renderable draws with the material its entity references, so a renderable
whose entity names none reads index 0. An index is an identity for as long as
the renderer holds its material: once the material is destroyed and nothing
draws with its name, the name leaves `slots` and the next material takes its
index. A renderable still naming a destroyed material keeps a row for that name
until it stops drawing with it. So the table holds a row per name the renderer
holds a material under or something draws with, and a session that makes and
destroys materials under fresh names keeps it the same size.

**A population's members carry their own shader data.** One material is what
keeps a population one draw, so what a member differs in has to arrive
per-instance. `instanceDataBuffer` is that channel: a GPU buffer holding one
block of four `vec4` lanes per instance, which the renderer copies into those
instances' lanes every frame, and which a surface shader reads as
`input.shader_data[lane]` — the same lanes an entity gets from
`renderer.instanceData.set`, on slots no entity owns.

```lua
local lanes = substrate.createBuffer({ name = "crowd.lanes", type = "vec4", len = COUNT * 4, kind = "gpu" })
renderer.mesh.drawInstanced(mesh, {
    transformBuffer = "crowd.xf",
    instanceDataBuffer = lanes.name,   -- the NAME, not the buffer
    instanceCount = COUNT,
    material = M,
})
```

The copy re-runs every frame, so a compute pass (or a `:write`) that rewrites
the buffer changes the next frame with no second registration. The buffer must
back the whole reservation — a short one is refused at the call rather than
letting the instances past its end read the next population's lanes. Omit it and
the lanes read zero, whatever the slots held before the registration took them.
