[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 で同じ動きを保つのが難しくなります。そこで、カメラも列車も動かさず、客車をカメラの回りに持ってくることにしました。
- 席の「目」の目印が、カメラの位置に重なるように、客車を動かす
- 席の「前」の向きが、カメラの向きに合うように、客車を回す
- 前後の客車は、同じ向きで並べる(窓の外に見える)
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.avatar | VRM の大きさ |
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)