[Astro] #207 星空の列車に乗り込む(客車 GLB の加工・席の特定・VRM を座らせる)

[Astro] #207 星空の列車に乗り込む(客車 GLB の加工・席の特定・VRM を座らせる)

はじめに

#206 で、空の渦から列車が来て止まるところまで作りました。今回は、止まった列車の看板をクリックして、乗り込むところまでの記録です。

  • 客車は、Sketchfab の GLB(Katydid 作 Resident-evil train)を、一両に切り出して使います
  • 看板をクリックすると確認が出て、「乗る」で暗転して車内に入ります
  • 席は、進行方向左側の、向かい合う席の窓際です。向かいの席に、VRM が座ります
  • 列車は動かしません。窓の外の空の時刻を早送りして、星が流れるのを見せます
  • 汽笛を 1 回鳴らし、あとは走行音をループします
車内の席から見た景色
ファイル役割
WiredGalaxyTrain.tsx看板のクリック、乗車の流れ、VRM の読み込み
utils/galaxyTrain.ts客車の取り付け、乗車(board)、揺れ、席の目印
utils/galaxyTrainSign.ts看板。通常の駅名と、確認(乗る / やめる)の 2 つの絵
utils/galaxyTrainSound.ts汽笛と走行音

1. 客車の GLB を加工する

元のモデルは何両もつながっていて重いので、Blender で一両に切り出しました。そのまま使うと、次の問題がありました。

読み込めない(EXT_texture_webp)

テクスチャが WebP で、EXT_texture_webp を必須にしています。gltf-transform で扱うときは、拡張を登録します。

import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);

GLB Compressor(自作)も、この登録が抜けていて読めませんでした。同じ一行で直しました。

ドアがない

切り出した一両の端には、ドアがありませんでした。別の車両のドアのノードを、この車両へ移し替えました(mergeDocuments で 2 つの文書を一つにして、ノードを付け替えます)。ドアの裏側は穴が開くので、板を一枚置いて塞いでいます。

外壁がない

外側の壁は、ドアや窓とは別の「外殻」の 3 つのマテリアルでできていて、切り出しのときに落ちていました。元のモデルから外殻だけを取り出し、頂点を溶接して、meshoptimizer で三角形を減らしました(41,315 → 8,228)。面ごとの法線にして、黒一色のマテリアル(hull_black)を付けています。

ちらつき

外殻と内装が同じ位置に重なっていて、動くたびにちらつきました(Z-fighting)。外殻のマテリアルに polygonOffset を付けて、少し奥へ押しています。

m.polygonOffset = true;
m.polygonOffsetFactor = -2;
m.polygonOffsetUnits = -8;

車内にいるときは、これを切って、FrontSide にします。

白くなる

走っている間だけ白く光る問題は、空に溶ける色の補間が、客車では白に寄っていたのが原因でした。客車には、その補間をやめて、MeshBasicMaterial に元の色を入れました。凝った演出は入れない方が安定します。

2. 席の場所を調べる

座席は多いのですが、向かい合う席は、左側では 1 か所だけでした。GLB の座標で、床の高さと座面の高さを断面で調べて、目の高さを決めました。

項目GLB の座標
床z = -2.0
座面の上z ≈ -1.68
座ったときの目z ≈ -0.95

この座標は、seat の設定で上書きできます。コンソールの __train.seatNow() で、いまの値を出せます。

"seat": { "eye": [-17.0, 13.85, -0.95], "fwd": [0, 1, 0], "avatar": [-17.0, 15.05, -2.0], "avatarFwd": [0, -1, 0] }

3. 乗車の方式

列車を道に沿って動かして、カメラの所まで持ってくる方法もありますが、PC・スマホ・VR で同じ動きを保つのが難しくなります。そこで、カメラも列車も動かさず、客車をカメラの回りに持ってくることにしました。

  1. 席の「目」の目印が、カメラの位置に重なるように、客車を動かす
  2. 席の「前」の向きが、カメラの向きに合うように、客車を回す
  3. 前後の客車は、同じ向きで並べる(窓の外に見える)
c.rotation.y = camYaw - Math.atan2(f.x, f.z);   // 向きを合わせる
c.position.add(camPos.clone().sub(eyeWorld));    // 目をカメラに重ねる

窓の外は、もとの空のドームです。車内の大きさは、カメラの位置を中心に拡大して整えます(board.cabin、既定 1.3)。

4. 看板のクリック

看板は、キャンバスに描いた絵を、箱の前の面(マテリアル 4 番)に貼っています。クリックされた場所は、レイの当たった UV で分かるので、絵の中のボタンの四角と比べて判定します。

<primitive object={sign.root} onClick={(e) => {
  if (e.face?.materialIndex !== 4) return;
  const hit = sign.hit(e.uv);    // 'name' | 'yes' | 'no' | null
}} />
  • 通常の絵をクリックすると、「この列車に 乗りますか?」と、「乗る」「やめる」の絵に変わります
  • 「乗る」で 1.1 秒かけて暗転し、乗車の処理をします
  • UV で判定するので、マウスでも VR のレーザーでも、同じ処理で動きます

5. VRM を座らせる

向かいの席の目印に、VRM を置きます。読み込みは、既存の VRM と同じ方法です。

  • GLTFLoader に VRM 用の拡張を足して読み込み、VRMUtils.rotateVRM0 で向きをそろえる
  • Sitting Idle.vrma を createVRMAnimationClip でクリップにして、AnimationMixer で再生する
  • 向きは、両腕の骨の位置から、アバターの正面を求めて、席の「前」に合わせる
  • 夜でも顔が見えるように、小さな点光源を一つ付ける

大きさは、客車に合わせると VRM が大きく見えたので、board.avatar(既定 0.75)で縮めています。座面に埋まるときは、board.lift(上へ)と board.shift(カメラ側へ)で合わせます。

暗い車内では、アバターだけが光って見えます。VRM の材質の色(色・影の色・縁の光・発光)を、同じ割合で暗くしています(board.dim)。点光源を足しても、シーンの照明が支配的なので、効きませんでした。

発車したら、首と頭を 4 対 6 で窓の側へ回します(board.lookYaw)。目の向きは、窓の外の方向に置いた見えない点を見ます。アニメが首を動かしていない場合は、毎フレーム重ねて回らないように、回す前の姿勢を覚えておきます。

6. 発車の演出

座って 3 秒待つと、汽笛が鳴って、発車します。

  • 列車は動かしません。外のドーム(半径 100)にぶつかる矛盾を避けるためです
  • かわりに、窓の外の空の時刻を、14 秒かけて少しずつ早送りします(window.__skyOffsetMs を、毎フレーム足す)。星が流れます
  • 車両は、カメラの位置を軸に、ごくわずかに傾けて揺らします
  • 降りるときは、Esc キーか、視界の下の「降りる」を押します

音

Web Audio で、汽笛を 1 回だけ鳴らし、そのあと走行音を、ループで重ねます。音量は、走る速さに合わせて上がります。

"sound": { "run": "/assets/se/電車走行中2.mp3", "volRun": 0.7 }

7. 設定のまとめ

data/weather.json の skyToday.coalsack.train に書きます。

キー意味
coach客車の GLB のパス
coachLen外から見た客車の長さ
board.cabin乗ったときの車内の拡大率
board.avatarVRM の大きさ
board.delay / ramp / rate発車までの秒数 / 加速の秒数 / 空の早送りの倍率
seat席の座標
board.lift / board.shiftアバターを、座面へ持ち上げる量 / カメラ側へ寄せる量(GLB の元の座標。決定値 0.1 / 0.15)
board.dimアバターの明るさ(1 = 元の色)。暗い車内で、光って見えないように下げる
board.lookYaw / lookSec発車後に、首を窓の側へ回す角度(度)/ かける秒数
coachBrightIn車内の明るさ(0 = 真っ暗、1 = 本来の色)。決定値 0.05
sound音のファイルと音量

8. 間違いの記録

  • ドアだと思ったものが外殻だった: 端面の黒い板を、ドアの葉だと取り違えました。外殻の一部でした
  • 設定の置き場所の案内を間違えた: weather.json に skyToday がないのに、「既にある項目へ足す」と案内しました。新しいブロックとして足します
  • capDark の説明を間違えた: 暗くなるのは地平線の下だけで、夜空の明るさは変わりません
  • 開発サーバーの更新で、アバターが消えた: 更新のたびに部品が二重に動き、VRM が別の部品に持っていかれました。VRM が外れていたら付け直す処理で防ぎます。本番(更新なし)では起きません
  • 車両の間が広すぎた: 突き出した金色の枠まで含めて長さを測っていました。外殻の箱で測り直しました

9. 制約

  • VR(Quest)とスマホでの確認は、していません
  • 窓の外は、星が流れるだけで、地上の景色はありません
  • 車輪はありません
  • 座席の位置は、このモデル専用の値です。別の客車にするときは、調べ直します

関連

  • #206 星空に回転する暗い渦を開ける
  • Sky Today(今日の空)

モデル: Katydid「Resident-evil train」(Sketchfab、Free Standard)