API
Animorph expone dos APIs: una de servidor (IMorphAPI) para plugins Bukkit/Paper y mods Fabric server,
y una de cliente (ClientMorphAPI) para mods Fabric client.
Ambas incluyen un sistema de eventos para reaccionar a cambios de modelo, emotes y layers.
Agregar la dependencia
Agrega el repositorio de JitPack y la dependencia correspondiente:
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
} Plugin Bukkit/Paper
dependencies {
compileOnly 'com.github.feeldev12.Animorph-API:server:{version}'
} Mod de servidor (Fabric)
dependencies {
implementation 'com.github.feeldev12.Animorph-API:server:{version}'
} Cliente (mod Fabric)
dependencies {
modImplementation 'com.github.feeldev12.Animorph-API:client:{version}'
} API de Servidor (IMorphAPI)
IMorphAPI<P> es la interfaz principal para controlar Animorph desde el servidor.
Funciona tanto en plugins Bukkit/Paper como en mods Fabric server.
import me.feeldev.animorph.api.AnimorphProvider;
import me.feeldev.animorph.api.IMorphAPI;
// Verificar disponibilidad
if (AnimorphProvider.isAvailable()) {
IMorphAPI<Player> api = AnimorphProvider.getApi();
// usar la API...
} P es Player en Bukkit/Paper
o ServerPlayerEntity en Fabric server.
Todos los métodos que envían datos aceptan viewers opcionales al final:
si se omiten, se envía al jugador y a todos sus trackers.
Modelos
IMorphAPI<Player> api = AnimorphProvider.getApi();
// Aplicar un modelo al jugador
api.updateModel(player, "skeleton", false);
// Aplicar modelo forzando re-envío de datos
api.updateModel(player, "skeleton", true);
// Quitar el morph
api.clearModel(player);
// Re-enviar el modelo actual (sincronización con nuevos viewers)
api.updateModelPersistent(player);
// Re-enviar forzando datos completos
api.updateModelPersistent(player, true);
// Consultar el modelo activo
Optional<? extends IModelData> model = api.getModel(player);
// Obtener un modelo registrado por ID
Optional<? extends IModelData> modelo = api.getModel("skeleton");
// Listar todos los modelos registrados
Map<String, ? extends IModelData> modelos = api.registeredModels(); Registrar modelos por código
Si tu mod o plugin trae sus propios modelos (geometría, animación, textura) empaquetados dentro
de su propio JAR, puedes registrarlos programáticamente en vez de pedirle al administrador del
servidor que copie archivos sueltos a plugins/Animorph/models/. Extiende
AnimorphModelDefinition — el mismo patrón que usa GeckoLib para sus
GeoModel — y registra una instancia.
public class FoxModel extends AnimorphModelDefinition {
@Override
public String getModelId() { return "fox"; }
@Override
public String getDisplayName() { return "&6Fox"; }
@Override
protected String modelResourcePath() { return "/assets/mymod/fox.geo.json"; }
@Override
protected String animationResourcePath() { return "/assets/mymod/fox.animation.json"; }
@Override
protected String textureResourcePath() { return "/assets/mymod/fox.png"; }
}
boolean registered = api.registerModel(new FoxModel());
if (!registered) {
// Ya existe un modelo en disco con ese ID — el registro fue rechazado
} getModelId() y compañía corren dentro de tu propia clase, Animorph ya usa el
classloader correcto solo. Las rutas son relativas al classpath de tu JAR, igual que
getClass().getResourceAsStream(...).
Contenido generado en runtime
Si no quieres leer un archivo — por ejemplo, generas la textura o la geometría por código —
sobrescribe los métodos get*Content() / getTextureBytes()
directamente en vez de los *ResourcePath():
public class ProceduralModel extends AnimorphModelDefinition {
@Override
public String getModelId() { return "procedural"; }
@Override
public byte[] getTextureBytes() { return generateTexture(); }
@Override
public String getModelContent() { return generateGeometry(); }
} Layers
Sobrescribe getLayers(). Para leer la textura de un layer, usa el método heredado readBytes(path):
@Override
public List<AnimorphLayerDefinition> getLayers() {
return List.of(
AnimorphLayerDefinition.texture("scarf", readBytes("/assets/mymod/scarf.png"))
.defaultEnabled(true)
);
} Propiedades (properties:)
Sobrescribe getAnimationControllers(), isLayer(),
getRenderType(), isHideNametag(), getFirstPersonProperty(),
getTextPlaceholders(), getHitboxContents(), getEquipmentContents()
y getItemEquipmentContents() — cada una refleja el mismo
campo del bloque properties: de un model.yml normal (ver
Modelos → Referencia de campos para el detalle completo de
cada campo). Ninguna es obligatoria: si no sobrescribes nada, el modelo se comporta igual que
un model.yml sin bloque properties:.
@Override
public List<AnimorphControllerDefinition> getAnimationControllers() {
return List.of(
AnimorphControllerDefinition.nativeController("idle").transitionTime(5)
);
}
@Override
public String getRenderType() { return "entity_translucent"; } @Override
public List<AnimorphControllerDefinition> getAnimationControllers() {
return List.of(
AnimorphControllerDefinition.content("walk", readText("/assets/mymod/walk.controller.json"))
.animationTransition("walk_anim", 3)
);
}
@Override
public IFirstPersonProperty getFirstPersonProperty() {
return IFirstPersonProperty.builder().showModel(true).build();
} Quitar un modelo registrado
api.unregisterModel("fox"); plugins/Animorph/models/
(un archivo .yml normal), registerModel devuelve false y
no hace nada — el modelo en disco tiene prioridad. Si dos mods/plugins distintos registran el
mismo ID por código, el último en registrarse gana.
/animorph reload — no hace falta
volver a llamar registerModel después de un reload.
Bundles automáticos (sin código)
Si no quieres escribir ningún código, Animorph puede detectar y registrar modelos automáticamente
desde dentro del JAR de tu propio mod o plugin, con solo seguir una estructura de carpetas
específica. Funciona igual en Bukkit/Paper, Fabric, Forge y NeoForge — Animorph escanea todos
los mods/plugins cargados al iniciar y busca esta estructura dentro de cada uno. Está separada
por tipo de asset, igual que la convención de disco (carpetas hermanas models/,
animations/, textures/):
animorph/
models/
<id>.yml (obligatorio — mismo esquema que un model.yml normal en disco)
<id>.geo.json (geometría por defecto; auto-detectada por nombre, o vía model.default en el yml)
<id>_slim.geo.json (opcional; geometría slim, o vía model.slim en el yml)
animations/
<id>.animation.json (referenciado por la clave `animation:` del yml)
textures/
<id>.png (referenciado por la clave `texture:` del yml)
<id>.png.mcmeta (opcional, textura animada)
No existe una sub-carpeta anidada layers/<layer_id>/. Un layer type: model
(referenciado desde la sección layers: de un modelo) es arquitectónicamente idéntico a
cualquier otro modelo — es simplemente otro .yml hermano en models/ con
properties.is_layer: true. Todos los animorph/models/*.yml de una misma fuente
se descubren en un único escaneo plano, se ordenan para que las entradas is_layer: true se
registren primero (igual que el orden de carga en disco), y luego se parsean en ese orden. Un layer
type: texture no es un bundle aparte — es solo una referencia texture: resuelta
contra la carpeta compartida animorph/textures/, igual que la textura propia de un modelo de nivel superior.
<id>.yml usa el mismo formato que un YAML de modelo normal (ver
Modelos) — display_name, properties, etc. — pero
vive empaquetado en tu JAR en vez de en la carpeta de Animorph.
<id> de tu bundle coincide con un
modelo YAML normal en plugins/Animorph/models/, el modelo en disco gana y tu bundle
se ignora (queda un warning en el log). Esto le da al administrador del servidor la última palabra
para sobrescribir cualquier modelo que traiga un mod/plugin.
Emotes
Los emotes pueden reproducirse a nivel de modelo o de layers específicos.
// Reproducir un emote en el modelo
api.playEmote(player, "greet:wave", null);
// Reproducir un emote en layers específicos
api.playEmote(player, "dance:spin", Set.of("pompompurin"));
// Reproducir emote desde un tick específico (sincronización)
api.playEmote(player, "dance:spin", 40.0, null);
// Reproducir emote en layers desde un tick específico
api.playEmote(player, "dance:spin", 40.0, Set.of("pompompurin"));
// Detener todos los emotes (modelo + layers)
api.clearEmote(player);
// Detener emotes solo en layers específicos
api.clearEmote(player, Set.of("pompompurin"));
// Re-enviar emote de modelo (sincronización)
api.playEmote(player);
// Re-enviar emote de un tipo específico
api.playEmote(player, EmoteType.MODEL);
api.playEmote(player, EmoteType.LAYER);
// Consultar emote activo en un layer específico
String layerEmote = api.getLayerEmoteId(player, "pompompurin");
// Consultar animaciones registradas
Optional<? extends IEmoteData> emote = api.getAnimation("greet");
Map<String, ? extends IEmoteData> emotes = api.registeredAnimations(); Layers
// Activar un layer
api.applyLayer(player, "pompompurin", true);
// Desactivar un layer
api.applyLayer(player, "pompompurin", false);
// Activar layer en un modelo específico
api.applyLayer(player, "player", "pompompurin", true);
// Re-enviar todos los layers (sincronización)
api.updateLayers(player); applyLayerColor
Sobrescribe el tinte ARGB de un layer activo en runtime. color es un int ARGB (por ejemplo 0xFFFF5500 para naranja). null revierte al color del YAML del modelo.
// Poner layer con tinte rojo semitransparente
api.applyLayerColor(player, "mi_accesorio", 0x88FF0000);
// Revertir al color del YAML
api.applyLayerColor(player, "mi_accesorio", null); Texto
// Actualizar un placeholder
api.updateTextPlaceholder(player, "{name}", "Animorph Pro");
// Actualizar un placeholder solo para viewers específicos
api.updateTextPlaceholder(player, "{name}", "Animorph Pro", viewer1, viewer2);
// Re-enviar todos los placeholders
api.updateTextPlaceholder(player); Primera persona
Controla la configuración de primera persona de un jugador en tiempo real, sin modificar el YAML del modelo. Las propiedades se aplican por jugador y sobrescriben la configuración del modelo.
import me.feeldev.animorph.api.IFirstPersonProperty;
// Crear propiedades de primera persona con el Builder
IFirstPersonProperty property = IFirstPersonProperty.builder()
.showModel(true)
.modelOffset(0, 1.5, 0)
.customArmsShow(true)
.customArmsBothHands(true)
.customArmsRenderItems(true)
.showEquipment(false)
.build();
// Aplicar al jugador
api.updateFirstPersonProperty(player, property);
// Consultar las propiedades actuales
Optional<IFirstPersonProperty> fp = api.getFirstPersonProperty(player);
// Restaurar al valor por defecto del modelo
api.clearFirstPersonProperty(player); Referencia del Builder
| Método | Tipo | Descripción |
|---|---|---|
showModel(bool) | boolean | Muestra el cuerpo completo del modelo en primera persona. |
modelOffset(x, y, z) | double | Desplazamiento del modelo respecto a la cámara. |
customArmsShow(bool) | boolean | Habilita los brazos del modelo en lugar de los vanilla. |
customArmsBothHands(bool) | boolean | Renderiza ambas manos (derecha e izquierda). |
customArmsRenderItems(bool) | boolean | Renderiza ítems a través de las manos del modelo. |
showEquipment(bool) | boolean | Muestra la armadura en primera persona. |
Estado del jugador (servidor)
// ID del modelo actual ("empty" si no tiene)
String modelId = api.getModelId(player);
// ¿Tiene un modelo aplicado?
boolean hasMorph = api.hasModel(player);
// ID del emote actual ("empty" si no tiene)
String emoteId = api.getEmoteId(player);
// ¿Está reproduciendo un emote?
boolean isEmoting = api.isEmoting(player);
// Emote en un layer específico
String layerEmote = api.getLayerEmoteId(player, "pompompurin");
// ¿Un layer está activo?
boolean active = api.isLayerActive(player, "pompompurin");
// Todos los layers y su estado
Map<String, Boolean> layers = api.getLayerStates(player);
// Todos los placeholders y sus valores
Map<String, String> texts = api.getTextPlaceholders(player);
// Propiedades de primera persona
Optional<IFirstPersonProperty> fp = api.getFirstPersonProperty(player); setHideNametag
Sobrescribe en runtime la visibilidad del nametag del jugador, independientemente del campo hide_nametag del YAML. Se limpia automáticamente al aplicar un nuevo modelo.
setModelDisplayName
Sobrescribe el nombre visible del modelo en runtime: nametag 3D, tab list, mensajes de muerte. null revierte al display_name del YAML (o al nombre real del jugador si no tiene).
// Ocultar nametag
api.setHideNametag(player, true);
// Cambiar nombre visible
api.setModelDisplayName(player, "§6Héroe");
// Revertir
api.setModelDisplayName(player, null); Sistema de Eventos
Animorph incluye un AnimorphEventBus ligero y thread-safe.
Los eventos de servidor se disparan antes de aplicar el cambio y son cancelables.
Los eventos de cliente se disparan después de aplicar el cambio y no son cancelables.
AnimorphEventBus bus = api.getEventBus();
// Registrar un listener
bus.register(ModelUpdateEvent.class, event -> {
Player player = event.getPlayer();
String newModel = event.getModelId();
String oldModel = event.getPreviousModelId();
// Cancelar si es un modelo prohibido
if (newModel.equals("banned_model")) {
event.setCancelled(true);
}
});
// Desregistrar un listener
bus.unregister(ModelUpdateEvent.class, myListener); Eventos de servidor
Todos los eventos de servidor extienden AnimorphEvent<P> y los cancelables implementan Cancellable.
| Evento | Cancelable | Descripción | Métodos |
|---|---|---|---|
ModelUpdateEvent | Sí | Se aplica o quita un modelo. | getModelId(), getPreviousModelId(), isClearing() |
EmotePlayEvent | Sí | Se reproduce un emote. | getEmoteId(), getPreviousEmoteId(), getLayerIds(), getStartTick(), isLayerEmote() |
EmoteStopEvent | Sí | Se detiene un emote. | getEmoteId(), getLayerIds(), isLayerEmote() |
LayerUpdateEvent | Sí | Se activa o desactiva un layer. | getLayerId(), getState() |
FirstPersonPropertyUpdateEvent | Sí | Se cambian las propiedades de primera persona. | getProperty() |
AnimorphReloadEvent | No | Se ejecuta /animorph reload. | — |
api.getEventBus().register(EmotePlayEvent.class, event -> {
Player player = event.getPlayer();
if (isInCombat(player)) {
event.setCancelled(true);
player.sendMessage("No puedes usar emotes en combate.");
}
}); Eventos de cliente
Los eventos de cliente se registran en ClientMorphAPI.getEventBus(). Se disparan después de que el cambio ya se aplicó.
| Evento | Descripción |
|---|---|
ClientModelUpdateEvent | El modelo de un jugador cambió. |
ClientEmotePlayEvent | Se inició un emote. |
ClientEmoteStopEvent | Se detuvo un emote. |
ClientLayerUpdateEvent | Un layer cambió de estado. |
ClientFirstPersonPropertyUpdateEvent | Cambiaron las propiedades de primera persona. |
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.event.ClientModelUpdateEvent;
ClientMorphAPI.getEventBus().register(ClientModelUpdateEvent.class, event -> {
UUID playerId = event.getPlayerId();
String modelId = event.getModelId();
// Actualizar UI, efectos visuales, etc.
}); Eventos de render
Los eventos de render se disparan cada frame, dentro del render pass del morph.
Se registran en ClientMorphAPI.getEventBus() igual que el resto de eventos de cliente,
pero están diseñados para integrarse con el pipeline gráfico en lugar de reaccionar a cambios de estado.
Son útiles para mods que normalmente modifican el renderer del jugador vanilla mediante mixins — con Animorph esos mixins no tienen efecto porque el renderer está reemplazado, y estos eventos son la alternativa sin mixins.
| Evento | Cuándo se dispara | Modificable | Cancelable |
|---|---|---|---|
PlayerMorphPreRenderEvent | Antes de actuallyRender — el modelo aún no se dibujó. | PoseStack, bones | Sí — cancela todo el render del frame |
PlayerMorphPostRenderEvent | Después de todas las render layers — geometría ya dibujada. | Renderizar encima | No |
PlayerMorphRenderColorEvent | Al resolver el color ARGB del modelo (una vez por frame por jugador). | setColor(Color) | No |
PlayerMorphNametagRenderEvent | Justo antes de dibujar el nametag del jugador morfeado. | PoseStack, setLabel(Component) | No |
PlayerMorphPreRenderEvent se cancela, se omiten actuallyRender,
todas las render layers y PlayerMorphPostRenderEvent.
El renderer vanilla no se restaura: el jugador quedará invisible ese frame.
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.event.PlayerMorphPreRenderEvent;
ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
event.getPoseStack().pushPose();
event.getPoseStack().scale(1.2f, 1.2f, 1.2f);
// Renderizar geometría detrás del modelo si es necesario
event.getPoseStack().popPose();
}); ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
if (shouldHideForThisPlayer(event.getPlayer())) {
event.setCancelled(true);
}
}); ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
// Ocultar un hueso antes de que se renderice el modelo
event.getModel().getBone("left_wing").ifPresent(bone -> bone.setHidden(true));
}); import me.feeldev.animorph.client.api.event.PlayerMorphPostRenderEvent;
import net.minecraft.client.renderer.RenderType;
ClientMorphAPI.getEventBus().register(PlayerMorphPostRenderEvent.class, event -> {
VertexConsumer buffer = event.getBufferSource().getBuffer(RenderType.lines());
// Dibujar un bounding box, outline, indicador de partículas, etc.
}); import me.feeldev.animorph.client.api.event.PlayerMorphRenderColorEvent;
import software.bernie.geckolib.util.Color;
ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> {
Color c = event.getColor();
// Reducir el alpha al 50%
event.setColor(Color.ofARGB(c.getAlpha() / 2, c.getRed(), c.getGreen(), c.getBlue()));
}); ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> {
if (event.getPlayer().hurtTime > 0) {
// Flash rojo cuando recibe daño
event.setColor(Color.ofARGB(255, 255, 80, 80));
}
}); import me.feeldev.animorph.client.api.event.PlayerMorphNametagRenderEvent;
ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> {
event.getPoseStack().pushPose();
event.getPoseStack().translate(0.0, -0.25, 0.0);
// Renderizar un ícono/badge usando event.getBufferSource()
event.getPoseStack().popPose();
}); ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> {
if (isVip(event.getPlayer())) {
event.setLabel(Component.literal("★ ").append(event.getLabel()));
}
}); Campos disponibles en los eventos de render
| Método | Disponible en | Descripción |
|---|---|---|
getPlayer() | Todos | El jugador cuyo morph se está renderizando. |
getPoseStack() | Pre / Post / Nametag | PoseStack activo del frame, en coordenadas del jugador (pies) o del anchor del nametag. |
getModel() | Pre / Post | Modelo GeckoLib ya horneado — acceso a huesos vía getBone(name). |
getBufferSource() | Pre / Post / Nametag | Buffer source para dibujar geometría adicional. |
getPartialTick() | Todos | Fracción del tick para interpolación. |
getPackedLight() | Todos | Valor de luz empaquetado del bloque. |
getPackedOverlay() | Pre / Post | Overlay empaquetado (daño, flash, etc.). |
isCancelled() / setCancelled(bool) | Pre | Cancela todo el render del morph para este frame. |
getColor() / setColor(Color) | Color | Color ARGB del modelo. Se acumula en orden de registro. |
getLabel() / setLabel(Component) | Nametag | Texto del nametag ya resuelto (prefijo/sufijo de equipo, etc). Reemplazable. |
API de Cliente (ClientMorphAPI)
ClientMorphAPI permite extender Animorph desde el lado del cliente:
registrar controladores de animación custom, agregar queries Molang personalizadas,
consultar el estado de los jugadores y escuchar eventos.
Solo disponible en mods Fabric client.
import me.feeldev.animorph.client.api.ClientMorphAPI;
// Verificar disponibilidad
if (ClientMorphAPI.isAvailable()) {
// usar la API...
} Controladores de animación custom
Extiende AnimorphController para crear controladores de animación propios.
Una vez registrados, los modelos pueden referenciarlos por su ID en animation_controllers.
import me.feeldev.animorph.client.api.AnimorphController;
import me.feeldev.animorph.client.interfaces.IPlayerData;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.animation.RawAnimation;
public class WingsController extends AnimorphController {
public WingsController() {
// nombre del controlador, tiempo de transición en ticks
super("wings", 5);
}
@Override
protected AnimationController.AnimationStateHandler<IPlayerData>
createAnimationStateHandler(IPlayerData entity) {
return animationState -> {
var player = entity.animorph$getPlayer();
if (player.isFallFlying()) {
return animationState.setAndContinue(
RawAnimation.begin().thenLoop("animation.wings_flap")
);
}
return PlayState.STOP;
};
}
@Override
protected boolean interrupt(AnimationState<IPlayerData> animationState) {
// Interrumpir si hay un emote activo, por ejemplo
return false;
}
} import me.feeldev.animorph.client.api.ClientMorphAPI;
// En la inicialización de tu mod
ClientMorphAPI.registerController("wings", new WingsController());
// Verificar si un controlador existe
boolean exists = ClientMorphAPI.hasController("wings"); Después, referéncialo en el YAML del modelo:
properties:
animation_controllers:
idle:
simple_pose:
wings: # tu controlador custom Controladores de primera persona
Los controladores custom también pueden usarse para los brazos en primera persona.
Se registran de la misma forma con ClientMorphAPI.registerController() y se referencian
en fp_animation_controllers en el YAML del modelo.
Si no se especifica fp_animation_controllers, se usan los controladores por defecto:
fp_arm_right, fp_arm_left y fp_arms.
properties:
fp_animation_controllers:
- fp_arm_right
- fp_arm_left
- fp_arms
- my_custom_fp_controller # tu controlador custom de primera persona Variables y queries Molang custom
Puedes registrar variables Molang personalizadas y actualizarlas cada tick de animación. Esto permite crear condiciones custom en animation controllers de Blockbench.
import me.feeldev.animorph.client.api.ClientMorphAPI;
import software.bernie.geckolib.loading.math.MathParser;
// 1. Registrar la variable (una vez, durante la inicialización del mod)
ClientMorphAPI.registerMolangVariable("query.my_stamina");
// 2. Agregar un query que actualice la variable cada tick
ClientMorphAPI.addMolangQuery((animationState, animTime) -> {
var playerData = animationState.getAnimatable();
var player = playerData.animorph$getPlayer();
// Calcular el valor que quieres exponer
double stamina = getStamina(player); // tu lógica
// Actualizar la variable Molang
MathParser.setVariable("query.my_stamina", () -> stamina);
}); Después puedes usar query.my_stamina en las condiciones de transición de tus animation controllers de Blockbench.
Estado del jugador (cliente)
Consulta el estado de cualquier jugador visible desde el cliente:
import me.feeldev.animorph.client.api.ClientMorphAPI;
import java.util.UUID;
UUID playerId = player.getUuid();
// Obtener el modelo activo
Optional<String> modelId = ClientMorphAPI.getPlayerModelId(playerId);
// ¿Tiene un modelo aplicado?
boolean hasModel = ClientMorphAPI.hasModel(playerId);
// Obtener el emote activo
Optional<String> emoteId = ClientMorphAPI.getEmoteId(playerId);
// ¿Está reproduciendo un emote?
boolean isEmoting = ClientMorphAPI.isEmoting(playerId); Huesos de modelo y layers (cliente)
Para integraciones de gameplay (por ejemplo, raycasts o partículas desde un punto del modelo),
puedes consultar huesos del modelo principal o de un layer de tipo MODEL.
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.BoneTransformSnapshot;
import software.bernie.geckolib.cache.object.GeoBone;
UUID playerId = player.getUuid();
// Hueso del modelo principal
Optional<GeoBone> headBone = ClientMorphAPI.getPlayerModelBone(playerId, "head");
// Hueso del modelo principal en contexto de primera persona
Optional<GeoBone> headBoneFp = ClientMorphAPI.getPlayerModelBone(playerId, "head", true);
// Hueso de un layer de modelo activo
Optional<GeoBone> muzzleBone = ClientMorphAPI.getPlayerLayerBone(playerId, "rifle_layer", "muzzle");
// Hueso de layer en contexto explícito (true = primera persona, false = tercera persona)
Optional<GeoBone> muzzleBoneFp = ClientMorphAPI.getPlayerLayerBone(playerId, "rifle_layer", "muzzle", true);
// Snapshot inmutable recomendado para gameplay (estable fuera del render pass)
Optional<BoneTransformSnapshot> muzzleSnapshot = ClientMorphAPI.getPlayerLayerBoneSnapshot(playerId, "rifle_layer", "muzzle", true);
if (muzzleBone.isPresent()) {
GeoBone bone = muzzleBone.get();
float x = bone.getPosX();
float y = bone.getPosY();
float z = bone.getPosZ();
// Usa también rotación/escala si tu lógica lo necesita
} getPlayerLayerBone solo funciona para layers de tipo
MODEL. Los layers de textura no tienen esqueleto/huesos.
muzzle),
usa la sobrecarga con firstPerson=true y preferentemente
getPlayerLayerBoneSnapshot para obtener datos inmutables con menor riesgo de estado stale.
Primera persona (cliente)
Consulta las propiedades de primera persona desde el cliente:
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.api.IFirstPersonProperty;
UUID playerId = player.getUuid();
// Obtener las propiedades de primera persona
Optional<IFirstPersonProperty> fp = ClientMorphAPI.getFirstPersonProperty(playerId);
fp.ifPresent(property -> {
boolean showsModel = property.showModel();
boolean showsCustomArms = property.customArms().show();
boolean showsEquipment = property.showEquipment();
// Acceder a las opciones del modelo
double offsetY = property.modelOptions().offsetY();
// Acceder a la configuración de brazos custom
boolean bothHands = property.customArms().bothHands();
}); Armaduras en morphs (AnimorphArmorRenderer)
La API de Fabric ArmorRenderer recibe un BipedEntityModel contextModel
que no existe en modelos GeckoLib. AnimorphArmorRenderer es una interfaz de extensión
que puedes agregar junto a tu ArmorRenderer existente.
Cuando Animorph la detecta, llama a renderOnMorph en lugar del método original,
dándote acceso directo al IPlayerData del morph.
import me.feeldev.animorph.client.api.AnimorphArmorRenderer;
import me.feeldev.animorph.client.interfaces.IPlayerData;
import net.fabricmc.fabric.api.client.rendering.v1.ArmorRenderer;
public class MyCoolArmorRenderer implements ArmorRenderer, AnimorphArmorRenderer {
// Fabric lo llama cuando el jugador NO tiene morph activo (fallback vanilla)
@Override
public void render(MatrixStack matrices, VertexConsumerProvider bufferSource,
ItemStack stack, LivingEntity entity, EquipmentSlot slot,
int light, BipedEntityModel<LivingEntity> contextModel) {
// ... tu render vanilla/GeoArmorRenderer
}
// Animorph lo llama cuando el jugador SÍ tiene morph activo
@Override
public void renderOnMorph(MatrixStack matrices, VertexConsumerProvider bufferSource,
ItemStack stack, LivingEntity entity, EquipmentSlot slot,
int light, IPlayerData morphContext) {
// IPlayerData da acceso al modelo GeckoLib, bones, animaciones, etc.
BakedGeoModel bakedModel = morphContext.animorph$getModel().getBakedModel(morphContext);
GeoBone rightArmBone = bakedModel.getBone("right_arm").orElse(null);
// ... lógica de render usando los huesos del modelo morph
}
} MyCoolArmorRenderer renderer = new MyCoolArmorRenderer();
// Se registra una sola vez en la API de Fabric — Animorph detecta la interfaz automáticamente
ArmorRenderer.register(renderer, Items.NETHERITE_HELMET, Items.NETHERITE_CHESTPLATE); instanceof AnimorphArmorRenderer en tiempo de render
y llama al método correcto según si el jugador tiene morph activo o no.
No se necesita registro adicional con la API de Animorph.
GeoArmorRenderer de GeckoLib, Animorph lo detecta
directamente y no necesitas implementar AnimorphArmorRenderer.
Esta interfaz es para renderers custom que necesitan contexto del morph.
Render layers custom (AnimorphRenderLayer)
Puedes registrar render layers que se ejecutan dentro del contexto del render pass del modelo morph.
Son equivalentes a los GeoRenderLayer de GeckoLib pero registrables desde cualquier mod cliente,
sin necesidad de modificar el renderer de Animorph.
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.AnimorphRenderLayer;
import me.feeldev.animorph.client.api.AnimorphRenderContext;
// En la inicialización del mod cliente
// Layer global — se ejecuta para cualquier jugador con morph
ClientMorphAPI.registerRenderLayer(context -> {
MatrixStack matrices = context.matrices();
IPlayerData animatable = context.animatable();
VertexConsumerProvider bufferSource = context.bufferSource();
int packedLight = context.packedLight();
// Renderizar algo sobre todos los jugadores con morph
});
// Layer específico de modelo — solo se ejecuta cuando el jugador usa "mi_modelo"
ClientMorphAPI.registerRenderLayer("mi_modelo", context -> {
// Renderizar algo exclusivo para el modelo "mi_modelo"
}); AnimorphRenderContext
| Método | Tipo | Descripción |
|---|---|---|
matrices() | MatrixStack | Pila de transformaciones del render pass actual. |
animatable() | IPlayerData | Datos del jugador con morph (acceso al modelo GeckoLib, player, bones, etc.). |
bakedModel() | BakedGeoModel | El modelo GeckoLib ya horneado del jugador. |
renderType() | RenderLayer (nullable) | Tipo de render activo en este pass. |
bufferSource() | VertexConsumerProvider | Proveedor de vertex buffers para dibujar geometría. |
partialTick() | float | Fracción del tick para interpolar animaciones. |
packedLight() | int | Valor de luz empaquetado del bloque. |
packedOverlay() | int | Valor de overlay (daño, flash, etc.). |
ClientMorphAPI.registerRenderLayer("mi_modelo", context -> {
BakedGeoModel bakedModel = context.bakedModel();
bakedModel.getBone("particle_origin").ifPresent(bone -> {
float x = bone.getPosX();
float y = bone.getPosY();
float z = bone.getPosZ();
// Spawnear partículas en la posición del hueso
});
}); Eventos de render (resumen de uso)
A diferencia de los render layers, los eventos de render permiten
modificar el estado del render (transformaciones, color, visibilidad de huesos)
además de agregar geometría.
Son la alternativa directa a mixins en PlayerRenderer para mods que necesitan
integrarse con el pipeline sin parchear Animorph.
Ver la referencia completa en Eventos › Eventos de render.
// Pre-render: modificar PoseStack, ocultar bones, cancelar el render
ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> { ... });
// Post-render: dibujar sobre el modelo ya renderizado
ClientMorphAPI.getEventBus().register(PlayerMorphPostRenderEvent.class, event -> { ... });
// Color: modificar el ARGB del modelo (transparencia, tinte, efectos)
ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> { ... });
// Nametag: modificar PoseStack o reemplazar el texto antes de dibujar el nametag
ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> { ... });