学会発表の前になると、毎回ひとりで部屋で喋って練習している。でも「人前」の緊張感はどうやっても再現できない。そこで、Meta Quest 3 で聴衆の前に立てるVR世界を自作してみた。ブラウザ(WebXR / A-Frame)で動くので、ヘッドセットでURLを開くだけで講堂に立てる。PCからでもWASD+マウスで歩ける。
参考にしたのはこの論文。
Bachmann, Subramaniam, Born & Weibel (2023). Virtual reality public speaking training: effectiveness and user technology acceptance. Frontiers in Virtual Reality. https://www.frontiersin.org/journals/virtual-reality/articles/10.3389/frvir.2023.1242544/full
ざっくり内容:
つまり「仮想でも聴衆の前で話すと上達する。ただし聴衆の反応(フィードバック)が効きそう」というのが大きな学び。まずは"聴衆の前に立つ"体験を作るところから始めた。
論文の聴衆は4人だったが、せっかくなので満員の講堂にしてみた(人数は設定で変えられる)。
リアルな3D人物モデルを大量に並べるのは重いし、無料で使えるものも無い。そこで定番のビルボード方式にした。常にこちらを向く板に人物写真を貼る。
写真は対話型の生成AIに「緑背景・椅子に座った全身・正面」をグリッド状に18人まとめて出してもらい、それを切り分けて使った。クロマキー(緑抜き)はPillowだけで軽く処理:
def chroma_key(img):
a = np.asarray(img.convert("RGBA")).astype(np.int16)
r, g, b = a[..., 0], a[..., 1], a[..., 2]
is_green = (g > 90) & (g - r > 30) & (g - b > 30)
a[..., 3] = np.where(is_green, 0, a[..., 3]) # 緑を透過に
return Image.fromarray(a.astype(np.uint8), "RGBA")
グリッド画像をマスに分割→緑抜き→余白を詰める、という切り出しスクリプトを書いて、高解像度シート3枚から54人ぶんのスプライトを生成した。セル間の白い区切り線が残ってチラついたり、上の段の足が侵入したりと地味にハマったが、端の細い明線だけをピンポイントで消す処理でなんとかなった。
板は three.js のメッシュにして、毎フレーム、カメラの方向へヨーだけ回す:
tick: function () {
this.cam.object3D.getWorldPosition(this._c);
for (var i = 0; i < this.sprites.length; i++) {
var p = this.sprites[i].position;
this.sprites[i].rotation.y = Math.atan2(this._c.x - p.x, this._c.z - p.z);
}
}
フラットな板なので数百人並べても軽い。遠目には立派な観客席に見える。
最初の聴衆は、生成AIに一度にたくさん(6×3=18人)出してもらって切り出していたが、1人あたりの解像度が低くて荒かった。そこで 1枚あたりの人数を減らして(4×2=8人)高解像度で作り直したら、ぐっと鮮明になった。表情は集中して聞いている雰囲気に寄せて、基本は真顔で生成している。
止まった写真だと人形っぽいので、エンジン側で軽く動きを足した。最初は全員に常時「呼吸」の上下を入れたら、全員がずっと揺れていて逆に気持ち悪かった。なので「普段は静止 → たまに1秒くらい小さく動く」方式に変更。人ごとに開始タイミングをずらすと、客席全体では時々誰かがもぞっと動く自然な感じになった。
// 普段は静止。たまに小さな動き(約1秒)を入れ、しばらく止まる
if (t < u.animEnd) {
var e = Math.sin(Math.PI * (1 - (u.animEnd - t) / u.animDur)); // 0→1→0
yOff = e * u.amp; rOff = e * u.swing;
} else if (t >= u.nextT) {
u.animEnd = t + (u.animDur = 0.8 + Math.random() * 0.8);
u.amp = 0.02 + Math.random() * 0.03;
u.swing = (Math.random() < 0.5 ? -1 : 1) * (0.02 + Math.random() * 0.03);
u.nextT = u.animEnd + 6 + Math.random() * 20; // 次までしばらく静止
}
ヘッドセットを被って演台に立つと、たしかに「うっ」と一瞬身構える。人の形と視線が並んでいるだけでこれだけ違うのは発見だった。論文の「仮想聴衆でも効果あり」が体感としても腑に落ちる。
一方で、論文の指摘どおり聴衆が無反応だと張り合いがない。次は、
あたりを入れて、論文の「フィードバックが効くのでは」という仮説を自分の練習で試してみたい。
デモはここ → /vr6(高画質+たまに動く・最新版)//vr4(写真の聴衆)//vr3(3Dモデル版)。一覧は /vr_menu から。Quest 3 のブラウザでそのままVRに入れる。