[Unity]ArtNet経由で照明を操作できるようにしてみた


https://twitter.com/VR_ize/status/1440709516710989824
こちらのイベントにてQLC+&Artnetを使ってUnityの照明操作をできるようにしたのでその時のデバッグを含めた感じの記事です.

当然っちゃ当然だけど記事が超すくないので少しでも助けになればいいかな~の思いで要所抑えた感じのカロリー低めで書いていきます.

用語について

  • QLC+ : 照明を操作するためのフリーのソフトウェア おそらく誰でも利用できるとなるとQLCくらいでそれ以外は基本有料
  • ArtNet : 現実の照明において,照明卓から各照明に信号を送るのに利用するプロトコル.LANで送れるので便利
    • これのおかげでQLC操作用PCからUnityPCへの接続がスイッチングハブを介したLAN接続のみで済む.
  • OpCode:ArtNet経由で送られてきた信号の種類を判別するためのもの.

UnityでのArtnet受け取りについて

プロトコル解釈,受信等含めこちらのGithubのライブラリを利用させていただいてます.
https://github.com/Taku3939/ArtNetReceiver

readmeにもあるように,ArtNetClientをなんらかのゲームオブジェクトにアタッチし,
Artnetから送られたデータを処理したいメソッドをイベントとしてArtNetClientに登録すれば
データを受信するたびにそれらが実行される,という仕組みです.簡単ですね!

ムービングライトの実装

実際にムービングライト用に作成したスクリプトです.


public class ArtNetMovingLight : MonoBehaviour
{
    [SerializeField] public uint startChannel = 1;
    [SerializeField] private ArtNetClient artNetClient;
    [SerializeField] private float angularVelocity;//灯体の回転速度
    [SerializeField] private Light spotLight;//灯体に仕込んだスポットライト
    [SerializeField] private Transform tiltBody;//ムービングヘッドを指定します
    [SerializeField] private Transform panBody; //ムービングライトのボディにあたる部分を指定

    private Color artNetColor = Color.black;
    public float panOffset = 0;
    public float tiltOffset = -135;
    public float zoomOffset = 6;
    private float pan = 0;
    private float tilt = 0;


    private void OnEnable()
    {
        pan = panBody.localEulerAngles.z;
        tilt = tiltBody.localEulerAngles.x;
        artNetClient.OnDataReceived += EventHandler; 
    }

    private void EventHandler(ArtNetData data)
    {

        var targetPan = data.Channels[startChannel - 1] * (540.0f / 255.0f);
        var targetTilt = data.Channels[startChannel] * (270.0f / 255.0f);
        var zoom = data.Channels[startChannel + 1] * (54.0f / 255.0f);
        artNetColor.r = data.Channels[startChannel + 2] / 255.0f;
        artNetColor.g = data.Channels[startChannel + 3] / 255.0f;
        artNetColor.b = data.Channels[startChannel + 4] / 255.0f;

        pan = Mathf.MoveTowards(pan, targetPan + panOffset, angularVelocity * Time.deltaTime);
        tilt = Mathf.MoveTowards(tilt, targetTilt + tiltOffset, angularVelocity * Time.deltaTime);
        panBody.localEulerAngles = new Vector3(0, 0, pan);
        tiltBody.localEulerAngles = new Vector3(tilt, 0, 0);

        spotLight.spotAngle = zoom + zoomOffset;
        spotLight.color = artNetColor;


    }
}

先ほど述べたようにデータを受信した際の処理をEventHandlerメソッドとして実装しています.OnEnable時にArtNetClientにEventHandlerを追加しています.

実際に行っている処理ですが,ArtNetではまずユニバースという単位灯体をグループ分けできます.そこからさらにチャンネルという単位で灯体ごとに設定したスタートチャンネルを元に,受け取るデータを区別することができます.
今回はユニバースを区別しないため,その処理を省いたものが上記のスクリプトです.
また,panの処理が少々厄介で,灯体は基本的に0~540度の間で回転するので,そのままUnityのRotateで扱うと360度を超えた時点でUnity側で0度から再度スタートしてしまい,QLC側と同じ動きをするのが難しくなってしまいます.そのため,一度別の変数にかませてからUnityのRotateに変換する必要があります.

ライトのちらつき問題

現在は修正されています
ライトの操作ができるようになりはしゃいでいたんですが,Unity側のライトが定期的にちらつくという現象が発生しました.
こちらは割と解決に時間がかかったのですが,ArtNet側が定期的にライトの操作以外に必要な通信もするのですがそれを正直に受け取ってしまっていたのが原因でした.
それを区別するのが用語説明で出現したOpCodeという数字の羅列です.ライト操作にかかわるOpCodeは20480なのでArtNetClient側のスクリプトを以下のように書き換えました.

       private async Task Loop(CancellationToken token)
        {
            if (client == null) return;

            while (true)
            {
                if (token.IsCancellationRequested) return;
                try
                {
                    while (client.Available != 0)
                    {
                        var buf = await client.ReceiveAsync();
                        var data = new ArtNetData(buf.Buffer);
                        if(data.OpCode == 20480)//opコードでデータの判別
                            context.Post(_ => OnDataReceived?.Invoke(data), null);
                    }
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
        }

変更を加えたのはif(data.OpCode == 20480)の部分と,OpCodeを解釈する都合であらかじめArtNetDataを生成する必要があったため,
あらかじめdata変数に格納.Invoke時には生成済みのデータを渡すようになっています.

 var data = new ArtNetData(buf.Buffer);
 if(data.OpCode == 20480)//opコードでデータの判別
     context.Post(_ => OnDataReceived?.Invoke(data), null);

以上,雑記でした.