← 記事一覧に戻る
2026-06-15

発表練習用のVR世界を作ってみた

学会発表の前になると、毎回ひとりで部屋で喋って練習している。でも「人前」の緊張感はどうやっても再現できない。そこで、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に入れる。