3d-orbit-inspect-demo
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese3D Orbit and Inspect
3D Orbit and Inspect
Three.js · WebGL. No OrbitControls, no custom shaders.
The mechanism: hover leans the camera a few degrees around the object with a gain that rises only while a raycast hits it, and click hands the object (not the camera) to a second render layer that lifts it to a fixed spot in front of the lens. The two modes share no state except one rule: the lean freezes whenever a click is possible or the piece is in the hand. Remove that rule and the target slides out from under the click.
Nearest skills: (a floating mesh, no inspect), (its "orbit inspection" is one mode of a walkthrough; the camera itself flies). Reach for this when there is one object and two gestures on it.
webgl-3d-object3d-virtual-tourReuse : the joint, the room, the lean, the lift and the return are all in one module. Copy , , , , the pointer block and . Swap the meshes for your own object; nothing else names the joint except , and .
demo.htmlupdateLeanupdateHandopenInspectcloseInspectdrawFROMRRQ_BENCHThree.js · WebGL。不使用 OrbitControls,也不使用自定义 shaders。
机制: hover 会让 camera 在对象周围倾斜几度,其 gain 仅在 raycast 命中对象时上升;click 则把 object(而不是 camera)交给第二个 render layer,将它抬升到镜头前的固定位置。这两种模式除了一个规则外不共享任何状态:只要有可能触发 click,或物件已经在手中,lean 就会冻结。移除该规则后,目标会从 click 下方滑走。
最接近的 skills:(悬浮 mesh,无 inspect)、(其 “orbit inspection” 只是 walkthrough 的一种模式;飞行的是 camera 本身)。当场景中只有一个对象,并且该对象承载两种 gesture 时,使用本 skill。
webgl-3d-object3d-virtual-tour复用 :joint、room、lean、lift 和 return 全部位于同一个 module 中。复制 、、、、pointer 代码块和 。将 meshes 替换为你自己的对象;除 、 和 外,没有其他内容依赖 joint 的名称。
demo.htmlupdateLeanupdateHandopenInspectcloseInspectdrawFROMRRQ_BENCHWhat is and is not in the source
源中包含与不包含的内容
Kibori (, section "8b. Inspect — pick an item up") has click-to-lift with drag-to-turn, an idle wheel, a "Look closer" cue and a zoom-in cursor on hover, and close by click-away, Esc, scroll or its Close button. It has no hover orbit: the camera only leans a few degrees as scroll dollies it between stations. It also has no free zoom, because wheel is the page scroll and closes the piece. So:
kibori.html- Ported unchanged: the item is lifted, the camera is not flown; the layer-1 second pass; the lift ease; 1.0 s in, 0.60 s home, 0.28 s fast close; the way home straightens before it descends (
1-(1-k)^4.4thensstep(0,.36)); drag 0.0085 / 0.0065 rad per px; spin dampingsstep(.24,1); idle turn 0.26 rad/s after 2.2 s; click = under 6 px and 600 ms; the band between the top bar and the caption; the cue pill. Not ported: Kibori's hand-written tone-mapping pass and blur of the room behind the piece; the demo usesexp(-2.4 dt)and a flat dim.ACESFilmic - Built here in the Kibori manner: the hover lean, the wheel/pinch dolly, arrow-key and +/- control, focus return, the live region, the touch sway.
Kibori(,“8b. Inspect — pick an item up”一节)具备 click-to-lift 与 drag-to-turn、idle wheel、“Look closer”提示以及 hover 时显示的 zoom-in cursor;可通过 click-away、Esc、scroll 或其 Close 按钮关闭。它没有 hover orbit:scroll 在各 station 之间 dolly camera 时,camera 只会倾斜几度。它也没有 free zoom,因为 wheel 对应的是页面 scroll,并会关闭物件。因此:
kibori.html- 原样移植:物件被抬起,camera 不飞行;layer-1 second pass;lift ease;1.0 s 进入、0.60 s 归位、0.28 s 快速关闭;归位时先摆正、再下降的路径(先
1-(1-k)^4.4,再sstep(0,.36));drag 为 0.0085 / 0.0065 rad/px;spin damping 为sstep(.24,1);闲置 2.2 s 后以 0.26 rad/s 转动;click 判定为低于 6 px 且短于 600 ms;top bar 与 caption 之间的 band;cue pill。未移植:Kibori 手写的 tone-mapping pass,以及物件后方 room 的 blur;demo 使用exp(-2.4 dt)和 flat dim。ACESFilmic - 按 Kibori 风格在此新建:hover lean、wheel/pinch dolly、方向键和 +/- 控制、focus return、live region、touch sway。
Rules, each with the failure it prevents
规则及其所防止的问题
- Freeze the lean on pointerdown and while inspecting. Otherwise the camera keeps easing during a 100 ms click and the raycast at pointerup misses a piece that moved 6 px. Raycast again at pointerup, on the same layer the piece is on.
- Gain from a raycast, target from the pointer relative to the piece. rises to 1 only while the ray hits (tau 0.30 s); the target is
g. Screen-centre pointer maps make the piece drift whenever the mouse is anywhere on the page.clamp((pointer - pieceCentre)/(0.18 W), -1, 1) - Bound and damp. Azimuth ±0.30 rad, elevation ±0.09 rad, . Unbounded lean turns the object edge-on; an undamped one jitters with a 1 px mouse.
1 - exp(-4.5 dt) - Hover is the last pointer, not a media query. lies on hybrid laptops and headless Chrome. Read
(hover:hover)onpointerType; touch gets a slow sway (±0.6 of the bound at 0.55 rad/s) so the depth is still shown without hover.pointermove - Click and drag are one pointerdown. Under 6 px and under 600 ms opens; anything else is a drag and never opens. In the hand the same 6 px test separates "turn" from "click away". Set on the canvas only while inspecting, so the page can still scroll when it is not.
touch-action:none - Lift by layer, not by clone or . The piece moves to layer 1, the room renders on layer 0, a fullscreen quad at opacity
.visiblegoes between,0.80 e, then layer 1. Lights needclearDepth()or the lifted piece renders black. The scene needslayers.enable(1)and manualbackground = null; a scene background re-clears pass two.clear() - Perspective-true hand-off, no pop. Interpolate in camera space: and
x/zlinearly, then multiply by the lerped depth. A world-space lerp slides in depth and the piece hardly moves sideways for the first 100 ms. The world "from" pose is stored, never re-read from the moving object, so a cancelled lift lands on the exact bench pose (verified equal to four decimals after Esc at 150 ms).y/z - Hold the camera in the hand; never chase it. The hand pose is defined in camera space; if the lean keeps easing while inspecting, the piece swims. Freeze it, resume on close.
- Sit at the band's middle, sized by it. with
fit = min(0.92 bandH, 0.88 W) / (2 R ppu), D = 1.0 m. Measure the caption's settled top (ppu = H / (2 D tan(fov/2))) before it fades in; a piece sized for the whole viewport runs under the caption.top - 14 - 40 - Close is its own clock. On close, and
e0 = r0 = insOut(k)restarts from 1; a piece turned further takes up to 1.5x longer (k). Closing from the middle of the lift must not restart from zero.span - Zoom is eased and bounded. 0.70 to 1.45 of fit, wheel , ease
exp(-dy 0.0012), pinch by finger distance ratio. Past 1.5 the piece overlaps the caption; below 0.7 it is smaller than it sat on the bench. Reset to 1 on close.1 - exp(-8 dt) - Pitch is bounded and relaxes. 0.25 to 1.30 rad, home 0.72, pull home at . A flat timber turned past ~1.3 is a hairline.
exp(-0.35 dt) - Keyboard and focus. A real button opens (); Enter and Space work. On open, focus the Close button and set the caption
Inspect the joint; Tab is trapped to it; arrows turn 0.14 rad,inert = false+zoom 15%, Esc closes. Return focus to the opener after the page copy is visible again: the copy is-while inspecting andvisibility:hiddenon a hidden element silently does nothing.focus() - A live region, not aria-live on the dialog. Announce "Inspecting ..." on open and "Back on the bench." on close through a separate node, cleared then set 30 ms later so a repeat is announced.
role=status
- 在 pointerdown 时及 inspect 期间冻结 lean。 否则,在一次 100 ms 的 click 期间 camera 会持续缓动,导致 pointerup 时的 raycast 错过已移动 6 px 的物件。应在 pointerup 时再次 raycast,并使用物件所在的同一 layer。
- Gain 来自 raycast,target 来自相对于物件的 pointer。 仅在 ray 命中时升至 1(tau 0.30 s);target 为
g。如果以屏幕中心为基准映射 pointer,那么无论鼠标位于页面何处,物件都会漂移。clamp((pointer - pieceCentre)/(0.18 W), -1, 1) - 设置边界并施加阻尼。 Azimuth 为 ±0.30 rad,elevation 为 ±0.09 rad,采用 。无边界的 lean 会让对象转到侧视;没有阻尼时,鼠标仅移动 1 px 也会引发抖动。
1 - exp(-4.5 dt) - Hover 应依据最后一次 pointer 位置,而不是 media query。 在混合型笔记本和 headless Chrome 上会“说谎”。在 pointermove 时读取 pointerType;touch 使用缓慢摆动(边界值的 ±0.6,速度 0.55 rad/s),以便在没有 hover 时仍能表现深度。
(hover:hover) - Click 与 drag 共用同一次 pointerdown。 位移低于 6 px 且时长短于 600 ms 时打开;其他任何操作都视为 drag,绝不会打开。在物件位于手中时,同一个 6 px 判定用于区分 “turn” 与 “click away”。仅在 inspect 期间为 canvas 设置 ,这样非 inspect 状态下页面仍可滚动。
touch-action:none - 通过 layer 完成 lift,而不是使用 clone 或 。 物件移至 layer 1,room 在 layer 0 上渲染,中间插入 opacity 为
.visible的 fullscreen quad,调用0.80 e,然后渲染 layer 1。Lights 必须执行clearDepth(),否则被抬起的物件会渲染为黑色。Scene 必须设置layers.enable(1)并手动调用background = null;scene background 会在第二遍 pass 中再次清屏。clear() - 以符合透视关系的方式完成 hand-off,不能出现跳变。 在 camera space 中插值:先将 和
x/z线性插值,再乘以插值后的 depth。World-space lerp 会在深度方向滑动,而且在最初 100 ms 内物件几乎不会横向移动。World 中的 “from” pose 应被存储,绝不能从移动中的对象重新读取;因此取消 lift 时,物件会准确回到原来的 bench pose(已验证在 150 ms 时按 Esc 取消后,两者精确到小数点后四位)。y/z - 在手中保持 camera 稳定,绝不能让它追逐物件。 Hand pose 在 camera space 中定义;如果 inspect 期间 lean 继续缓动,物件就会漂移。冻结 lean,并在 close 后恢复。
- 让物件位于 band 中央,并以 band 为基准确定尺寸。 ,其中
fit = min(0.92 bandH, 0.88 W) / (2 R ppu),D = 1.0 m。在 caption 淡入前,测量其稳定后的 top(ppu = H / (2 D tan(fov/2)));按整个 viewport 尺寸确定的物件会跑到 caption 下方。top - 14 - 40 - Close 拥有自己的时钟。 关闭时,,且
e0 = r0 = insOut(k)从 1 重新开始;旋转幅度更大的物件最多需要 1.5 倍时长(k)。从 lift 中途关闭时,不得从零重新开始。span - Zoom 需要缓动并设有边界。 范围为 fit 的 0.70 到 1.45;wheel 使用 ,缓动使用
exp(-dy 0.0012),pinch 使用手指间距之比。超过 1.5 后,物件会与 caption 重叠;低于 0.7 时,会比它放在 bench 上时更小。Close 时重置为 1。1 - exp(-8 dt) - Pitch 有边界且会松弛回位。 范围为 0.25 到 1.30 rad,home 为 0.72,并以 回归 home。将扁平木材转到约 1.3 rad 以上时,边缘会细如发丝。
exp(-0.35 dt) - 键盘与 focus。 使用真实的 button 打开();Enter 和 Space 均可用。打开时,将焦点置于 Close button,并设置 caption
Inspect the joint;Tab 会被限制在该按钮上;方向键旋转 0.14 rad,inert = false+缩放 15%,Esc 关闭。只有在页面文案重新可见之后,才把焦点还给打开按钮:inspect 期间文案为-,而对隐藏元素调用visibility:hidden不会产生任何效果。focus() - 使用独立的 live region,而不是在 dialog 上设置 aria-live。 通过单独的 节点,在打开时播报 “Inspecting ...”,在关闭时播报 “Back on the bench.”;先清空内容,再于 30 ms 后设置,以确保重复操作时仍会播报。
role=status
Numbers
参数
| thing | value |
|---|---|
| lens, rest camera | FOV 34, elevation 0.40 rad, 1.50 m (portrait: |
| hover lean | az ±0.30, el ±0.09, damping 4.5/s, gain tau 0.30 s |
| lift | 1.0 s, |
| home | 0.60 s (0.28 s fast), rotate |
| dim behind | opacity |
| lamp on the piece | warm point, |
| turn | 0.0085 / 0.0065 rad/px, damp 2.4/s, idle 0.26 rad/s after 2.2 s |
| budget | |
| 项目 | 值 |
|---|---|
| 镜头、静止 camera | FOV 34,elevation 0.40 rad,1.50 m(portrait: |
| hover lean | az ±0.30,el ±0.09,damping 4.5/s,gain tau 0.30 s |
| lift | 1.0 s, |
| home | 0.60 s(快速关闭为 0.28 s),rotate |
| dim behind | opacity |
| 物件上的 lamp | warm point, |
| turn | 0.0085 / 0.0065 rad/px,damp 2.4/s,闲置 2.2 s 后为 0.26 rad/s |
| 预算 | |
Room
房间
Dark room , (materials that must not go murky set ), key 2.0 from upper left with the only shadow, fill 0.76, rim 0.72, hemisphere 0.19, a low ember point . Environment: a PMREM of three emissive cards in a black box at . Ember is the only accent (cue ring, kicker, focus).
#0a0806FogExp2 0.42fog:false0xfff2df0xaec7df0xffdeb40xffb46aenvironmentIntensity 0.55#dbad64深色房间 ,(对必须保持清晰、不被雾化影响的 materials 设置 ),key 2.0 来自左上方,并且是唯一投射阴影的光源;fill 0.76;rim 0.72;hemisphere 0.19;低位 ember point 。Environment:在一个黑色 box 中放置三张 emissive card 并生成 PMREM,。Ember 是唯一的强调色(cue ring、kicker、focus)。
#0a0806FogExp2 0.42fog:false0xfff2df0xaec7df0xffdeb40xffb46aenvironmentIntensity 0.55#dbad64Reduced motion, hidden, cost
减少动态效果、页面隐藏与性能开销
- : no lean (the piece rests at the three-quarter bench pose), the lift is instant (
prefers-reduced-motion), no idle spin, no cue slide. Drag, keys and zoom still work: they are direct manipulation.rate = 1e3 - cancels the loop; resume resets
visibilitychangeso the first frame does not integrate the pause.last - Cost, measured on an Apple GPU in headless Chrome (Metal): about 0.3 ms of CPU per rendered frame, 3 draw calls, about 250 triangles; 8.6 ms per synchronous frame in the two-pass lifted state at 1440x900 and 14.2 ms at 2880x1800 (DPR 2). Idle at rest renders zero frames. The lever is DPR; the shadow map is static and free, and triangle count does not matter.
- :不执行 lean(物件保持在三分之四视角的 bench pose);lift 立即完成(
prefers-reduced-motion);无 idle spin;无 cue slide。Drag、按键和 zoom 仍然有效,因为它们属于直接操作。rate = 1e3 - 会取消 loop;恢复时重置
visibilitychange,使第一帧不会把暂停时间积分进去。last - 成本(测量环境:headless Chrome 中的 Apple GPU(Metal)):每个已渲染帧约消耗 0.3 ms CPU,3 draw calls,约 250 triangles;两遍渲染的 lifted 状态在 1440x900 下每个同步帧为 8.6 ms,在 2880x1800(DPR 2)下为 14.2 ms。静止空闲时渲染零帧。主要成本来自 DPR;shadow map 保持静态,因此不产生逐帧成本,而 triangle count 并不重要。
Expensive gotchas
容易踩坑之处
- The lifted piece is black. Lights are layer-filtered by the camera; .
light.layers.enable(1) - The room disappears when the piece appears. re-clears in the second
scene.background; userenderandsetClearColor.autoClear = false - A shader compile hitch on the first click. The lit-piece pass has a different light set (the lamp). Draw both states once at load.
- Shadow of a piece that has left. , then
shadowMap.autoUpdate = falsewhen the piece changes layer, or a ghost shadow stays on the bench.needsUpdate - Cue and cursor stay after a click. Clear both on open; cursor only on hit,
zoom-in/grabin the hand.grabbing - Focus lost on close. See rule 13.
- serialises
Object3D.clone. If you clone instead of switching layers, emptyuserDatafirst (render targets and polygons in there stall the clone).userData
- 被抬起的物件是黑色的。 Camera 会按 layer 过滤 Lights;使用 。
light.layers.enable(1) - 物件出现时 room 却消失了。 会在第二次
scene.background中清屏;使用render和setClearColor。autoClear = false - 首次 click 时发生 shader compile hitch。 Lit-piece pass 使用不同的 light set;在 load 时将两种状态都绘制一次。
- 已经离开的物件仍留下阴影。 设置 ,然后在物件切换 layer 时设置
shadowMap.autoUpdate = false,否则 bench 上会残留 ghost shadow。needsUpdate - Click 后 cue 和 cursor 没有消失。 打开时清除两者;cursor 仅在命中时显示,物件位于手中时使用
zoom-in/grab。grabbing - Close 后焦点丢失。 见规则 13。
- 会序列化
Object3D.clone。 如果选择 clone 而不是切换 layer,请先清空userData(其中的 render targets 和 polygons 会卡住 clone)。userData
Verify
验证
Load at 1440x900 and 390x844; hover at three pointer positions (lean left, right, none); press and release on the piece (opens), press-drag-release across it (does not open); drag, wheel, pinch in the hand; click-away, Esc and Close each return the piece to the exact bench pose, including when cancelled 150 ms into the lift; Tab reaches the button, Enter opens, focus lands on Close and returns; reduced motion shows the rest pose and an instant lift; console is clean.
Extracted from KIBORI 木彫, a dark WebGL workshop page where every bench piece can be picked up and turned in the hand (section 8b). The hover lean and dolly are new, built to the same easing and HUD.
在 1440x900 和 390x844 下加载;在三个 pointer 位置执行 hover(向左 lean、向右 lean、不 lean);在物件上按下并释放(应打开),横跨物件按下、拖动并释放(不应打开);在手中执行 drag、wheel 和 pinch;分别使用 click-away、Esc 和 Close 时,物件都应准确恢复到原始 bench pose,包括在 lift 开始 150 ms 后取消的情况;Tab 应能到达 button,Enter 应打开,焦点应落到 Close 并在关闭后返回;reduced motion 应显示 rest pose 并立即 lift;console 应无报错。
提取自 KIBORI 木彫,这是一个深色 WebGL 工艺工作坊页面,bench 上的每件作品都可以被拿起来,在手中旋转(见第 8b 节)。Hover lean 和 dolly 是新增功能,采用相同的 easing 和 HUD 构建。