Tiny World CCTV / Truman Cameras
Low-res black-and-white security cameras that watch the lobby and feed both
physical monitors and the big lobby presentation screen. They idle-sweep, then
pan to look at whoever is MOVING nearby — like the hidden cameras in The Truman
Show. Do not mount CCTV or the big screen in non-lobby worlds.
Files
engine/world/62-cctv-truman.js — the core system. IIFE exposing
window.__tinyworldCCTV. 4-space body indent on purpose so the
duplicate-declaration guard in tools/check.js (which only scans 2-space
top-level decls) ignores its locals.
engine/world/63-cctv-placement.js — mounts cams + monitors on lobby room
enter, tears down on non-lobby enter or leave. Exposes
window.__tinyworldCCTVPlacement. Lobby detection is
window.__TW_LOBBY_WORLD_SLUG || 'tidewater-bay'.
engine/world/58-lobby-presentation.js — the lobby-only big screen; its
tick() cuts between slides and the hottest live feed.
scripts/landing-feed.js + styles/landing.css — the public landing-page
live-worlds panel. World rows are buttons: click once to expand an island
CCTV preview (2D canvas from /api/worlds.preview.cells), click again to
collapse. This is a lightweight marketing/front-door CCTV treatment, not a
Three.js render target.
- Tick wiring:
engine/world/25-animation-loop-schema.js calls
window.__tinyworldCCTV.tick(t,dt) then window.__tinyworldLobby.tick(t,dt)
before renderScene() so feeds captured this frame appear this frame.
How it works
- Each camera owns a
PerspectiveCamera + a small WebGLRenderTarget
(FEED_W×FEED_H, 4:3). tick() round-robins captures (CAPTURES_PER_FRAME,
capped at FEED_FPS) using renderer.setRenderTarget(rt); renderer.render(scene, cam)
— always save/restore the previous render target (getRenderTarget()),
and bail if renderer/scene aren't ready.
- The monitor material is a
ShaderMaterial (CRT_FRAG): luminance B&W, scanlines,
a rolling interference bar, hash static, vignette, and uSignal dropout that
dissolves toward static. A second canvas texture (tCaption) bakes the camera
name + live date/time + blinking REC and is composited in the shader. The
material is tagged userData.windowLightEffect = true + lightVisual = true
so prepareFadeable() does NOT swap it for a fade material (skill:
tinyworld-render-performance).
- Must end the fragment with
#include <colorspace_fragment> (r185 output color
space), like every other ShaderMaterial here.
- Subjects:
setSubjectsProvider(() => [{pos:Vector3, name}]). The room
feeds WS.subjects() (self + peer avatar sprite positions). Cameras + monitors
live under WS.avatarParent() — the SAME local frame as the avatars and lobby
screen — so subject positions need no conversion.
- Truman tracking:
aim() scores each in-range subject by prox*0.6 + moved*1.0 (movement dominates) using per-feed last-position memory; it lerps
curLook toward the winner (TRACK_LERP) or idle-sweeps (IDLE_LERP). A
per-feed activity score spikes on motion and decays (ACTIVITY_DECAY).
- Hot feed:
activeFeed(minActivity) / feedsByActivity() let the lobby
screen auto-cut to whichever camera has something happening. glitch(id,amt)
drops uSignal briefly to sell a cut.
Placement (63)
On lobby enter only (after a 350ms delay so cells + lobby screen exist) it
mounts:
lobby-l / lobby-r — flank both sides of the presentation screen, angled at
the crowd (toward +z).
pumpkincam — over the biggest kind:'pumpkin' cell (scans world[][],
sorts by floors).
treecam-1/2 — over the tallest kind:'tree' cells.
Monitors stack up the sides of the lobby screen. window.__tinyworldCCTVFeeds
lists mounted feed ids. On non-lobby enter or leave, clear feeds, disable
capture, and remove monitors.
Lobby cutting (58)
build() stashes screenMesh + slideMat. The screen is shown only in the
configured lobby world, not every island. The state machine auto-advances slides
(AUTO_ADVANCE), then after SLIDE_DWELL swaps the screen material to a
monitorMaterialFor(hotFeed) for FEED_DWELL, then back. Manual presenter
go() and hide() snap back to slides. New API: tick, setCycle, showSlides, showFeed, liveFeed.
QA
?cctv=demo (or =1) drops 4 monitors around origin watching a bobbing test
subject and self-drives the tick — verifies the CRT look + tracking without a
multiplayer room.
- Landing page QA for
scripts/landing-feed.js: if local /api/worlds is not
available, mock window.fetch('/api/worlds'), append a fresh copy of the
script, click .hero-feed-link, and assert .hero-feed.is-expanded, one
.hero-feed-cctv-canvas, and the .hero-feed-cctv-meta status/link render.
The panel should anchor below the nav (.hero-feed.is-expanded) and scroll
internally instead of overflowing the hero.
- Headless sanity: eval module 62 under a THREE stub,
addCamera + tick a few
frames with a moving subject, assert activeFeed() returns the feed and
activity > 0.
npm run check (duplicate-decl + i18n), npm run smoke, ./publish.sh.
Pitfalls
- Don't capture every feed every frame — round-robin, or the extra
renderer.render passes tank FPS (the renderer contract is single-pass per
feed; this is the only sanctioned extra-RT path here besides pixelation).
- Don't forget to restore the render target after capturing, or the main scene
renders into the last feed's RT (black screen).
- Keep monitor materials off the fade pipeline via the
windowLightEffect tag.
- Cameras + monitors MUST be added under
avatarParent(), not scene, or they
won't inherit the tinyverse scale/offset and subject tracking will be wrong.
1---2name: tinyworld-cctv-truman3description: Use when changing the in-world CCTV / "Truman Show" surveillance cameras in Tiny World Builder — render-to-texture security feeds, the black-and-white CRT/VHS monitor shader, camera placement (lobby side-cams, pumpkincam, treecams), subject tracking, or the lobby screen cutting to live feeds.4---56# Tiny World CCTV / Truman Cameras78Low-res black-and-white security cameras that watch the lobby and feed both9physical monitors and the big lobby presentation screen. They idle-sweep, then10pan to look at whoever is MOVING nearby — like the hidden cameras in The Truman11Show. Do not mount CCTV or the big screen in non-lobby worlds.1213## Files1415- `engine/world/62-cctv-truman.js` — the core system. IIFE exposing16 `window.__tinyworldCCTV`. **4-space body indent on purpose** so the17 duplicate-declaration guard in `tools/check.js` (which only scans 2-space18 top-level decls) ignores its locals.19- `engine/world/63-cctv-placement.js` — mounts cams + monitors on lobby room20 `enter`, tears down on non-lobby `enter` or `leave`. Exposes21 `window.__tinyworldCCTVPlacement`. Lobby detection is22 `window.__TW_LOBBY_WORLD_SLUG || 'tidewater-bay'`.23- `engine/world/58-lobby-presentation.js` — the lobby-only big screen; its24 `tick()` cuts between slides and the hottest live feed.25- `scripts/landing-feed.js` + `styles/landing.css` — the public landing-page26 live-worlds panel. World rows are buttons: click once to expand an island27 CCTV preview (2D canvas from `/api/worlds.preview.cells`), click again to28 collapse. This is a lightweight marketing/front-door CCTV treatment, not a29 Three.js render target.30- Tick wiring: `engine/world/25-animation-loop-schema.js` calls31 `window.__tinyworldCCTV.tick(t,dt)` then `window.__tinyworldLobby.tick(t,dt)`32 **before** `renderScene()` so feeds captured this frame appear this frame.3334## How it works3536- Each camera owns a `PerspectiveCamera` + a small `WebGLRenderTarget`37 (`FEED_W×FEED_H`, 4:3). `tick()` round-robins captures (`CAPTURES_PER_FRAME`,38 capped at `FEED_FPS`) using `renderer.setRenderTarget(rt); renderer.render(scene, cam)`39 — **always save/restore the previous render target** (`getRenderTarget()`),40 and bail if `renderer`/`scene` aren't ready.41- The monitor material is a `ShaderMaterial` (CRT_FRAG): luminance B&W, scanlines,42 a rolling interference bar, hash static, vignette, and `uSignal` dropout that43 dissolves toward static. A second canvas texture (`tCaption`) bakes the camera44 name + live date/time + blinking REC and is composited in the shader. The45 material is tagged `userData.windowLightEffect = true` + `lightVisual = true`46 so `prepareFadeable()` does NOT swap it for a fade material (skill:47 tinyworld-render-performance).48- Must end the fragment with `#include <colorspace_fragment>` (r185 output color49 space), like every other ShaderMaterial here.50- **Subjects**: `setSubjectsProvider(() => [{pos:Vector3, name}])`. The room51 feeds `WS.subjects()` (self + peer avatar sprite positions). Cameras + monitors52 live under `WS.avatarParent()` — the SAME local frame as the avatars and lobby53 screen — so subject positions need no conversion.54- **Truman tracking**: `aim()` scores each in-range subject by `prox*0.6 +55 moved*1.0` (movement dominates) using per-feed last-position memory; it lerps56 `curLook` toward the winner (`TRACK_LERP`) or idle-sweeps (`IDLE_LERP`). A57 per-feed `activity` score spikes on motion and decays (`ACTIVITY_DECAY`).58- **Hot feed**: `activeFeed(minActivity)` / `feedsByActivity()` let the lobby59 screen auto-cut to whichever camera has something happening. `glitch(id,amt)`60 drops `uSignal` briefly to sell a cut.6162## Placement (63)6364On lobby `enter` only (after a 350ms delay so cells + lobby screen exist) it65mounts:66- `lobby-l` / `lobby-r` — flank both sides of the presentation screen, angled at67 the crowd (toward +z).68- `pumpkincam` — over the biggest `kind:'pumpkin'` cell (scans `world[][]`,69 sorts by floors).70- `treecam-1/2` — over the tallest `kind:'tree'` cells.71Monitors stack up the sides of the lobby screen. `window.__tinyworldCCTVFeeds`72lists mounted feed ids. On non-lobby `enter` or `leave`, clear feeds, disable73capture, and remove monitors.7475## Lobby cutting (58)7677`build()` stashes `screenMesh` + `slideMat`. The screen is shown only in the78configured lobby world, not every island. The state machine auto-advances slides79(`AUTO_ADVANCE`), then after `SLIDE_DWELL` swaps the screen material to a80`monitorMaterialFor(hotFeed)` for `FEED_DWELL`, then back. Manual presenter81`go()` and `hide()` snap back to slides. New API: `tick, setCycle, showSlides,82showFeed, liveFeed`.8384## QA8586- `?cctv=demo` (or `=1`) drops 4 monitors around origin watching a bobbing test87 subject and self-drives the tick — verifies the CRT look + tracking without a88 multiplayer room.89- Landing page QA for `scripts/landing-feed.js`: if local `/api/worlds` is not90 available, mock `window.fetch('/api/worlds')`, append a fresh copy of the91 script, click `.hero-feed-link`, and assert `.hero-feed.is-expanded`, one92 `.hero-feed-cctv-canvas`, and the `.hero-feed-cctv-meta` status/link render.93 The panel should anchor below the nav (`.hero-feed.is-expanded`) and scroll94 internally instead of overflowing the hero.95- Headless sanity: eval module 62 under a THREE stub, `addCamera` + `tick` a few96 frames with a moving subject, assert `activeFeed()` returns the feed and97 `activity > 0`.98- `npm run check` (duplicate-decl + i18n), `npm run smoke`, `./publish.sh`.99100## Pitfalls101102- Don't capture every feed every frame — round-robin, or the extra103 `renderer.render` passes tank FPS (the renderer contract is single-pass per104 feed; this is the only sanctioned extra-RT path here besides pixelation).105- Don't forget to restore the render target after capturing, or the main scene106 renders into the last feed's RT (black screen).107- Keep monitor materials off the fade pipeline via the `windowLightEffect` tag.108- Cameras + monitors MUST be added under `avatarParent()`, not `scene`, or they109 won't inherit the tinyverse scale/offset and subject tracking will be wrong.