この記事はウェブクルー Advent Calendar 2025 の24日目の記事です。
昨日は @wc-takahashi さんの「サマーウォーズの"あの"暗号を解読するプログラムを書いてみた」でした。
はじめに
メリークリスマス🎄
2歳の娘を持つフロントエンドエンジニアです。
近年は、外に出ると暑すぎたり寒すぎたりで、
子どもと一緒に体を動かして遊ぶ方法に悩むことが増えました。
そこでお家で快適に体を動かして遊ぶアプリを作ってみることにしました。
アプリ概要
- ブラウザで動作(インストール不要)
- カメラに映った人の動きを取得
- 上からボールが落ちてくる
- 手や体に当たると跳ね返る
マウスやキーボード操作は不要で、
体を動かすだけで遊べるアプリです。
作ったものはこちらです(デバイスのカメラ利用を想定しています)
https://nuxt-tensorflow.vercel.app/
🧩 使用技術
| 技術 | 役割 |
|---|---|
| TensorFlow.js | ブラウザ上で機械学習を実行 |
| @tensorflow-models/pose-detection | 人の姿勢(関節)検出 |
| Matter.js | 物理演算(ボールの挙動) |
TensorFlow.js とは?
TensorFlow.js は、JavaScript で機械学習モデルを扱えるライブラリです。
特徴
- ブラウザで動作(サーバー不要)
- WebGL による高速処理
- カメラやマイクとの相性が良い
今回のような、
- リアルタイム処理
- カメラ入力
- 体感型コンテンツ
と非常に相性が良い技術です。
Pose Detection(姿勢推定)
@tensorflow-models/pose-detection を使うと、
カメラに映った人の 関節位置 を取得できます。
取得できる主な部位
- 頭
- 肩
- 肘
- 手首
- 腰
- 膝
- 足首
つまり、
人の姿を「点(キーポイント)の集合」として扱える
ということです。
今回の使い方
- 手や腕、体の位置を取得
- 当たり判定用の形に変換
- ボールと衝突判定を行う
Matter.js(物理演算)
Matter.js は JavaScript 製の物理エンジンです。
できること
- 重力
- 衝突判定
- 反発
- 摩擦
今回のアプリでは、
- 上からボールを落とす
- 体に当たると跳ね返る
- 画面外に出たボールを消す
といった挙動を実装しました。
処理の流れ
以下は、アプリが動作するまでの大まかな処理の流れです。
1. TensorFlow.js を初期化
import * as tf from '@tensorflow/tfjs'
// WebGL を使って高速化
await tf.setBackend('webgl')
await tf.ready()
これだけで、
ブラウザ上で機械学習モデルを動かす準備が整います。
2. 姿勢推定モデル(Pose Detection)のロード
import * as poseDetection from '@tensorflow-models/pose-detection'
const detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
{
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING,
}
)
※ SINGLEPOSE / MULTIPOSE を切り替えることで、複数人同時検出にも対応できます。
3. カメラ映像を取得しvideoとcanvasに流し込む
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480, facingMode: 'user' }
})
videoRef.value.srcObject = stream
const poseCanvas = document.createElement('canvas')
const poseCtx = poseCanvas.getContext('2d')!
poseCtx.drawImage(
videoRef.value,
-320, 0, 320, 240 //解像度指定。x軸は-で反転。
)
videoは表示用。
canvas は姿勢検出処理用なので解像度は低めにしています。
解像度を高めると精度は上がりますが、デバイスが悲鳴をあげます。
カメラ映像は左右反転(ミラー)して表示しているので、推論入力の段階でミラー描画しています。
4. 物理演算エンジン(Matter.js)の初期化
import Matter from 'matter-js'
const engine = Matter.Engine.create()
const world = engine.world
Matter.Engine.run(engine)
MatterのWorldインスタンスを作成します。このWorldに追加されたもの同士が、物理条件に従って動くようになります。
5. 物理条件の設定
const ball = Matter.Bodies.circle(
200, // x座標
0, // y座標
20, // 半径(当たり判定の大きさ)
{
restitution: 0.1, // 反発係数
frictionAir: 0.05, // 空気抵抗
density: 0.01, //質量
}
)
Matter.World.add(world, ball)
当たり判定させるものに物理条件を設定し World に追加していきます。望む動きの物理条件にするには果てしない微調整が必要です。重力はデフォルトで働いています。
今回 World に作るものは、落下するボールと、体の各関節に対応する当たり判定です。
6. Pose Detection × Matter.js の連携
requestAnimationFrame()を使い毎フレームごとに下記処理をループさせていきます。
const poses = await detector.estimatePoses(poseCanvas as HTMLCanvasElement)
poses[0].keypoints.forEach(point=>{
...
Matter.Body.setPosition(指定した関節のMatter, {point.x, point.y})
...
)
detector.estimatePoses()にカメラ画像のcanvasを渡し姿勢検知。posesには各関節の位置情報が入っているので、関節毎に作ったMatterの位置情報を更新します。リアルタイムで検出される体の関節部分が当たり判定を持ってworld内に反映されていきます。
作ってみて
ひと昔は姿勢検知には専用機器が必要でしたが、ブラウザでサクッと実現できて面白いことが色々できそうですね。時代はLLMですが、僕はしばらくML(機械学習)の方で遊んでいこうかなと思っています。
肝心の娘のリアクションですが
- ゲーム性が無い
- 家のディスプレイが小さい
- そもそも家が狭い
などの理由(2歳児なので推測です)ですぐに遊ばなくなりました。
今回はあまりハマってもらえませんでしたが、
「ゲームデザインをしっかりする」、「大型テレビの購入を妻に嘆願する」、などの改善を引き続き行っていきたいと思っています。
明日は @kouares さんの投稿になります。よろしくお願いします。