🦒

Unity×gRPCでVR描画データを送信する

2025/02/27に公開

はじめに

本記事は2025年3月1日(土)に開催した、VRプロフェッショナルアカデミー主催の 『第16回VRフェス ~VRが創るミライ★ARが救うセカイ』virtual reality festival vol.16 にて、VRエキスパートコース16期生として出展させていただいた作品 『ペンと鍵の部屋VR』 の補助資料になります。

本プロジェクトでは、VR空間内で描画したものを解析し、アイテムとして生成する仕組み を実装するために gRPC通信 を活用しました。

概要

Unity(MetaQuest3)gRPCを活用して、VR内の描画データをサーバーへ送信する仕組みについて記述します。

  • gRPCとは?
    • Googleが開発した高性能なRPC(Remote Procedure Call)フレームワーク。
    • ProtoBufを使用して通信データを最適化。
    • リアルタイム通信に適している。

gRPCのプロトコル定義

以下は、VRの描画データを送信するために作成した ProtoBuf 定義です。

syntax = "proto3";

option csharp_namespace = "VRAcademyAudition";

package vracademyaudition;

// ヘルスチェック
message HealthCheckRequest {}
message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0; // 不明な状態
        SERVING = 1; // サービス中
        NOT_SERVING = 2; // サービス停止中
    }
    ServingStatus status = 1; // サービスの状態
    string message = 2; // エラーメッセージなど
}

// 3Dベクトル情報
message Vector3Proto {
    float x = 1;
    float y = 2;
    float z = 3;
}

// 色情報
message Color {
    float r = 1;
    float g = 2;
    float b = 3;
    float a = 4; // アルファ値(透明度)
}

// 線データ
message Line {
    repeated Vector3Proto positions = 1; // LineRenderer.GetPositions()
    float width = 2; // 線の太さ
    Color color = 3; // 線の色
}

// クライアント情報の定義
message ClientInfo {
    enum ClientType {
        UNKNOWN = 0;
        DEVELOPMENT = 1;    // 開発環境
        PRODUCTION = 2;     // 本番環境
    }
    ClientType type = 1;
    string device_id = 2;           // デバイス固有ID
    string device_name = 3;         // デバイス名
    string system_info = 4;         // システム情報
    string app_version = 5;         // アプリケーションバージョン
}

// 描画データの定義
message DrawingData {
    string drawing_id = 1;              // 一意識別子
    string scene_id = 2;                // シーンID
    int64 draw_timestamp = 3;           // 描画タイムスタンプ(UNIX時間)
    repeated Line draw_lines = 4;       // 複数のLineRendererデータ
    Vector3Proto center = 5;            // 描画全体の中心点    
    bool use_ai = 6;                    // AIによる処理を行うかどうか
    string client_id = 7;               // クライアントID
    ClientInfo client_info = 8;         // クライアント情報
    map<string, string> metadata = 9;   // メタデータ
}

// アップロード結果を表すメッセージ
message UploadResponse {
    bool success = 1;        // アップロード成功フラグ
    string message = 2;      // メッセージ(エラー時はエラー内容)
    string upload_id = 3;    // アップロードされたデータの識別子
}

// AI判定結果を表すメッセージ(クライアント)
message ShapeRecognitionClient {
    bool success = 1;         // 処理成功フラグ
    string drawing_id= 2;       // 描画データID
    string prefab_name = 3;     // 生成するプレハブ名
    string error_message = 4;   // エラーメッセージ
}

// AI判定結果を表すメッセージ(サーバー)
message ShapeRecognitionServer {
    string result_id = 1;       // 結果ID
    string drawing_id = 2;      // 描画データID
    string scene_id = 3;        // シーンID
    string shape_id = 4;        // 形状ID
    string prefab_name = 5;     // プレハブ名
    bool success = 6;           // 処理成功フラグ
    int32 score = 7;            // 信頼度スコア
    string reasoning = 8;       // AI判定の理由
    int32 process_time_ms = 9;  // 処理時間(ミリ秒)
    string model_name = 10;     // 利用したモデル名
    string api_response = 11;   // APIのレスポンス
    string error_message = 12;  // エラーメッセージ
    string client_id = 13;      // クライアントID
}

// サービス定義
service DrawingService {
    // ヘルスチェック用
    rpc CheckHealth (HealthCheckRequest) returns (HealthCheckResponse);
    
    // データのアップロード用
    rpc UploadDrawing (DrawingData) returns (UploadResponse);

    // 描画データを処理してAI判定結果を返す
    rpc ProcessDrawing (DrawingData) returns (ShapeRecognitionClient);
}

UnityでのgRPCクライアント実装

Grpc.Net.Clientと Cysharp.Threading.Tasks(UniTask) を使用してgRPC通信を非同期処理として実装しました。

1. gRPCクライアントの初期化

using UnityEngine;
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Net.Http;
using Cysharp.Threading.Tasks;
using Grpc.Net.Client;
using VRAcademyAudition;

public class GrpcClient : MonoBehaviour
{
    [SerializeField] private string serverAddress = "http://localhost:50051";
    private GrpcChannel channel;
    private DrawingService.DrawingServiceClient client;

    private async void Start()
    {
        await InitializeClient();
    }

    private async UniTask InitializeClient()
    {
        channel = GrpcChannel.ForAddress(serverAddress);
        client = new DrawingService.DrawingServiceClient(channel);
        Debug.Log("gRPC Client Initialized");
    }
}

2. サーバーへの描画データ送信

public async UniTask<string> SendDrawingDataAsync(LineRenderer[] lineRenderers, Vector3 center)
{
    List<Line> worldLines = new List<Line>();
    foreach (var lr in lineRenderers)
    {
        worldLines.Add(ConvertToProtoLine(lr));
    }

    var drawingData = new DrawingData
    {
        DrawingId = Guid.NewGuid().ToString(),
        SceneId = "TestScene",
        DrawTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
        Center = new Vector3Proto { X = center.x, Y = center.y, Z = center.z },
        UseAi = true,
        ClientId = SystemInfo.deviceUniqueIdentifier,
        DrawLines = { worldLines }
    };

    var uploadResponse = await client.UploadDrawingAsync(drawingData);
    return uploadResponse.Success ? uploadResponse.UploadId : null;
}

3. gRPCのヘルスチェック

public async UniTask<bool> CheckServerHealth()
{
    try
    {
        var request = new HealthCheckRequest();
        var response = await client.CheckHealthAsync(request);
        return response.Status == HealthCheckResponse.Types.ServingStatus.Serving;
    }
    catch (Exception ex)
    {
        Debug.LogError($"Health check failed: {ex.Message}");
        return false;
    }
}

4. サンプルデータ
MetaQuest3から送信するデータの例を示します。

{
  "drawing_id": "1234567890abcdef",
  "scene_id": "TestScene",
  "draw_timestamp": 1715678901234,
  "draw_lines": [
    {
      "positions": [
        { "x": 0.0, "y": 1.0, "z": 2.0 },
        { "x": 1.0, "y": 2.0, "z": 3.0 }
      ],
      "width": 0.05,
      "color": { "r": 1.0, "g": 0.0, "b": 0.0, "a": 1.0 }
    }
  ],
  "center": { "x": 0.5, "y": 1.5, "z": 2.5 },
  "use_ai": true,
  "client_id": "device-123456"
}

まとめ

  • gRPCを活用してVR描画データをサーバーへ効率的に送信
  • UniTaskを活用した非同期処理でスムーズな通信を実現
  • ProtoBufを定義し、軽量かつ高速な通信を可能に

参考

Discussion