[JavaScript] Three.js + WebXR で動画プレイヤーUIをクラス設計する

はじめに

過去記事で、Three.js上で動画の再生、及び、UIの操作パネルを設置してVRのレーザーポインタでクリックして操作できるまで実装しましたが、 さらに、クラス化とJSONリストを作成し、サムネイル画像を操作パネルの上に表示して、動画プレイヤーUI(サムネ・曲送り)まで実装したのでそのメモです。

過去、記事は以下。

今回もVRのキャプチャ動画をとってみたので置いておきます。





サーバにアップして実際に体験できる形にしたいのですが、勉強・実験・遊びで作ってるのと著作権問題でモデルデータ含めてアップできないので、 また、動画や画像・モデルを差し替えてアップできる状態になったら公開する予定です。

ちなみに、アニメのDVD-BOX、サントラは所有しています。

1. 前回までのおさらい

前回の記事では、Three.js を使って 動画を3D空間に表示するところまでを実装。

具体的には、

  • VideoTexture を使って、動画を PlaneGeometry に貼り付け
  • WebXR 環境でも表示
  • Raycaster を使って、3D空間上のボタンをクリックできる UI を作成

ここまでで、

「3D空間に動画を置いて、UIで操作できる」

という最低限の体験を成立。

ただし、

  • 動画制御がグローバル変数に散らばる
  • 再生・停止・切り替えの責務が曖昧
  • プレイリストや拡張を考えると、構造的に厳しい

つまり、 「動くけど、育てられないコード」になっていました。

そこで今回は、 動画再生まわりをすべて 1つのクラスに集約し、

  • プレイリスト管理
  • 次の動画への自動遷移
  • サムネイル表示
  • UI 操作との連動
  • WebXR / Three.js 両対応

をまとめて整理。

2. なぜ「Videoクラス」を作る必要があったか

  • 動画を切り替えたい
  • プレイリストで順番再生したい
  • 再生終了時に自動で次へ進めたい
  • XR / 非XR の両方で同じ仕組みを使いたい
  • 3D空間の UI ボタンと確実に連動させたい

これらを 関数ベースのまま継ぎ足していくと、次の状態になる。

  • video 要素の参照があちこちに散る
  • 「今どの動画を再生しているか」を管理する場所がなくなる
  • UI 側と動画側で状態がズレる
  • 終了イベントや切り替え処理が重複する

つまり、

再生・切り替え・UI連動の責務が分離できなくなった

という状態で、

  • 動画の生成と管理はどこが責任を持つのか
  • プレイリストの現在位置は誰が知っているのか
  • 「次へ」「前へ」という操作は、どこに命令すべきか

これらを明確にするために、

動画まわりを すべて1つのクラスに閉じ込める判断。

VideoPlay クラスが担う役割

  • 動画要素を内部に持つ
  • プレイリストとインデックスを管理する
  • 再生・停止・切り替えの唯一の窓口になる
  • Three.js / WebXR 側には「結果」だけを渡す

UI は「次へ」「前へ」を呼ぶだけ。 3D空間は VideoTexture を貼るだけ。

こうして初めて、

  • 機能追加しても壊れない
  • XR を足しても構造が崩れない
  • 「動画体験」を1つの部品として扱える

状態に。

3. JSONでプレイリストを定義する

動画プレイヤーをクラス化するにあたって、 最初に手を付けたのが プレイリストの定義方法。

今回は、 すべての基準になるデータを JSON にまとめる方針で実装。

{
  "videos": [
    {
      "name": "gotiusa_op",
      "video": "./assets/video/gotiusa_op.mp4",
      "thumbnail": "gotiusa_op.webp",
      "label": "ご注文はうさぎですか?(opening)"
    }
  ]
}

この JSON は、単なる動画リストではなく、

  • video → 実際に再生される動画ファイル
  • thumbnail → 操作パネルに表示するサムネイル画像
  • label → UI や演出用に表示するタイトル
  • name → 内部識別用の ID

というように、 再生・UI・表示のすべてが同じ1件のデータを参照する構造。

重要なのは、

  • UI が「次の曲」を選ぶ
  • Videoクラスが「次の動画」を再生する
  • サムネイルが「現在の動画」を表示する

これらが 別々の情報源を見ていないという点。

すべてが videos[currentIndex] という同じデータを起点に動くため、

  • 動画とサムネがズレない
  • UI 表示と再生内容が食い違わない
  • WebXR / 非XR で同じロジックが使える

という状態を保てる。

この JSON は、 「動画ファイルの一覧」ではなく、 XR 空間におけるメディア体験の定義ファイルだと考えると分かりやすい。

次のセクションでは、この JSON を読み込み、 動画再生を一手に引き受ける VideoPlay クラスを実装していく。

4. videoPlay クラスの最小構成

このクラスで押さえるべきポイントは、次の3つ。

(1) video要素を JS で生成する理由

this.video = document.createElement('video');

一見すると、HTML に <video> タグを置いたほうが簡単に見える。 しかし、Three.js + WebXR では JS 側で生成する方が都合が良い。

理由は明確で、

  • DOM に依存しない
  • XR / 非XR で同じコードがそのまま動く
  • Three.js の VideoTexture に直接渡せる

特に WebXR では、

  • 画面に表示しない video 要素
  • 再生状態だけを Three.js に反映する

という使い方が基本になる。

video 要素は 「UIとして表示するもの」ではなく、 動画フレームを供給するデータソースとして扱う。

この前提に立つと、 JS で生成するのが自然になる。


(2) VideoTexture を Three.js に渡す

this.texture = new THREE.VideoTexture(this.video);

ここが Three.js 側との接点になる。

VideoTexture を使うことで、

  • 通常の Texture と同じ感覚で扱える
  • MeshBasicMaterialShaderMaterial に渡せる
  • 毎フレーム自動で更新される

特に ShaderMaterial と組み合わせると、

  • 色補正
  • フィルタ処理
  • 演出的なエフェクト

などを 動画に対して直接かけられる。

つまり、 動画は「ただ再生するもの」ではなく、 空間表現の一部になる。

今回の構成では、 再生制御は videoPlay クラスが持ち、 Three.js 側は「貼るだけ」にしている。

この分離が後々効いてくる。


(3) ended イベントで次の動画へ

this.video.addEventListener('ended', () => {
  this.nextVideo();
});

プレイリスト再生の要になる部分。

HTML5 Video には、 再生が終了したタイミングで発火する ended イベントが用意されている。

これを使えば、

  • タイマーで監視する必要はない
  • XR / 非XR を意識しなくていい
  • ブラウザ任せで正確に検知できる

ここで重要なのは、 「次の動画へ進む判断」を UI 側で行っていない点。

UI は、

  • 次へ
  • 前へ

という操作を投げるだけ。

再生終了という 動画固有のイベントは、 動画を管理しているクラス自身が処理する。

これは HTML5 Video の基本的な使い方を、 そのまま XR 空間に応用しているだけだ。


この3点を押さえるだけで、 videoPlay クラスは

  • 再生状態を一元管理できる
  • Three.js と自然に接続できる
  • XR 対応を意識せず拡張できる

最小構成の動画プレイヤーとして成立する。

次のセクションでは、 このクラスを UI 操作やサムネ表示とどう連動させたかを見ていく。

5. サムネ画像を操作パネルに表示する

「操作する対象としての動画」を明確にする為、追加したのが、 操作パネル上に表示する サムネイル画像。

このサムネイルは、

  • 再生中の動画を示す
  • 次に何が流れるかを視覚的に伝える
  • 操作パネルと動画体験を結びつける

という役割を持っている。

UI として見ると、 これは単なる装飾ではない。

  • 再生中の状態が分かる
  • 視線を落とすだけで状況を把握できる
  • ボタン操作と視覚情報が同じ場所に集約される

結果として、 操作パネル全体が ジュークボックスのような振る舞いをする。

技術的には、

  • JSON に定義した thumbnail
  • Three.js の TextureLoader
  • PlaneGeometry に貼り付けたメッシュ

というシンプルな構成だが、 体験としては大きく変わる。

動画は「背景で流れるもの」から、 選ばれて再生されるメディアになる。

Three.js と WebXR を使うことで、 こうした UI を 平面UIではなく空間UIとして配置できる点も重要だ。

次のセクションでは、 この操作パネルを Raycaster と連動させ、 実際に動画を切り替えられるようにする。

6. UI操作と連動させる

操作パネル自体の実装については、 前回の記事で Raycaster を使った 3D UI ボタンとして解説している。

今回は、その UI を 動画プレイヤーとどう繋げたかに焦点を当てる。

考え方はとてもシンプルだ。

  • UI は「操作した」という事実だけを通知する
  • 動画の状態をどう変えるかは videoPlay クラスに任せる

たとえば、 「次へ」ボタンが押されたら、

videoPlay.nextVideo();

を呼ぶだけ。

UI 側は、

  • 今どの動画か
  • 再生中かどうか
  • プレイリストの構造

を一切知らない。

これは意図的な設計で、 UI を 状態を持たない入力装置として扱っている。


XR UI で重要になる「連打対策」

WebXR の UI は、 マウスクリックとは性質が違う。

  • 視線入力
  • コントローラのトリガー
  • 近接判定

どれも、意図せず 短時間に複数回発火しやすい。

そのため、 ボタンを押した直後に 一定時間操作を無効化する処理を入れている。

これは、

  • フラグ管理
  • 状態遷移の分岐

を増やすためではない。

「現実の操作感」に寄せるための時間的クールダウンだ。

現実のジュークボックスでも、 連打しても即座に反応し続けることはない。

XR 空間では、 こうした「少しの間」があるだけで、 操作の安定感が大きく変わる。


UI と動画を疎結合にする意味

この構成にすると、

  • UI を増やしても動画ロジックは変わらない
  • XR 以外の UI にもそのまま流用できる
  • 動画再生の不整合が起きにくい

という利点がある。

UI は「命令を送るだけ」、 動画は「命令を受けて状態を変えるだけ」。

この関係を守ることで、 WebXR という不確定要素の多い環境でも、 挙動を安定させることができる。

次のセクションでは、 この構成がなぜ WebXR 環境でもそのまま成立するのかを整理する。

7. WebXRでもそのまま動く理由

今回の構成がうまくいった一番の理由は、 WebXR を特別扱いしていない点にある。

実装を振り返ると、やっていることは一貫している。

  • VideoTexture は Three.js の通常のテクスチャ
  • 再生制御は HTML5 Video に任せている
  • UI 操作は Raycaster 経由でメソッドを呼ぶだけ

つまり、

  • XR 用の動画再生ロジック
  • XR 用の UI 制御

といった 専用処理を作っていない。


VideoTexture は XR / 非XR 共通

VideoTexture は、

  • 通常の Three.js シーン
  • WebXR セッション中のシーン

どちらでも 同じように更新される。

動画は DOM 側で再生され、 Three.js はそのフレームを描画しているだけなので、

「XR だから特別なことをする」

必要がない。


UI も共通

UI 側も同様で、

  • Raycaster がヒットしたら
  • videoPlay.nextVideo() を呼ぶ

という関係は変わらない。

入力方法が、

  • マウス
  • コントローラ
  • 視線

のどれに変わっても、 UI が呼び出す先は同じ。


表示先が変わっただけ

最終的に起きている変化は、とても単純だ。

  • 描画先が

    • 通常の canvas から
    • WebXR の視界に変わった

それだけ。

動画再生、UI 操作、プレイリスト管理という ロジックの中身は一切変わっていない。

この構成にしておくと、

  • 非XRでデバッグしやすい
  • XRを後から追加できる
  • 表現だけを差し替えられる

という利点が生まれる。

Three.js + WebXR で何かを作るとき、 最初から XR 専用の実装に寄せてしまうと、 コードが一気に扱いづらくなる。

今回のように、

「まず Three.js として正しく作る」 「表示先を XR に拡張する」

という順序を取ると、 結果的に 両方で安定して動く構成になる。

これが、 今回の動画プレイヤーが WebXR でもそのまま動いた理由だ。

おわりに

日々コツコツと、実装を続けてきましたが、ここまで出来るとは思わなかったです。

日々の開発の最初には、リファクタリングするのが習慣で、コードを整理している為、まだまだ実装できる余地は残ってます。

現状でもVRChatや、シンプルな軽量VRインディーズゲームを作れるぐらいの土俵には立てたような気はします。

ただ、まだまだ勉強する事は山ほどありますが…。

Three.jsはホント面白いですし、インストール不要でブラウザさえあれば動きますし、VR体験まで出来るのが、ホント凄いです。

ブロック崩しゲーム開発が止まってしまってますが、また飽きてきたら開発を再開したいと思います。