Arduino等と連携してバーチャルと現実を繋げる「OSC Sender」の紹介

clusterのワールドでは「OSC」という通信プロトコルを利用して、ワールド内のギミックから外部アプリケーション等とメッセージを送受信することができます。例えば、SwitchBotと連動してスマートホームとバーチャル空間を連動させたり、ArduinoやRaspberryPi等のマイコンを使えば面白いバーチャル連動デバイスをつくることができるかもしれません。

今回はCluster Creator Kit v2.35.0.1から利用可能になったOSC送信機能について、使い方とArduino連動やVMCデータの送信など作例の紹介をしていきます。

OSC(Open Sound Control)はネットワークを利用した通信プロトコルのひとつで、数値や文字列などのデータを送信することができます。
clusterはOSC送受信に対応しており、外部の様々なアプリケーションとデータをやり取りすることができます。

ワールドのギミックとOSC Senderを連動させるシンプルな例として、「プレイヤーが特定の範囲(コライダー)に入ったらTrue、範囲から出たらFalseの情報をOSCで送信する」というものをつくってみます。
Cluster Creator Kitを導入し、ワールドに必要なセットアップをおこなったUnityシーンを用意してください。

PlayerScriptの設定

OCS Senderはギミック機能のうち、各プレイヤーに対する挙動を設定する「Player Script」内で利用できます。

まずはシーンに空のGameObjectを作成し、「Scriptable Item」コンポーネントを追加してください。
Projectの右クリックから「Create>cluster>ClusterScript」でスクリプトファイルを作成し、Scriptable ItemのSource Code Assetに設定します。

$.onStart(() => {
    $.state.knownPlayers = [];
});

$.onUpdate((deltaTime) => {
    // 既知(PlayerScriptをset済み)のプレイヤーのうち、退出していないプレイヤー
    const knownPlayers = $.state.knownPlayers.filter(knownPlayer => knownPlayer?.exists());

    // ワールド内にいるすべてのプレイヤー
    const allPlayers = $.getPlayersNear($.getPosition(), Infinity).filter(player => player?.exists());

    // 新しく入室したプレイヤー=すべてのプレイヤーのうち既知のプレイヤーに含まれないプレイヤー
    const newPlayers = allPlayers.filter(player => !knownPlayers.some(knownPlayer => knownPlayer.id === player.id));

    // 新しく入室したプレイヤーにPlayerScriptを適用する
    newPlayers.forEach(newPlayer => {
        $.setPlayerScript(newPlayer);
    });

    // 既知プレイヤー一覧を更新
    $.state.knownPlayers = allPlayers;
});

続いて、同じGameObjectに「PlayerScript」コンポーネントを追加し、同様にClusterScriptファイルを作成・設定します。
こちらのスクリプトファイルは以下の内容にしてください。

// プレイヤーがギミックからメッセージを受け取ったときに、OSCメッセージを送信する
_.onReceive((messageType, arg, sender) => {
    if (messageType === "SendOsc") {
        // ギミックから受け取った値
        const value = arg.value;
        
        // OSCメッセージ用のValueを生成する
        // boolやnumberの値そのままでは送信できないことに注意
        const oscValues = [
            OscValue.bool(value),
        ];

        // AddressとValuesを設定してOSCメッセージを生成
        const oscMessage = new OscMessage("/Sample/Enter", oscValues);

        // OSCメッセージを送信
        _.oscHandle.send(oscMessage);

        _.log(`OSC Send ${value}`);
    }
}, { item: true, player: true });

ひとつ目のスクリプトはワールドに入室したプレイヤーにPlayerScriptをセットするもの、ふたつ目のスクリプトが実際にOSCメッセージを送信するPlayerScriptです。
このPlayerScriptではプレイヤーがアイテムからメッセージを受け取った際に、その内容に応じてOSCメッセージを送信するようになっています。
各メソッドの詳細はスクリプトリファレンスを参照してください。

ギミックの設定

続いて、ワールドのギミックからプレイヤーにメッセージを送信する部分を作成します。

シーン上にもうひとつ空のGameObjectを作成し、「Scriptable Item」コンポーネントを追加してください。

続いてScriptable Itemの子階層にGameObjectを追加し、Box Colliderコンポーネントと「Overlap Detector Shape」コンポーネントを追加します。
コライダーにOverlap Detector Shapeを設定することで「コライダーの範囲内にいるプレイヤーやアイテムを取得する」ために利用できるようになります。

スクリプトファイルを作成してScriptable ItemのSource Code Assetに設定し、以下の内容を書き込んでください。
Overlap Detector Shapeの範囲にプレイヤーが出入りしたときに、そのプレイヤーにBool値を送信します。

$.onStart(() => {
    $.state.overlappingPlayers = [];
});

$.onUpdate((deltaTime) => {
    const lastOverlappingPlayers = $.state.overlappingPlayers;

    // Overlap Detector Shapeのコライダーに入っているオブジェクトを取得し、そのうちプレイヤーであるものを抽出する
    const overlappingObjects = $.getOverlaps();
    const overlappingHandles = overlappingObjects.map(overlappingObject => overlappingObject.handle);
    const overlappingPlayers = overlappingHandles.filter(handle => handle?.type === "player");

    // 新しくコライダーに入ったプレイヤー=前のフレームでコライダーに入っておらず、このフレームで入っている
    const enterPlayers = overlappingPlayers.filter(player => !lastOverlappingPlayers.some(lastOverlappingPlayer => lastOverlappingPlayer.id === player.id));
    enterPlayers.forEach(player => {
        player.send("SendOsc", { value: true });
        $.log(`Send True to @${player.userId}`);
    });

    // コライダーから出たプレイヤー=前のフレームでコライダーに入っており、このフレームで入っていない
    const exitPlayers = lastOverlappingPlayers.filter(player => !overlappingPlayers.some(overlappingPlayer => overlappingPlayer.id === player.id));
    exitPlayers.forEach(player => {
        player.send("SendOsc", { value: false });
        $.log(`Send False to @${player.userId}`);
    });

    $.state.overlappingPlayers = overlappingPlayers;
});

OSC機能の設定

OSC機能を利用するためには、各プレイヤーのアプリ設定で通信を許可する必要があります。

右上のclusterアイコンからメニューを開き、歯車アイコンで設定を開きます。
設定の「その他」から「OSC送信」の設定を変更してください。
「OSC送信の有効化」のトグルをオンに変更し、「OSC送信の設定」の歯車アイコンから送信先IPアドレスとポート番号を確認、必要に応じて変更します。

複数メッセージをまとめて送信

OSCメッセージを送信する際、複数のメッセージをBundleにまとめて送信することもできます。
PlayerScriptを以下のように書き換えてみましょう。上と同じBool型のメッセージに加えて、ユーザーIDの情報をStringで送信しています。

_.onReceive((messageType, arg, sender) => {
    if (messageType === "SendOsc") {
        const value = arg.value;

        const enterValues = [
            OscValue.bool(value),
        ];
        const enterMessage = new OscMessage("/Sample/Enter", enterValues);

        const userId = _.getUserId(_.playerId);
        const userIdValues = [
            OscValue.asciiString(userId),
        ];
        userIdMessage = new OscMessage("/Sample/UserId", userIdValues);

        // 複数のメッセージをまとめたBundleをsendできる
        const bundle = new OscBundle([
            enterMessage,
            userIdMessage,
        ]);
        _.oscHandle.send(bundle);
    }
}, { item: true, player: true })

Arduinoなどハードウェアとの連動

OSCを使うと外部のさまざまなアプリやデバイスと連動することができます。
例として、Arduinoを利用して上記のサンプルと連動する仕組みをつくってみました。アドレス/Sample/EnterのOSCメッセージを受け取り、プレイヤーが範囲に出入りするとLEDが点灯・消灯します。

VMCデータの送信

OSCの中でも人型モデルのモーションデータを送信するために定義されているVMCプロトコルの形式で、ワールド内のアバターのモーションを外部の対応アプリケーションに送信してみます。
動画ではVMC対応アプリケーションとして「VMagicMirror」を例にしています。

以下がPlayerScriptの例です。

// 約30FPSで送信
const updateCycleSeconds = 0.03;

// ボーンの親子関係を定義
class BoneInfo {
    constructor(name, children = []) {
        this.name = name;
        this.children = children;
        this.humanoidBoneId = HumanoidBone[name];
        if (this.humanoidBoneId == null) {
            _.log(`Invalid Humanoid Bone: ${name}`);
        }
    }
}

const humanoidBones = [
    new BoneInfo("Hips", [
        new BoneInfo("Spine", [
            new BoneInfo("Chest", [
                new BoneInfo("UpperChest", [
                    new BoneInfo("Neck", [
                        new BoneInfo("Head", [
                            new BoneInfo("LeftEye"),
                            new BoneInfo("RightEye"),
                            new BoneInfo("Jaw"),
                        ]),
                    ]),
                    new BoneInfo("LeftShoulder", [
                        new BoneInfo("LeftUpperArm", [
                            new BoneInfo("LeftLowerArm", [
                                new BoneInfo("LeftHand", [
                                    new BoneInfo("LeftThumbProximal", [
                                        new BoneInfo("LeftThumbIntermediate", [
                                            new BoneInfo("LeftThumbDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("LeftIndexProximal", [
                                        new BoneInfo("LeftIndexIntermediate", [
                                            new BoneInfo("LeftIndexDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("LeftMiddleProximal", [
                                        new BoneInfo("LeftMiddleIntermediate", [
                                            new BoneInfo("LeftMiddleDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("LeftRingProximal", [
                                        new BoneInfo("LeftRingIntermediate", [
                                            new BoneInfo("LeftRingDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("LeftLittleProximal", [
                                        new BoneInfo("LeftLittleIntermediate", [
                                            new BoneInfo("LeftLittleDistal"),
                                        ]),
                                    ]),
                                ]),
                            ]),
                        ]),
                    ]),
                    new BoneInfo("RightShoulder", [
                        new BoneInfo("RightUpperArm", [
                            new BoneInfo("RightLowerArm", [
                                new BoneInfo("RightHand", [
                                    new BoneInfo("RightThumbProximal", [
                                        new BoneInfo("RightThumbIntermediate", [
                                            new BoneInfo("RightThumbDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("RightIndexProximal", [
                                        new BoneInfo("RightIndexIntermediate", [
                                            new BoneInfo("RightIndexDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("RightMiddleProximal", [
                                        new BoneInfo("RightMiddleIntermediate", [
                                            new BoneInfo("RightMiddleDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("RightRingProximal", [
                                        new BoneInfo("RightRingIntermediate", [
                                            new BoneInfo("RightRingDistal"),
                                        ]),
                                    ]),
                                    new BoneInfo("RightLittleProximal", [
                                        new BoneInfo("RightLittleIntermediate", [
                                            new BoneInfo("RightLittleDistal"),
                                        ]),
                                    ]),
                                ]),
                            ]),
                        ]),
                    ]),
                ]),
            ]),
        ]),
        new BoneInfo("LeftUpperLeg", [
            new BoneInfo("LeftLowerLeg", [
                new BoneInfo("LeftFoot", [
                    new BoneInfo("LeftToes"),
                ]),
            ]),
        ]),
        new BoneInfo("RightUpperLeg", [
            new BoneInfo("RightLowerLeg", [
                new BoneInfo("RightFoot", [
                    new BoneInfo("RightToes"),
                ]),
            ]),
        ]),
    ]),
];

function createVMCData () {
    const boneTransformsList = getLocalBoneTransformList(humanoidBones, _.getPosition(), _.getRotation());
    const messages = boneTransformsList.map(bone => {
        const boneName = bone.name;
        const bonePosition = bone.position;
        const boneRotation = bone.rotation;
        const msg = new OscMessage(
            "/VMC/Ext/Bone/Pos",
            [
                OscValue.asciiString(boneName),
                OscValue.float(bonePosition.x), OscValue.float(bonePosition.y), OscValue.float(bonePosition.z),
                OscValue.float(boneRotation.x), OscValue.float(boneRotation.y), OscValue.float(boneRotation.z), OscValue.float(boneRotation.w)
            ]);
        return msg;
    });
    const oscBundle = new OscBundle(messages);
    return oscBundle;
}

// 再帰的にボーン階層を辿りながら、各ボーンのLocal Transformを求める
function getLocalBoneTransformList(bones, parentPosition, parentRotation) {
    let boneTransformsList = [];
    const parentRotationInverse = parentRotation.clone().invert();

    bones.forEach(bone => {
        const humanoidBoneId = bone.humanoidBoneId;
        const globalPosition = _.getHumanoidBonePosition(humanoidBoneId);
        const globalRotation = _.getHumanoidBoneRotation(humanoidBoneId);
        if (globalPosition == null || globalRotation == null) {
            // 該当のボーンが省略されている場合、親ボーンから孫ボーンに直接接続されているとみなす
            boneTransformsList = boneTransformsList.concat(getLocalBoneTransformList(bone.children, parentPosition, parentRotation));
        }
        else {
            const localPosition = globalPosition.clone().sub(parentPosition).applyQuaternion(parentRotationInverse);
            const localRotation = parentRotationInverse.clone().multiply(globalRotation);
            boneTransformsList.push({ name: bone.name, position: localPosition, rotation: localRotation });
            boneTransformsList = boneTransformsList.concat(getLocalBoneTransformList(bone.children, globalPosition, globalRotation));
        }
    });
    return boneTransformsList;
}

let updateWaitingTime = 0;

_.onFrame((deltaTime) => {
    updateWaitingTime += deltaTime;
    if (updateWaitingTime >= updateCycleSeconds) {
        updateWaitingTime -= updateCycleSeconds;
        _.oscHandle.send(createVMCData());
    }
});
記事をシェアしてワールド制作を盛り上げよう!

Cluster Creators Guide|バーチャル空間での創作を学ぶならをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む