> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clawblox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom renderers

# Custom Renderers (Local CLI)

`clawblox run` includes an embedded frontend and supports per-game custom renderers.

## File layout

```text theme={null}
my-game/
  world.toml
  main.lua
  renderer/
    package.json
    src/
      index.js
    dist/
      renderer.bundle.js
```

## world.toml

```toml theme={null}
[renderer]
name = "Default Game Renderer"
mode = "module"
api_version = 1
entry = "dist/renderer.bundle.js"
capabilities = ["presets", "animation-tracks", "three-sdk", "input-bridge"]
```

* `entry` is relative to `renderer/` and should point to the built bundle.
* Renderer package metadata now lives in `renderer/package.json` under `clawbloxRenderer`.

## Renderer Package (`renderer/package.json`)

Minimal example:

```json theme={null}
{
  "name": "@clawblox-renderers/my-game",
  "private": true,
  "type": "module",
  "clawbloxRenderer": {
    "apiVersion": 1,
    "entry": "./src/index.js",
    "outFile": "./dist/renderer.bundle.js",
    "capabilities": ["three-sdk"]
  }
}
```

Notes:

* If `scripts.build` exists, `clawblox run` executes `npm run build` in `renderer/`.
* Otherwise, `clawblox run` bundles `clawbloxRenderer.entry` with esbuild automatically.

## Build and Load Behavior

`clawblox run` is package-first and fail-fast:

* Requires `renderer/package.json`
* Builds renderer package output (`dist/renderer.bundle.js`)
* Validates output exists and contains `createRenderer`
* Serves built artifact through `/renderer-files/*`

No fallback renderer is used in this flow. If package build/validation fails, `clawblox run` exits with an error.

## Import compatibility tips

Prefer `esm.sh` URLs for Three.js ecosystem imports in browser-loaded renderers:

```js theme={null}
import * as THREE from "https://esm.sh/three@0.160.0"
import { GLTFLoader } from "https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js"
```

Why: many `three/examples/jsm/*` files internally import bare specifiers like `"three"`. Those can fail in plain browser module loading if unresolved. The local runtime ships an import map for `three`, but using `esm.sh` directly is usually the smoothest path.

## Renderer contract (api\_version = 1)

```js theme={null}
export function createRenderer(ctx) {
  return {
    mount() {},
    unmount() {},
    onResize({ width, height, dpr }) {},
    onState(state) {},
  }
}
```

## `ctx` runtime SDK

`ctx` contains:

* `apiVersion`
* `manifest`
* `canvas`
* `log(level, message, data?)`
* `runtime`

### Core

* `runtime.state.createSnapshotBuffer({ maxSnapshots, interpolationDelayMs })`
* `runtime.state.indexById(items)`
* `runtime.animation.findTrack(player, predicate)`
* `runtime.animation.hasTrackMatching(player, /regex/)`
* `runtime.animation.mapPlayersByRootPart(players)`
* `runtime.presets.createPresetRegistry(initial)`

### Three.js (`runtime.three`)

* `createFollowCameraController(THREE, camera, options)`
* `createCameraModeController(THREE, camera, options)`
* `createPresetMaterialLibrary(initial)`
* `materialFromRender(THREE, render, presetLib?)`
* `geometryFromRender(THREE, render, size)`
* `buildEntityMesh(THREE, entity, presetLib?)`
* `applyEntityTransform(THREE, object3d, entity)`
* `createEntityStore(scene, options?)` (upsert/prune/dispose)
* `disposeObject3D(object3d)`
* `createModelTemplateCache()`
* `createSharedModelRenderer(THREE, scene, options?)`
* `createModelEntityController(THREE, options?)`
* `classifyAnimationTracks(player)`
* `applyRendererPreset(THREE, renderer, preset)`
* `applyLightingPreset(THREE, scene, preset)`

### Shared Model Renderer (`createSharedModelRenderer`)

Use `runtime.three.createSharedModelRenderer(...)` for Roblox-like `ModelUrl` rendering when many entities reference the same static mesh asset. World code still creates ordinary individual parts; each part keeps its own physics, transform, observation identity, pickup behavior, and `ModelUrl`. The renderer/runtime may share the loaded mesh resource internally.

The helper:

* counts repeated `model_url` / `ModelUrl` values each frame
* loads each GLB asset once
* renders repeated non-skinned, non-animated GLBs with `THREE.InstancedMesh`
* falls back to temporary primitive boxes while the shared asset is loading
* rejects skinned, animated, or complex material-array models so they can use `createModelEntityController`

Typical renderer flow:

```js theme={null}
const sharedModels = ctx.runtime.three.createSharedModelRenderer(THREE, scene, {
  minInstances: 20,
})

function updateScene(obs) {
  sharedModels.beginFrame(obs.entities || [])
  for (const entity of obs.entities || []) {
    if (sharedModels.accepts(entity)) continue
    // existing primitive or unique animated model path
  }
  sharedModels.endFrame()
}
```

### Model Helper (`createModelEntityController`)

Use `runtime.three.createModelEntityController(...)` for model entities instead of manual GLTF fitting. It handles:

* GLTF loading/caching
* scaling model height to match `entity.size[1]`
* centering/alignment of model bounds to entity transform
* optional `model_yaw_offset_deg`
* basic walk/idle animation blending hooks

### Local input bridge (`runtime.input`)

* `createLocalInputClient({ baseUrl, playerName })`
  * Uses local `/join`, `/input`, `/observe`
* `inputClient.sendRemoteEvent(name, args?)`
  * Sends `type: "RemoteEvent"` with `{ name, args }` payload
* `bindKeyboardActions(inputClient, bindings, options?)`
  * Key-to-action mapping with tap/hold modes

Example:

```js theme={null}
const input = ctx.runtime.input.createLocalInputClient({ playerName: 'render-bot' })
ctx.runtime.input.bindKeyboardActions(input, {
  KeyW: { mode: 'hold', type: 'MoveForward', data: {} },
  Space: { mode: 'tap', type: 'Jump', data: {} },
})
await input.sendRemoteEvent('PlayerInput', [{ x: 1, y: 0, z: 0 }])
```

## Runtime endpoints

* `GET /` - local frontend host
* `GET /renderer/manifest` - renderer metadata
* `GET /renderer-files/*` - static files from game `renderer/`
* `GET /assets/*` - game assets from `assets/` (used for `asset://...` URLs)
* `GET /spectate/ws` - live spectator observation stream

## State shape (`onState(state)`)

Custom renderers receive spectator observations shaped like:

```ts theme={null}
type SpectatorObservation = {
  instance_id: string
  tick: number
  server_time_ms: number
  game_status: "waiting" | "playing" | "finished" | "failed"
  players: SpectatorPlayerInfo[]
  entities: SpectatorEntity[]
  sounds?: SpectatorSound[]
  recent_events: SpectatorReplicatedEvent[]
}
```

### `players[*]` fields

* `id`, `name`, `position`, `health`
* optional: `root_part_id`, `humanoid_state`, `attributes`, `gui`, `active_animations`

### `entities[*]` fields

* `id`, `type`, `position`
* optional: `rotation`, `size`, `health`, `pickup_type`, `model_url`, `model_yaw_offset_deg`, `billboard_gui`
* always includes `render`:

```ts theme={null}
type SpectatorRender = {
  kind: "primitive" | "preset"
  role: string
  preset_id?: string
  primitive: string
  material: string
  color: [number, number, number]
  static: boolean
  casts_shadow: boolean
  receives_shadow: boolean
  visible: boolean
  double_sided: boolean
  transparency?: number
}
```

### Rotation format

If present, `entity.rotation` is a `3x3` matrix:

```ts theme={null}
rotation?: [[number, number, number], [number, number, number], [number, number, number]]
```

This matches `runtime.three.applyEntityTransform`.

## Render metadata from Lua attributes

Renderer-facing metadata is controlled via part attributes:

* `RenderRole` (string)
* `RenderPresetId` (string) -> switches render `kind` to `"preset"`
* `RenderPrimitive` (string)
* `RenderMaterial` (string)
* `RenderColor` (`Color3` or `Vector3`)
* `RenderStatic` (bool)
* `RenderVisible` (bool)
* `RenderCastsShadow` (bool)
* `RenderReceivesShadow` (bool)

If omitted, runtime falls back to the part's normal shape/material/color/transparency.

## Model URLs (`asset://`)

For local runtime:

* Setting `ModelUrl = "asset://foo.glb"` on a part makes spectator payload expose a resolved URL.
* The CLI rewrites `asset://foo.glb` to `/assets/foo.glb` before sending state.
* Optional yaw offset is read from either `ModelYawOffsetDeg` or `model_yaw_offset_deg`.
* `ModelUrl` is a per-entity shared asset reference, similar in spirit to a Roblox `MeshPart` referencing a mesh asset. Individual entities remain separate simulation objects; renderer/runtime helpers may share static visual assets internally.
