Log inGet started

Render layers

Updated 4 September 2026

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.

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:

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

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

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:

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.

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.

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

A prop on its own layer, end to end

-- 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.

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.

  • documentation
  • guide