Webカメラでトラッキングした手をThree.js上で動かす
こちらの記事は、リバイバル記事としてVketマガジンに移行しました。
はじめに
VR法人HIKKYのフロントエンドエンジニアのしべです。
WebVR + Webカメラのトラッキングで何か作れないか興味があり、A-FRAMEとMediaPipeの併用を試してみたので、これらの紹介も兼ねて記事にしました。
デモ
https://szgk.github.io/a-frame-mediapipe-template/
A-FRAMEとは
Three.jsでWebVRサービスを作成するためのECS(entity-component-system)フレームワークです。
MediaPipeとは
動画などのメディアの推論をデバイスで行うためのパイプライン(機械学習に必要な処理をまとめる仕組み)を提供する、Google製のフレームワークです。
https://google.github.io/mediapipe/
MediaPipeを使用したボディトラッキング等のコードが「Solution」として公開されており、今回はそのうちの「Hands」を使用しました。
MediaPipeを利用している既存サービスとしては、kalidofaceなどがあります。
大まかな流れ
MediaPipeの各Solutionでは動画から得た各部位の座標を配列として取得でき、landmarkという命名がされています。
// 例
[
// x = x座標, y = y座標, z = z座標, visibility = 動画に映っているかどうか
{x: 0.2786056697368622, y: 0.9737217426300049, z: 0.0000010346717544962303, visibility: undefined},
...
]
Hands で取得できるlandmarkは計20箇所です。
今回は簡単に
上記landmarkをA-FRAME上に球体として描画。
コライダーをつけて他オブジェクトを手の動きに合わせて動かせるようにする。
というような実装を試してみました。
実装
下記にMediaPipeでトラッキングしたデータをA-FRAME上で描画する処理を記載します。
1 MediaPipeのJavaScript用のSolutionをいずれかの方法で読み込む
// src/script.js
// 手のトラッキング用のpackage
import {Hands} from '@mediapipe/hands'
// webカメラの動画を<video>に描画する際のutil
import {Camera} from '@mediapipe/camera_utils'
// src/index.html
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
2 ドキュメントを参考にwindow.onload時に初期化
https://google.github.io/mediapipe/solutions/hands.html#javascript-solution-api
// src/script.js
const init = () => {
const videoElement = document.querySelector('#input-video')
// hands.jsを実行する際に必要なファイルを自動で読み込めるようにする
const hands = new Hands({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
}
})
// ここでフレーム毎の処理を渡します(※後述
hands.onResults(onResults)
const camera = new Camera(videoElement, {
onFrame: async () => {
await hands.send({ image: videoElement })
},
width: 1280,
height: 720
})
camera.start()
}
window.onload = init
3 フレーム毎にA-FRAME上の球体の位置をカメラでトラッキングした手の位置に合わせる
// src/script.js
// 手のlandmarkに対応するa-frame上の球体を格納する
const handSpheres = []
const createSphere = (attributes) => {
const sphere = document.createElement('a-sphere')
for (const key in attributes) {
sphere.setAttribute(key, attributes[key])
}
document.querySelector('a-scene').appendChild(sphere)
return sphere
}
// A-FRAMEに球体のエンティティを追加する処理
const onResults = (results) => {
if (results.multiHandLandmarks) {
for (const landmarks of results.multiHandLandmarks) {
// 初期化時にlandmarkに対応するA-FRAME上の球体を作成、参照を配列へ格納しておく
if (handSpheres.length <= 0) {
landmarks.forEach((landmark) => {
const sphere = createSphere({
position: { x: landmark.x, y: landmark.y, z: landmark.z },
radius: 0.04,
color: '#000',
['ammo-body']: 'type: kinematic',
['ammo-shape']: 'type: sphere'
})
handSpheres.push(sphere)
})
}
// トラッキングして得た座標を、A-FRAME上の球体の座標に反映する
handSpheres.forEach((sphere, i) => {
sphere.setAttribute('position', {
// ちょうどいい位置に描画するために、ひとまずべた書きで値を修正してます
x: landmarks[i].x * -3 + 1,
y: landmarks[i].y * -2.5 + 2,
z: landmarks[i].z * 8 - 2,
})
})
}
}
}
まとめ
今回は手の各部位の座標をそのまま描画しましたが、これをfbxなどのモデルに反映させればトラッキングでキャラクターを動かすことなども可能です。
またスマホでうまく動けば、ヘッドマウントディスプレイなしで、スマホVR機材のみで操作できるVRゲームが作れるかもしれません。
(スマホのVRモードで試してみたところ、挙動が不安定で処理が止まってしまうのが今後の課題です…。
ソースコードはこちらに置いています。
https://github.com/szgk/a-frame-mediapipe-template
参考
最後に
HIKKYにおけるWebフロントエンド開発では、WebVRを使用する業務はまだありませんが、今後の展開によっては増えていくかもしれません。
またブラウザで動くVRエンジン、VketCloudに関わる作業では、WebVRやメタバースに関する技術を用いた開発に、フロントエンドエンジニアとして参加することができます。
少しでも興味ありましたら、カジュアルな面談から可能ですので
下記ページよりぜひご応募ください!