用途:在 Tiny World Builder 中修改世界观 CCTV /“楚门的世界”式监控摄像头。摄像头为低分辨率黑白安防画面,监视大厅,并把画面送入实体监视器和大厅演示大屏。摄像头待机时会扫视,随后平移看向附近正在移动的对象;不要在非大厅世界挂载 CCTV 或大屏。
涉及文件:engine/world/62-cctv-truman.js 是核心系统,以 IIFE 暴露 window.__tinyworldCCTV,其函数体刻意使用 4 空格缩进,以便 tools/check.js 的重复声明检查(只扫描 2 空格顶层声明)忽略其局部变量。engine/world/63-cctv-placement.js 在进入大厅房间时挂载摄像头与监视器,进入非大厅或离开时拆除,暴露 window.__tinyworldCCTVPlacement,大厅检测依据 window.__TW_LOBBY_WORLD_SLUG || 'tidewater-bay'。engine/world/58-lobby-presentation.js 是仅限大厅的大屏,其 tick() 在幻灯片与最热实时画面之间切换。scripts/landing-feed.js 与 styles/landing.css 是公开落地页 live-worlds 面板:世界行是可点击按钮,首次点击展开岛屿 CCTV 预览(由 /api/worlds.preview.cells 生成的 2D canvas),再次点击收起;这是轻量级营销/门面 CCTV 处理,不是 Three.js 渲染目标。Tick 接线由 engine/world/25-animation-loop-schema.js 在 renderScene() 之前依次调用 window.__tinyworldCCTV.tick(t,dt) 和 window.__tinyworldLobby.tick(t,dt),以保证本帧捕获的画面在本帧可见。
工作原理:每个摄像头拥有一个 PerspectiveCamera 和一个小型 WebGLRenderTarget(FEED_W×FEED_H,4:3)。tick() 按轮转方式捕获(CAPTURES_PER_FRAME,受 FEED_FPS 限制),使用 renderer.setRenderTarget(rt); renderer.render(scene, cam),并始终保存/恢复之前的渲染目标(getRenderTarget());renderer/scene 未就绪时退出。监视器材质是 ShaderMaterial(CRT_FRAG),实现亮度黑白、扫描线、滚动干扰条、哈希噪点、暗角以及 uSignal 掉线时向噪点溶解。第二个 canvas 纹理(tCaption)烘焙摄像头名称、实时日期/时间和闪烁 REC,并在着色器中合成。材质标记 userData.windowLightEffect = true 与 lightVisual = true,使 prepareFadeable() 不将其替换为淡出材质。片元必须以 #include <colorspace_fragment> 结尾(r185 输出色彩空间)。
目标与追踪:通过 setSubjectsProvider(() => [{pos:Vector3, name}]) 设置目标来源,房间画面源为 WS.subjects()(自身与对等头像精灵位置)。摄像头与监视器位于 WS.avatarParent() 之下,与头像和大屏处于同一局部坐标系,因此目标位置无需转换。aim() 使用每个画面的上次位置记忆,按 prox0.6 + moved1.0 为范围内目标打分(移动占主导),将 curLook 向获胜者插值(TRACK_LERP)或进行待机扫视(IDLE_LERP)。每个画面的活动分在运动时升高并衰减(ACTIVITY_DECAY)。activeFeed(minActivity) / feedsByActivity() 让大厅大屏自动切到有事件发生的摄像头;glitch(id,amt) 短暂压低 uSignal 以营造切换感。
摆放(63):仅在进入大厅时(延迟 350ms,等待 cells 与大屏就绪)挂载 lobby-l / lobby-r(分列演示屏两侧,朝向人群 +z)、pumpkincam(位于最大的 kind:'pumpkin' cell 上方,扫描 world[][] 并按层排序)、treecam-1/2(位于最高的 kind:'tree' cell 上方)。监视器沿大厅大屏两侧堆叠。window.__tinyworldCCTVFeeds 列出已挂载的 feed id。进入非大厅或离开时清空 feeds、禁用捕获并移除监视器。
大厅切换(58):build() 保存 screenMesh + slideMat。大屏仅在配置的大厅世界显示,而非每个岛屿。状态机自动推进幻灯片(AUTO_ADVANCE),在 SLIDE_DWELL 之后将屏幕材质替换为 monitorMaterialFor(hotFeed) 并保持 FEED_DWELL,然后切回。手动 presenter 的 go() 与 hide() 会立即回到幻灯片。新增 API:tick、setCycle、showSlides、showFeed、liveFeed。
QA:?cctv=demo(或 =1)在原点周围放置 4 个监视器,观察上下浮动的测试目标并自驱动 tick,无需多人房间即可验证 CRT 外观与追踪。落地页 QA:若本地 /api/worlds 不可用,mock window.fetch('/api/worlds'),追加脚本的新副本,点击 .hero-feed-link,断言 .hero-feed.is-expanded、一个 .hero-feed-cctv-canvas、以及 .hero-feed-cctv-meta 状态/链接渲染;面板应锚定在导航下方(.hero-feed.is-expanded)并在内部滚动,不溢出 hero。无头检查:在 THREE stub 下 eval 模块 62,addCamera 并用移动目标 tick 几帧,断言 activeFeed() 返回该 feed 且 activity > 0。命令:npm run check(重复声明 + i18n)、npm run smoke、./publish.sh。
注意事项:不要每帧捕获所有画面,应轮转,否则额外的 renderer.render 会拖垮 FPS(渲染器约定每个 feed 单次渲染;这是除像素化外唯一被批准的额外 RT 路径)。捕获后务必恢复渲染目标,否则主场景会渲染进最后一个 feed 的 RT(黑屏)。通过 windowLightEffect 标记让监视器材质避开淡出管线。摄像头与监视器必须添加在 avatarParent() 下而非 scene 下,否则不会继承 tinyverse 缩放/偏移,目标追踪会出错。