Skills 项目 3D Virtual Tour 技能:用 Three.js 构建可导航、可检查、可验证的虚拟漫游

原创2026-10-08 18:56:581,663 阅读

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" 一节给出了完整的验收矩阵,这是把漫游交付给用户的最后一道关:

  1. 完整走完路线(正向与反向):检查每一个开口、每一处高程变化、每一次室内/室外过渡;
  2. 从列表与户型图各选一次每个房间:标签、门、资产与相机目的地必须一致;
  3. 从起点、室内、终点三个位置进入检查:拖拽/缩放后,返回时必须精确恢复到保存的停靠点与播放状态;
  4. 中断过渡:用另一个目的地、Escape、窗口 resize 或隐藏标签页间隔打断过渡,确认始终只有一个相机所有者且没有卡死的滚动锁;
  5. 键盘导航、触摸滚动、指针取消、减少动效、资产加载失败逐项测试;
  6. 检查桌面与竖屏(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 场景升级为"看得懂、走得通、查得清"的专业虚拟漫游体验。

登录后查看全文
Skills