はじめに
こんにちは!MYJLab Advent Calendar 2025の18日目を担当します、4年のどんちゃんです。
17日目は佐藤さんがMediaPipeについて触れてくれたので、本日はその応用編をしていこうと思います!
今回は、MediaPipeとThree.jsを使って、手のジェスチャー認識で操作するAR風シューティングゲームを作成しました。カメラに向かって手をかざすだけで、リアルタイムで手の動きを検出し、3D空間でターゲットを撃つことができます。
デモ動画
プロジェクト概要
このプロジェクトは、ブラウザ上で動作する手のジェスチャー認識を使ったシューティングゲームです。Webカメラを使用して手の動きを検出し、リアルタイムで3Dターゲットを撃つことができます。
主な特徴
- 手のジェスチャー認識: MediaPipeを使用したリアルタイム手検出
- 3Dレンダリング: Three.jsによる3Dターゲットとエフェクト
- ブラウザ完結: すべての処理がブラウザ内で完結(サーバー不要)
- プライバシー保護: カメラ映像はサーバーに送信されません
技術スタック
- React 19 + TypeScript: UIフレームワーク
- Vite: ビルドツール
- MediaPipe Tasks Vision: 手のランドマーク検出
- Three.js: 3Dレンダリング
- Web Audio API: 音響効果(BGM、ヒット音、ミス音)
実装のポイント
1. 手のジェスチャー認識
MediaPipeを使用して、手の21個のランドマークを検出します。特に重要なのは以下のジェスチャー検出です:
銃の形の検出
export function detectGunShape(landmarks: HandLandmark[]): boolean {
const wrist = landmarks[0] // 手首
const thumbTip = landmarks[4] // 親指の先端
const indexTip = landmarks[8] // 人差し指の先端
// 人差し指が伸びているかチェック(指先が手首より上)
const indexExtended = indexTip.y < wrist.y
// 親指が立てられているかチェック(親指の先端が手首より上)
const thumbUp = thumbTip.y < wrist.y
return indexExtended && thumbUp
}
引き金を引く動作の検出
人差し指を曲げる動作を検出して発射判定を行います:
export function detectTriggerPull(
landmarks: HandLandmark[],
prevAngle: number | null,
): { fired: boolean; angle: number | null } {
// 人差し指のPIP(6) → DIP(7) → 指先(8)の角度を計算
const indexPip = landmarks[6]
const indexDip = landmarks[7]
const indexTip = landmarks[8]
const currentAngle = calculateAngle(indexPip, indexDip, indexTip)
// 前フレームと比較して、角度が減少した場合(曲がった場合)に発射
let fired = false
if (prevAngle !== null && prevAngle > 150 && currentAngle < 150) {
fired = true
}
return { fired, angle: currentAngle }
}
2. 3D空間への座標変換
MediaPipeの正規化座標(0-1)をThree.jsの3D空間座標に変換します。カメラ映像が左右反転(ミラー表示)されているため、X座標も反転させる必要があります:
export function calculateHandPositionAndDirection(
landmarks: HandLandmark[],
camera: THREE.PerspectiveCamera,
canvasWidth: number,
canvasHeight: number,
): { position: THREE.Vector3; direction: THREE.Vector3 } | null {
const indexTip = landmarks[8] // 人差し指の先端
const indexMcp = landmarks[5] // 人差し指のMCP関節
// カメラの視野角を考慮して変換
const fov = camera.fov * (Math.PI / 180)
const aspect = camera.aspect
const cameraZ = camera.position.z
const targetZ = 2
const distance = cameraZ - targetZ
const height = 2 * Math.tan(fov / 2) * distance
const width = height * aspect
// 正規化座標を3D空間座標に変換(X座標を反転)
const x = (0.5 - indexTip.x) * width
const y = (indexTip.y - 0.5) * -height
const z = targetZ
const position = new THREE.Vector3(x, y, z)
// 方向ベクトルも計算
const direction = new THREE.Vector3(
indexMcp.x - indexTip.x,
indexTip.y - indexMcp.y,
(indexTip.z || 0) - (indexMcp.z || 0)
).normalize()
return { position, direction }
}
3. ゲームループとパフォーマンス最適化
ゲームループはrequestAnimationFrameを使用して60FPSで動作します。パフォーマンスを考慮して以下の最適化を行いました:
- ターゲット数の制限(最大5個)
- リソースの明示的なdispose
- WebGLのパフォーマンス設定
const gameLoop = () => {
// ターゲットの自動生成(0.5秒ごとに1個ずつ)
if (canSpawn && targetsRef.current.length < MAX_TARGETS) {
const newTarget = createTarget(sceneRef.current, cameraRef.current)
setTargets((prev) => [...prev, newTarget])
}
// ターゲットと弾の更新
const updatedTargets = targetsRef.current.map(updateTarget)
const updatedBullets = bulletsRef.current.map(updateBullet)
// 衝突判定
const { updatedTargets: finalTargets, score: newScore } =
processCollisions(updatedTargets, updatedBullets, sceneRef.current)
// スコア更新
setScore((prev) => prev + newScore)
gameLoopIdRef.current = requestAnimationFrame(gameLoop)
}
4. 音響効果
Web Audio APIを使用して、BGM、ヒット音、ミス音を生成します:
// BGMの生成
const createBGM = () => {
const oscillator1 = audioContext.createOscillator()
const gain1 = audioContext.createGain()
oscillator1.frequency.value = 110 // A2
oscillator1.type = 'sawtooth'
gain1.gain.value = 0.7
oscillator1.connect(gain1)
gain1.connect(gainNode)
oscillator1.start()
// 4秒後に繰り返し
setTimeout(() => {
oscillator1.stop()
createBGM()
}, 4000)
}
セットアップ
必要な環境
- Node.js 18以上
- npm または yarn
- Webカメラ
インストール
# リポジトリをクローン
git clone https://github.com/shokkun47/hand-gesture-shooting-game.git
cd hand-gesture-shooting-game
# 依存関係をインストール
npm install
# 開発サーバーを起動
npm run dev
ブラウザで http://localhost:5173 を開き、カメラへのアクセスを許可してください。
使い方
- カメラの許可: ブラウザでカメラへのアクセスを許可します
-
銃の形を作る: 人差し指を伸ばし、親指を立てて銃の形を作ります
- カーソルが緑色になれば準備完了です
- 引き金を引く: 人差し指を曲げると弾が発射されます
- ターゲットを撃つ: 画面に出現するターゲットを撃ってスコアを稼ぎましょう
ゲームの仕様
- ターゲット: 0.5秒ごとに1個ずつ、ランダムな場所から生成
- 最大ターゲット数: 同時に5個まで存在可能
- 当たり判定: 距離2.5以内で当たり
- スコア: 1ターゲット当たり100ポイント
- エフェクト: ヒット時にパーティクルエフェクトと「HIT!」テキストを表示
実装の工夫点
1. カメラ映像のミラー表示対応
カメラ映像を左右反転(ミラー表示)しているため、手の位置と方向の計算でもX座標を反転させる必要がありました。
2. ジェスチャー検出の最適化
銃の形の検出と引き金を引く動作が競合しないように、検出条件を調整しました。
3. パフォーマンス最適化
- ターゲット数の制限
- リソースの明示的なdispose
- WebGLのパフォーマンス設定
今後の改善案
- 難易度設定(ターゲットの速度、生成頻度の調整)
- ハイスコアの保存(LocalStorage)
- 複数の武器タイプ
- より高度なエフェクト
まとめ
MediaPipeとThree.jsを組み合わせることで、ブラウザ上で動作する手のジェスチャー認識ゲームを作成できました。すべての処理がブラウザ内で完結するため、サーバー不要で動作します。
ぜひ試してみてください!
リポジトリ
GitHub: https://github.com/shokkun47/hand-gesture-shooting-game
