3
1

Tauriでばもきゃと連携する話: Webでもトラッカーの情報を扱いたい!

Last updated at Posted at 2023-12-12

この記事は クラスター Advent Calendar 2023 の13日目です。
こんにちは、Koutaです。主にフロントエンドを担当しています。昨日は@yooozさんの「自分用のブックマークリストを作って育ててみる」でしたね!追加する手間があることで覚えやすくなるのは良さそうですね。自分も試してみたいです。

さて、今回はトラッカーの情報を使って遊べないかと試した内容を記事にしてみました。

clusterアプリではトラッキング機器を使うことで全身の動きをアバターに反映することができます。
全身を動かせることで表現力にも幅を出すこともできますし、自分の動きに連動することで没入感があがってより世界を楽しむことができます。

全身でトラッキングを実現するためには機器をそろえる必要があります。機器をそろえてデータが扱えるようになるとそれを様々な箇所で活用して遊びたくなってきますね。…きますよね?

私はフロントエンドを主領域としていますので、フロントエンド技術でデータを扱ってみたいという気持ちを強くもってます。
調べてみると、バーチャルモーションキャプチャー(以下「ばもきゃ」と表記)というソフトウェアが、トラッキング処理と3Dアバター制御に加え、情報の送受信に使えるプロトコルを提供しており、他のソフトウェアでもトラッキングデータを簡単に扱えるようになっていることが分かりました。
データを受信できれば、フロントエンド技術でもデータを扱うことができ、上記の目的が達成可能となります。
そこで、この記事では、ばもきゃのデータを受信し、フロントエンドで扱うプロセスについて解説します。

概要

目的

フロントエンド技術でトラッキングしたデータを扱うこと
この記事では、トラッキングしたデータを元にアバターを表示するところまでを扱う

記事で扱う関連技術

  • Tauri
  • VMC Protocol
  • three-vrm

データの取り扱いと構成図

トラッキングデータを扱うために、以下の点を整理する必要があります。

  • どこでトラッキングデータを扱うのか
  • データの送受信方法
  • 表示方法

それぞれについて整理したいと思います。

どこでトラッキングデータを扱うのか

今回はTauriでデータを扱うことに決めました。
Tauriというのは、Rust製のクロスプラットフォームのGUIフレームワークです。UIはWebviewとなっており、Webの技術が使える形になってます。
Tauriを活用すると、データの送受信とWebでの表示の両立することができます。

データの送受信方法

トラッキングデータの取り扱いはばもきゃの機能を使います。
なので、考える必要があるのはばもきゃデータの受信をどうするかです。
ばもきゃは、先行配信版ではOSC方式でデータ送信が可能となっています。
つまり、OSCのデータ受信とパースができればデータを取り扱うことができます。
TauriはRustでできているため、RustでOSC受信、およびWebviewへのデータを渡すことができれば目的達成できます。

表示方法

three-vrmを使って、ばもきゃのデータをもとにアバターの表示・制御を行います。

ばもきゃで受信するデータはVRMデータの各ボーンの座標情報となっています。
このデータから表示をするためには、VRMのボーン構造とボーンの座標を制御が必要になります。
three-vrmはwebでVRMを扱うためのライブラリとなっており、提供されているAPIにボーンを扱うAPI群が存在しています。そのため、ばもきゃで受信したデータをそのまま扱ってアバターの表示・制御が可能となります。

構成図

これらを踏まえて、下記図のような構成でデータを送信とWebviewでアバターの表示を行おうと思います。

構成図

実装方法

ばもきゃのデータをTauriで受信

まず、Tauriでのデータ受信方法です。
ばもきゃはOSCでデータを送信しているため、RustでOSCの受信部分を実装する必要があります。
RustにはroscというOSCのライブラリがあります。これを使って受信部分を実装できます。
roscのexamplesに受信部分のサンプルがあります。参考にしてばもきゃの受信部分を書くと下記コードになります。

extern crate rosc;

use rosc::OscPacket;
use std::net::{SocketAddrV4, UdpSocket};
use std::str::FromStr;

fn main() {

    let addr = match SocketAddrV4::from_str("127.0.0.1:39539") {
        Ok(addr) => addr,
        Err(_) => panic!("Usage {} IP:PORT", "127.0.0.1:39539"),
    };
    let sock = UdpSocket::bind(addr).unwrap();
    println!("Listening to {}", addr);

    let mut buf = [0u8; rosc::decoder::MTU];

    loop {
        match sock.recv_from(&mut buf) {
            Ok((size, addr)) => {
                let (_, packet) = rosc::decoder::decode_udp(&buf[..size]).unwrap();
                handle_packet(packet);
            }
            Err(e) => {
                println!("Error receiving from socket: {}", e);
                break;
            }
        }
    }
}

fn handle_packet(packet: OscPacket) {
    match packet {
        OscPacket::Message(msg) => {
            // msgにばもきゃから受信した情報が格納されている
        }
        OscPacket::Bundle(bundle) => {
            for ele in bundle.content {
                handle_packet(ele)
            }
        }
    }
}

受信したデータのパースと受け渡し

OSCを受信できるようになりましたが、受け取ったデータは整形されておらず使いにくい状態です。
そこで、Rust側でデータをパースしてからWebviewに渡すようにします。

記事内では、要所のみを解説します。詳細が気になる場合はソースコードを参照してください(受信したデータのパース, Tauriからwebviewへデータ受け渡し

受信したデータのパース

データ受信が完了したので、それをパースして扱える形に変換します。
ばもきゃの送受信データはVirtual Motion Capture Protocolとプロトコルを決めています。
今回は、受信データをもとにアバターを制御できれば良いので、Boneの姿勢制御に焦点をあてます。

受信の仕様を見ると、Boneの姿勢は以下の構造でデータが格納されていることが分かります。

/VMC/Ext/Bone/Pos (string){name} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w}  

nameはUnityEngineのHumanBodyBonesに準拠して命名されています。

データの構造が分かったので、仕様に従ってパースが可能になります。
今回は、AddressのEnumを定義し、受信したデータをAddressにパースします。

pub enum Address {
    Bone(BonePosition),
    None,
}

#[derive(Serialize)]
pub struct BonePosition {
    pub name: BonePositionName, // ボーンの名前のEnum
    pub transform: Transform, // ボーンの座標情報
}

具体的なコードは、型に従って受け取ったデータを整形する作業なので、ここでは説明を省略します。詳細はこちらを参照してください。

Tauriからwebviewへデータ受け渡し

TauriのEventsシステムを利用することで、coreとWebView間の通信が可能です。
今回はBone構造をパースした結果をAddressに格納したので、AddressをWebViewに渡すことで目的を達成できます。

流れとしては、OSCからデータを受信後、データをパースしてEvents経由でAddressを渡す形になります。下記に処理内容のみをコメントで書いた疑似コードを示します。こちらも省略されてないものはこちらにあるので気になる場合は参照していただけたらと思います。


fn parse_osc_bundle(packet: OscPacket) -> Message {
    match packet {
        OscPacket::Message(_) => Message { bone: vec![] },
        OscPacket::Bundle(bundle) => {
            bundle
                .content
                .iter()
                .fold(Message { bone: vec![] }, |mut acc, packet| {
                    let m = parse_osc_message(packet);
                    match m {
                        Ok(address) => {
                            match address {
                                Address::Bone(bone) => acc.bone.push(bone),
                                Address::None => {}
                            }
                            acc
                        }
                        Err(_) => acc,
                    }
                })
        }
    }
}

fn parse_osc_message(packet: &OscPacket) -> Result<Address, String> {
    match packet {
        OscPacket::Message(msg) => {
            // ここにボーンの情報が入ったメッセージが送られてくるのでパースして返す
            return msg.parse();
        }
        OscPacket::Bundle(_) => Err("想定と違うデータです".to_string()),
    }
}

// OSCの受信コード
fn receive_osc(app: &mut App) {
    let app_handle = app.app_handle();

    thread::spawn(move || {
        // ここにOSCの接続コード
        loop {
            match sock.recv_from(&mut buf) {
                Ok((size, addr)) => {
                    // パースする
                    let message = parse_osc_bundle(packet);
                    if !message.is_empty() {
                        app_handle.emit_all("OscPacket", &message).unwrap();
                    }
                }
                Err(e) => {
                    println!("Error receiving from socket: {}", e);
                    break;
                }
            }
        }
    });
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // OSC受信とEvents受け渡し処理を呼び出す
            receive_osc(app);
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

今回は、受信したデータの受け渡しができればよいと考えていたので、main.rsにべた書きました。
TauriにはPluginsの仕組みがあり、ばもきゃのOSCの送受信・パースまでを切り出すことも検討してもよさそうです。

受信したデータから描画

最後に、Coreから送信されたデータを受け取り、アバター表示を制御します。
Eventsシステムに、Webview側での受信コード例があるのでそれに従い、以下のコードでパースした型付きのデータを扱います。

const boneName = [
    "RightHand",
    "RightLowerArm",
    "RightUpperArm",
    "RightShoulder",
    "LeftHand",
    "LeftLowerArm",
    "LeftUpperArm",
    "LeftShoulder",
    "RightUpperLeg",
    "RightLowerLeg",
    "RightToes",
    "LeftUpperLeg",
    "LeftLowerLeg",
    "LeftToes",
    "Hips",
    "Head",
] as const;


type BoneName = typeof boneName[number];

type Transform = {
    position: {
        x: number;
        y: number;
        z: number;
    };
    rotation: {
        x: number;
        y: number;
        z: number;
        w: number;
    }
};
type BonePosition = {
    transform: Transform;
    name: BoneName;
}

type Message = {
    bone: readonly BonePosition[];
}

listen<Message>('OscPacket', (e) => {
  setPositionFromBonePosition(e.payload.bone, vrm);
});

データを受け取れたので、描画します。
再び、Virtual Motion Capture ProtocolでのBoneの定義を見てみると、オブジェクトのLocal姿勢を扱っています。
つまり、three-vrmで受信したボーンと同じボーンNodeを取得、座標設定で受け取ったデータと同じ動きをアバターに適用できます。

three-vrmでは、getNormalizedBoneNodeとBone名からNodeを取得するAPIがあるので、取得したnodeにpositionとquotationをセットすることで、読み込んだVRMの制御ができます。
コードだと下記のようになります。

export const setPositionFromBonePosition = (boneArray: readonly BonePosition[], vrm: VRM | undefined) => {
    if (!vrm) {
        return;
    }
    for (const bone of boneArray) {
        const node = vrm.humanoid.getNormalizedBoneNode(VRMHumanBoneName[bone.name]);
        if (!!node) {
            const t = bone.transform;
            node.position.set(t.position.x, t.position.y, t.position.z);
            node.quaternion.set(-t.rotation.x, -t.rotation.y, t.rotation.z, t.rotation.w);
        }
    }
    vrm.humanoid.update();
}

VRMの読み込み、three.jsのセットアップについては省略します。コードはこちらにあるので気になる場合は参考していただけたらと思います。

実際に描画してみると手の動きが連動していることが見えます(カメラ位置などは今回は同期してないので見た目が若干異なります)。
image.png

これで、ばもきゃから受け取ったデータからアバターを制御できてることがわかり、Webでもトラッカー情報が扱えることがわかりました!

まとめと今後の展望

今回はばもきゃのデータをTauriで受信し、表示するところまでを行いました。

この記事ではTauriでの扱いになっていたのですが、OSCの受信とパースさえできればよいのでElectronやローカルサーバーを立てるなどやり方はほかにもあると思います。

今回は受け取ったデータを表示しただけなのですが、他にもたくさん活用することができるのではないかと思ってます。
たとえば、ダンスした際のモーションキャプチャー・再生をWebだけで完結などもできそうです!
実際に応用するところまで開発していけたらよいなと夢見つつ、この記事はここまでとしたいと思います。

明日は@zisupさんの「Unityのプロファイル簡単な使い方」です!お楽しみに!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1