ウェブカメラで体を動かすと音が生成され、そのモーションに応じて音が変わります。
ページを開くと「Start」というボタンが表示されます。
ボタンをクリックすると、音声の生成が開始され、PoseNetが動作を開始します。
手の高さによって音の周波数が変わり、手の位置が低いほど低い音、位置が高いほど高い音になります。
手の位置が画面の下に近いほど音が大きくなり、上に近いほど音が小さくなります。
<!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>