---
title: "Render layers"
description: "A render layer is a name. An entity is a member of one or more layers; a camera draws a filter over them (\"all !shell\"). Those two sides are what let geometry exist for lighting, shadows and physics…"
section: "Topics"
slug: "topics-render-layers"
canonical: "https://origozero.ai/docs/topics-render-layers"
updated: "2026-09-04T19:42:52.094121896+00:00"
tags: ["documentation", "guide"]
---

# Render layers

Everything here speaks layer names. No caller builds a bitmask.

The `renderLayer` toolbox is the whole surface: `set` and `get` for membership,
`camera` and `screen` for what draws them, `list` for what exists. From Luau a
tool is invoked as `tools.use("<toolbox>", "<tool>", ...)`; the same calls read
as `zero renderLayer set …` in the engine shell.

## Putting entities on a layer

`renderLayer.set` writes membership. Targets take an entity name or id, an array
of them, `scene.find` records, or a **query** resolved for you, so a selection
can be re-layered without looking its ids up first. Layers are a single name, an
array of names, or a space-separated string.

```lua
tools.use("renderLayer", "set", "frontWall", "shell")
tools.use("renderLayer", "set", { "marker_a", "marker_b" }, "debug")
tools.use("renderLayer", "set", { name = { "wall" } }, "shell")
```

Setting **replaces** an entity's membership rather than adding to it.

## A hierarchy moves in one call, and the reply says how far it reached

A prop, a building or a character is a tree of entities, and the thing you
usually want on a layer is the whole tree rather than its root mesh. `tree = true`
writes the target **and every descendant**, in one call:

```lua
tools.use("renderLayer", "set", "Mansion", "stage_props", { tree = true })
```

The reply carries the extent of the write, one record per target:

```lua
tools.use("renderLayer", "set", "Mansion", "stage_props")
--> { { id = "ent_7be0…", name = "Mansion", layers = { "stage_props" }, moved = 1, skipped = 46 } }

tools.use("renderLayer", "set", "Mansion", "stage_props", { tree = true })
--> { { id = "ent_7be0…", name = "Mansion", layers = { "stage_props" }, moved = 47, skipped = 0 } }
```

`moved` counts the entities that write reached. `skipped` counts the descendants
it left where they were, so a `skipped` above zero is the part of the hierarchy
still drawing where it was. The two numbers are what tell a parent-only write
from a whole-building write, which is the question a re-layered hierarchy
usually raises — the reply answers it instead of a walk over the subtree
afterwards. An entity two targets both span is written once and counted once, so
the `moved` counts sum to the entities the call moved.

## Reading membership back

`renderLayer.get` answers with layer names and takes the same `tree` option —
one row per entity, which is how a subtree write is checked:

```lua
tools.use("renderLayer", "get", "Mansion")
--> { { id = "ent_7be0…", name = "Mansion", layers = { "stage_props" }, skipped = 46 } }

tools.use("renderLayer", "get", "Mansion", { tree = true })   -- 47 rows, one per entity
```

`skipped` reads the same way it does on `set`: the descendants this read left
out. An entity nothing has re-layered reads back as `default`.

On an entity proxy the same answer is the read-only `.renderLayer` property, as
a space-separated string, and `entity(id):setRenderLayer(names)` is the raw
single-entity write behind the tool.

## Which layers a camera draws

`renderLayer.camera` sets a camera's filter, and reads it back when the filter
argument is omitted. The same spec is the Camera component's `renderLayers`
field: a space-separated list where `all` seeds every layer, `name` adds one and
`!name` drops one. `default`, `ui`, `debug`, `sky` and `EditorUI` are built-in
layers.

```lua
tools.use("renderLayer", "camera", "stageCam", "all !stage_props")   -- hide the props from this camera
tools.use("renderLayer", "camera", "stageCam")                       -- read the filter back
```

That is what makes a layer worth having: one write puts a whole prop on
`stage_props`, and one filter per camera decides which shots contain it.

`renderLayer.screen` does the same for a UI screen — the layers that screen
draws into — so a HUD stays out of a camera whose filter excludes them.

```lua
tools.use("renderLayer", "screen", "hud", "ui")
```

## A prop on its own layer, end to end

```lua
-- one prop, spawned as a hierarchy
local prop = entity.spawn("Mansion")
local wing = entity.spawn("wing", { parent = prop })
for i = 1, 3 do
    entity.spawn("wall_" .. i, { parent = wing })
end

-- the whole thing onto its own layer, in one call
tools.use("renderLayer", "set", "Mansion", "stage_props", { tree = true })
--> { { name = "Mansion", layers = { "stage_props" }, moved = 5, skipped = 0 } }

-- the stage camera drops it
tools.use("renderLayer", "camera", "stageCam", "all !stage_props")

-- and the read-back names every entity carrying the layer
tools.use("renderLayer", "get", "Mansion", { tree = true })   -- 5 rows, all { "stage_props" }
```

`moved = 5, skipped = 0` is what says the write covered the prop. The same call
without `tree` answers `moved = 1, skipped = 4`, and those four entities keep
drawing in the shot the camera excludes.

## Seeing what exists

Referencing a layer name that does not exist yet **creates** it, so `"shel"`
instead of `"shell"` produces a real but empty layer that renders nothing and
raises no error. `renderLayer.list` is where that shows up — every layer in bit
order, the entities on each, and every camera with its filter.

```lua
tools.use("renderLayer", "list")
```

## Render layers are not scene layers

`wld.layers` is a different concept: additive **scene composition**, the
mechanism for merging one scene's content into another. Render layers govern
what draws.
