【VR】筋電を使ってゲームコントローラーを作る
今回作ったものいいよね
せっかくVRゲームするなら、ハンドコントローラーなしでやりたくない?
ってことで筋電センサを使って魔法を発射できるシステムを作った。
動作↓
動作環境
器具
- Meta Quest 2
- Arduino Uno R4 wifi
- Grove-EMG Sensor v1.1 SCH
Unity
- Unity 2021.3.42f1
- XR Interaction Toolkit 2.6.3
- XR Plugin Management 4.40
- OpenXR Plugin 1.11.0
- XR Hands 1.4.3
Arduino
- Arduino IDE 2.3.2(バージョン1.xは非対応)
- arduinoFFT 1.6.2(バージョン2.xは非対応)
概要
ざっくりした流れはこんな感じ
筋電センサを使って筋肉の動きをセンシング→FFT解析→UDPでUnityに送信→Unity側で受信して魔法を発射。
実装
arudino側
概要
筋電センサから受け取った値をFFTで分析し、力が入ったことを感知したらUDPで送信する。
準備
arduinoFFTライブラリをインストールする。以下の画面を参考に。
コード
コードは以下。arduino_secrets.hのSECRET_SSID
SECRET_PASS
IP_ADDRESS[4]
に自分のwifiの情報を入れる。
#define SECRET_SSID "" // wifiのSSID
#define SECRET_PASS "" // wifiのパスワード
const uint8_t IP_ADDRESS[4] = {, , , };
const unsigned int LOCAL_PORT = 8889;
#include <SPI.h>
#include <WiFiS3.h>
#include <WiFiUdp.h>
#include "arduino_secrets.h"
#include "arduinoFFT.h"
// 設定の代入
char ssid[] = SECRET_SSID;
char pass[] = SECRET_PASS;
IPAddress destinationIP(IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
WiFiUDP Udp;
// FFT設定
const uint16_t SAMPLES = 64; // 常に2のべき乗
const uint16_t PAST_SAMPLES = 48; // 過去サンプル数
const double SAMPLING_FREQUENCY = 2000.0; // サンプリング周波数
const uint16_t FFT_RANGE_START = 6; // FFT計算対象範囲開始
const uint16_t FFT_RANGE_END = 28; // FFT計算対象範囲終了
const uint16_t SIGNAL_THRESHOLD = 200; // 信号の閾値
arduinoFFT FFT;
double vReal[SAMPLES];
double vImag[SAMPLES];
double vReal_fft[SAMPLES];
bool rightMuscleActive = false;
byte dataToSend = 0b00000000;
byte SendNumber = 0b00000010; // 送る数
void setup() {
Serial.begin(115200);
connectToWiFi();
initializeFFT();
}
void loop() {
updateSamples();
performFFT();
processFFTData(vReal_fft, SAMPLES / 2);
}
// --- WiFi接続 ---
void connectToWiFi() {
WiFi.begin(ssid, pass);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Udp.begin(LOCAL_PORT);
Serial.println("\nWiFi connected");
}
// --- FFT初期化 ---
void initializeFFT() {
for (uint16_t i = 0; i < SAMPLES; i++) {
vReal[i] = analogRead(A1);
vImag[i] = 0.0;
}
}
// --- サンプル更新 ---
void updateSamples() {
// 過去のサンプルを更新
for (uint16_t i = 0; i < PAST_SAMPLES; i++) {
vReal[i] = vReal[i + 8];
vImag[i] = 0.0;
vReal_fft[i] = vReal[i];
}
// 新しいサンプルを読み込み
for (uint16_t i = PAST_SAMPLES; i < SAMPLES; i++) {
vReal[i] = analogRead(A1);
vImag[i] = 0.0;
vReal_fft[i] = vReal[i];
}
}
// --- FFT実行 ---
void performFFT() {
FFT = arduinoFFT(vReal_fft, vImag, SAMPLES, SAMPLING_FREQUENCY);
FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(FFT_FORWARD);
FFT.ComplexToMagnitude();
}
// --- FFTデータ処理 ---
void processFFTData(double *fftData, uint16_t bufferSize) {
int signalSum = calculateSignalSum(fftData, bufferSize);
// デバッグ出力
Serial.print(700); // 表示の最大値
Serial.print(" , ");
Serial.print(-50); // 表示の最小値
Serial.print(" , ");
Serial.print(SIGNAL_THRESHOLD); // 閾値
Serial.print(" , ");
Serial.println(signalSum);
// 信号に応じた処理
handleSignal(signalSum);
}
// --- 信号値の集計 ---
int calculateSignalSum(double *fftData, uint16_t bufferSize) {
int sum = 0;
for (uint16_t i = FFT_RANGE_START; i < FFT_RANGE_END; i++) {
if (fftData[i] < 1000) {
sum += fftData[i];
}
}
return sum;
}
// --- 信号処理 ---
void handleSignal(int signalSum) {
dataToSend = 0b00000000;
if (signalSum > SIGNAL_THRESHOLD) {
if (!rightMuscleActive) {
rightMuscleActive = true;
dataToSend |= SendNumber;
sendUDPData(dataToSend);
}
} else {
if (rightMuscleActive) {
rightMuscleActive = false;
}
}
}
// --- UDPデータ送信 ---
void sendUDPData(byte data) {
Udp.beginPacket(destinationIP, LOCAL_PORT);
Udp.write(data);
Udp.endPacket();
}
配線
筋電センサとArduinoの接続は以下
実行しシリアルプロッターで見ると以下の感じ
緑が閾値で黄色が筋電の値
脱力時
力を入れたとき
unity側
概要
Unityでは、Arduinoから送られてきたデータを受信し、魔法の弾を生成&発射する。
ハンドトラッキングのやり方は以下とかを参考に。
コード
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class PlayerAttack : MonoBehaviour
{
// Public Variables
public GameObject MagicBullet; // 発射する弾のオブジェクト
public float ShotSpeed = 500; // 弾の速度
public float ShotInterval = 1.0f; // 射撃の間隔(秒)
// Private Variables
private UdpClient udpClient; // UDP通信
private Thread receiveThread; // データ受信スレッド
private bool udpMessageReceived = false; // UDPメッセージ受信フラグ
private float lastShotTime = 0f; // 最後に射撃した時間
public int listenPort = 8888; // UDP受信ポート番号
void Start()
{
// UDPクライアントと受信スレッドの初期化
udpClient = new UdpClient(listenPort);
receiveThread = new Thread(ReceiveData)
{
IsBackground = true
};
receiveThread.Start();
Debug.Log("UDP Receiver started.");
}
void Update()
{
// マウスクリックで射撃
if (Input.GetMouseButtonDown(0) && lastShotTime > ShotInterval)
{
MagicAttack();
}
// UDPメッセージを受信している場合
if (udpMessageReceived && lastShotTime > ShotInterval)
{
MagicAttack();
udpMessageReceived = false; // フラグをリセット
}
// 時間更新
lastShotTime += Time.deltaTime;
}
void ReceiveData()
{
// UDPメッセージ受信処理
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, listenPort);
while (true)
{
try
{
byte[] receiveBytes = udpClient.Receive(ref remoteEndPoint);
string receiveString = Encoding.ASCII.GetString(receiveBytes);
Debug.Log("Received UDP message: " + receiveString);
// UDPメッセージ受信フラグを立てる
udpMessageReceived = true;
}
catch (Exception e)
{
Debug.LogError("Error receiving UDP data: " + e.Message);
}
}
}
private void OnApplicationQuit()
{
// アプリケーション終了時のリソース解放
if (receiveThread != null && receiveThread.IsAlive)
{
receiveThread.Abort();
}
udpClient?.Close();
}
void MagicAttack()
{
// 弾を生成し、発射する
GameObject bullet = Instantiate(MagicBullet, transform.position, Quaternion.Euler(transform.parent.eulerAngles.x, transform.parent.eulerAngles.y, 0));
Rigidbody bulletRb = bullet.GetComponent<Rigidbody>();
bulletRb.AddForce(transform.forward * ShotSpeed);
// 射撃時間をリセット
lastShotTime = 0;
// 3秒後に弾オブジェクトを破棄
Destroy(bullet, 3.0f);
}
}
作業
- ハンドトラッキングをできるように
- 右手の
R_Palm
にSphere
を追加 -
Sphere
にPlayerAttack.cs
をアタをッチ - 弾用のプレハブを作成(
rigitbody
をアタッチ)し、PlayerAttack.cs
にアタッチ -
Sphere
の位置と角度を調節しmesh
とcollider
をオフに
セキュリティ
PC実行時
Unityで外部デバイスと通信する際、Windowsのセキュリティに引っかかる場合がある。ここでは簡易的に載せておく。参考サイトは以下
コントロールパネル→システムとセキュリティ→Windows Defender ファイアウォール→詳細設定→受信の規則→Unity関係をすべて許可
以下の状態に
それでもだめならファイアウォールを一時切る。
設定→ネットワークとインターネット→Wi-Fi→~のプロパティ→ファイアウォールとセキュリティ設定の構成→~(アクティブ)→オフ
オキュラス実行時
オキュラスでもUDPの許可が必要。ここでも簡易的に載せておく。参考サイトは以下
Player -> Other Settings-> Internet Access
をAutoからRequireに
また、Project Settingsから
XR Plug-in Management -> OpenXR
AndroidタブからMeta Quest Supportの歯車マークを選択
Force Remove Internet Permissionのチェックボックスを外す
ここまでで作業は終了。お疲れさまでした!
参考一覧
Discussion