PosenetとTensorFlowを用いた身体動作を持つブラウザでのビートサーベルの再生js


私はギアを所有していないので、私は多くのVRゲームをプレイしていないが、私は試してみて、愛していたBeat Saber .

あなたはそれに精通していない場合は、このトロンは、曲のリズムに“ビート”をヒットするあなたのコントローラを使用してゲームを見ている.それは本当に楽しいですが、それはあなたのいずれかを持っている必要がありますHTC Vive , 安Oculus Rift またはPlaystation VR .
これらのコンソールは、したがって、誰にでもアクセスできないので、高価でありえます.
数ヶ月前、私は遭遇したthis repo そばSupermedium . これは、Web技術で作られたビートサーベルのクローンですA-Frame そして、私はそれが本当に感動を見つけました!
あなたは、曲を再生を開始することができますが生成されているビートを見て、シーンを見て、それはあなたが再生することができますように見えるか、少なくとも、再び、任意のVRデバイスを持っていない場合ではない.
私は本当に私はそれについて何かを行うことができるかどうかを見たかったので、私はPosenet、テンソルフローでポーズ検出モデルを追加することを決めた.JSは、私の手でブラウザでこのゲームをプレイできるようにする.そして、それは働く!🤩🎉
OK、カメラのトラッキングがジョイスティックを使用するのと同じくらい正確でないので、それは演奏者としてではありません、しかし、正直であるために、私の主なゴールは可能であるかどうか見ることでした.
私はスーパー、それが動作し、人々が必要とする“唯一の”ものは、現代のラップトップです!
最後の結果は次のようになります.

あなたはどのように構築されたの詳細に興味がない場合は、単にチェックアウトすることができますlive demo または、すべてのコードを見つけることができますGithub repo .
さもなければ、あなたがうまくいけば、私がそうであるように、これについて興奮しているように、現在、それがどのように働くかについて話しましょう!

STEP 1リバースエンジニアリング
コードベースのほとんどはBeatSaver Viewer オープンソースプロジェクト.
通常、私の側のプロジェクトでは、ゼロからすべてを開始します.私は物事がどこにあるかを正確に知っています、そして、それは私が変化を速くするのを簡単にします.しかし、この場合、私は彼らのコードベースから始めたので、アイデアはBeatSaverの既存のrepoを見つけることから来ました.他の人がすでにそのようなものすごい仕事をしたとき、それはゲームを再現している時間を過ごすのに役に立たないでしょう.
私はすぐにいくつかの問題に走った.どこから始めればいいのかわからなかった.通常のdevツールを使ってブラウザで3 Dシーンを検査する場合は、どのコンポーネントを変更すべきかを見つけ出してください.the canvas ; あなたはシーン内のさまざまな3 D要素を検査することができないつもりはない.
Aフレームを使用すると、使用できますCTRL + Option + i 検査官をトグルするが、それでも私が探していた要素を見つけるのを助けてくれなかった.
私が代わりにしなければならなかったことは、コードベースに深く飛び込んで、何が起こっていたかについて理解しようとすることです.私はAフレームで多くの経験を持っていなかったので、私はいくつかのミックスの名前について少し混乱していました.
結局、私はbeat コンポーネントIはそれを探していましたdestroyBeat 方法、有望に見えた!
ちょうど私が私が必要としたものを見つけたことをテストするためにbeat トリガーするコンポーネントdestroyBeat ページの本体をクリックするたびに機能します.
document.body.onclick = () => this.destroyBeat();
ページを再読み込みした後、私はゲームを開始し、ビートを待って、表示される、どこかの体の上にクリックし、ビート爆発を見た.それは良い最初のステップだった!
私がコードの変更をどこで行うかについてのより良い考えがあったので、私は、私がどんな種類のデータを使うことができるかについて見るために、Posenetと遊ぶのを見始めました.

手順2.Posenetモデルによる身体追跡
The PoseNet model TensorFlowで.JSを使用すると、ブラウザでポーズの推定を行うことができますし、肩、腕、手首などの位置のようないくつかの“キーポイント”についての情報を取得します.
それをゲームに導入する前に、私はそれがどのように働くかについて見るために別にそれをテストしました.
基本的な実装は次のようになります.
HTMLファイルでは、TensorFlowをインポートして起動します.JSとPosenetモデル:
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet"></script>
また、私たちは、私のケースでは、手首で追跡している体の部分にウェブカメラのフィードとマーカーを表示することができます.
そうするために、我々はビデオタグとビデオの上に置かれるキャンバスを加えることから始めます:
    <video id="video" playsinline style=" -moz-transform: scaleX(-1);
    -o-transform: scaleX(-1);
    -webkit-transform: scaleX(-1);
    transform: scaleX(-1);
    ">
    </video>
    <canvas id="output" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
ポーズ検出のJavaScript部分はいくつかのステップを含んでいます.
まず、私たちはposenetを設定する必要があります.
// We create an object with the parameters that we want for the model. 
const poseNetState = {
  algorithm: 'single-pose',
  input: {
    architecture: 'MobileNetV1',
    outputStride: 16,
    inputResolution: 513,
    multiplier: 0.75,
    quantBytes: 2
  },
  singlePoseDetection: {
    minPoseConfidence: 0.1,
    minPartConfidence: 0.5,
  },
  output: {
    showVideo: true,
    showPoints: true,
  },
};

// We load the model.
let poseNetModel = await posenet.load({
    architecture: poseNetState.input.architecture,
    outputStride: poseNetState.input.outputStride,
    inputResolution: poseNetState.input.inputResolution,
    multiplier: poseNetState.input.multiplier,
    quantBytes: poseNetState.input.quantBytes
});
モデルが読み込まれると、ビデオストリームをインスタンス化します.
let video;

try {
  video = await setupCamera();
  video.play();
} catch (e) {
  throw e;
}

async function setupCamera() {
  const video = document.getElementById('video');
  video.width = videoWidth;
  video.height = videoHeight;

  const stream = await navigator.mediaDevices.getUserMedia({
    'audio': false,
    'video': {
      width: videoWidth,
      height: videoHeight,
    },
  });
  video.srcObject = stream;

  return new Promise((resolve) => {
    video.onloadedmetadata = () => resolve(video);
  });
}
ビデオストリームが準備されると、ポーズを検出します.
function detectPoseInRealTime(video) {
  const canvas = document.getElementById('output');
  const ctx = canvas.getContext('2d');
  const flipPoseHorizontal = true;

  canvas.width = videoWidth;
  canvas.height = videoHeight;

  async function poseDetectionFrame() {
    let poses = [];
    let minPoseConfidence;
    let minPartConfidence;

    switch (poseNetState.algorithm) {
      case 'single-pose':
        const pose = await poseNetModel.estimatePoses(video, {
          flipHorizontal: flipPoseHorizontal,
          decodingMethod: 'single-person'
        });
        poses = poses.concat(pose);
        minPoseConfidence = +poseNetState.singlePoseDetection.minPoseConfidence;
        minPartConfidence = +poseNetState.singlePoseDetection.minPartConfidence;
        break;
    }

    ctx.clearRect(0, 0, videoWidth, videoHeight);

    if (poseNetState.output.showVideo) {
      ctx.save();
      ctx.scale(-1, 1);
      ctx.translate(-videoWidth, 0);
      ctx.restore();
    }

    poses.forEach(({score, keypoints}) => {
      if (score >= minPoseConfidence) {
        if (poseNetState.output.showPoints) {
          drawKeypoints(keypoints, minPartConfidence, ctx);
        }
      }
    });
    requestAnimationFrame(poseDetectionFrame);
  }

  poseDetectionFrame();
}
上のサンプルではdrawKeypoints キャンバス上の手の上にドットを描画する機能.このコードは以下の通りです:
function drawKeypoints(keypoints, minConfidence, ctx, scale = 1) {
    let leftWrist = keypoints.find(point => point.part === 'leftWrist');
    let rightWrist = keypoints.find(point => point.part === 'rightWrist');

    if (leftWrist.score > minConfidence) {
        const {y, x} = leftWrist.position;
        drawPoint(ctx, y * scale, x * scale, 10, colorLeft);
    }

    if (rightWrist.score > minConfidence) {
        const {y, x} = rightWrist.position;
        drawPoint(ctx, y * scale, x * scale, 10, colorRight);
    }
}

function drawPoint(ctx, y, x, r, color) {
  ctx.beginPath();
  ctx.arc(x, y, r, 0, 2 * Math.PI);
  ctx.fillStyle = color;
  ctx.fill();
}
結果は以下の通りです.

今ではトラッキングが独自に動作しているので、これをBeatWizers CodeBaseに追加してみましょう.

ステップ3.ポーズを追加する
3 Dゲームに我々のポーズ検出を加えることを始めるために、我々は上で書いたコードをとって、Beatsaverコードの中でそれを実行する必要があります.
我々がしなければならないすべては、主なHTMLファイルに我々のビデオ・タッグを加えて、我々の上で我々のJSコードを含んでいるそれの上で我々が輸入する新しいJSファイルをつくることです.
この段階では、次のようにしてください.

それは良い最初のステップですが、我々はまだかなりではない.今、我々はよりトリッキーになるこのプロジェクトの部分を入力し始めている.A - frameゲームが3 Dにある間、Posenetで位置の追跡は2 Dにあります、そのため、手追跡からの我々の青と赤い点は実際に場面に加えられません.しかし、ビートを破壊することができるように、我々はすべてのゲームの一部になる必要があります.
これを行うには、我々はキャンバス上で円として表示するから、我々は正しい座標で配置する必要がある実際の3 Dオブジェクトを作成するに切り替える必要がありますが、それは簡単ではない.
これらの環境での作業の座標は異なります.The (x,y) キャンバス上の左手の座標は同じに変換されません(x,y) 3 Dでオブジェクトのコーディネート.
したがって、次のステップは、我々の2 Dと3 D世界の間の位置をマップする方法を見つけることです.

マッピング2 Dと3 D座標
前述のように、2 Dと3 D世界の座標は異なって働きます.
それらをマップすることができる前に、我々はゲームで私たちの手を表すために起こっている新しい3 Dオブジェクトを作成する必要があります.
Aフレームでは、エンティティコンポーネントと呼ばれるものを作成できます.これは、カスタムプレースホルダオブジェクトです.

カスタムの3 Dオブジェクトを作成する
簡単なキューブを作りたいと思います.
let el, self;

AFRAME.registerComponent('right-hand-controller', {
    schema: {
        width: {type: 'number', default: 1},
        height: {type: 'number', default: 1},
        depth: {type: 'number', default: 1},
        color: {type: 'color', default: '#AAA'},
    },
    init: function () {
        var data = this.data;
        el = this.el;
        self = this;

        this.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth);
        this.material = new THREE.MeshStandardMaterial({color: data.color});
        this.mesh = new THREE.Mesh(this.geometry, this.material);
        el.setObject3D('mesh', this.mesh);
    }
});
それから、我々のカスタム実体をスクリーンに見ることができるために、我々は我々のHTMLでこのファイルを輸入して、使用する必要がありますa-entity タグ.
<a-entity id="right-hand" right-hand-controller="width: 0.1; height: 0.1; depth: 0.1; color: #036657" position="1 1 -0.2"></a-entity>
上のコードでは、型の新しいエンティティを作成しますright-hand-controller そして、我々はそれにいくつかの特性を与えます.
現在、我々はページに立方体を見なければなりません.

位置を変更するには、Posenetから取得したデータを使用できます.エンティティコンポーネントでは、いくつかの関数を追加する必要があります.
// this function runs when the component is initialised AND when a property updates.
update: function(){
  this.checkHands();
},
checkHands: function getHandsPosition() {
  // if we get the right hand position from PoseNet and it's different from the previous one, trigger the `onHandMove` function.
  if(rightHandPosition && rightHandPosition !== previousRightHandPosition){
    self.onHandMove();
    previousRightHandPosition = rightHandPosition;
  }
  window.requestAnimationFrame(getHandsPosition);
},
onHandMove: function(){
  //First, we create a 3-dimensional vector to hold the values of our PoseNet hand detection, mapped to the dimension of the screen.
  const handVector = new THREE.Vector3();
  handVector.x = (rightHandPosition.x / window.innerWidth) * 2 - 1;
  handVector.y = - (rightHandPosition.y / window.innerHeight) * 2 + 1; 
  handVector.z = 0; // that z value can be set to 0 because we don't get depth from the webcam.

  // We get the camera element and 'unproject' our hand vector with the camera's projection matrix (some magic I can't explain).
  const camera = self.el.sceneEl.camera;
  handVector.unproject(camera);

  // We get the position of our camera object.
  const cameraObjectPosition = camera.el.object3D.position;
  // The next 3 lines are what allows us to map between the position of our hand on the screen to a position in the 3D world. 
  const dir = handVector.sub(cameraObjectPosition).normalize();
  const distance = - cameraObjectPosition.z / dir.z;
  const pos = cameraObjectPosition.clone().add(dir.multiplyScalar(distance));
  // We use this new position to determine the position of our 'right-hand-controller' cube in the 3D scene. 
  el.object3D.position.copy(pos);
  el.object3D.position.z = -0.2;
}
この段階では、カメラの前で手を動かし、3 Dキューブの動きを見ることができます.

我々がする必要がある最終的なものは、ビートを破壊することができると呼ばれるものです.

レイキャスティング
3時に.JSは、レイキャスティングは通常マウスピッキングのために使用されます、マウスが終わっている3 Dスペースのオブジェクトを考え出すことを意味します.これは衝突検出に使用することができます.
我々のケースでは、それは我々が気にするマウスでなく、我々の「キューブ手」です.
我々の手が終わっているオブジェクトをチェックするために、我々は以下のコードを我々のものに加える必要がありますonMoveHands 機能
// Create a raycaster with our hand vector.
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(handVector, camera);

// Get all the <a-entity beatObject> elements.
const entities = document.querySelectorAll('[beatObject]'); 
const entitiesObjects = [];

if(Array.from(entities).length){
  // If there are beats entities, get the actual beat mesh and push it into an array.
  for(var i = 0; i < Array.from(entities).length; i++){
    const beatMesh = entities[i].object3D.el.object3D.el.object3D.el.object3D.children[0].children[1];
    entitiesObjects.push(beatMesh);
  }

  // From the raycaster, check if we intersect with any beat mesh. 
  let intersects = raycaster.intersectObjects(entitiesObjects, true);
    if(intersects.length){
      // If we collide, get the entity, its color and type.
      const beat = intersects[0].object.el.attributes[0].ownerElement.parentEl.components.beat;
      const beatColor = beat.attrValue.color;
      const beatType = beat.attrValue.type;
      // If the beat is blue and not a mine, destroy it!
      if(beatColor === "blue"){
        if(beatType === "arrow" || beatType === "dot"){
          beat.destroyBeat();
        } 
      }
    }
}
そして、我々は完了です!
posenetとtensorflowを使った.JSは手とその位置を検出するために、我々はキャンバス上でそれらを描いた、我々は3 D座標にそれらをマッピングし、我々はRaycasterを使用してビートとの衝突を検出し、それらを破壊する!🎉 🎉 🎉
それは間違いなく私にこのすべてを把握するためにいくつかのより多くのステップを取ったが、それは非常に興味深い挑戦だった!

限界
もちろん、いつものように、言及する必要がある制限があります.

レイテンシー
デモを試している場合は、おそらくあなたの手を移動し、画面上に反映された瞬間の間にいくつかの潜在性に気づいただろう.
私の意見では、それは予想されます、しかし、私は実際にそれが私の手首を認識することができて、彼らがスクリーンに置かれるべきところを計算する方法を速く感じ取っています.

照明
私は、コンピュータビジョンで一般的に考えます、あなたが構築するどんな経験でも、非常に高性能であるか、部屋の照明が十分でないならば、利用できません.これは、ウェブカメラからの流れを使用している場合は、ボディの形状に近いので、光の量が不足している場合は、それを行うことができなくなりますし、ゲームが動作しないことがわかります.

ユーザー経験
リアルビートサーベルゲームでは、ジョイスティックは、ビートとの衝突に反応すると思いますか?それがそうでないならば、本当にそうしなければならないので、ユーザーは何が起こったかについて若干のハプティック・フィードバックを得ることができます.
しかし、この特定のプロジェクトでは、フィードバックだけでは、少し奇妙な感じている視覚的な、あなたはそれらをヒットするときに“感じ”爆発の爆発を感じるだろう.
これは、Webブルートゥースを介していくつかのArduinoと振動センサーを接続することで修正することができますが、それは別の日のためです.😂
それはかなりだ!
この動画はお気に入りから削除されています.❤️✌️