feat(gamedev-2d): pipeline walk_cycle_oneshot — personaje andando en pixel-art animado
Promueve el caso 1 del report 0217 (animacion de sprites de personaje) a un pipeline one-shot: de un prompt de personaje a un sprite sheet + GIF/WEBP en loop, frame-by-frame dirigido por pose (ControlNet OpenPose + seed fija + Rembg) con cada frame pixelizado a NxN RGBA. Nuevas funciones reutilizables (issue 0087, crecimiento por composicion): - comfyui_walk_cycle_oneshot (pipeline): orquesta poses -> generacion -> pixelizado -> ensamblado. No-throw, salta frames que fallan. Modo openpose (esqueletos reales) con fallback prompt-pose. - render_openpose_walk_skeletons: dibuja N esqueletos OpenPose COCO-18 del walk cycle (el insumo que el report 0217 marco como faltante). - comfyui_pixelize_sprite_png: PNG existente -> NxN RGBA pixel-art real (compone crop_to_content + pixeloe_downscale + comfyui_pixelize_image). - assemble_animated_sprite: frames RGBA -> sprite sheet horizontal + WEBP/GIF loop. - comfyui_build_walk_cycle_workflow (pura): grafo API del workflow animado para la UI (ControlNet OpenPose -> KSampler xN seed fija -> ImageBatch -> Rembg -> SaveAnimatedWEBP). Verificado en GPU: GIF/WEBP de caballero andando, 4 frames 32x32 (y 64x64) RGBA con fondo transparente y 16 colores, identidad de silueta consistente, piernas que cambian. Metodo de poses usado: OpenPose real (sin fallback). Evidencia en report 0221. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: assemble_animated_sprite
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def assemble_animated_sprite(frame_paths: list, out_dir: str, *, name: str = \"anim\", fps: int = 8, fmt: str = \"webp\", loop: bool = True, spritesheet: bool = True, pad: int = 0) -> dict"
|
||||
description: "Ensambla N frames PNG RGBA (p.ej. los frames de un walk cycle ya pixelizados a 32x32 con alpha) en DOS entregables: un sprite sheet horizontal (1 fila x N columnas) PNG RGBA con la transparencia intacta, y una animacion en loop WEBP lossless o GIF animado. Es la pieza de ensamblado final de cualquier animacion de sprite. Salta frames que falten o no abran (aviso en error, no aborta); normaliza tamano al primer frame valido reescalando con NEAREST. Solo PIL. No-throw. Devuelve {ok, spritesheet_path, animation_path, n_frames, frame_size, fmt, error}."
|
||||
tags: [gamedev-2d, comfyui, sprite, animation]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: frame_paths
|
||||
desc: "lista de rutas a PNG RGBA en orden de reproduccion; los que falten o no abran se saltan (aviso en error)."
|
||||
- name: out_dir
|
||||
desc: "directorio de salida; se crea si no existe. Se escriben '<name>_sheet.png' y '<name>.<ext>' dentro."
|
||||
- name: name
|
||||
desc: "nombre base de los ficheros generados (keyword-only, default 'anim')."
|
||||
- name: fps
|
||||
desc: "frames por segundo de la animacion; duration_ms = round(1000/max(1,fps)) por frame (keyword-only, default 8)."
|
||||
- name: fmt
|
||||
desc: "formato de la animacion: 'webp' (recomendado, lossless, alpha completo) o 'gif' (alpha binario) (keyword-only)."
|
||||
- name: loop
|
||||
desc: "si True la animacion se repite indefinidamente (loop=0); si False una sola vez (keyword-only, default True)."
|
||||
- name: spritesheet
|
||||
desc: "si True genera tambien el sprite sheet horizontal PNG RGBA (keyword-only, default True)."
|
||||
- name: pad
|
||||
desc: "pixeles de separacion transparente entre columnas del sheet (keyword-only, default 0)."
|
||||
output: "dict con ok (bool, True si se produjo la animacion con >=1 frame valido), spritesheet_path (str, '' si spritesheet=False o fallo), animation_path (str, '' si fallo), n_frames (int, frames validos usados), frame_size ([w,h] del frame normalizado), fmt (str, 'webp'|'gif'), error (str, avisos y/o error; '' si limpio)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/assemble_animated_sprite.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.assemble_animated_sprite import assemble_animated_sprite
|
||||
|
||||
# Frames de un walk cycle ya pixelizados a 32x32 RGBA (p.ej. salida del pipeline ComfyUI):
|
||||
frames = [
|
||||
"/tmp/walk/frame_00.png",
|
||||
"/tmp/walk/frame_01.png",
|
||||
"/tmp/walk/frame_02.png",
|
||||
"/tmp/walk/frame_03.png",
|
||||
]
|
||||
res = assemble_animated_sprite(frames, "/tmp/walk_out", name="hero_walk", fps=8, fmt="webp")
|
||||
# {'ok': True,
|
||||
# 'spritesheet_path': '/tmp/walk_out/hero_walk_sheet.png',
|
||||
# 'animation_path': '/tmp/walk_out/hero_walk.webp',
|
||||
# 'n_frames': 4, 'frame_size': [32, 32], 'fmt': 'webp', 'error': ''}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al final de cualquier pipeline de animacion de sprite, cuando ya tienes los frames
|
||||
sueltos (pixelizados, con alpha) y necesitas (a) verlos animados en bucle para validar
|
||||
el ciclo a ojo y (b) un sprite sheet horizontal listo para que un motor de juego lo
|
||||
trocee por columnas. Tipico despues de generar un walk cycle frame a frame con ComfyUI
|
||||
y pasarlo por el pixelizado: este es el paso de "juntarlo todo". Usa `fmt="webp"` por
|
||||
defecto; `fmt="gif"` solo si necesitas compatibilidad con visores que no abren WEBP.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **GIF solo tiene alpha binario** (1 bit): cada pixel es opaco o totalmente
|
||||
transparente, los pixeles con `alpha < 128` se vuelven transparentes y se pierde el
|
||||
anti-aliasing del borde. **WEBP (lossless) es el formato recomendado** para sprites con
|
||||
alpha — conserva el canal alpha completo y no ensucia el pixel-art. Usa GIF solo por
|
||||
compatibilidad.
|
||||
- Al guardar GIF, PIL **reoptimiza la paleta** y el indice de transparencia puede
|
||||
cambiar (p.ej. de 255 a 1 al releer): es normal, los pixeles transparentes se
|
||||
preservan (verificable convirtiendo el frame a RGBA y mirando el canal alpha).
|
||||
- **Frames que faltan o no abren se SALTAN** (se anota en `error`), no se aborta: la
|
||||
animacion se monta con los frames validos. Si quedan **0 frames validos** → `ok=False`.
|
||||
- El campo `error` puede venir **no vacio aunque `ok=True`**: ahi van los avisos de
|
||||
frames saltados. `ok` refleja si se genero la animacion, no la ausencia de avisos.
|
||||
- El tamano se normaliza al **primer frame valido**; los frames de tamano distinto se
|
||||
reescalan con **NEAREST** (sin interpolacion, preserva el pixel-art duro), lo que puede
|
||||
deformarlos si su aspect ratio difiere. Asegurate de que todos los frames ya vienen al
|
||||
mismo tamano.
|
||||
- Escribe en disco: crea `out_dir` si no existe; si no hay permiso de escritura, el
|
||||
fallo del sheet va a `error` como aviso y el de la animacion pone `ok=False`.
|
||||
- `disposal=2` limpia el lienzo entre frames (transparencia correcta en cada paso); sin
|
||||
el, los frames se acumularian unos sobre otros.
|
||||
Reference in New Issue
Block a user