3d-orbit-inspect-demo について
単一の 3D オブジェクトを 2 種類のジェスチャーで調べる Three.js スキルです。ホバーでカメラを傾け、クリックでオブジェクトをレンズ前に持ち上げます。描画、入力、アクセシビリティ、動きの制御方法を扱います。
主な機能
- 01ホバー時のカメラ傾斜
- 02オブジェクトの持ち上げと検査
- 03ドラッグ回転、ズーム、復帰
- 04キーボード、フォーカス、状態通知
- 05タッチと動きを減らす設定への対応
活用シーン
- 013D 展示物にホバー検査を追加
- 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 もカスタムシェーダーも使いません。
仕組み: ホバーすると、レイキャストがオブジェクトにヒットしている間だけ感度が上がり、カメラがオブジェクトの周りを数度傾きます。クリックすると、カメラではなくオブジェクトが第2のレンダリングレイヤーに渡され、レンズの前の固定位置まで持ち上がります。2つのモードで共有する状態は、「クリック可能な間、またはオブジェクトを手に持っている間は傾きを止める」というルールだけです。このルールを外すと、クリックする前に対象がずれてしまいます。
近いスキル:webgl-3d-object(浮遊するメッシュ。インスペクトなし)、3d-virtual-tour(「オービットインスペクション」はウォークスルーの1モードで、カメラ自体が移動します)。オブジェクトが1つで、それに対するジェスチャーが2つある場合に使ってください。
demo.htmlを再利用してください。ジョイント、部屋、傾き、持ち上げ、元の位置への復帰はすべて1つのモジュールにあります。updateLean、updateHand、openInspect、closeInspect、ポインターブロック、drawをコピーしてください。メッシュを独自のオブジェクトに差し替えれば、ほかにジョイントの名前が出てくるのはFROM、RR、Q_BENCHだけです。
ソースに含まれるもの/含まれないもの
Kibori(kibori.html、セクション「8b. インスペクト — アイテムを手に取る」)には、クリックで持ち上げてドラッグで回転する機能、アイドル時のホイール回転、「近くで見る」という合図、ホバー時のズームインカーソルがあり、クリックで対象外を選ぶ、Esc、スクロール、または閉じるボタンで閉じます。ホバーオービットはありません。カメラはスクロールで各ステーション間を移動するときに数度傾くだけです。また、自由なズームもありません。ホイールはページをスクロールし、アイテムを閉じるためです。したがって:
- 変更せずに移植したもの:アイテムを持ち上げ、カメラは移動させないこと、layer-1の第2パス、持ち上げ時のイージング
1-(1-k)^4.4、表示に1.0秒、元に戻すのに0.60秒、素早く閉じるのに0.28秒、下降前にまっすぐ元の位置へ戻る動き(sstep(0,.36)の後にsstep(.24,1))、ドラッグ時の1pxあたり0.0085/0.0065ラジアン、スピンの減衰exp(-2.4 dt)、2.2秒後に0.26ラジアン/秒でアイドル回転すること、クリック判定が6px未満かつ600msであること、上部バーとキャプションの間の帯、合図のピル。移植していないもの: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 動いただけで揺れます。 - ホバー状態はメディアクエリーではなく、最後のポインター入力で判定します。
(hover:hover)はハイブリッドノート PC やヘッドレス Chrome では正しく機能しません。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()が必要です。シーンの背景があると、2 回目の描画パスで再度クリアされてしまいます。 - パースペクティブを保って切り替え、位置を急に変えません。 カメラ空間で補間します。
x/zとy/zを線形補間してから、補間した奥行きを掛けます。ワールド空間で補間すると奥行き方向に滑り、最初の 100 ms はパーツが横方向にほとんど動きません。ワールド空間の「開始」ポーズは保存し、移動中のオブジェクトから読み直しません。こうすることで、キャンセルした持ち上げ動作は正確なベンチ上のポーズに戻ります(150 ms 後に Esc を押し、小数点以下 4 桁まで一致することを確認済みです)。 - 手の中ではカメラを固定し、追従させません。 手の中のポーズはカメラ空間で定義されています。インスペクション中も傾きが滑らかに変化し続けると、パーツが泳ぐように動きます。固定し、閉じるときに再開します。
- 帯の中央に配置し、帯に合わせたサイズにします。
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)。持ち上げ動作の途中から閉じても、ゼロから再開してはいけません。 - ズームは滑らかにし、範囲を制限します。 fit の 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) |
| ホバー時の傾き | az ±0.30、el ±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。環境:黒い箱の中にある 3 枚の発光カードから生成した PMREM、environmentIntensity 0.55。アクセントカラーは残り火の #dbad64 のみ(キューリング、補助ライト、フォーカス)。
モーションの低減、非表示、負荷
prefers-reduced-motion:傾きなし(作品はベンチ上の斜め向きの静止ポーズ)、持ち上げは瞬時(rate = 1e3)、アイドル回転なし、キューのスライドなし。ドラッグ、キー操作、ズームは引き続き動作します。これらは直接操作です。visibilitychangeでループをキャンセルします。再開時にlastをリセットし、最初のフレームで停止中の時間を積算しないようにします。- 負荷:Apple GPU 上のヘッドレス Chrome(Metal)で測定。レンダリング 1 フレームあたり CPU 約 0.3 ms、ドローコール 3 回、約 250 三角形。1440x900 で 2 パスの持ち上げ状態の同期フレームは 8.6 ms、2880x1800(DPR 2)では 14.2 ms。静止時のアイドル状態ではフレームをレンダリングしません。調整するのは DPR です。シャドウマップは固定で負荷はなく、三角形の数は重要ではありません。
高負荷につながる落とし穴
- 持ち上げた作品が黒くなる。 ライトはカメラのレイヤーでフィルターされます。
light.layers.enable(1)を設定してください。 - 作品が現れると部屋が消える。 2 回目の
renderでscene.backgroundが再クリアされます。setClearColorとautoClear = falseを使用してください。 - 最初のクリックでシェーダーのコンパイルが引っかかる。 作品を照らすパスはライトの構成(ランプ)が異なります。読み込み時に両方の状態を一度ずつ描画してください。
- 作品を動かした後も影が残る。
shadowMap.autoUpdate = falseに設定し、作品のレイヤーが変わったときにneedsUpdateを設定してください。設定しないとベンチに影が残ります。 - クリック後もキューとカーソルが残る。 開くときに両方をクリアしてください。
zoom-inカーソルはヒット時のみ、手の上ではgrab/grabbingを表示します。 - 閉じるとフォーカスが失われる。 ルール 13 を参照してください。
Object3D.cloneはuserDataをシリアライズする。 レイヤーの切り替えではなくクローンを使う場合は、事前にuserDataを空にしてください(そこにレンダーターゲットやポリゴンがあると、クローン処理が止まります)。
検証
1440x900 と 390x844 で読み込みます。ポインターを 3 つの位置にホバーさせます(左に傾く、右に傾く、傾かない)。作品上で押して離すと開き、作品上を押したままドラッグして離しても開きません。手の上でドラッグ、ホイール、ピンチを操作します。空白部分のクリック、Esc、Close のそれぞれで、持ち上げの途中 150 ms でキャンセルした場合も含め、作品がベンチ上の正確な静止ポーズに戻ります。Tab でボタンに移動でき、Enter で開き、フォーカスが Close に移ってから戻ります。モーション低減設定では静止ポーズが表示され、持ち上げは瞬時に行われます。コンソールにエラーがないことを確認します。
KIBORI 木彫から抜粋。ベンチ上の作品を手に取って回せる、暗い WebGL の工房ページです(セクション 8b)。ホバー時の傾きとドリーは新たに追加され、同じイージングと HUD に合わせて作られています。