关于 3d-orbit-inspect-demo
面向制作单个 3D 物体交互的 Three.js 技能,演示悬停时相机轻微倾斜、点击后将物体提至镜头前检查。重点说明双渲染层、输入判定、无障碍与动效处理。
主要功能
- 01悬停时相机倾斜
- 02点击提起并检查物体
- 03拖动旋转、缩放与归位
- 04键盘、焦点及状态播报支持
- 05触控与减少动态效果支持
适用场景
- 01为单个 3D 展品添加悬停观察
- 02制作点击提起物体的检查交互
- 03实现键盘与触控可用的物体查看器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill 3d-orbit-inspect-demo安装后在 Claude Code 中输入/3d-orbit-inspect-demo确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
demo.htmlPROMPT.mdagents/openai.yaml详细介绍
3D 环绕与查看
Three.js · WebGL。不使用 OrbitControls,也不使用自定义着色器。
**实现机制:**悬停时,摄像机会绕着物体倾斜几度;只有射线检测命中物体时,倾斜幅度才会逐渐增大。点击后,物体(而非摄像机)会被移交给第二个渲染层,并抬升到镜头前方的固定位置。这两种模式不共享状态,只有一条规则:只要点击操作可用,或物体已拿在手中,倾斜就会冻结。去掉这条规则,目标就会在点击前从鼠标下方滑开。
最接近的技能:webgl-3d-object(悬浮网格,无查看功能)、3d-virtual-tour(其中的“环绕查看”是漫游的一种模式;摄像机本身会飞行)。如果只有一个物体,且需要在其上提供两种手势,就选用此技能。
复用 demo.html:关节、房间、倾斜、抬升和归位都在同一个模块中。复制 updateLean、updateHand、openInspect、closeInspect、指针代码块和 draw。将网格替换为自己的物体即可;除 FROM、RR 和 Q_BENCH 外,其他部分都不依赖关节名称。
源码中包含和不包含的功能
Kibori(kibori.html,章节“8b. 查看 — 拿起一件物品”)支持点击抬起并拖动旋转、静止时自动转动、显示“仔细看看”提示和悬停时显示缩放光标,还支持点击外部、按 Esc、滚动或点击关闭按钮来关闭。它没有悬停环绕:摄像机只会在不同站点之间随滚动平移时倾斜几度。它也没有自由缩放,因为滚轮用于滚动页面,并会关闭物体。因此:
- 原样移植:抬起物体,不让摄像机飞行;layer-1 二次渲染;
1-(1-k)^4.4抬升缓动;进入用时 1.0 s,归位用时 0.60 s,快速关闭用时 0.28 s;归位时先扶正再下降的方式(先执行sstep(0,.36),再执行sstep(.24,1));拖动时每像素旋转 0.0085 / 0.0065 rad;旋转阻尼exp(-2.4 dt);静止 2.2 s 后以 0.26 rad/s 自动转动;点击条件为移动距离小于 6 px 且用时不超过 600 ms;顶部栏与说明文字之间的带状区域;提示气泡。未移植:Kibori 手写的色调映射过程和物体后方房间的模糊效果;此演示使用ACESFilmic和纯色暗化。 - 在此处按照 Kibori 的方式实现:悬停倾斜、滚轮/双指捏合平移、方向键和 +/- 控制、焦点返回、实时区域、触摸摆动。
规则及其防止的问题
- 在
pointerdown时以及检查期间冻结倾斜。 否则,摄像机在 100 ms 的点击过程中会持续缓动,导致pointerup时的射线检测错过移动了 6 px 的部件。在pointerup时再次进行射线检测,并使用部件所在的同一图层。 - 射线检测决定增益,指针相对部件的位置决定目标值。 只有射线命中时,
g才会上升至 1(tau 为 0.30 s);目标值为clamp((pointer - pieceCentre)/(0.18 W), -1, 1)。以屏幕中心为基准映射指针,会导致鼠标无论在页面何处,部件都会漂移。 - 限制范围并加入阻尼。 方位角 ±0.30 rad,仰角 ±0.09 rad,采用
1 - exp(-4.5 dt)。不受限的倾斜会让物体转到侧对镜头;没有阻尼则会因 1 px 的鼠标移动而抖动。 - 悬停状态取决于最后一次指针事件,而不是媒体查询。 在混合型笔记本和无头 Chrome 中,
(hover:hover)并不可靠。在pointermove时读取pointerType;触摸设备使用缓慢摆动(幅度为限制值的 ±0.6,角速度为 0.55 rad/s),这样即使没有悬停也能展现深度。 - 点击和拖动共用一次
pointerdown。 移动距离小于 6 px 且耗时小于 600 ms 时打开;其他情况均视为拖动,且绝不打开。在手持交互中,同样以 6 px 为界区分“转动”和“点空白处”。仅在检查期间对画布设置touch-action:none,这样未检查时页面仍可滚动。 - 按图层提起,而不是克隆物体或设置
.visible。 将部件移至图层 1,房间使用图层 0 渲染,在两者之间插入一个不透明度为0.80 e的全屏四边形,调用clearDepth(),然后渲染图层 1。灯光必须调用layers.enable(1),否则提起的部件会渲染成黑色。场景必须设置background = null并手动调用clear();场景背景会在第二个渲染通道中再次清屏。 - 透视正确地交接,不产生跳变。 在摄像机空间中插值:对
x/z和y/z进行线性插值,然后乘以经过插值的深度。世界空间线性插值会导致物体沿深度滑动,而且在最初 100 ms 内,部件几乎不会横向移动。“from”世界坐标姿态需保存,而不是从移动中的物体重新读取,这样取消提起时,部件会准确回到工作台姿态(验证过在 150 ms 时按 Esc,精确到小数点后四位相等)。 - 在手持交互中固定摄像机;绝不追随。 手持姿态定义在摄像机空间中;如果检查期间倾斜仍在持续缓动,部件就会漂移。冻结倾斜,并在关闭时恢复。
- 位于带状区域的中间,并根据该区域确定尺寸。
fit = min(0.92 bandH, 0.88 W) / (2 R ppu),其中ppu = H / (2 D tan(fov/2)),D = 1.0 m。在说明文字淡入前,测量其稳定后的顶部位置(top - 14 - 40);按整个视口设置尺寸的部件会延伸到说明文字下方。 - 关闭使用独立的计时器。 关闭时,
e0 = r0 = insOut(k),并将k重置为 1;转动幅度更大的部件最多需要 1.5 倍的时间(span)。从提起过程的中途关闭时,不能从零重新开始。 - 缩放需缓动且限制范围。 缩放范围为适配尺寸的 0.70 到 1.45 倍;滚轮缩放使用
exp(-dy 0.0012),缓动使用1 - exp(-8 dt),双指缩放按手指间距比例计算。超过 1.5 倍时,部件会与说明文字重叠;低于 0.7 倍时,部件会比放在工作台上时还小。关闭时重置为 1。 - 限制俯仰角并使其恢复。 范围为 0.25 至 1.30 rad,初始值为 0.72,以
exp(-0.35 dt)回到初始值。平坦的木料转过约 1.3 rad 后就会变成一条细线。 - 键盘与焦点。 使用真正的按钮打开(
Inspect the joint);Enter 和 Space 均可操作。打开时,将焦点设到 Close 按钮,并设置说明文字的inert = false;Tab 焦点限制在其中;方向键每次转动 0.14 rad,+和-缩放 15%,Esc 关闭。等页面文本重新可见之后再将焦点返回触发按钮:检查期间文本的visibility:hidden,对隐藏元素调用focus()不会产生任何效果。 - 使用实时区域,不要在对话框上设置 aria-live。 通过单独的
role=status节点,在打开时播报“正在检查……”,关闭时播报“已返回工作台。”;先清空,再在 30 ms 后设置内容,以确保重复播报也能被读出。
数值
| 项目 | 数值 |
|---|---|
| 镜头,静止时的镜头 | FOV 34,仰角 0.40 rad,1.50 m(纵向:0.92 / aspect) |
| 悬停倾斜 | 方位角 ±0.30,仰角 ±0.09,阻尼 4.5/s,增益时间常数 0.30 s |
| 抬升 | 1.0 s,1 - (1-k)^4.4,弧线 sin(pi e) * 0.06 m,手持时距离 D = 1.0 m |
| 归位 | 0.60 s(快速模式下 0.28 s),旋转 sstep(0,.36),下降 sstep(.24,1) |
| 后方变暗 | 不透明度 0.80 e,与房间同色 |
| 藏品上的灯 | 暖色点光源,3.0 e,向上 0.5 / 向左 0.6 / 朝眼睛方向 0.35 m,仅照亮图层 1 |
| 旋转 | 0.0085 / 0.0065 rad/px,阻尼 2.4/s,静置 2.2 s 后以 0.26 rad/s 自动旋转 |
| 性能预算 | dt 上限为 1/30,DPR 上限为 2,阴影贴图静态(2048),仅在有物体移动时渲染 |
房间
暗色房间 #0a0806,FogExp2 0.42(不应变暗的材质设置 fog:false),左上方主光 0xfff2df 2.0,且只有主光投射阴影;补光 0xaec7df 0.76,轮廓光 0xffdeb4 0.72,半球光 0.19,低处有一盏余烬色点光源 0xffb46a。环境:在黑色盒子中放置三张自发光卡片,生成 PMREM,environmentIntensity 0.55。余烬色 #dbad64 是唯一的强调色(提示环、补光、焦点)。
减少动态效果、页面隐藏与性能开销
prefers-reduced-motion:不倾斜(藏品保持在工作台上的四分之三视角),抬升瞬间完成(rate = 1e3),不自动旋转,不滑动提示。拖动、按键和缩放仍可使用:这些属于直接操作。visibilitychange会取消循环;恢复时重置last,使首帧不会累计暂停期间的时间。- 性能开销:在 Apple GPU 的无头 Chrome(Metal)上测得,每个渲染帧约占用 0.3 ms CPU,3 次绘制调用,约 250 个三角形;在 1440x900 分辨率下,双通道抬升状态的同步帧耗时 8.6 ms,在 2880x1800 分辨率(DPR 2)下为 14.2 ms。静止时闲置状态渲染帧数为零。影响最大的因素是 DPR;阴影贴图静态且开销可忽略,三角形数量并不重要。
高开销陷阱
- 抬起的藏品是黑色的。 摄像机按图层筛选灯光;使用
light.layers.enable(1)。 - 藏品出现时房间消失。 第二次
render时,scene.background会重新清屏;使用setClearColor和autoClear = false。 - 首次点击时着色器编译卡顿。 藏品受光通道使用不同的灯光组合(包括灯具)。加载时先绘制两种状态各一次。
- 藏品离开后仍有阴影。 设置
shadowMap.autoUpdate = false,然后在藏品切换图层时设置needsUpdate,否则工作台上会残留阴影。 - 点击后提示和光标仍然保留。 打开时清除两者;仅命中目标时显示
zoom-in光标,手持时显示grab/grabbing。 - 关闭后焦点丢失。 参见规则 13。
Object3D.clone会序列化userData。 如果选择克隆而不是切换图层,请先清空userData(其中的渲染目标和多边形会拖慢克隆)。
验证
在 1440x900 和 390x844 分辨率下加载;在三个指针位置悬停(向左倾斜、向右倾斜、不倾斜);在藏品上按下并松开(打开);按下后拖过藏品再松开(不打开);在手持状态下拖动、滚轮缩放和双指缩放;点击空白处、按 Esc 和点击 Close 均会让藏品精确回到工作台姿势,包括在抬升 150 ms 后取消的情况;按 Tab 可聚焦按钮,按 Enter 可打开,焦点落在 Close 上并在关闭后返回;减少动态效果时显示静止姿势且瞬间抬升;控制台无报错。
摘自 KIBORI 木彫,这是一个暗色 WebGL 工坊页面,其中每件工作台上的藏品都可以拿起并在手中转动(第 8b 节)。悬停倾斜和推拉镜头是新增功能,采用相同的缓动和 HUD。