2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MediaPipeとThree.jsで作る!手のジェスチャー認識シューティングゲーム!!

Posted at

はじめに

こんにちは!MYJLab Advent Calendar 2025の18日目を担当します、4年のどんちゃんです。
17日目は佐藤さんがMediaPipeについて触れてくれたので、本日はその応用編をしていこうと思います!
今回は、MediaPipeとThree.jsを使って、手のジェスチャー認識で操作するAR風シューティングゲームを作成しました。カメラに向かって手をかざすだけで、リアルタイムで手の動きを検出し、3D空間でターゲットを撃つことができます。

デモ動画

デモ動画

YouTubeで見る

プロジェクト概要

このプロジェクトは、ブラウザ上で動作する手のジェスチャー認識を使ったシューティングゲームです。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 を開き、カメラへのアクセスを許可してください。

使い方

  1. カメラの許可: ブラウザでカメラへのアクセスを許可します
  2. 銃の形を作る: 人差し指を伸ばし、親指を立てて銃の形を作ります
    • カーソルが緑色になれば準備完了です
  3. 引き金を引く: 人差し指を曲げると弾が発射されます
  4. ターゲットを撃つ: 画面に出現するターゲットを撃ってスコアを稼ぎましょう

ゲームの仕様

  • ターゲット: 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

参考資料

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?