3d-orbit-inspect-demo

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

3D 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:
webgl-3d-object
(a floating mesh, no inspect),
3d-virtual-tour
(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.
Reuse
demo.html
: the joint, the room, the lean, the lift and the return are all in one module. Copy
updateLean
,
updateHand
,
openInspect
,
closeInspect
, the pointer block and
draw
. Swap the meshes for your own object; nothing else names the joint except
FROM
,
RR
and
Q_BENCH
.
Three.js · WebGL。不使用 OrbitControls,也不使用自定义 shaders。
机制: hover 会让 camera 在对象周围倾斜几度,其 gain 仅在 raycast 命中对象时上升;click 则把 object(而不是 camera)交给第二个 render layer,将它抬升到镜头前的固定位置。这两种模式除了一个规则外不共享任何状态:只要有可能触发 click,或物件已经在手中,lean 就会冻结。移除该规则后,目标会从 click 下方滑走。
最接近的 skills:
webgl-3d-object
(悬浮 mesh,无 inspect)、
3d-virtual-tour
(其 “orbit inspection” 只是 walkthrough 的一种模式;飞行的是 camera 本身)。当场景中只有一个对象,并且该对象承载两种 gesture 时,使用本 skill。
复用
demo.html
:joint、room、lean、lift 和 return 全部位于同一个 module 中。复制
updateLean
、
updateHand
、
openInspect
、
closeInspect
、pointer 代码块和
draw
。将 meshes 替换为你自己的对象;除
FROM
、
RR
和
Q_BENCH
外,没有其他内容依赖 joint 的名称。

What is and is not in the source

源中包含与不包含的内容

Kibori (
kibori.html
, 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:
  • Ported unchanged: the item is lifted, the camera is not flown; the layer-1 second pass; the
    1-(1-k)^4.4
    lift ease; 1.0 s in, 0.60 s home, 0.28 s fast close; the way home straightens before it descends (
    sstep(0,.36)
    then
    sstep(.24,1)
    ); drag 0.0085 / 0.0065 rad per px; spin damping
    exp(-2.4 dt)
    ; 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 uses
    ACESFilmic
    and a flat dim.
  • 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(
kibori.html
,“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,并会关闭物件。因此:
  • 原样移植:物件被抬起,camera 不飞行;layer-1 second pass;
    1-(1-k)^4.4
    lift ease;1.0 s 进入、0.60 s 归位、0.28 s 快速关闭;归位时先摆正、再下降的路径(先
    sstep(0,.36)
    ,再
    sstep(.24,1)
    );drag 为 0.0085 / 0.0065 rad/px;spin damping 为
    exp(-2.4 dt)
    ;闲置 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 使用
    ACESFilmic
    和 flat dim。
  • 按 Kibori 风格在此新建:hover lean、wheel/pinch dolly、方向键和 +/- 控制、focus return、live region、touch sway。

Rules, each with the failure it prevents

规则及其所防止的问题

  1. 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.
  2. Gain from a raycast, target from the pointer relative to the piece.
    g
    rises to 1 only while the ray hits (tau 0.30 s); the target is
    clamp((pointer - pieceCentre)/(0.18 W), -1, 1)
    . Screen-centre pointer maps make the piece drift whenever the mouse is anywhere on the page.
  3. Bound and damp. Azimuth ±0.30 rad, elevation ±0.09 rad,
    1 - exp(-4.5 dt)
    . Unbounded lean turns the object edge-on; an undamped one jitters with a 1 px mouse.
  4. Hover is the last pointer, not a media query.
    (hover:hover)
    lies on hybrid laptops and headless Chrome. Read
    pointerType
    on
    pointermove
    ; touch gets a slow sway (±0.6 of the bound at 0.55 rad/s) so the depth is still shown without hover.
  5. 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
    touch-action:none
    on the canvas only while inspecting, so the page can still scroll when it is not.
  6. Lift by layer, not by clone or
    .visible
    .
    The piece moves to layer 1, the room renders on layer 0, a fullscreen quad at opacity
    0.80 e
    goes between,
    clearDepth()
    , then layer 1. Lights need
    layers.enable(1)
    or the lifted piece renders black. The scene needs
    background = null
    and manual
    clear()
    ; a scene background re-clears pass two.
  7. Perspective-true hand-off, no pop. Interpolate in camera space:
    x/z
    and
    y/z
    linearly, 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).
  8. 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.
  9. Sit at the band's middle, sized by it.
    fit = min(0.92 bandH, 0.88 W) / (2 R ppu)
    with
    ppu = H / (2 D tan(fov/2))
    , D = 1.0 m. Measure the caption's settled top (
    top - 14 - 40
    ) before it fades in; a piece sized for the whole viewport runs under the caption.
  10. Close is its own clock. On close,
    e0 = r0 = insOut(k)
    and
    k
    restarts from 1; a piece turned further takes up to 1.5x longer (
    span
    ). Closing from the middle of the lift must not restart from zero.
  11. Zoom is eased and bounded. 0.70 to 1.45 of fit, wheel
    exp(-dy 0.0012)
    , ease
    1 - exp(-8 dt)
    , 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.
  12. Pitch is bounded and relaxes. 0.25 to 1.30 rad, home 0.72, pull home at
    exp(-0.35 dt)
    . A flat timber turned past ~1.3 is a hairline.
  13. Keyboard and focus. A real button opens (
    Inspect the joint
    ); Enter and Space work. On open, focus the Close button and set the caption
    inert = false
    ; Tab is trapped to it; arrows turn 0.14 rad,
    +
    -
    zoom 15%, Esc closes. Return focus to the opener after the page copy is visible again: the copy is
    visibility:hidden
    while inspecting and
    focus()
    on a hidden element silently does nothing.
  14. A live region, not aria-live on the dialog. Announce "Inspecting ..." on open and "Back on the bench." on close through a separate
    role=status
    node, cleared then set 30 ms later so a repeat is announced.
  1. 在 pointerdown 时及 inspect 期间冻结 lean。 否则,在一次 100 ms 的 click 期间 camera 会持续缓动,导致 pointerup 时的 raycast 错过已移动 6 px 的物件。应在 pointerup 时再次 raycast,并使用物件所在的同一 layer。
  2. Gain 来自 raycast,target 来自相对于物件的 pointer。
    g
    仅在 ray 命中时升至 1(tau 0.30 s);target 为
    clamp((pointer - pieceCentre)/(0.18 W), -1, 1)
    。如果以屏幕中心为基准映射 pointer,那么无论鼠标位于页面何处,物件都会漂移。
  3. 设置边界并施加阻尼。 Azimuth 为 ±0.30 rad,elevation 为 ±0.09 rad,采用
    1 - exp(-4.5 dt)
    。无边界的 lean 会让对象转到侧视;没有阻尼时,鼠标仅移动 1 px 也会引发抖动。
  4. Hover 应依据最后一次 pointer 位置,而不是 media query。
    (hover:hover)
    在混合型笔记本和 headless Chrome 上会“说谎”。在 pointermove 时读取 pointerType;touch 使用缓慢摆动(边界值的 ±0.6,速度 0.55 rad/s),以便在没有 hover 时仍能表现深度。
  5. Click 与 drag 共用同一次 pointerdown。 位移低于 6 px 且时长短于 600 ms 时打开;其他任何操作都视为 drag,绝不会打开。在物件位于手中时,同一个 6 px 判定用于区分 “turn” 与 “click away”。仅在 inspect 期间为 canvas 设置
    touch-action:none
    ,这样非 inspect 状态下页面仍可滚动。
  6. 通过 layer 完成 lift,而不是使用 clone 或
    .visible
    。
    物件移至 layer 1,room 在 layer 0 上渲染,中间插入 opacity 为
    0.80 e
    的 fullscreen quad,调用
    clearDepth()
    ,然后渲染 layer 1。Lights 必须执行
    layers.enable(1)
    ,否则被抬起的物件会渲染为黑色。Scene 必须设置
    background = null
    并手动调用
    clear()
    ;scene background 会在第二遍 pass 中再次清屏。
  7. 以符合透视关系的方式完成 hand-off,不能出现跳变。 在 camera space 中插值:先将
    x/z
    和
    y/z
    线性插值,再乘以插值后的 depth。World-space lerp 会在深度方向滑动,而且在最初 100 ms 内物件几乎不会横向移动。World 中的 “from” pose 应被存储,绝不能从移动中的对象重新读取;因此取消 lift 时,物件会准确回到原来的 bench pose(已验证在 150 ms 时按 Esc 取消后,两者精确到小数点后四位)。
  8. 在手中保持 camera 稳定,绝不能让它追逐物件。 Hand pose 在 camera space 中定义;如果 inspect 期间 lean 继续缓动,物件就会漂移。冻结 lean,并在 close 后恢复。
  9. 让物件位于 band 中央,并以 band 为基准确定尺寸。
    fit = min(0.92 bandH, 0.88 W) / (2 R ppu)
    ,其中
    ppu = H / (2 D tan(fov/2))
    ,D = 1.0 m。在 caption 淡入前,测量其稳定后的 top(
    top - 14 - 40
    );按整个 viewport 尺寸确定的物件会跑到 caption 下方。
  10. Close 拥有自己的时钟。 关闭时,
    e0 = r0 = insOut(k)
    ,且
    k
    从 1 重新开始;旋转幅度更大的物件最多需要 1.5 倍时长(
    span
    )。从 lift 中途关闭时,不得从零重新开始。
  11. Zoom 需要缓动并设有边界。 范围为 fit 的 0.70 到 1.45;wheel 使用
    exp(-dy 0.0012)
    ,缓动使用
    1 - exp(-8 dt)
    ,pinch 使用手指间距之比。超过 1.5 后,物件会与 caption 重叠;低于 0.7 时,会比它放在 bench 上时更小。Close 时重置为 1。
  12. Pitch 有边界且会松弛回位。 范围为 0.25 到 1.30 rad,home 为 0.72,并以
    exp(-0.35 dt)
    回归 home。将扁平木材转到约 1.3 rad 以上时,边缘会细如发丝。
  13. 键盘与 focus。 使用真实的 button 打开(
    Inspect the joint
    );Enter 和 Space 均可用。打开时,将焦点置于 Close button,并设置 caption
    inert = false
    ;Tab 会被限制在该按钮上;方向键旋转 0.14 rad,
    +
    -
    缩放 15%,Esc 关闭。只有在页面文案重新可见之后,才把焦点还给打开按钮:inspect 期间文案为
    visibility:hidden
    ,而对隐藏元素调用
    focus()
    不会产生任何效果。
  14. 使用独立的 live region,而不是在 dialog 上设置 aria-live。 通过单独的
    role=status
    节点,在打开时播报 “Inspecting ...”,在关闭时播报 “Back on the bench.”;先清空内容,再于 30 ms 后设置,以确保重复操作时仍会播报。

Numbers

参数

thingvalue
lens, rest cameraFOV 34, elevation 0.40 rad, 1.50 m (portrait:
0.92 / aspect
)
hover leanaz ±0.30, el ±0.09, damping 4.5/s, gain tau 0.30 s
lift1.0 s,
1 - (1-k)^4.4
, arc
sin(pi e) * 0.06
m, hand at D = 1.0 m
home0.60 s (0.28 s fast), rotate
sstep(0,.36)
, descend
sstep(.24,1)
dim behindopacity
0.80 e
, same colour as the room
lamp on the piecewarm point,
3.0 e
, up 0.5 / left 0.6 / toward eye 0.35 m, layer 1 only
turn0.0085 / 0.0065 rad/px, damp 2.4/s, idle 0.26 rad/s after 2.2 s
budget
dt
clamp 1/30, DPR cap 2, shadow map static (2048), render only when something moved
项目值
镜头、静止 cameraFOV 34,elevation 0.40 rad,1.50 m(portrait:
0.92 / aspect
)
hover leanaz ±0.30,el ±0.09,damping 4.5/s,gain tau 0.30 s
lift1.0 s,
1 - (1-k)^4.4
,arc
sin(pi e) * 0.06
m,hand at D = 1.0 m
home0.60 s(快速关闭为 0.28 s),rotate
sstep(0,.36)
,descend
sstep(.24,1)
dim behindopacity
0.80 e
,与 room 同色
物件上的 lampwarm point,
3.0 e
,上移 0.5 / 左移 0.6 / 朝向 eye 0.35 m,仅 layer 1
turn0.0085 / 0.0065 rad/px,damp 2.4/s,闲置 2.2 s 后为 0.26 rad/s
预算
dt
clamp 1/30,DPR cap 2,shadow map static(2048),仅在有对象移动时渲染

Room

房间

Dark room
#0a0806
,
FogExp2 0.42
(materials that must not go murky set
fog:false
), key
0xfff2df
2.0 from upper left with the only shadow, fill
0xaec7df
0.76, rim
0xffdeb4
0.72, hemisphere 0.19, a low ember point
0xffb46a
. Environment: a PMREM of three emissive cards in a black box at
environmentIntensity 0.55
. Ember
#dbad64
is the only accent (cue ring, kicker, focus).
深色房间
#0a0806
,
FogExp2 0.42
(对必须保持清晰、不被雾化影响的 materials 设置
fog:false
),key
0xfff2df
2.0 来自左上方,并且是唯一投射阴影的光源;fill
0xaec7df
0.76;rim
0xffdeb4
0.72;hemisphere 0.19;低位 ember point
0xffb46a
。Environment:在一个黑色 box 中放置三张 emissive card 并生成 PMREM,
environmentIntensity 0.55
。Ember
#dbad64
是唯一的强调色(cue ring、kicker、focus)。

Reduced motion, hidden, cost

减少动态效果、页面隐藏与性能开销

  • prefers-reduced-motion
    : no lean (the piece rests at the three-quarter bench pose), the lift is instant (
    rate = 1e3
    ), no idle spin, no cue slide. Drag, keys and zoom still work: they are direct manipulation.
  • visibilitychange
    cancels the loop; resume resets
    last
    so the first frame does not integrate the pause.
  • 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.
  • prefers-reduced-motion
    :不执行 lean(物件保持在三分之四视角的 bench pose);lift 立即完成(
    rate = 1e3
    );无 idle spin;无 cue slide。Drag、按键和 zoom 仍然有效,因为它们属于直接操作。
  • visibilitychange
    会取消 loop;恢复时重置
    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.
    scene.background
    re-clears in the second
    render
    ; use
    setClearColor
    and
    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.
    shadowMap.autoUpdate = false
    , then
    needsUpdate
    when the piece changes layer, or a ghost shadow stays on the bench.
  • Cue and cursor stay after a click. Clear both on open;
    zoom-in
    cursor only on hit,
    grab
    /
    grabbing
    in the hand.
  • Focus lost on close. See rule 13.
  • Object3D.clone
    serialises
    userData
    .
    If you clone instead of switching layers, empty
    userData
    first (render targets and polygons in there stall the clone).
  • 被抬起的物件是黑色的。 Camera 会按 layer 过滤 Lights;使用
    light.layers.enable(1)
    。
  • 物件出现时 room 却消失了。
    scene.background
    会在第二次
    render
    中清屏;使用
    setClearColor
    和
    autoClear = false
    。
  • 首次 click 时发生 shader compile hitch。 Lit-piece pass 使用不同的 light set;在 load 时将两种状态都绘制一次。
  • 已经离开的物件仍留下阴影。 设置
    shadowMap.autoUpdate = false
    ,然后在物件切换 layer 时设置
    needsUpdate
    ,否则 bench 上会残留 ghost shadow。
  • Click 后 cue 和 cursor 没有消失。 打开时清除两者;
    zoom-in
    cursor 仅在命中时显示,物件位于手中时使用
    grab
    /
    grabbing
    。
  • Close 后焦点丢失。 见规则 13。
  • Object3D.clone
    会序列化
    userData
    。
    如果选择 clone 而不是切换 layer,请先清空
    userData
    (其中的 render targets 和 polygons 会卡住 clone)。

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 构建。