Canal de simulation
Vue d'ensemble
Le canal de simulation est un canal WebSocket bidirectionnel à haute fréquence, utilisé pour échanger des informations surl'état des appareils et envoyer des commandes de session oud'appareil.
URL par défaut : ws://localhost:10001
Le port peut être modifié dans les paramètres.
Contrat de base (important) :
- Lorsqu'un client se connecte, le service envoie un message d'inventaire initial contenant la liste complète des appareils.
- Ensuite, le serveur envoie exactement un message de mise à jour d'état pour chaque message reçu du client.
- Chaque mise à jour d'état contient l'état (et le statut) de tous les appareils.
Cette page présente le protocole et les commandes de base du canal de simulation.
Des modules ou fonctionnalités supplémentaires peuvent enrichir et étendre le système grâce à leurs propres commandes et/ou champs d'état. Ceux-ci sont décrits séparément dans la section « Modules ».
Règles de protocole
Une réponse par message du client
Le service envoie un message de mise à jour d'état contenant l'état de tous les appareils, pour chaque message reçu du client.
Cela signifie que :
- Si vous avez besoin d'un aperçu récent de l'état des appareils, vous devez envoyer une commande (une commande de session, une commande d'interrogation ou une commande d'appareil).
- Le canal fonctionne comme une boucle synchronisée (« tick ») par les messages du client.
Interrogation sans recours à la force (commandes de sondage)
Si vous souhaitez observer les changements d'état sans appliquer de forces ni modifier les paramètres de simulation, utilisez les commandes de sondage :
probe_positionverserinverse3probe_orientationpour les poignées Verse
Les commandes de sondage ne contiennent aucune donnée de commande ; elles forcent simplement l'interrogation des informations de l'appareil, de sorte que les données de position et d'orientation soient à jour lors de la prochaine mise à jour d'état.
Les sessions de simulation qui envoient déjà des commandes de contrôle (set_cursor_force, set_cursor_position, etc.) n'ont pas pas Il faut envoyer des commandes de sondage — l'état de l'appareil est toujours inclus dans la trame de réponse. Les commandes de sondage sont destinées aux sessions à des fins de surveillance uniquement (par exemple, le Hub « Haply ») qui doivent interroger l'état de l'appareil sans le commander.
Configuration et état
- Le message deinventaire initial comprend les champs
config,stateetstatusde l'appareil. - Les messages de mise à jour de l'état Les champs réguliers comprennent les champs suivants :
stateetstatusde l'appareil.
Si vous avez à nouveau besoin d'un instantané incluant la configuration, utilisez session.force_render_full_state.
configure et commands
Les messages de l'appareil prennent en charge deux dictionnaires distincts : configure et commands. Ils ont des rôles différents :
| Dictionnaire | Rôle | Fréquence |
|---|---|---|
configure | Configuration persistante ponctuelle : préréglage, base, montage, filtres, paramètres des modules | Envoi unique ou en cas de modification |
commands | Par cycle, non persistant : force, position, couples | Doit être envoyé à chaque cycle |
Les entrées de commands (set_cursor_force, set_cursor_position, set_angular_torques, set_angular_position) sont appliquées une fois par cycle et ne sont pas enregistrées -- si vous cessez de les envoyer, l'effet prend fin immédiatement et l'appareil revient à l'état de veille. Utilisez configure pour tout ce qui doit être conservé d'un cycle à l'autre sans être supprimé.
set_transform fait exceptionset_transform se trouve sous commands, mais il est persistant -- Le service conserve la dernière transformation envoyée jusqu'à ce que vous en envoyiez une nouvelle. Vous n'êtes pas obligé de l'envoyer à chaque cycle. Cela dit, comme sa fonction est de permettre la navigation dans la caméra ou la scène, le diffuser au rythme des cycles (par exemple une transformation par image lorsque l'utilisateur déplace la scène) est le mode d'utilisation attendu pour une navigation en mouvement continu.
Systèmes de coordonnées
Haply utilisent par défaut un système de coordonnées droitier, avec l'axe Z vers le haut.
Deux paramètres de configuration influencent la manière dont les coordonnées sont interprétées et renvoyées :
session.configure.basis: modifie la correspondance entre les coordonnées de Haply et le système de coordonnées de votre application (voir Base ci-dessous).inverse3[*].configure.preset: sélectionne une configuration d'usine nommée qui définit l'origine de l'espace de travail (par ex.arm_front_centeredlieu(0, 0, 0)au centre de l'espace de travail). Voir la section « Configuration spécifique à chaque appareil ».
Formats des messages
Cette section décrit la structure générale et les types de messages de haut niveau. Des exemples complets sont fournis plus loin dans ce document.
Groupes d'appareils
Les messages sont classés par type d'appareil au premier niveau (par exemple, inverse3, verse_grip, wireless_verse_grip). Chaque clé de type d'appareil correspond à un tableau d'objets, à raison d'un par appareil.
Inventaire initial (serveur → client)
Envoyé une seule fois, immédiatement après l'établissement d'une connexion WebSocket.
Chaque plat comprend :
device_idconfigstatestatus
Voir : Exemple : charge utile de l'inventaire initial
Mise à jour de l'état (serveur → client)
Envoyée une fois pour chaque message reçu du client.
Chaque plat comprend :
device_idstatestatus
Voir : Exemple : charge utile de mise à jour d'état
Enveloppe des commandes de session (client → serveur)
Les commandes de session sont des actions qui s'appliquent à la connexion ou à la session en cours et qui ne sont pas spécifiques à un appareil.
{
"session": {
"<command_name>": {
"...": "..."
}
}
}
Enveloppe des commandes de l'appareil (client → serveur)
Les commandes destinées aux appareils sont envoyées sous la clé correspondant au type d'appareil, sous forme de tableau, ce qui permet d'envoyer des commandes à un ou plusieurs appareils dans un même message. Chaque entrée d'appareil prend en charge à la fois un dictionnaire configure et un dictionnaire commands.
{
"<device_type>": [
{
"device_id": "<id>",
"configure": {
"<config_key>": { "...": "..." }
},
"commands": {
"<command_name>": { "...": "..." }
}
}
]
}
Vous pouvez inclure plusieurs lignes pour commander plusieurs appareils du même type dans un seul message.
Notez que commands est un dictionnaire pouvant contenir plusieurs commandes pour un même appareil, mais une seule par type de commande.
Le champ execute
Chaque entrée de commande ou de configuration accepte une valeur booléenne "execute" facultatif (par défaut true). Définissez "execute": false afin que l'entrée soit analysée sans être appliquée. Cela est utile pour les sérialiseurs basés sur la réflexion (par exemple, Unity JsonUtility), qui émettent toujours tous les champs.
"set_cursor_force": { "execute": false, "vector": { "x": 0.0, "y": 0.0, "z": 0.0 } }
Référence des commandes
Session
Forcer l'affichage complet de l'état
Demande un instantané de tous les états et configurations des appareils.
{
"session": {
"force_render_full_state": {}
}
}
Configuration de session
La configuration persistante au niveau de la session est transmise via session.configure. Elle reprend le même principe que le dictionnaire configure au niveau de l'appareil.
Profil
Définit le nom du profil de session, utilisé par Hub Haply pour identifier la simulation et conserver les réglages spécifiques à chaque application.
{
"session": {
"configure": {
"profile": {
"name": "my_profile"
}
}
}
}
Base (niveau de session)
Définit le système de coordonnées de référence pour l'ensemble de la session. Ce système décrit comment les coordonnées de Haply sont converties dans le système de coordonnées de votre application ; une fois défini, tous les états des appareils sont renvoyés dans ce système de référence et toutes les valeurs que vous envoyez y sont interprétées.
La chaîne de permutation s'exprime par rapport au système de coordonnées droitier, avec l'axe Z vers le haut, de Haply : une permutation de X, Y, Z, éventuellement précédée de + ou de -. Exemples :
XYZ,ZYX,+Y-Z+X,X-ZYYZXsignifie que votreYcorrespondent à la droite de Haply, votreZà l'avant de Haply et votreXen haut de Haply.
Exemple de système gaucher avec l'axe Z vers le haut (Unreal, X-YZ) :
{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
session.set_basisLa convention de signe des axes a changé par rapport à l'ancienne commande session.set_basis et session.configure.basis. Une permutation qui fonctionnait avec l'ancienne commande peut entraîner une transformation inversée avec la nouvelle ; dans ce cas, inversez les signes des axes si nécessaire (par exemple, X-ZY devient XZ-Y).
Commandes de tous les appareils
Sondage (tous les appareils)
Utilisez les commandes de sondage pour obtenir des informations actualisées sur la position et l'orientation, sans appliquer de forces ni d'autres modifications à la simulation.
- inverse3 :
probe_position - Poignées Verse (
verse_grip,wireless_verse_grip) :probe_orientation
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"probe_position": {}
}
}
],
"verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
],
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
]
}
Définir la transformation (tous les appareils)
Définit la transformation de l'espace de travail (de l'espace de l'appareil vers l'espace de l'application) pour un appareil.
Contrairement aux autres entrées de commands, set_transform est persistant -- Le service conserve la dernière valeur envoyée jusqu'à ce que vous en envoyiez une nouvelle. Il n'est pas nécessaire de l'envoyer à chaque cycle ; toutefois, comme il sert à contrôler le déplacement de la caméra ou de la scène, il est recommandé de le diffuser à la fréquence des cycles pour obtenir un mouvement continu (par exemple, une transformation par image lorsque l'utilisateur déplace la scène).
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_transform": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
Pour les appareils Verse grip et Wireless Verse grip, seule la composante rotation Cette transformation a un effet : la poignée ne transmet que l'orientation ; il n'y a donc rien à mettre à l'échelle ni à déplacer pour position ou scale. Les champs position et scale sont acceptés (et renvoyés dans l'instantané) uniquement pour garantir la cohérence du schéma avec Inverse3 ; les laisser à leurs valeurs d'identité (position = {0,0,0}, scale = {1,1,1}) est la pratique courante.
Configuration de l'appareil Inverse3
Les entrées de configuration sont envoyées dans le dictionnaire configure. Elles sont permanentes : envoyez-les une seule fois ou en cas de modification.
Préréglage
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front_centered" }
}
}
]
}
Valeurs disponibles : device_defaults, arm_front, arm_front_centered, led_front, led_front_centered, custom.
Base (niveau de l'appareil)
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"basis": { "permutation": "ZXY" }
}
}
]
}
Montage
Définit la transformation de montage de l'appareil. Notez l'enveloppe transform à l'intérieur de mount.
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"mount": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
Le côté commande (client-serveur) englobe la transformation : "mount": { "transform": { ... } }.
Le côté instantané (du serveur vers le client) est hors service : "mount": { "position": {...}, "rotation": {...}, "scale": {...} }.
Ce choix est délibéré : les commandes utilisent une enveloppe unifiée, tandis que les instantanés sérialisent directement la transformation. Veillez à ne pas copier la structure de l'instantané dans la charge utile d'une commande.
Amortissement
Contrôle l'amortissement uniforme et/ou directionnel. Au moins un champ doit être renseigné.
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"damping": { "scalar": 0.5 }
}
}
]
}
Vous pouvez également définir un amortissement directionnel, ou les deux à la fois :
"damping": { "scalar": 0.5, "vector": { "x": 0.0, "y": 1.0, "z": 0.0 } }
Porte de force
Définit le gain d'atténuation de la porte de force (de 0,0 à 1,0).
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"force_gate": { "gain": 0.3 }
}
}
]
}
Commandes Inverse3
Pour envoyer des commandes à un appareil inverse3, ajoutez une entrée avec un device_id correspondant sous la clé inverse3. Les commandes sont envoyées à chaque cycle et doivent être renvoyées à chaque cycle pour rester actives.
Commander un seul appareil :
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Utiliser plusieurs commandes dans un même message :
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
},
{
"device_id": "049E",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Définir la position du curseur
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_position": {
"position": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Définir la force du curseur
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Définir la position angulaire
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_position": {
"angles": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
Définir les couples angulaires
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_torques": {
"torques": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
Verse Grip : configuration
Les appareils Verse Grip et Wireless Verse Grip prennent en charge les mêmes clés configure que Inverse3 pour preset, basis et mount.
Verse Grip étendu
La commande set_extension_data fait partie du protocole étendu des Verse Grip, utilisé avec les versions de poignée qui prennent en charge le protocole de communication d'extension de carte.
Définir les données d'extension de la poignée
Longueurs de données prises en charge :
- Jusqu'à 20 octets en amont (client → appareil).
- Jusqu'à 12 octets en aval (appareil → client), renvoyés dans le message « State Update » sous la forme
state.extension_data.
Spécification des données :
- Longueur du tableau : 20 octets
- Plage de valeurs : chaque valeur est comprise entre 0 et 255
{
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"set_extension_data": {
"extension_data": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
}
}
}
]
}
Exemples
Exemple : charge utile de l'inventaire initial
Le service envoie un message contenant la liste complète des appareils lorsqu'une connexion WebSocket est établie. Le message initial respecte le format JSON suivant :
{
"inverse3": [
{
"device_id": "04BA",
"config": {
"type": "inverse3",
"port": "COM13",
"device_info": {
"major_version": 7,
"minor_version": 1,
"id": "04BA",
"device_type": 4,
"uuid": "2D35F80DD9005F599B68F49944CB04BA"
},
"extended_device_id": "2D35F80DD9005F599B68F49944CB04BA",
"extended_firmware_version": "8C20FDC8010AA1E15AA133CDA2534874",
"gravity_compensation": {
"enabled": true,
"scaling_factor": 1
},
"handedness": "right",
"streaming_mode": "USB",
"torque_scaling": {
"enabled": true
},
"home_return": {
"enabled": false
},
"filters": {
"force_gate": { "gain": 0.3 },
"damping": { "scalar": 0 }
},
"preset": "defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"config": {
"type": "verse_grip",
"port": "COM3",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"config": {
"type": "wireless_verse_grip",
"port": "COM6",
"major_version": 1,
"minor_version": 4,
"hardware_version": 1,
"streaming_mode": "Radio",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
]
}
Exemple : mise à jour de l'état de la charge utile
Le service enverra un message de mise à jour d'état indiquant l'état de tous les appareils pour chaque message reçu.
Si vous souhaitez connaître l'état de la machine, vous devez d'abord lui envoyer un message (par exemple une commande de sondage ou une valeur de force, même si ces valeurs sont nulles). Ceci est particulièrement important lorsque vous utilisez des appareils comme sources d'entrée (par exemple pour le suivi de position) sans appliquer de forces.
Le message « State Update » respecte le format JSON suivant :
{
"inverse3": [
{
"device_id": "04BA",
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
],
"custom_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"extension_data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"ready": true
}
}
]
}