たるこすの日記

たるこすの日記

リアルからバーチャルへ、バーチャルからリアルへ

[Unity]UNET の LLAPI(Low-Level API) を使ってテクスチャを送受信する

こんにちは、たるこすです。

Unity には複数のUnityアプリ間で通信を行うための、UNET という仕組みがあります。
UNET には HLAPI (High-Level API) と LLAPI (Low-Level API) があり、 今回は LLAPI を用いてテクスチャの送受信を行いたいと思います。
LLAPI はソケット通信をラップしたようなものなので、 別の Unity アプリ間でも通信が可能です。

今回紹介するコードやサンプルシーンを含んだUnityプロジェクトは以下のリポジトリで公開しているので、 良ければ参考にしてみてください。

github.com

初期化(Server側, Client側)

public class LLAPINetworkManager : MonoBehaviour
{
    public int localPort = 8212;
    public int maxConnection = 10;
    private Dictionary<QosType, int> channelIdDictionary = new Dictionary<QosType, int>();
    private int hostId;

    void Start()
    {
        NetworkTransport.Init();
        ConnectionConfig config = new ConnectionConfig();
        foreach (QosType qosType in Enum.GetValues(typeof(QosType)))
        {
            var channeld = config.AddChannel(qosType);
            channelIdDictionary.Add(qosType, channeld);
        }
        HostTopology topology = new HostTopology(config, maxConnection);
        hostId = NetworkTransport.AddHost(topology, localPort);
    }
    ...
}

はじめに、使用するポートやチャンネルなどの初期設定を行います。
チャンネルには QoS(Quality of Service) を設定します。
QoS はデータの送り方(再送処理を行うかどうかなど)を表しています。

接続(Client側)

public class LLAPINetworkManager : MonoBehaviour
{
    public string serverAddress = "127.0.0.1";
    public int serverPort = 8212;
    private int connectionId;

    void Connect()
    {
        byte error;
        connectionId = NetworkTransport.Connect(hostId, serverAddress, serverPort, 0, out error);
    }
    ...
}

Client側からServer側に接続します。
戻り値で取得した connectionId はデータ送信時に利用します。

データ送信(Server側、Clien側)

public class LLAPINetworkManager : MonoBehaviour
{
    public void SendPacketData(byte[] data, QosType qos = QosType.Reliable)
    {
        var channelId = channelIdDictionary[qos];
        byte error;
        NetworkTransport.Send(hostId, connectionId, channelId, data, data.Length, out error);
    }
    ...
}

hostId, connectionId, channelId を指定して、byte 配列のデータを送信します。
Client側の場合は Connect 時に connectionId を取得できますが、 Server側では次に紹介するデータ受信の ConnectEvent を受け取った際に connectionId を取得できます。

データ受信(Server側、Client側)

public class LLAPINetworkEventArgs : EventArgs
{
    public NetworkEventType eventType { set; get; }
    public byte[] data { set; get; }
    public LLAPINetworkEventArgs(NetworkEventType t, byte[] d)
    {
        eventType = t;
        data = d;
    }
}

public class LLAPINetworkManager : MonoBehaviour
{
    public bool isServer = true;
    private bool connected = false;
    public readonly int MaxBufferSize = 65535;

    public delegate void NetworkEventHandler(object sender, LLAPINetworkEventArgs e);
    public event NetworkEventHandler OnConnected = delegate (object s, LLAPINetworkEventArgs e) { };
    public event NetworkEventHandler OnDisconnected = delegate (object s, LLAPINetworkEventArgs e) { };
    public event NetworkEventHandler OnDataReceived = delegate (object s, LLAPINetworkEventArgs e) { };
    public event NetworkEventHandler DataReceived = delegate (object s, LLAPINetworkEventArgs e) { };

    void Update()
    {
        int recHostId;
        int connectionId;
        int channelId;
        byte[] recBuffer = new byte[MaxBufferSize];
        int bufferSize = MaxBufferSize;
        int dataSize;
        byte error;
        NetworkEventType recData = NetworkTransport.Receive(out recHostId, out connectionId, out channelId, recBuffer, bufferSize, out dataSize, out error);
        LLAPINetworkEventArgs e;
        switch (recData)
        {
            case NetworkEventType.Nothing:
                if (!isServer && !connected)
                {
                    Connect();
                    connected = true;
                }
                break;
            case NetworkEventType.ConnectEvent:
                this.connectionId = connectionId;
                connected = true;
                e = new LLAPINetworkEventArgs(recData, null);
                OnConnected(this, e);
                break;
            case NetworkEventType.DataEvent:
                e = new LLAPINetworkEventArgs(recData, recBuffer);
                OnDataReceived(this, e);
                break;
            case NetworkEventType.DisconnectEvent:
                connected = false;
                e = new LLAPINetworkEventArgs(recData, null);
                OnDisconnected(this, e);
                break;
        }
    }
    ...
}

Update 関数内で、NetworkTransport.Receive を呼び、接続イベントの受け取りやデータ受信を行います。
先程書いたとおり、Server 側ではここで connectionId を取得できます。
(ただし、上記コードでは複数のClientから接続された場合、connectionId を上書いてしまいます。)

テクスチャの送信

public class TextureSender : MonoBehaviour
{
    public LLAPINetworkManager NetworkManager;
    Type textureType;
    Texture2D texture2D;
    RenderTexture renderTexture;

    void SendTexture()
    {
        var texture = GetComponent<Renderer>().material.mainTexture;
        textureType = texture.GetType();
        if (textureType == typeof(Texture2D))
        {
            texture2D = texture as Texture2D;
        }
        else if (textureType == typeof(RenderTexture))
        {
            renderTexture = texture as RenderTexture;
        }

        if (textureType == typeof(RenderTexture))
        {
            RenderTexture currentActiveRT = RenderTexture.active;
            RenderTexture.active = renderTexture;
            texture2D = new Texture2D(renderTexture.width, renderTexture.height);
            texture2D.ReadPixels(new Rect(0, 0, texture2D.width, texture2D.height), 0, 0);
        }

        var pngTexture = texture2D.EncodeToPNG();
        NetworkManager.SendPacketData(pngTexture, QosType.UnreliableFragmented);
    }
}

Texture2D の EncodeToPNG 関数を呼ぶことで、png 画像形式のバイト配列を取得できます。
これを、先程のデータ送信のコードを使って送信します。

テクスチャの受信

public class TextureReceiver : MonoBehaviour {
    public LLAPINetworkManager NetworkManager;
    Texture2D mainTexture;

    private void Awake()
    {
        mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture;
        NetworkManager.OnDataReceived += OnDataReceived;
    }

    void OnDataReceived(object o, LLAPINetworkEventArgs args)
    {
        ApplyTextureData(args.data);
    }

    void ApplyTextureData(byte[] data)
    {
        using (MemoryStream inputStream = new MemoryStream(data))
        {
            BinaryReader reader = new BinaryReader(inputStream);
            byte[] readBinary = reader.ReadBytes((int)reader.BaseStream.Length);

            // get texture size
            int pos = 16;
            int width = 0;
            for (int i = 0; i < 4; i++)
            {
                width = width * 256 + readBinary[pos++];
            }
            int height = 0;
            for (int i = 0; i < 4; i++)
            {
                height = height * 256 + readBinary[pos++];
            }

            var texture = new Texture2D(width, height);
            texture.LoadImage(readBinary);
            Destroy(GetComponent<Renderer>().material.mainTexture);
            GetComponent<Renderer>().material.mainTexture = texture;
        }
    }
}

テクスチャの縦横の大きさをPNGデータから取得し、 その値でTexture2Dを初期化しています。
受信したバイト配列のPNGデータからTextureに変換するには、 Texture2D の LoadImage を使います。

おまけ

今回紹介した仕組みを使って Android アプリと HoloLens アプリで通信を行い、 Android アプリから書いた絵やコマンドが送れるようになりました。