Skills 项目 3D Virtual Tour 技能:用 Three.js 构建可导航、可检查、可验证的虚拟漫游
Skills 项目 3D Virtual Tour 技能:用 Three.js 构建可导航、可检查、可验证的虚拟漫游
导读
3d-virtual-tour 是 Skills 仓库 3D 渲染技能族 中的一员,面向用 Three.js(或类似渲染器)构建"有引导、可交互"的三维虚拟漫游场景:作者化相机路径、房间与户型图导航、轨道检查(orbit inspection)、模式平滑交接以及渐进加载。本文以 SKILL.md 为主干,结合其 REFERENCES.md 与仓库中相邻技能的实现证据,完整讲解漫游模型建立、路线编排、相机所有权、导航同步、性能与生命周期等落地要点。读完你将掌握一套可直接指导 Agent 或人工开发者在 Three.js 场景中实现"建筑漫游 / 房产看房 / 博物馆 / 展厅"级虚拟导览的完整工作流与验收清单。
技能定位:什么场景使用 3d-virtual-tour
技能 frontmatter 中给出了明确的触发条件(description):用于构建有引导且可交互的三维环境虚拟漫游,支持作者化相机路径、房间与户型图导航、轨道检查、模式平滑交接与渐进加载;典型场景包括建筑漫游、房产看房、博物馆、展厅,以及任何基于 Three.js 或相似渲染器的可探索空间。
在 3D 渲染技能总览 的选型表中,它的定位是"Guided walkthroughs, room and floor-plan navigation, orbit inspection, and smooth return to the tour"(引导式漫游、房间与户型图导航、轨道检查、平滑返回漫游)。agents/openai.yaml 中的默认提示词给出了 Agent 调用范式:
Use $3d-virtual-tour to build a guided walkthrough of this scene with room navigation, floor-plan links, and orbit inspection.
值得注意的是它与相邻技能的分工(可参见 3D Orbit and Inspect Demo 技能 中的"Nearest skills"说明):3d-orbit-inspect-demo 解决的是单个物体的悬停环绕与点击举起检查(相机不飞行,物体本身被移交到第二渲染层);而 3d-virtual-tour 的"orbit inspection"是漫游的一种模式——相机本身会飞行、属于整个漫游状态机的一部分。选型时注意:webgl-3d-object 是漂浮的 hero 网格(无检查模式),3d-virtual-tour 是一条穿过某个场所的相机路径。三者不要混用。
一、建立漫游模型:先盘点场景,再决定导航模式
动手写代码之前,必须完整盘点既有场景:坐标系、比例尺、楼层高度、门洞开口、渲染器、相机控制与资源加载路径(asset-loading path),并保留既有架构与交互,而不是推倒重来。随后选择体验真正需要的导航模式,技能给出了四种模式及其"相机所有者"分工表:
| 模式 | 相机所有者 | 导航方式 |
|---|---|---|
| 引导漫游(Guided tour) | 作者化路线采样器(authored route sampler) | 滚动、播放/暂停、上一站/下一站 |
| 房间目的地(Room destination) | 作者化的房间位姿或路线停靠点 | 房间列表、热区(hotspot)或户型图 |
| 检查(Inspect) | 有边界的轨道控制器(bounded orbit controller) | 拖拽、双指缩放、缩放、返回 |
| 自由行走(Free walk,按需) | 带碰撞的第一人称控制器 | 移动与视角控制 |
技能给出的默认组合是 Seijaku 风格体验:以引导路线为主,辅以房间目的地与可选的模型检查;只有被明确要求或确有必要时才加自由行走——仅仅使用轨道控制器并不能提供可步入的室内空间。另一个重要的诚实性约束:如果源数据只是一组全景图片,应使用链接式全景视点(linked panorama viewpoints),而不是暗示场景中存在可导航的几何深度。这一条决定了整个实现的数据模型走向,必须在建模阶段就明确。
二、编排一条穿过真实开口的路线
2.1 路线键(Route Keys)的数据结构
技能要求路线键至少存储四类信息:
- 进度/时间(progress/time);
- 位置(position);
- 注视目标或朝向(look target or orientation);
- 视野(field of view,FOV)。
停靠点(stops)需要稳定的 ID、标签、路线坐标与关联的资产组(asset groups)。特别强调:路线坐标必须独立于页面高度或动画时钟,这样当页面布局变化(如滚动页面的段落高度改变)时,房间链接依然有效——这是把"UI 状态"与"3D 相机状态"解耦的关键设计。
2.2 开口、眼高与节奏
编排路径时的硬规则:
- 在狭窄开口的之前、之中、之后都要放置关键帧(Place keys before, within, and after narrow openings),否则相机在穿越门洞时会失控;
- 使用相对楼层高度的眼高(floor-relative eye height),并考虑抬高的地板与台阶;
- 在转弯或重要景观前减速;
- 保持地平线稳定,避免位置、注视方向与视野同时大幅变化;
- 优先选择物理上合理的宽高比,而不是用过度超广角镜头把房间拉伸变形。
2.3 插值方式:非均匀时间 Hermite 是默认,弧长采样解决匀速问题
技能明确指出 Seijaku 的实现采用非均匀时间 Hermite 关键帧(nonuniform timed Hermite keys),分别用于位置、注视与视野;centripetal spline(向心样条)也是可用的替代方案。但无论用哪种曲线,都必须意识到:任意平滑曲线都可能把角切掉(cut a corner)——即使两个关键帧本身不与墙壁碰撞,插值出的中间路径也可能穿墙。因此必须检查完整的插值路径,包括相机近裁剪面(near-plane)的净空,对墙壁、天花板、家具与门框逐项验证。
另一个关键认知:曲线参数不等于距离或旅行时间。当需要近似匀速移动时,应当做弧长采样(arc-length sampling);而当需要制造停顿、逐次揭示地标时,则应有意按时间分配片段(intentionally time segments)。技能建议查阅所安装渲染器的 Curve 参数化方法(Three.js 的 Curve、CatmullRomCurve3 详见 REFERENCES.md 中收集的官方 API 文档链接)。
2.4 朝向与投影矩阵
- 使用朝向键时,用四元数 slerp(quaternion slerp)插值朝向;
- 如果插值的是注视目标(look target),必须防止目标与相机交叉或重合——目标越过相机位置会翻转相机方向;
- 当 FOV 或宽高比(aspect)改变时,必须更新投影矩阵(updateProjectionMatrix),否则画面会失真或裁剪错误。
三、单一控制器拥有相机:状态机与模式交接
3.1 每帧一次最终写入
技能的核心原则是:使用一个小型导航状态机(navigation state machine),并且每帧只有一次对相机的最终写入。输入只负责请求状态变更;路线采样器、轨道控制器、过渡处理器不能各自独立地修改同一个活动相机。这从根本上杜绝了"多个控制器互相拉扯相机"的经典 bug。
3.2 进入检查模式:先保存,再初始化
进入检查(inspection)前必须保存:
- 当前路线坐标(route coordinate);
- 活动停靠点(active stop);
- 播放状态(playback state);
- 任何滚动书签(scroll bookmark)。
随后从观察者当前所在建筑侧的一个兼容视角初始化检查(即不要从室内瞬间切到完全对立的室外视角)。检查期间要冻结引导进度(Freeze guided progression while inspecting)。
3.3 交接过渡:直线会穿屋顶,弧线也要验证
位置、朝向与视野的混合交接必须沿一条经过验证的过渡路径进行。技能给出的现实教训非常具体:
- 从室内到室外的直线过渡可能穿过屋顶;
- 即使是一条向上的弧线,也需要检查(它可能撞上楼板或挑檐);
- 没有可信的连续路径时,使用安全航点(safe waypoints),或者短暂淡出 + 目的地硬切(brief fade and destination cut);
- 轨道控制器的距离与俯仰角必须限制在目标检查区域内(clamp orbit distance and pitch),防止用户把相机拖出建筑、拖进地面或无限拉远。
3.4 返回漫游:恢复、清除、防拉扯
从检查返回时:
- 恢复之前保存的引导状态;
- 重新对账滚动位置(reconcile scroll position);
- 清除陈旧的拖拽速度(stale drag velocity,否则物体会继续惯性滑动);
- 仅在返回前确实在播放时才恢复播放(resume playback only if it was previously running);
- 禁用非活动控制器与过期的阻尼更新,防止它们在过渡完成后继续把相机拉走;
- 新的导航请求应从当前位姿替换待定目的地,而不是排队多个互相竞争的 tween(New navigation requests should replace the pending destination from the current pose rather than queue several competing tweens)。
四、连接房间、地图与控件:同一份停靠点注册表
4.1 单数据源驱动一切 UI
房间卡片(room cards)、章节导轨(chapter rail)、户型图区域(floor-plan regions)与场景热区(scene hotspots)必须由同一份停靠点注册表(stop registry)驱动,保证"列表里选房间"和"在户型图上点房间"最终到达完全一致的相机位姿。
两个易错点:
- 必须使用清晰的世界坐标到平面坐标的变换(world-to-plan transform),不能假设平面图的竖直轴就是世界 Z 轴——户型图通常是俯视投影,坐标轴映射需要显式定义;
- 当前房间或路线段的高亮需要带滞回(hysteresis),在区域边界处避免来回闪烁。
4.2 选房导航:安全路线或有意剪切,绝不直线穿墙
房间选择要么沿一条安全的连接路线行进,要么使用有意的剪切/淡入淡出(intentional cut/fade);绝不能通过直线插值穿过几堵墙(Do not fly a direct interpolation through several walls)。
同时要保持状态一致性:选中的标签与 URL 状态(如果存在)必须与实际目的地同步;只在有意义的导航发生时更新浏览器历史,而不是每帧都写 history。
4.3 可访问性:语义化、键盘可达、焦点管理
- 停靠点用语义化 HTML 按钮或链接,并在图形化地图旁提供键盘可达的房间列表;
- 热区要有标签(label hotspots),焦点必须可见;
- 退出检查时把焦点恢复到触发检查的元素;
- Escape 键与可见的返回按钮都要能释放检查模式。
4.4 滚动页面上的触控策略
- 在滚动页面上,触摸手势应继续滚动,直到观察者显式进入交互模式;
- 指针捕获与
touch-action应限定在活动 canvas 区域内(Scope pointer capture and touch-action to the active canvas region); - 处理指针取消(pointer cancellation)、捕获丢失(lost capture)与失焦(blur);
- 退出时恢复之前的滚动样式;
- 不要拦截表单或无关页面控件上的滚轮或键盘输入——这是"隔离交互区域"的边界纪律。
五、让世界与导航同步
5.1 由路线坐标驱动场景状态
门(doors)、字幕(captions)与曝光(exposure)等"路线驱动"的状态,必须从当前路线坐标派生(derive),而不是由计时器驱动。技能给出了具体失败案例:仅靠计时器的事件,在用户 seek 回退或跳转到某个房间后,可能留下一个挡在相机路径上的关闭的门。此外,除非漫游显式拥有这些设置,否则要保留用户选择的季节与光照覆盖——这与 3D Four Seasons 技能 的"跨季节保持相机状态与交互一致"原则互相呼应。
5.2 预取与加载策略
- 按**路线邻近度(route proximity)**预取即将到达的房间资产;
- 直接选择的(非顺路的)目的地,在揭示之前先加载好;
- 加载期间显示有用的占位内容或保持稳定的视图;
- 失败的可选资产不应困住导航(a failed optional asset should not trap navigation);
- 缓存共享纹理与网格;新的请求到来后,取消或忽略过期的目的地工作。
5.3 区域可见性按模式重算
"区域可见性(region visibility)"(如按房间裁剪/隐藏墙体)只有在当前相机模式下仍然正确时才可使用。沿着引导路径隐藏的区域,在用户切换到轨道检查(绕模型观察)时可能变得可见;此时必须重算可见性或启用所需区域,否则会出现"缺墙少房"的穿帮画面。
5.4 自由行走的边界
如果包含自由行走:
- 移动必须约束在可行走的表面上,并提供碰撞/净空检查;
- Pointer Lock 只提供视角输入,不提供碰撞与地板跟随(Pointer lock supplies look input, not collision or floor following);
- 触屏用户需要等价的导航路径,以及从沉浸式控制中可见的退出方式。
六、运动、加载与生命周期
6.1 时间基准与页面隐藏
- 使用基于帧时间(frame-time)的插值,并在页面隐藏后重置时间基准(reset the time base after hiding the page),否则恢复可见时第一帧会把隐藏期间累积的 dt 一次性补上,造成"瞬移";
- 页面隐藏时暂停播放与不必要的渲染。
6.2 减少动效(Reduced Motion)
在 prefers-reduced-motion 下:
- 保留一个静态但有信息量的视图;
- 房间按钮应能直接选择合成视图(composed views),而不是强制长距离相机飞行。
6.3 音频
- 音频只在用户手势之后开始播放;
- 跨导航保持静音状态(preserve its mute state across navigation)。
6.4 性能纪律与卸载
- 保持单一渲染循环,复用 scratch 对象,避免在指针处理器内做布局读取或大分配;
- 先准备开场房间,把昂贵的场景生成、纹理上传与着色器编译分散到合适的阶段;
- 相机取景(framing)与渲染目标(render targets)要一起 resize;
- 调整窄视构图时不要移动相机进入几何体内部(tune narrow-view compositions without moving the camera into geometry);
- 漫游卸载(unmount)时移除监听器、释放捕获、销毁自有资源(remove listeners, release capture, and dispose owned resources)。
七、验证清单:按技能要求逐项验收
技能在 "Verify" 一节给出了完整的验收矩阵,这是把漫游交付给用户的最后一道关:
- 完整走完路线(正向与反向):检查每一个开口、每一处高程变化、每一次室内/室外过渡;
- 从列表与户型图各选一次每个房间:标签、门、资产与相机目的地必须一致;
- 从起点、室内、终点三个位置进入检查:拖拽/缩放后,返回时必须精确恢复到保存的停靠点与播放状态;
- 中断过渡:用另一个目的地、Escape、窗口 resize 或隐藏标签页间隔打断过渡,确认始终只有一个相机所有者且没有卡死的滚动锁;
- 键盘导航、触摸滚动、指针取消、减少动效、资产加载失败逐项测试;
- 检查桌面与竖屏(portrait)布局、着色器/控制台错误、开场加载、目的地加载与交互帧时间(interaction frame time)。
八、参考资料与源码依据
技能的 REFERENCES.md 是"仅链接"式的参考清单,其中:
- Seijaku 实现参考(外部开源项目):包括作者化相机路线与采样器(authored camera route and sampler)、轨道检查与交接(orbit inspection and handoff)、房间与户型图导航(rooms and floor-plan navigation)三处带行号锚点的实现位置;
- Three.js 官方 API:
Curve(参数化方法)、CatmullRomCurve3(向心/中心样条)、PerspectiveCamera(投影矩阵与 FOV)、OrbitControls(有界轨道控制)与PointerLockControls(指针锁定输入)。
需要澄清的边界:技能的 "Reference" 一节明确说明,Seijaku 的引导漫游与外观检查属于参考技术(reference techniques);而基于碰撞的自由行走与上文额外的可访问性检查,属于实现指导(implementation guidance),并不是对参考实现特性的声称。这一点保证了文章/技能中的"做法"与"对参考项目的断言"严格区分,不会误导读者。
在仓库内部,与本技能配套的使用入口还包括:3D 渲染技能总览(技能选型表与调用示例)、Agent 接口元数据(Codex 等 Agent 的展示名与默认提示词),以及与检查模式形成对照的 3D Orbit and Inspect Demo。按照仓库约定(见根目录 README.md),每个技能文件夹由 SKILL.md(Agent 加载并遵循的操作手册)+ 可选的 REFERENCES.md(仅链接)+ agents/openai.yaml(Codex 接口元数据)组成,3d-virtual-tour 恰好三件齐全。
结语
3d-virtual-tour 提供了一条从"盘点场景 → 选择导航模式 → 编排真实开口的路线 → 单一相机所有者 → 同步世界状态 → 性能与生命周期 → 逐项验收"的完整实施链路。它的价值不在于某一段炫技代码,而在于一整套可被 Agent 与开发者共同遵循的约束系统:路由数据独立于页面布局、插值路径必须逐帧验证净空、交接过渡必须验证可行路径、导航 UI 共享同一停靠点注册表、以及每一处交互都配套可访问性与降级策略。无论是给 Codex/Claude/Cursor 等 Agent 加载,还是作为人工开发的检查清单,这套流程都足以支撑把任意 Three.js 场景升级为"看得懂、走得通、查得清"的专业虚拟漫游体验。