2
3

体を動かすとモーションに応じて音が変わるゲーム。

Posted at

ウェブカメラで体を動かすと音が生成され、そのモーションに応じて音が変わります。

ページを開くと「Start」というボタンが表示されます。
ボタンをクリックすると、音声の生成が開始され、PoseNetが動作を開始します。

手の高さによって音の周波数が変わり、手の位置が低いほど低い音、位置が高いほど高い音になります。
手の位置が画面の下に近いほど音が大きくなり、上に近いほど音が小さくなります。

スクリーンショット 2024-10-01 042342.png

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3D骨格推定と音生成</title>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.135.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.135.0/examples/js/controls/OrbitControls.js"></script>

  <style>
    body { margin: 0; overflow: hidden; }
    canvas { display: block; background-color: green; }
    #startButton {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      padding: 10px 20px;
      font-size: 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <video id="video" width="640" height="480" autoplay style="display:none;"></video>
  <button id="startButton">Start</button> <!-- スタートボタン -->

  <script>
    let scene, camera, renderer, audioContext, oscillator, gainNode;

    // Three.jsの初期化
    function initThreeJS() {
      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);
      camera.position.z = 5;

      const light = new THREE.DirectionalLight(0xffffff, 1);
      light.position.set(0, 1, 1).normalize();
      scene.add(light);

      const controls = new THREE.OrbitControls(camera, renderer.domElement);
    }

    // オーディオの初期化
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      oscillator = audioContext.createOscillator();
      gainNode = audioContext.createGain();

      oscillator.type = 'sine';  // 波形のタイプ
      oscillator.connect(gainNode);
      gainNode.connect(audioContext.destination);

      gainNode.gain.value = 0;  // 初期状態で音を消す
      oscillator.start();
    }

    // 音声の再開(ボタンが押されたときに呼び出される)
    function startAudio() {
      if (audioContext && audioContext.state === 'suspended') {
        audioContext.resume();
      }
    }

    // 骨格データを描画する
    function drawSkeletonOnThreeJS(keypoints) {
      const existingSpheres = scene.children.filter(child => child.type === 'Mesh' && child.geometry.type === 'SphereGeometry');
      existingSpheres.forEach(sphere => scene.remove(sphere));

      const existingLines = scene.children.filter(child => child.type === 'Line');
      existingLines.forEach(line => scene.remove(line));

      const adjacentKeyPoints = posenet.getAdjacentKeyPoints(keypoints, 0.5);
      adjacentKeyPoints.forEach(keypointsPair => {
        const [start, end] = keypointsPair;
        if (start.score > 0.5 && end.score > 0.5) {
          const material = new THREE.LineBasicMaterial({ color: 0xffffff });
          const geometry = new THREE.BufferGeometry().setFromPoints([
            new THREE.Vector3((start.position.x - video.width / 2) / 100, -(start.position.y - video.height / 2) / 100, 0),
            new THREE.Vector3((end.position.x - video.width / 2) / 100, -(end.position.y - video.height / 2) / 100, 0)
          ]);
          const line = new THREE.Line(geometry, material);
          scene.add(line);
        }
      });

      keypoints.forEach(keypoint => {
        if (keypoint.score > 0.5) {
          const { y, x } = keypoint.position;
          const z = 0;

          let color;
          if (keypoint.part === 'nose') color = 0x0000ff;
          else if (keypoint.part.includes('leftWrist') || keypoint.part.includes('rightWrist')) color = 0xff0000;
          else color = 0xffffff;

          const geometry = new THREE.SphereGeometry(0.1, 32, 32);
          const material = new THREE.MeshBasicMaterial({ color: color });
          const sphere = new THREE.Mesh(geometry, material);
          sphere.position.set((x - video.width / 2) / 100, -(y - video.height / 2) / 100, z);
          scene.add(sphere);
        }
      });

      // 手首の位置に基づいて音を生成
      const leftWrist = keypoints.find(point => point.part === 'leftWrist');
      const rightWrist = keypoints.find(point => point.part === 'rightWrist');

      if (leftWrist && rightWrist && leftWrist.score > 0.5 && rightWrist.score > 0.5) {
        const leftY = leftWrist.position.y;
        const rightY = rightWrist.position.y;

        // 手の高さに基づいて音の周波数を変化
        const frequency = Math.max(100, Math.min(1000, 1000 - leftY));  // 周波数範囲を100Hz〜1000Hzに制限
        const volume = Math.max(0, Math.min(1, (video.height - rightY) / video.height));  // ボリュームを0〜1に制限

        oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
        gainNode.gain.setValueAtTime(volume, audioContext.currentTime);
      }
    }

    // アニメーションループ
    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }

    // カメラの映像を取得してPoseNetで骨格推定を行う
    async function startPoseNet() {
      const video = document.getElementById('video');

      navigator.mediaDevices.getUserMedia({
        video: true
      }).then(stream => {
        video.srcObject = stream;
      });

      const net = await posenet.load();

      video.onloadeddata = async () => {
        setInterval(async () => {
          const pose = await net.estimateSinglePose(video, {
            flipHorizontal: false
          });

          drawSkeletonOnThreeJS(pose.keypoints);
        }, 100);
      };
    }

    // 初期化とアニメーション開始
    initThreeJS();

    // ボタンのクリックイベントに音声の開始をバインド
    document.getElementById('startButton').addEventListener('click', () => {
      initAudio();   // オーディオを初期化
      startAudio();  // 音声を再開
      startPoseNet(); // PoseNetを開始
      document.getElementById('startButton').style.display = 'none';  // ボタンを非表示にする
    });

    animate();  // アニメーションを開始
  </script>
</body>
</html>


2
3
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
3