5.3K Views
August 31, 24
スライド概要
Meta Questを使い現実世界と仮想世界を融合させる。
現実世界のデバイスの操作や情報の表示を、MRデバイスを使い仮想のインターフェース上で実現する方法を解説します。
・現実世界の扇風機をMRのインターフェースから操作(コンセントのオンオフ)
・現実世界の温度/湿度センサーの情報をMRで表示
・現実世界のスイッチを押すと、仮想世界が変化する
MRで現実と仮想の境界をなくす - META QUEST ⇔ デバイス連動 - 2025.01.18
大久保 聡 Mail [email protected] Twitter @followapp
1.それほど遠くない未来 2.仮想を通じて現実を操作する 3.仮想を通じてみえない情報を見る 4.現実を通じて仮想を操作する
1.それほど遠くない未来
インターフェース がMRデバイスに統合される 1 Human Interface 2 Sensor Device Life Log 新しいデータが蓄積され 3 Spatial Map +Data データが現実の位置情報と 連動できる
必要な情報が 必要な時に 必要な場所で利用でき 現実のものに 仮想のIFを通じて働きかけることができる
仮想というIFを通じて現実を操作 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 現実の操作 情報の利用
2.仮想を通じて現実を操作する
仮想を介して現実(デバイス)を操作する 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 現実の操作
META QUESTを用い、現実の操作を行う Meta Questの仮想スイッチから、現実空間の扇風機(コンセントのオン・オフ)を操作する。 今回は家電操作のPythonのライブラリが充実していることもあり、現実へのIF部分はPythonを用いる。 Meta QuestからはWeb APIを経由してアクセスさせる。
スマートコンセント https://www.tp-link.com/jp/smart-home/tapo/tapo-p105/ ネットワークからオン・オフできるスマートコンセント「tp-lin Tapo P105」を使って家電を操作す る。 1個1000-1500円くらい。 スマホのアプリ、音声アシスタント、IFTTTからコンセントのオン・オフができる。 Wifi(ローカルエリアネットワーク)で直接操作もできる。。 Tapo P105
システム構成 コンセントオンオフで制御できる家電ということで、扇風機をP105 経由で操作します。複雑な操作 が必要な場合は、IFTTTや赤外線リモコンを経由して操作すれば良いと思います。 Webサーバー 兼 家電とのIFはRaspberryPiを利用します。 Web Server (fastapi) Client API (tapo) Python 3 Tapo P105 Quest3 WebAPI RaspberryPI 5
RASPBERRY PIの環境構築 Python仮想環境の作成し、仮想環境に切替ます。 python3 -m venv .venv source .venv/bin/activate Webサーバーをインストールします。 pip install --upgrade fastapi 開発環境はVS Codeを使います。 Pythonのプログラム開発用に、Visual Studio Codeをインストールします。 sudo apt update sudo apt install code
開発環境(VS CODE)の設定 Pythonの実行環境のエクステンションをインストールします。
環境構築 Tp-Link のスマートプラグ操作用のライブラリインストールします。 pip install --upgrade tapo
準備、スマホでTAPOのIPアドレスの確認 TAPOのスマホアプリを起動し、デバイス一覧からTapo P105を選択します。 詳細ボタンを押し、割り当てられているIPアドレスを確認します。
TAPOへのアクセスとWEB API
onplug/{name}とoffplug/{name}というIFを作成し実装します。
Tapoの利用時に作成したユーザーのIDとPWが接続時に必要となります。
調べたIPアドレスとnameを対応させ、それぞれにアクセスできるようなIFにします。
Import uvicorn
From fastapi import FastAPI, HTTPException, Request
From starlette.exceptions import HTTPException as StarletteHTTPException
Import asyncio
Import os
From tapo import ApiClient
From fastapi.responses import JSONResponse
Import struct
@app.get("/offplug/{name}")
async def off(
name: str
):
ip = plugs[name]
device = await client.p105(ip)
await device.off()
return {"Result": "OK"}
app = FastAPI()
Client = ApiClient(“id", “password")
Plugs = {'Plug1':'192.168.11.10', 'Plug2':'192.168.11.11’}
if __name__ == "__main__":
uvicorn.run(app, host="192.168.11.9", port=8000)
@app.get("/onplug/{name}")
async def on(
name: str
):
ip = plugs[name]
device = await client.p105(ip)
await device.on()
return {"Result": "OK"}
QUESTからWEB APIを叩く
トグルボタンのオン/オフでコンセントと、仮想空間内の風のオン/オフを切り替えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;
public class DeviceControll : MonoBehaviour
{
private Toggle toggle;
[SerializeField]
WindZone windZone;
public void OnToggleChanged()
{
if(toggle.isOn)
{
SendWebRequest("onplug", this.GetCancellationTokenOnDestroy()).Forget();
windZone.windTurbulence = 0.5f;
}
else
{
SendWebRequest("offplug", this.GetCancellationTokenOnDestroy()).Forget();
windZone.windTurbulence = 0;
}
}
[SerializeField]
string url = "http://192.168.11.9:8000";
async UniTask SendWebRequest(string command, CancellationToken ct = default)
{
var uriBuilder = new System.UriBuilder(url + "/" + command + "/" + deviceName);
var request = UnityWebRequest.Get(uriBuilder.Uri);
[SerializeField]
string deviceName = "Plug1";
await request.SendWebRequest().ToUniTask(cancellationToken: ct);
// Start is called before the first frame update
void Start()
{
toggle = GetComponent<Toggle>();
}
}
}
動作イメージ 現実世界の扇風機が動き出すと、仮想の世界の木が風で揺れるデモです。 Youtube動画をご覧ください。 https://youtu.be/kfgXEEcWHXQ
3.仮想を通じてみえない情報を見る
仮想を通じてみえない情報(デジタル情報)を見る 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 情報の利用
META QUESTを用い、現実の可視化を行う 温度湿度(IoT機器のセンサー情報)を、Meta Questの仮想空間に表示する。 今回はIoT機器のPythonのライブラリが充実していることもあり、温湿度計へのIF部分はPythonを用 いる。 Meta QuestからはWeb APIを経由してアクセスさせる。
BLE温度・湿度計 https://inkbird.com/collections/thermometers-and-hygrometers Bluetooth Low Energy(無線)で温度・湿度が取得できる「INK BIRD IBS-TH1 MINI 」を使って、 環境情報を可視化する。 IBS-TH2が2000円くらい。 スマホアプリ、Bluetooth接続で値の取得ができる。 IBS-TH2 IBS-TH1 MINI
システム構成 Webサーバー 兼 家電とのIFはRaspberryPiを利用します。 Web Server (fastapi) BLE Library (bluepy3) Python 3 Quest3 IBS-TH1 MINI WebAPI RaspberryPI 5
RASPBERRY PIの環境構築 Python仮想環境の作成し、仮想環境に切替ます。 python3 -m venv .venv source .venv/bin/activate Webサーバーをインストールします。 pip install --upgrade fastapi 開発環境はVS Codeを使います。 Pythonのプログラム開発用に、Visual Studio Codeをインストールします。 sudo apt update sudo apt install code
開発環境(VS CODE)の設定 Pythonの実行環境のエクステンションをインストールします。
準備、IBS-TH1 MINI のMACアドレス確認 MACアドレスを調べる Bluetoothデバイスの一覧を表示し、デバイス名がspsのものを確認する。 $ sudo hcitool lescan LE Scan ... DE:FF:EF:47:C8:C8 (unknown) B0:7E:11:EE:71:51 sps 67:AA:3D:83:1D:34 (unknown) 67:AA:3D:83:1D:34 (unknown) A8:10:87:50:F7:96 (unknown) A8:10:87:50:F7:96 sps 45:11:89:9C:19:89 (unknown) CF:9B:A7:81:69:62 (unknown)
環境構築 BLEアクセス用のライブラリをインストール pip install bs4 lxml requests sudo apt-get install libglib2.0-dev libbluetooth-dev pip install --upgrade bluepy3 bluepy3-helperに権限付与 sudo setcap cap_net_raw,cap_net_admin+ep .venv/lib/python3.11/sitepackages/bluepy3/bluepy3-helper
BLEへのアクセスとWEB API
gettemp/{name}というIFを作成し実装します。
調べたMACアドレスとnameを対応させ、それぞれにアクセスできるようなIFにします。
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from starlette.exceptions import HTTPException as StarletteHTTPException
import asyncio
import os
from fastapi.responses import JSONResponse
from bluepy3 import btle
import struct
app = FastAPI()
Thermometers
= {'Thermometer1':'B0:7E:11:EE:71:51', 'Thermometer2':'A8:10:87:50:F7:96'}
@app.get("/gettemp/{name}")
async def off(
name: str
):
mac = Thermometers[name]
sensorValue = await get_ibsth1_mini_data(mac)
return {
"result": "OK",
"temp": sensorValue['Temperature’],
"humidity": sensorValue['Humidity’]
}
if __name__ == "__main__":
uvicorn.run(app, host="192.168.11.9", port=8000)
async def get_ibsth1_mini_data(macaddr):
peripheral = btle.Peripheral(macaddr)
characteristic = peripheral.readCharacteristic(0x002d)
(temp, humid, unknown1, unknown2, unknown3) = struct.unpack('<hhBBB', characteristic)
sensorValue = {
'Temperature': temp / 100,
'Humidity': humid / 100,
'unknown1': unknown1,
'unknown2': unknown2,
'unknown3': unknown3,
}
peripheral.disconnect()
return sensorValue
QUESTからWEB APIを叩く
一定間隔ごとに、温度と湿度を取得しTextの文字を更新します。
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;
using TMPro;
[System.Serializable]
public class Result
{
public string result;
public string temp;
public string humidity;
}
public class GetTemp : MonoBehaviour
{
[SerializeField]
string url = "http://192.168.11.9:8000";
[SerializeField]
string deviceName = "Thermometer1";
[SerializeField]
TextMeshProUGUI temperatureText;
[SerializeField]
TextMeshProUGUI humidityText;
[SerializeField]
Image image;
private async void Start()
{
while (true)
{
// n秒ごとに処理を行う
await UniTask.Delay(5000);
SendWebRequest("gettemp", this.GetCancellationTokenOnDestroy()).Forget();
}
}
QUESTからWEB APIを叩く
async UniTask SendWebRequest(string command, CancellationToken ct = default)
{
var uriBuilder = new System.UriBuilder(url + "/" + command + "/" + deviceName);
var request = UnityWebRequest.Get(uriBuilder.Uri);
await request.SendWebRequest().ToUniTask(cancellationToken: ct);
// Result:{"Result":"OK","Temp":27.25,"Humidity":62.66}
Result result = JsonUtility.FromJson<Result>(request.downloadHandler.text);
if (double.TryParse(result.temp, out double temp))
{
temperatureText.text = temp.ToString("F1") + "℃";
}
else
{
temperatureText.text = "- ℃";
}
if (double.TryParse(result.humidity, out double humidity))
{
humidityText.text = humidity.ToString("F1") + "%";
}
else
{
humidityText.text = "- %";
}
}
}
動作イメージ 現実の温度と湿度が、仮想世界に重畳して表示されるデモです。 Youtube動画をご覧ください。https://youtu.be/XnPf5r7HTsM
4.現実を通じ仮想を操作する
現実(スイッチ)を通じ仮想を操作する 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 情報の利用
現実(M5STACK)のスイッチを用い、仮想の操作を行う M5Stack AtomS3Fの現実のスイッチから、仮想空間のオブジェクトを操作する。 Atomのボタンを押すとオンとオフが交互に切り替わる。そのボタンのオンとオフの状態をQuest側で 受け取り、リアクションを行う。 今回はMQTT(パブサブモデル)を使い、スイッチのオン・オフの情報をパブリッシュして、Quest側で その情報をサブスクライブさせ仮想のオブジェクトを変化させる。
スイッチに使うデバイス M5Stack AtomS3Rをスイッチとして利用する。小型でWifiにつながりスイッチとディスプレイもつ いており、充電式ボタン電池(3.7V)で駆動させることも可能で携帯性にすぐれる。 AtomS3Rが2800円くらい。電池基盤が1600円くらい。バッテリーと充電器が2000円くらい。 M5ATOM用LIR2032コイン形 リチウムイオン電池基板
システム構成 IoT通信に広く使われるMQTT(Message Queuing Telemetry Transport)を使いメッセージを送受 信させる。MQTTの実装は、AtomはPubSubClient、QuestはMQTT for Unity($32.99)を利用。 メッセージを仕分けするブローカーは、パブリックブローカーのEMQX(サーバーレス無料枠)を利用す る。 MQTT for Unity Quest3 Subsctiber パブリッシュ Topic:Quest/Button Topic:Quest/Button サブスク ライブ MQTT for Unity M5Stack AtomS3R Subsctiber Broker PubSubClient Publisher Quest3
開発環境(VS CODE)の設定 開発環境はVS Codeを使います。WindowsでもMacでもお好きなプラットフォームにインストール してください。 (Arduino IDEを利用していただいても構いませんが、コンパイル時間がかなり遅かっ たためPlatfromIOを利用しました。) M5Stack開発用のPlatformIO IDEとJapanise Language Packの拡張機能(エクステンション)をイ ンストールします。拡張機能のアイコンを選択し、拡張機能を検索してインストールします。
プロジェクトを作成する PlatformIOのアイコンを選択し、Create New Projectを選択します。 プロジェクト名、BoardにAtom、FrameworkにArduinoを選択し作成します。
設定ファイルの作成 platformio.iniを開き、iniファイルを下記のように書き換える。必要なライブラリなどの依存関係と、 M5Stackと接続するためのシリアル通信のポートが記載されています。ポート番号はご自分の環境に あわせて書き換えてください。
設定の確認 この状態で一度ビルドしてみます。ビルドアイコンをクリックします。SUCCESSが表示されたらビ ルド成功です。 シリアルモニター ビルドしてデバイスへプログラムを転送 ビルド platformIOのHome画面表示
プログラムの作成(ATOM側) 接続に必要な情報
接続に必要なクラス、設定回り。
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);
// Wi-FiのSSID
const char* ssid = "Shinkansen_Free_Wi-Fi";
// Wi-Fiのパスワード
const char* password = "Password";
// MQTTの接続先のIP
const char* endpoint = "broker.emqx.io";
// MQTTのポート
const int mqttPort = 8883;
const char* mqttUsername = "emqx";
const char* mqttPassword = "public";
// メッセージを知らせるトピック
const char* pubTopic = "Quest/Button";
// メッセージを待つトピック
const char* subTopic = "Quest/+";
// Root CA Certificate
// Load DigiCert Global Root G2, which is used by EMQX Public Broker: broker.emqx.io
const char* caCert = ¥
"-----BEGIN CERTIFICATE-----¥n" ¥
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh¥n" ¥
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3¥n" ¥
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD¥n" ¥
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT¥n" ¥
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j¥n" ¥
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG¥n" ¥
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB¥n" ¥
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97¥n" ¥
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt¥n" ¥
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P¥n" ¥
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4¥n" ¥
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO¥n" ¥
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR¥n" ¥
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw¥n" ¥
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr¥n" ¥
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg¥n" ¥
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF¥n" ¥
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls¥n" ¥
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk¥n" ¥
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=" ¥
"-----END CERTIFICATE-----¥n";
プログラムの作成(ATOM側)
WIFI接続
SSIDとパスワードを指定して接続する。接続するまでひたすら待ち続ける。
void wifiLoop() {
if (WiFi.status() != WL_CONNECTED) {
connectToWiFi();
}
}
void connectToWiFi() {
// Start WiFi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.println("connecting ...");
delay(1000);
}
// WiFi Connected
Serial.println("¥nWiFi Connected.");
}
プログラムの作成(ATOM側)
MQTT接続
setServerで接続情報、setCallbackで購読しているトピックが届いた際に呼び出す関数を指定する。
購読するトピックは、接続完了後にsubscribeメソッドで指定する。
void mqttLoop() {
if (!mqttClient.connected()) {
connectMQTT();
}
mqttClient.loop();
}
void connectMQTT() {
// Set Root CA certificate
httpsClient.setCACert(caCert);
mqttClient.setServer(endpoint, mqttPort);
mqttClient.setKeepAlive(60);
mqttClient.setCallback(mqttCallback);
}
String deviceID = "AtomS3R-";
deviceID += String(random(0xffff), HEX); // 接続デバイスごとにユニークなID
while (WiFi.status() == WL_CONNECTED && !mqttClient.connected()) {
if (mqttClient.connect(deviceID.c_str(), mqttUsername, mqttPassword)) {
Serial.println("MQTT Broker Connected.");
int qos = 0;
mqttClient.subscribe(subTopic, qos);
Serial.println("Subscribed.");
} else {
Serial.print("Failed. Error state=");
Serial.println(mqttClient.state());
char buf[256];
httpsClient.lastError(buf, 256);
Serial.print("SSL error: ");
Serial.println(buf);
// Wait 5 seconds before retrying
delay(5000);
}
}
プログラム作成(ATOM側) SETUP関数
初期画面表示と、WifiとMQTTブローカーへの接続を行う。
void setup() {
auto cfg = M5.config(); // 本体初期設定
M5.begin(cfg);
Serial.begin(115200);
delay(1000);
// シリアル出力開始待ち
pinMode(BTN_GPIO, INPUT_PULLUP); // 本体ボタン端子入力設定
// 液晶初期化
M5.Lcd.init();
// Wifi接続
connectToWiFi();
// 画面表示
M5.Lcd.fillRect(0, 30, 128, 128, TFT_BLACK);
M5.Lcd.setTextColor(M5.Lcd.color565(90, 90, 90));
M5.Lcd.drawString("MQTT", 12, 2, &fonts::Font4);
toggle(isOn);
// MQTTサーバー接続
connectMQTT();
}
プログラム作成(ATOM側) LOOP関数
Atomのボタン押下状態を監視、ボタンを押すとオンとオフが交互に切り替わるようにする。
void loop() {
// 常にチェックして切断されたら復帰できるように
wifiLoop();
mqttLoop();
// ボタンOFF
if (digitalRead(BTN_GPIO) == true) { // 本体ボタンがOFFなら
btn_state = false;
// ボタン単押し状態をfalseへ
press_state = false;
// ボタン長押し状態をfalseへ
}
// ボタン長押しカウント値リセット
if (digitalRead(BTN_GPIO) == LOW) {
// 本体ボタンONなら
If (press_state == false) {
// ボタン長押し状態でなければ
press_time = millis();
// ボタン長押し時間初期値セット
press_state = true;
// ボタン長押し状態に変更
}
if (millis() >= (press_time + 800)) { // ボタン長押し時間初期値 + 800msなら
// 長押しの処理
Serial.println("Long press");
}
}
// ボタンON
if (digitalRead(BTN_GPIO) == LOW && btn_state == false) {
btn_state = true;
// ボタン単押し状態をtrueへ
Serial.println("Press");
// トグルの反転
isOn = !isOn;
toggle(isOn);
}
// ボタン押下の処理
}
プログラム作成(ATOM側) パブリッシュ部分
ボタンのオンとオフの切り替わりで、その状態をQuest/Buttonとうトピックでパブリッシュする。
void toggle(bool button) {
if (button) {
// オン
Serial.println("On");
mqttClient.publish(pubTopic, "ON");
M5.Lcd.fillRect(0, 30, 128, 128, TFT_WHITE);
M5.Lcd.setTextColor(M5.Lcd.color565(0, 0, 0));
M5.Lcd.drawString("ON", 43, 65, &fonts::Font4);
} else {
// オフ
Serial.println("Off");
mqttClient.publish(pubTopic, "OFF");
M5.Lcd.fillRect(0, 30, 128, 128, M5.Lcd.color565(30, 30, 30));
M5.Lcd.setTextColor(M5.Lcd.color565(90, 90, 90));
M5.Lcd.drawString("OFF", 38, 65, &fonts::Font4);
}
}
UNITY側設定 MQTT for UnityをAsset Storeで購入。Assetをインストール。 SimpleMqttフォルダのPrefabsにある、MqttClientをシーンに配置する。
MQTT CLIENTの設定 Inspectorで、Mqtt ClientのHost/Portの設定。どのメッセージを購読するかサブスクライブの設定。 購読したメッセージ受信時に呼び出す関数を指定。
購読したメッセージに応じた処理を記載 受け取ったメッセージをハンドリングする処理。オン・オフに合わせてGameObjectのアクティブを 切り替え。今回は、雨雲の雨のパーティクルのアクティブを切り替えるように設定しました。 public void OnMessageArrived(MqttMessage m) { string value; switch (m.GetTopic()) { case "Quest/Button": Debug.Log(m.GetString()); value = m.GetString(); break; default: value = ""; break; } switch (value) { case "ON": ObjectToHandle.SetActive(true); break; case "OFF": ObjectToHandle.SetActive(false); break; default: break; } }
パーミッションの設定 Player SettingsでCustom Main Manifestを有効にし、通信回りのパーミッション設定を行います。 usesCleartextTrafficは、Android9以降はデフォルトがfalseになっているのでHTTP通信の場合は Trueに変更が必要です。INTERNET/ACCESS_NETWORK_STATEを追記しました。 <?xml version="1.0" encoding="utf-8" standalone="no"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:label="@string/app_name" android:icon="@mipmap/app_icon" android:allowBackup="false"> <activity android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" android:configChanges="locale|fontScale|keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touc hscreen|uiMode" android:launchMode="singleTask" android:name="com.unity3d.player.UnityPlayerGameActivity" android:excludeFromRecents="true" android:exported="true" android:usesCleartextTraffic="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="com.oculus.intent.category.VR" /> </intent-filter> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> </activity> <meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="false" /> <meta-data android:name="com.samsung.android.vr.application.mode" android:value="vr_only" /> <meta-data android:name="com.oculus.ossplash.background" android:value="black" /> <meta-data android:name="com.oculus.telemetry.project_guid" android:value="11f20e45-8162-4c89-9cf8-003f285cbdb3" /> <meta-data android:name="com.oculus.supportedDevices" android:value="quest|quest2|questpro|quest3|quest3s" /> </application> <uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="true" /> </manifest>
動作イメージ QuestとAtomがネットワークにつながる状態で起動。 現実世界のAtomのボタンを押すと、Questの仮想環境に雨が降るデモです。 https://youtu.be/s0xmgifXtqs