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

## はじめに

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

:::note info
こちらの作品で **VRアカデミー賞・審査員特別賞** をいただきました！
:::

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

## 概要

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

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

## gRPCのプロトコル定義

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

```proto
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クライアントの初期化**
```c#
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.  サーバーへの描画データ送信**
```C#
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のヘルスチェック**
```C#
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から送信するデータの例を示します。
```json
{
  "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を定義し、軽量かつ高速な通信を可能に

## 参考
- [UnityとC#で簡単にgRPCする in 2023](https://zenn.dev/turing_motors/articles/7f5cc78c5d5b55)