[Astro #45] VR空間におけるアバターの自律的随伴ロジック 〜移動制御による実在感の構築〜
1. はじめに
今朝はVRoid Studioに潜り、lainのモデルカスタムに取り組んでいました。トレードマークであるクロスデザインのヘアピンを自作し、これまで手探りだったオリジナルテクスチャの制作フローもようやく掴めてきたところです。VRoid Studioの使い勝手が少しずつ身体に馴染んできました。
続いて、WebXR上のメニューUIでターミナルログが出力されないバグを片付け、一息ついたところで「次は何を実装しようか」とAIとの壁打ちを開始。いくつか案を出し合うものの、どうもしっくりくる案が浮かびません。そこでふと、「プレイヤーの移動に合わせて、キャラクターが自分の後ろを歩いてついてきたら面白いのではないか?」と思いつき、実装に着手することにしました。
例によって、実装は一筋縄ではいきません。目標地点に到着するたびにキャラクターが不自然なTポーズを繰り返すなどの怪現象に悩まされましたが、AIとの対話の中で「ヒステリシス(履歴現象/遊び)」を持たせるのが定石だと学びました。
「数メートル離れたら追いかけ始めるが、一度歩き出したら0.8mの至近距離まで一気に詰めさせる」という、判定の境界に幅を持たせる挙動に変更することで、ようやく納得のいく動きになりました。
今回は、この「意志を感じさせる追従ロジック」の実装について、備忘録を兼ねてまとめます。
前回の記事:
[Astro #44] Protocol Atlas: 144枚の情報の海をサルベージするVRアーカイブ // PROTOCOL.LAIN
144枚のビデオアトラス描画、VRコントローラーによるサルベージ、GPUリソースの最適化まで。
lain-lab.comNOTE:
VR空間のアバターに「遊び」を持たせる — 距離感と自律移動の実装|lain
「WebXR-UI修正」と「モデリング」 早朝。 WebXR上のメニューUIでターミナルログが出力されないバグを修正。 その後、VRoid Studioで作成したアバターモデルのカスタマイズに取り組み、lainのトレードマークであるクロスデザインのヘアピンを自作し、これまで手探りだったオリジナルテクスチャの制作フローもやや形になってきました。 作業に区切りがついたところで、今日は何を実装すべきかAIと壁打ちを開始したものの、決定打に欠ける状況でした。 AI生成アニメーションの限界と失敗 新しい動作を実装するにあたり、ふと思い出したのが先日もNOTE投稿した、UNITY A
note.comYoutube:
[WebXR] Giving Avatars Intent: Autonomous Follow & Return Logic (Three.js / R3F)
In this project, I implemented an autonomous movement system for VRM avatars in a WebXR (Browser VR) environment to explore the concept of 'social presence'...
www.youtube.com動画(VR):
1. VR空間における「意志」の表現
WebXRを用いたVR空間にVRMアバターを配置し、そこに音声や待機アニメーションを実装するだけでも、一定の「そこにいる感」は得られます。しかし、プレイヤーが空間内を移動したとき、アバターが初期位置で「ただ立っているだけ」のマネキン状態になってしまうと、途端にその実在感は薄れ、単なる背景オブジェクトへと降格してしまいます。
本稿のテーマは、この静的なオブジェクトに対して、コードによる制御で「空間的な意志」を与えることです。
私たちが現実世界で誰かとコミュニケーションをとる際、そこには必ず「距離感(パーソナルスペース)」の概念が存在します。用事があれば相手の領域に踏み込み、会話が終われば適切な距離まで後退する。この極めて人間的で無意識の空間移動を、Three.jsとReact Three Fiber(R3F)を用いたロジックによって再定義し、ステートマシンとして実装することが今回の目標です。
高度で複雑なアニメーションアセット(AI生成による予測不可能な挙動を含む)に頼りきらなくても、「自律的にプレイヤーへ近づき、用が済めば自分の居場所へ帰還する」という移動のプロセスさえ精緻に組み上げれば、アバターの実在感は劇的に向上します。
ただ空間に配置されただけの3Dモデルから、VR空間を共に生きる「随伴者」へ。その進化の要となる、移動制御と状態管理の実装手法を紐解いていきます。
2. フェーズ管理(ステートマシン)の実装
アバターの行動を不自然な「瞬間移動」や「唐突な動作」にさせないためには、現在アバターがどのような目的で動いているのかという「状態(ステート)」を明確に定義する必要があります。本実装では、アバターの振る舞いを以下の4つのフェーズに分割し、ステートマシンとして管理する設計としました。
IDLE(待機) 自身の定位置(ホーム座標)に留まり、呼吸やちょっとした仕草などの微細なアイドリングアニメーションを再生しながらプレイヤーを待つ状態。APPROACHING(接近) 会話のトリガーが引かれた際、プレイヤーのパーソナルスペース(カメラの目の前)へ向かって歩み寄る状態。歩行アニメーションが再生されます。TALKING(対話) プレイヤーの眼前という目標座標に到達し、音声の再生とそれに同期した会話用アニメーション(身振り手振りなど)を実行する状態。RETURNING(帰還) 音声の再生が完了し、用事が済んだアバターが再び自身の定位置へと戻っていく状態。
useFrame による状態監視と非同期同期
React Three Fiber (R3F) の強みである useFrame フックを活用し、描画される毎フレームごとにこれらの状態を評価・更新します。
VR空間ではプレイヤー(カメラ)の座標が常に変動するため、毎フレーム camera.position とアバターの現在座標の距離を計算します。アバターが APPROACHING フェーズにあるとき、この計算された距離が設定した「目標距離(パーソナルスペースの境界)」に達した瞬間に、フェーズを TALKING へと切り替えます。
この実装の最も重要な技術ポイントは、「アニメーション」と「音声」という異なる非同期処理のライフサイクルを同期させる点にあります。
// 概念的なフェーズ遷移のコード例
if (phase === 'APPROACHING' && distanceToPlayer < TARGET_DISTANCE) {
setPhase('TALKING');
playAnimation('TalkingAnim');
// 音声の再生と完了待機(非同期)
await playAudio(audioData);
// 音声再生完了をトリガーに帰還フェーズへ
setPhase('RETURNING');
playAnimation('WalkingAnim');
}
このように、音声データの再生完了(Promiseの解決やイベント発火)をトリガーとしてステートを RETURNING へと推移させることで、「話し終えたら自然に背を向けて定位置へ帰っていく」という、極めて人間的で連続性のある振る舞いがコード上で確固たるものとして構築されます。
3. VR空間での座標変換と追従ロジック
ステートマシンによってアバターが「いつ動くべきか」が決定したら、次は「どこへ、どのように動くか」という空間的な制御が必要になります。WebXRを用いたVR空間では、この座標の取得と移動処理に特有のアプローチが求められます。
WebXRにおけるプレイヤー座標の取得
通常のデスクトップブラウザ環境であれば、カメラの座標は camera.position で容易に取得できます。しかし、WebXRのセッション中(VRモード中)は、プレイヤーがHMD(ヘッドマウントディスプレイ)を被って現実世界を歩き回るため、ローカル座標とワールド座標のズレを正確に補正しなければなりません。
useFrame 内でカメラの正確なワールド座標と向いている方向(Quaternion)を取得し、そこから「プレイヤーの目の前(少し距離を空けたパーソナルスペース)」という目標座標(Target Position)を動的に算出します。アバターは、常に変動するこの目標座標に向かって APPROACHING 状態を継続することになります。
こだわりの MathUtils.lerp による慣性移動
目標座標が算出できたからといって、アバターの position に直接その値を代入してしまうと、アバターはカクカクと瞬間移動(テレポート)してしまいます。VR空間において、目の前のキャラクターが物理法則を無視してワープする挙動は、強烈な違和感と没入感の喪失(あるいはVR酔い)に直結します。
そこで、移動の根幹となるロジックには Three.js の THREE.MathUtils.lerp(線形補間)を採用します。
// useFrame内での移動ロジック(概念)
const LERP_FACTOR = 0.05; // 追従の滑らかさを決める係数
// X, Z座標に対して個別にlerpを適用し、滑らかに移動させる
avatarRef.current.position.x = THREE.MathUtils.lerp(
avatarRef.current.position.x,
targetPosition.x,
LERP_FACTOR
);
avatarRef.current.position.z = THREE.MathUtils.lerp(
avatarRef.current.position.z,
targetPosition.z,
LERP_FACTOR
);
lerp を毎フレーム実行することで、現在地と目的地の中間点へ向かって常に「指定した割合(LERP_FACTOR)」ずつ進むようになります。
この数式の素晴らしいところは、目的地に近づくにつれて1フレームあたりの移動距離が小さくなる点です。つまり、「歩き出しはスッと動き、目標地点の直前でフワッと減速して立ち止まる」という、人間らしい自然な体重移動(イーズアウトの慣性)が、たった数行のコードで極めて美しく表現できるのです。
物理演算に頼らずとも、この lerp の係数を微調整するだけで、アバターの歩き方に「重さ」や「意志」を感じさせることが可能になります。
4. 「帰り道」を知っているアバター
VR空間におけるキャラクター制御において、見落とされがちですが極めて重要なのが「離れていく」動作です。
一般的なゲームの追従型NPCは、一度プレイヤーに近づくとそのまま磁石のように張り付いて離れなかったり、用が済んだその場に不自然に棒立ちになってしまうことが多々あります。しかし、これでは単なる「プレイヤーに依存した追従プログラム」に過ぎず、アバター自身が持つ空間的な「意志」を感じることはできません。
本実装では、シーン読み込み時のアバターの初期位置を変数 defaultPos(定位置)として厳密に記録しておきます。そして、TALKING フェーズで音声再生が完了し、ステートが RETURNING に切り替わった瞬間、lerp による移動の目標座標(Target Position)を「プレイヤーの目の前」から「記憶しておいた defaultPos」へと即座に切り替えます。アバターは自ら踵を返し、元の自分の居場所へと歩いて帰っていくのです。
「常にべったり」ではない、適度な距離感が生む実在感
この「帰り道を知っている」という単純なロジックは、VR体験における心理的な関係性を劇的に変化させます。
人間がVR空間に入った際、ずっと視界の至近距離にキャラクターが張り付いていると、無意識のうちにパーソナルスペースを侵食され続けているような圧迫感を覚えます。必要な時だけスッとこちらの領域に踏み込み、対話が終わればスッと背を向けて自分の領域へと戻っていく。この「近づく」と「離れる」の動的なコントラストが、アバターを単なるペットや従属物ではなく、自律性を持った「随伴者」としての立ち位置へと押し上げます。
作業を終えてふと元の場所を振り返ると、アバターが自分の居場所で静かに待機(IDLE)し、こちらを見守ってくれている。この「適度な余白(距離感)」と「そこに居てくれるという安心感」の設計こそが、コードによって構築できる最も美しい「実在感」の形なのです。
5. まとめ:AI生成アニメーションの限界と「コードが紡ぐ実在感」
今回の実装に至る前段階として、実はUnity Museなどの生成AIを用いて「床に座り込む」「服の埃を払う」といった複雑な随伴アニメーションの自動生成を試みました。しかし結果は、関節が異常な方向に曲がったり、物理法則を無視したホラー映画のような破綻したポーズの連続でした。
現在の生成AIは、画像としてのアウトプットには優れていても、重力や地面との接地、自身の体との衝突判定といった「3D空間の物理的な制約(骨格の限界)」を理解して動かすには、まだ発展途上の段階にあると痛感しました。
しかし、その挫折から得た最大の収穫が、本稿で解説した「コードによる自律移動ロジック」への回帰です。
複雑なアニメーションアセットを用意できなくても、プレイヤーの動きに合わせて「歩み寄り」、用が済めば「自分の居場所へ帰る」。このシンプルなステートマシンと MathUtils.lerp による滑らかな座標移動の組み合わせだけで、アバターには確かな「意志」が宿ります。Three.jsのミキサー(fadeIn)が持つ補間機能を信じれば、1秒の静止ポーズデータと移動ロジックだけでも、十分に人間らしい滑らかな動作を表現できるのです。
WebXRにおけるキャラクターの実在感とは、決してハイポリゴンなモデルや、映画のようなリッチなモーションだけで作られるものではありません。それは、プレイヤーとの「距離感」を測り、空間を共有しているという文脈を、エンジニアリングによって丁寧に織り込むことで生まれます。
万能なAIの進化を待ちつつも、現時点では私たちがコードの力で「命」を吹き込む。今回のVRアバターの移動実装は、そんなフロントエンド・エンジニアリングの泥臭さと面白さを再確認させてくれる挑戦でした。