---
title: "Render textures"
description: "A render texture is a camera's view drawn into a texture instead of onto the screen. That texture then becomes something you use elsewhere — a UI panel, a material map, a mirror, a minimap, a…"
section: "Topics"
slug: "topics-render-textures"
canonical: "https://origozero.ai/docs/topics-render-textures"
updated: "2026-09-04T19:42:52.106860365+00:00"
tags: ["documentation", "guide"]
---

# Render textures

## A camera renders into a texture you create

A render texture is just a **GPU texture** — the same kind a `.texture` asset uploads to — that starts empty and gets its pixels from a camera instead of a CPU upload. So you create the texture first (it owns its own size), then point a camera at it:

```lua
-- The texture the camera renders into — a GPU texture with no pixels yet.
local tex = renderer.texture.create({ width = 512, height = 512, name = "mirror" })

local e = entity.spawn("mirror_cam")   -- returns the entity proxy directly
e.position = { 0, 3, 5 }
e.component.add("Camera", { priority = 5 })

local cam = e.component.get("Camera")
cam:setTargetTexture(tex)    -- render into the texture (nil = back to the viewport)
cam:lookAt({ 0, 1, 0 })      -- aim it (entity id or {x, y, z})
```

A second camera like this renders a view distinct from the player's primary camera (which the scene already spawns — the scenes guide). Its Camera fields `fov` / `near` / `far` / `priority` / `postProcessing` and the `renderLayers` spec (e.g. `"all !ui"`, where `ui` / `sky` / `debug` / `EditorUI` are built-in layers) control what and how it draws.

### How it looks when something draws it larger than it is

A render target takes `filter` the way raw pixels do, and means the same thing
by it:

```lua
local panel = renderer.texture.create({ width = 64, height = 32, filter = "nearest" })
```

`"linear"` (the default) smooths between the target's pixels, which is right for
a camera's view shown at about its own size. `"nearest"` keeps them square,
which is what an image whose pixels ARE the subject needs — a 64x32 LED matrix
holds no detail between its pixels to interpolate, so smoothing one is not a
softer picture but a different one.

It is said about the TARGET, not about the window showing it, so every surface
wearing the target agrees: a `viewport` widget, a magnified capture, a
material's texture slot. And it is kept against the handle, so a resize that
reallocates the texture does not lose it.

The camera only **references** the texture — it doesn't own it. The texture's lifetime is yours: free it with `renderer.destroy(tex)` when you're done (this reclaims the GPU texture and the camera's render scratch). Don't rely on the camera's despawn to clean it up.

## Using the result

`tex` is a texture like any other; its `tex.guid` is what other things reference. Two common destinations:

- **In UI** — the `viewport` widget takes the texture guid, giving you in-UI 3D views and minimaps (the ui guide):

  ```lua
  { type = "viewport", props = { renderTarget = tex.guid } }
  ```

- **On a surface** — feed `tex.guid` to a material as a texture so a mesh displays the rendered view (a screen, a mirror) (the materials guide):

  ```lua
  Material.setTexture("monitor_mat", "base_color_texture", tex.guid)
  ```

- **Off this machine, as bytes** — `frameStream` carries the texture's frames into a byte stream, so what a camera renders reaches a browser, a phone, an LED panel or another process, live. `stream.open` makes the destination (`tcp://host:port`, or `loopback://name` to read the frames back in Luau), and `frameStream.attach` binds the two:

  ```lua
  local out = task.await(stream.open("loopback://panel", { inboundCapacity = 512 * 512 * 3 * 4 }))
  local session = frameStream.attach(tex.guid, out, { fps = 30, format = "rgb24" })
  local frame = stream.read(out, 512 * 512 * 3)   -- one whole frame's pixels, or ""
  ```

  Each frame is `width * height * bytesPerPixel` bytes of tight rows, written in one call, so a reader gets a whole frame or none of one. `frameStream.status(session)` separates a consumer that is too slow (`droppedBackpressure`) from a GPU that is (`stalledReadbacks`). The readback runs off the render thread, so the renderer is never held up. The **`topics/frame-streaming`** guide covers this end to end, and **`topics/byte-streams`** covers the stream the pixels travel down.

## Building or reading textures directly

`renderer.texture.create` also builds a runtime GPU texture from raw pixels (`{ rgba, width, height }`) or from a `.texture` AssetRef's loaded CPU pixels; `textureRef:load()` reads a `.texture` asset's pixels on the CPU. `renderer.destroy(handle)` frees any of them.

## Finding the rest

`asset.inspect("@builtin::components.Camera")` documents the Camera's render-to-texture surface (`textureHandle`, `setTargetTexture`, `render`, `capture`); `lsp.methods("renderer")` lists the `renderer.texture` API; `lsp.methods("frameStream")` and `lsp.methods("stream")` list the egress side, and `stream.transports()` names the transports this build carries; the ui guide covers the `viewport` widget. The model to hold: a camera renders into a GPU texture, and that texture goes anywhere a texture goes — UI or materials. There is no separate "render target" concept; it's the same texture, just filled by a camera instead of an upload.
