diff --git a/src/main/java/com/tiedup/remake/core/TiedUpMod.java b/src/main/java/com/tiedup/remake/core/TiedUpMod.java index 3aa3615..26d2d93 100644 --- a/src/main/java/com/tiedup/remake/core/TiedUpMod.java +++ b/src/main/java/com/tiedup/remake/core/TiedUpMod.java @@ -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); } /** diff --git a/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java b/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java new file mode 100644 index 0000000..33f936b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java @@ -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. + * + *

Placeholder assets

+ *

Les JSON associés sont des placeholders procéduraux (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).

+ * + *

Lifecycle

+ * + * + *

Dist

+ *

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()}).

+ */ +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. + * + *

Attention init order : 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.).

+ * + *

Utilise {@link DirectStaticAnimation} (vs un hand-written + * {@code StaticAnimation}) pour hériter du pattern accessor=self + + * registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.

+ */ + public static DirectStaticAnimation CONTEXT_STAND_IDLE; + + /** + * Construit les {@link StaticAnimation} TiedUp. À appeler exactement une + * fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}. + * + *

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}.

+ * + *

Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne + * devrait jamais être appelé hors du mod bus.

+ */ + 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}. + * + *

{@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)}).

+ */ + 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; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java index 1d17d20..e78462d 100644 --- a/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java +++ b/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java @@ -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 extends LivingEntityPatch @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 + ); } } diff --git a/src/main/java/com/tiedup/remake/rig/tick/RigAnimationTickHandler.java b/src/main/java/com/tiedup/remake/rig/tick/RigAnimationTickHandler.java new file mode 100644 index 0000000..c67c3f8 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/tick/RigAnimationTickHandler.java @@ -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 : + * + *
    + *
  1. Récupère le {@link LivingEntityPatch} via + * {@link TiedUpCapabilities#getEntityPatch(net.minecraft.world.entity.Entity, Class)}
  2. + *
  3. Ticke l'{@link Animator} (avance les layers EF, gère les transitions, + * etc.)
  4. + *
  5. 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.
  6. + *
+ * + *

Choix de design

+ * + * + *

Futur Phase 3+

+ *

Ce handler est minimal — Phase 3 ajoutera :

+ * + * + *

Cf. {@code docs/plans/rig/MIGRATION.md} Phase 3 — Animation consumers.

+ */ +@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 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 : + *
    + *
  • Pas de capability attachée (ex : le type d'entity n'est pas dans + * {@code EntityPatchProvider.CAPABILITIES})
  • + *
  • L'{@code Animator} est null (entitypatch pas encore onConstructed)
  • + *
+ * + *

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}).

+ */ + 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 : + *
    + *
  • Le registry est ready (placeholder asset loadé)
  • + *
  • Le patch est dans {@code LivingMotions.IDLE} (pas en action + * particulière — walk, sit, etc. Phase 3+)
  • + *
  • L'animation actuellement jouée sur le base layer n'est pas déjà + * CONTEXT_STAND_IDLE (évite le re-trigger à chaque tick)
  • + *
+ * + *

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.

+ */ + 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 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(); + } +} diff --git a/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json b/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json new file mode 100644 index 0000000..090fcb3 --- /dev/null +++ b/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json @@ -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] } + ] + } + ] +} diff --git a/src/main/resources/assets/tiedup/armatures/biped.json b/src/main/resources/assets/tiedup/armatures/biped.json new file mode 100644 index 0000000..dff2563 --- /dev/null +++ b/src/main/resources/assets/tiedup/armatures/biped.json @@ -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": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } +}