Phase 2.7 : animation registry + tick handler + placeholder assets

TiedUpAnimationRegistry.CONTEXT_STAND_IDLE instancié au FMLCommonSetupEvent
via DirectStaticAnimation + armature BIPED. Placeholder JSON 2-keyframes
identity (joueur statique) à assets/tiedup/animmodels/animations/ jusqu'à
ce qu'un export Blender authored remplace. biped.json de même (hiérarchie
identity) placé dans assets/tiedup/armatures/ — parse via JsonAssetLoader
mais pas encore chargé au runtime (l'armature reste procédurale côté Java).

RigAnimationTickHandler tick chaque player visible côté client (phase END) :
- patch.getAnimator().tick() → avance les layers EF
- trigger playAnimation(CONTEXT_STAND_IDLE, 0.2f) quand motion=IDLE et anim
  courante ≠ CONTEXT_STAND_IDLE (idempotent)
- try/catch per-entity avec dedup des erreurs par UUID (pattern
  TiedUpRenderEngine.loggedRenderErrors)
- skip si level null / paused

PlayerPatch.initAnimator bind désormais IDLE → CONTEXT_STAND_IDLE quand le
registry est prêt (fallback EMPTY_ANIMATION si patch construit avant setup).

Voir docs/plans/rig/ASSETS_NEEDED.md pour la spec des assets authored
définitifs (anim idle swing respiration 3 keyframes + offsets biped
anatomiques).
This commit is contained in:
notevil
2026-04-23 00:07:32 +02:00
parent 73264db3c6
commit 08808dbcc1
6 changed files with 653 additions and 1 deletions

View File

@@ -140,6 +140,11 @@ public class TiedUpMod {
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)
event.enqueueWork(com.tiedup.remake.rig.patch.EntityPatchProvider::registerEntityPatches);
// RIG Phase 2.7 — registre des StaticAnimation (CONTEXT_STAND_IDLE).
// Placeholder JSON procédural jusqu'à ce que les assets Blender arrivent
// (cf. docs/plans/rig/ASSETS_NEEDED.md).
event.enqueueWork(com.tiedup.remake.rig.TiedUpAnimationRegistry::initStaticAnimations);
}
/**

View File

@@ -0,0 +1,156 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import com.tiedup.remake.rig.anim.client.Layer;
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
import com.tiedup.remake.rig.anim.types.StaticAnimation;
/**
* Phase 2.7 — registry central des {@link StaticAnimation} TiedUp. Expose les
* accessors statiques (ex. {@link #CONTEXT_STAND_IDLE}) utilisés par les
* patches + tick handler pour jouer les animations idle / walk / etc.
*
* <h2>Placeholder assets</h2>
* <p>Les JSON associés sont des <b>placeholders procéduraux</b> (2 keyframes
* identity) à remplacer par des assets Blender-authored. Voir
* {@code docs/plans/rig/ASSETS_NEEDED.md} section 2 pour la spec de l'anim
* idle définitive (swing respiration subtle 3 keyframes, 2s boucle).</p>
*
* <h2>Lifecycle</h2>
* <ul>
* <li>{@link #initStaticAnimations()} appelé au {@code FMLCommonSetupEvent}
* (via {@code event.enqueueWork(...)}). Crée les instances
* {@link DirectStaticAnimation} — pas de chargement JSON ici, juste les
* métadonnées (registry name, armature, repeat flag).</li>
* <li>Le chargement effectif du JSON ({@code JsonAssetLoader}) est lazy :
* à la première lecture de {@link StaticAnimation#getAnimationClip()},
* donc typiquement à la première frame où l'animation est jouée.</li>
* <li>Si l'asset JSON est absent / corrompu, {@code StaticAnimation.loadAnimation}
* relance une {@code AssetLoadingException}. Le tick handler
* ({@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}) attrape ces
* throwables pour éviter un crash complet.</li>
* </ul>
*
* <h2>Dist</h2>
* <p>Les animations tournent côté client (l'{@code Animator} est créé via
* {@code ClientAnimator::getAnimator} côté physical client), donc le registry
* est indépendant du side pour le bootstrap mais toute la lecture JSON passe
* par {@code Minecraft.getInstance().getResourceManager()} côté client. Les
* champs sont accessibles côté serveur (validation / logs), seul
* {@link StaticAnimation#loadAnimation()} est client-heavy (et protégé par
* la dispatch server/client du {@code AnimationManager.getAnimationResourceManager()}).</p>
*/
public final class TiedUpAnimationRegistry {
private TiedUpAnimationRegistry() {}
/** Registry name de l'anim idle par défaut (résolue en
* {@code assets/tiedup/animmodels/animations/context_stand_idle.json}). */
public static final ResourceLocation CONTEXT_STAND_IDLE_ID =
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle");
/**
* Anim idle par défaut — joue quand aucune motion active. Placeholder 2
* keyframes identity (joueur sans mouvement visible) jusqu'à ce qu'un
* asset authored Blender arrive.
*
* <p><b>Attention init order</b> : ce field est {@code null} tant que
* {@link #initStaticAnimations()} n'a pas tourné. Les sites qui
* référencent ce field doivent être gardés par un null-check, ou être
* appelés post-setup (tick handler, patch init, etc.).</p>
*
* <p>Utilise {@link DirectStaticAnimation} (vs un hand-written
* {@code StaticAnimation}) pour hériter du pattern accessor=self +
* registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.</p>
*/
public static DirectStaticAnimation CONTEXT_STAND_IDLE;
/**
* Construit les {@link StaticAnimation} TiedUp. À appeler exactement une
* fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}.
*
* <p>Pas de chargement JSON ici — juste l'instanciation des accessors.
* La première lecture de {@code getAnimationClip()} déclenchera le load
* via {@link com.tiedup.remake.rig.asset.JsonAssetLoader}.</p>
*
* <p>Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne
* devrait jamais être appelé hors du mod bus.</p>
*/
public static void initStaticAnimations() {
if (CONTEXT_STAND_IDLE != null) {
// Déjà init (hot-reload setup, test double-init). Log debug seulement.
TiedUpRigConstants.LOGGER.debug(
"TiedUpAnimationRegistry.initStaticAnimations: déjà initialisé, skip."
);
return;
}
try {
// transitionTime=GENERAL (6 ticks) + isRepeat=true + registryName +
// armature=BIPED. L'ordre match le ctor DirectStaticAnimation(float, boolean, ResourceLocation, AssetAccessor).
CONTEXT_STAND_IDLE = new DirectStaticAnimation(
TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME,
/* isRepeat */ true,
CONTEXT_STAND_IDLE_ID,
TiedUpArmatures.BIPED
);
// Layer BASE + priority LOWEST — idle default, écrasable par toute
// autre anim. Sans ces props la default du StaticAnimation (LOWEST /
// BASE_LAYER) s'applique déjà — on les set explicitement pour la doc.
setLowestBaseLayer(CONTEXT_STAND_IDLE);
TiedUpRigConstants.LOGGER.info(
"TiedUpAnimationRegistry: CONTEXT_STAND_IDLE registered ({})",
CONTEXT_STAND_IDLE_ID
);
} catch (Throwable t) {
// Fallback : log + laisse CONTEXT_STAND_IDLE null. Le tick handler
// verra null et skippera silencieusement. Évite de tout casser si
// un asset placeholder est malformé en dev.
TiedUpRigConstants.LOGGER.error(
"TiedUpAnimationRegistry: init échoué pour CONTEXT_STAND_IDLE — "
+ "animation idle désactivée. Voir docs/plans/rig/ASSETS_NEEDED.md §2.",
t
);
}
}
/**
* Helper — set LayerType=BASE_LAYER + Priority=LOWEST sur une anim.
* Évite de forcer les callers à importer {@link ClientAnimationProperties}
* + {@link Layer}.
*
* <p>{@code @OnlyIn(CLIENT)} indirect : les properties
* {@code LAYER_TYPE}/{@code PRIORITY} sont client-only mais leur écriture
* via {@code addProperty} ne déclenche pas de class-load de
* {@code net.minecraft.client.*}. Le tag d'{@code @OnlyIn} serait
* incorrect ici (le method serait appelé depuis commonSetup). La safety
* réelle est assurée par le fait que les properties sont juste stockées
* dans la map et lues plus tard sur client uniquement (via
* {@code getLayerType()}/{@code getPriority()} tagués
* {@code @OnlyIn(CLIENT)}).</p>
*/
private static void setLowestBaseLayer(StaticAnimation anim) {
anim.addProperty(ClientAnimationProperties.LAYER_TYPE, Layer.LayerType.BASE_LAYER);
anim.addProperty(ClientAnimationProperties.PRIORITY, Layer.Priority.LOWEST);
}
/**
* Helper — vrai ssi la static anim de référence a fini l'init (tick
* handler l'utilise en early-return quand Phase 2.7 assets sont absents
* en dev test).
*/
@OnlyIn(Dist.CLIENT)
public static boolean isReady() {
return CONTEXT_STAND_IDLE != null;
}
}

View File

@@ -8,10 +8,12 @@ package com.tiedup.remake.rig.patch;
import net.minecraft.world.entity.player.Player;
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
import com.tiedup.remake.rig.TiedUpArmatures;
import com.tiedup.remake.rig.TiedUpRigRegistry;
import com.tiedup.remake.rig.anim.Animator;
import com.tiedup.remake.rig.anim.LivingMotions;
import com.tiedup.remake.rig.anim.types.StaticAnimation;
import com.tiedup.remake.rig.armature.HumanoidArmature;
import com.tiedup.remake.rig.math.MathUtils;
import com.tiedup.remake.rig.math.OpenMatrix4f;
@@ -152,7 +154,16 @@ public abstract class PlayerPatch<T extends Player> extends LivingEntityPatch<T>
@Override
protected void initAnimator(Animator animator) {
super.initAnimator(animator);
animator.addLivingAnimation(LivingMotions.IDLE, TiedUpRigRegistry.EMPTY_ANIMATION);
// Phase 2.7 : si le registry a init, on bind sur CONTEXT_STAND_IDLE
// (placeholder procédural 2-keyframes identity — voir
// TiedUpAnimationRegistry + ASSETS_NEEDED.md §2). Sinon fallback sur
// EMPTY_ANIMATION — peut arriver si le patch est construit avant que
// FMLCommonSetupEvent n'ait tourné (rare mais pas impossible en dev).
StaticAnimation idle = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE;
animator.addLivingAnimation(
LivingMotions.IDLE,
idle != null ? idle.getAccessor() : TiedUpRigRegistry.EMPTY_ANIMATION
);
}
}

View File

@@ -0,0 +1,240 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig.tick;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.slf4j.Logger;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
import com.tiedup.remake.rig.anim.Animator;
import com.tiedup.remake.rig.anim.LivingMotions;
import com.tiedup.remake.rig.anim.client.ClientAnimator;
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
import com.tiedup.remake.rig.anim.types.StaticAnimation;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.patch.LivingEntityPatch;
import com.tiedup.remake.rig.patch.TiedUpCapabilities;
/**
* Phase 2.7 — tick handler RIG côté client. Pour chaque player visible :
*
* <ol>
* <li>Récupère le {@link LivingEntityPatch} via
* {@link TiedUpCapabilities#getEntityPatch(net.minecraft.world.entity.Entity, Class)}</li>
* <li>Ticke l'{@link Animator} (avance les layers EF, gère les transitions,
* etc.)</li>
* <li>Si aucune animation "réelle" n'est active (= on joue encore
* l'{@code EMPTY_ANIMATION} du fallback initial), déclenche une
* transition vers {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE}
* (0.2s) — preuve vivante que le pipeline tourne Phase 2.7.</li>
* </ol>
*
* <h2>Choix de design</h2>
* <ul>
* <li><b>Phase END</b> : on tick après la logique vanilla de la frame pour
* que l'animator voit le dernier {@code yBodyRot}, position, vélocité
* écrits par MC. Pattern EF standard.</li>
* <li><b>Itération {@code level.players()}</b> : uniquement les joueurs
* dans la dimension courante — les autres ont leurs patches suspendus
* de toutes façons. Évite d'itérer les entities tiers non-patchées
* (Phase 5+ quand NPCs auront RIG).</li>
* <li><b>Error handling per-entity</b> : try/catch autour de chaque patch
* tick. Un patch qui throw ne cascade pas sur les autres. L'erreur
* est logguée <b>une seule fois par UUID</b> pour éviter de spammer
* la console.</li>
* <li><b>Registry pas ready → noop</b> : si
* {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE} est null (setup
* pas encore exécuté, ou fallback d'échec), on ne crash pas — on
* skippe juste le trigger idle. L'animator tourne quand même
* (EMPTY_ANIMATION en boucle).</li>
* </ul>
*
* <h2>Futur Phase 3+</h2>
* <p>Ce handler est minimal — Phase 3 ajoutera :</p>
* <ul>
* <li>Détection motion (WALK, SIT, KNEEL, etc.) via {@code updateMotion}
* et binding dynamic des anims correspondantes</li>
* <li>Resolve context bondage (arms_bound / legs_bound / etc.) via
* {@code RigContextResolver}</li>
* <li>Tick des NPCs TiedUp quand leurs patches seront enregistrés</li>
* </ul>
*
* <p>Cf. {@code docs/plans/rig/MIGRATION.md} Phase 3 — Animation consumers.</p>
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public final class RigAnimationTickHandler {
private static final Logger LOGGER = LogUtils.getLogger();
/** Set des UUIDs ayant déjà loggé une erreur tick — évite spam console. */
private static final Set<UUID> LOGGED_ERRORS = ConcurrentHashMap.newKeySet();
/** Cap de taille — reset si dépassé (évite leak en session infinie). */
private static final int MAX_LOGGED_UUIDS = 1024;
private RigAnimationTickHandler() {}
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) {
return;
}
Minecraft mc = Minecraft.getInstance();
// Loading screen, splash, disconnect, F3+T reload in progress — level null.
// Early-return silencieux (évite les logs parasites).
if (mc.level == null) {
return;
}
// Pause menu / options screen — on skip le tick anim pour matcher
// le comportement vanilla MC (tick world paused).
if (mc.isPaused()) {
return;
}
// Cap de taille du set loggé — reset préventif.
if (LOGGED_ERRORS.size() > MAX_LOGGED_UUIDS) {
LOGGED_ERRORS.clear();
}
// Copie défensive : le set level.players() peut muter pendant notre
// itération si un joueur join/leave (MP). Le CME-safe serait itérer
// sur une copie. On tolère le CME via try/catch, le coût copy n'en
// vaut pas la peine pour une liste typiquement petite (~10 joueurs
// visibles max).
for (Player player : new HashSet<>(mc.level.players())) {
tickPlayer(player);
}
}
/**
* Ticke le patch RIG d'un joueur. Noop si :
* <ul>
* <li>Pas de capability attachée (ex : le type d'entity n'est pas dans
* {@code EntityPatchProvider.CAPABILITIES})</li>
* <li>L'{@code Animator} est null (entitypatch pas encore onConstructed)</li>
* </ul>
*
* <p>Toute exception thrown par le patch / animator est caught et logguée
* une seule fois par UUID — le player reste rendu par le fallback vanilla
* du pipeline render engine (cf. {@code TiedUpRenderEngine.onRenderLiving}).</p>
*/
private static void tickPlayer(Player player) {
LivingEntityPatch<?> patch =
TiedUpCapabilities.getEntityPatch(player, LivingEntityPatch.class);
if (patch == null) {
return;
}
Animator animator = patch.getAnimator();
if (animator == null) {
return;
}
UUID uuid = player.getUUID();
try {
// 1. Tick de l'animator — avance les layers + gère les transitions
// EF natifs. C'est ce que le MixinLivingEntity.tick() ferait en
// Phase 3+ pattern push EF (cf. MIGRATION.md §2.2.1). Ici on
// tick côté client uniquement pour Phase 2.7.
animator.tick();
// 2. Trigger transition vers CONTEXT_STAND_IDLE si rien de mieux
// n'est actif. Idempotent : ne re-déclenche pas si l'anim
// courante est déjà CONTEXT_STAND_IDLE.
maybePlayIdle(patch, animator);
} catch (Throwable t) {
if (LOGGED_ERRORS.add(uuid)) {
LOGGER.error(
"RigAnimationTickHandler: tick animator failed for player {} ({}) — "
+ "subsequent errors suppressed for this UUID.",
player.getGameProfile().getName(), uuid, t
);
}
// Pas de rethrow — le joueur continue d'être rendu en vanilla (ou
// stale pose). Le pipeline render dispose de son propre fallback.
}
}
/**
* Déclenche la transition vers {@code CONTEXT_STAND_IDLE} si :
* <ul>
* <li>Le registry est ready (placeholder asset loadé)</li>
* <li>Le patch est dans {@code LivingMotions.IDLE} (pas en action
* particulière — walk, sit, etc. Phase 3+)</li>
* <li>L'animation actuellement jouée sur le base layer n'est pas déjà
* CONTEXT_STAND_IDLE (évite le re-trigger à chaque tick)</li>
* </ul>
*
* <p>Transition 0.2s (4 ticks @ 20tps) pour un fondu doux avec
* l'EMPTY_ANIMATION de départ. Ça reste court — si Phase 3+ veut un fondu
* plus long on passera la valeur en param.</p>
*/
private static void maybePlayIdle(LivingEntityPatch<?> patch, Animator animator) {
if (!TiedUpAnimationRegistry.isReady()) {
return;
}
if (patch.getCurrentLivingMotion() != LivingMotions.IDLE) {
return;
}
if (!(animator instanceof ClientAnimator clientAnimator)) {
// Server-side Animator — rien à rendre. Phase 3+ si on veut sync
// motion serveur, c'est là qu'on le fera.
return;
}
// Check si la CONTEXT_STAND_IDLE est déjà l'anim courante du base layer.
// getPlayerFor(null) retourne base layer player (non-null en EF).
AssetAccessor<? extends DynamicAnimation> currentAnim =
clientAnimator.baseLayer.animationPlayer.getAnimation();
StaticAnimation target = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE;
if (currentAnim != null && currentAnim.get() != null) {
// Compare directement les instances — les StaticAnimation sont
// singletons (registry constructor pattern). equals() fallback
// sur id et les accessors sont null pour EMPTY, donc ID-based
// comparison pas fiable ici. Instance check suffit.
if (currentAnim.get() == target) {
return;
}
}
clientAnimator.playAnimation(target.getAccessor(), 0.2F);
}
/**
* Reset des erreurs loggées — utile aux tests unitaires et au F3+T
* reload (aligné avec {@code TiedUpRenderEngine.onAddLayers}).
*/
public static void resetLoggedErrors() {
LOGGED_ERRORS.clear();
}
}

View File

@@ -0,0 +1,15 @@
{
"_comment": "PLACEHOLDER Phase 2.7 — animation idle 2-keyframes identity (joueur statique, pas de balancement). Boucle 2 secondes. DOIT parser avec JsonAssetLoader.loadClipForAnimation(). À remplacer par un export Blender authored (respiration/balancement subtle) quand dispo. Voir docs/plans/rig/ASSETS_NEEDED.md section 2.",
"format": "ATTRIBUTES",
"armature": "tiedup:biped",
"animation": [
{
"name": "Root",
"time": [0.0, 2.0],
"transform": [
{ "loc": [0.0, 0.0, 0.0], "rot": [1.0, 0.0, 0.0, 0.0], "sca": [1.0, 1.0, 1.0] },
{ "loc": [0.0, 0.0, 0.0], "rot": [1.0, 0.0, 0.0, 0.0], "sca": [1.0, 1.0, 1.0] }
]
}
]
}

View File

@@ -0,0 +1,225 @@
{
"_comment": "PLACEHOLDER Phase 2.7 — identity transforms (joints empilés à l'origine). Doit parser avec JsonAssetLoader.loadArmature() mais n'est PAS encore utilisé au runtime (TiedUpArmatures.BIPED est construit proceduralement en Java). À remplacer par un export Blender addon Antikythera-Studios quand dispo. Voir docs/plans/rig/ASSETS_NEEDED.md section 1.",
"armature": {
"armature_format": "ATTRIBUTES",
"joints": [
"Root",
"Thigh_R",
"Leg_R",
"Knee_R",
"Thigh_L",
"Leg_L",
"Knee_L",
"Torso",
"Chest",
"Head",
"Shoulder_R",
"Arm_R",
"Hand_R",
"Tool_R",
"Elbow_R",
"Shoulder_L",
"Arm_L",
"Hand_L",
"Tool_L",
"Elbow_L"
],
"hierarchy": [
{
"name": "Root",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Thigh_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Leg_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Knee_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": []
}
]
}
]
},
{
"name": "Thigh_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Leg_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Knee_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": []
}
]
}
]
},
{
"name": "Torso",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Chest",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Head",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": []
},
{
"name": "Shoulder_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Arm_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Elbow_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Hand_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Tool_R",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": []
}
]
}
]
}
]
}
]
},
{
"name": "Shoulder_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Arm_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Elbow_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Hand_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": [
{
"name": "Tool_L",
"transform": {
"loc": [0.0, 0.0, 0.0],
"rot": [1.0, 0.0, 0.0, 0.0],
"sca": [1.0, 1.0, 1.0]
},
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
}