feat(gamedev): comfyui_generate_character_set_oneshot — set completo de un personaje coherente (2D + direccional 8-way + 3D)

Promueve a un pipeline one-shot la secuencia que hoy exige 4 llamadas a mano:
generar el set COMPLETO de un personaje de juego (imagen base 2D recortada,
sprite direccional N-way SV3D/Zero123 y malla 3D Hunyuan3D .glb), todos del
MISMO personaje. La coherencia cross-frontera se garantiza por construccion: el
direccional y el 3D parten de la MISMA base 2D aplanada (base_flat), no de tres
generaciones independientes. Es la culminacion de las 5 fronteras del grupo
gamedev-2d (issue 0087).

Compone builders del registry (enemy_creature/portrait_avatar/topdown_sprite
por introspeccion) + comfyui_flatten_alpha_on_color (nueva, aplana el sprite
recortado sobre fondo solido que SV3D/Hunyuan exigen) + comfyui_image_to_3d_oneshot
+ comfyui_build_directional_sprite_workflow + submit/wait/fetch + export Godot.
Secuencial liberando VRAM entre pasos pesados (3D antes que SV3D) para caber en
8 GB; fallo aislado deja set PARCIAL sin abortar.

Probado e2e en GPU (RTX 3070 8 GB) con 'armored paladin': base 2D RGBA 512
recortada + malla glTF 395600 triangulos + 8 vistas direccionales SV3D 576,
todos del mismo personaje. 9 tests offline verdes (incluye coherencia mockeada).
Ver reports/0189.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 09:02:24 +02:00
parent 2bab120d7c
commit 9f0d2e2338
6 changed files with 1059 additions and 0 deletions
@@ -0,0 +1,80 @@
---
name: comfyui_flatten_alpha_on_color
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def comfyui_flatten_alpha_on_color(image_path: str, *, out_path: str | None = None, color: tuple = (255, 255, 255), size: int | None = None, resample: str = 'lanczos') -> dict"
description: "Aplana (compone) un PNG con canal alpha sobre un fondo de color SOLIDO y opaco, produciendo un PNG RGB sin transparencia. Sirve para preparar un sprite ya recortado (fondo transparente) como entrada de modelos 3D / multivista (SV3D, Stable Zero123, Hunyuan3D), que esperan el sujeto sobre fondo limpio y opaco (tipicamente blanco), no sobre alpha 0. Evita el bug del LoadImage de ComfyUI, que hace convert('RGB') y descarta el alpha mostrando los RGB crudos bajo la transparencia (basura -> mala reconstruccion 3D). Opcionalmente reescala a size x size para encajar el tamano nativo del modelo (SV3D 576, Zero123 256). Nucleo PIL puro, CPU-only: sin GPU, sin red. Devuelve {ok, out_path, size, error}. Impura solo por la lectura/escritura de disco."
tags: [comfyui, gamedev-2d, ml, matting, alpha, flatten, composite, 3d, pil, image]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: []
params:
- name: image_path
desc: "ruta del PNG de entrada (idealmente con alpha; un sprite ya recortado). Se expande '~'. Si no existe -> ok=False sin lanzar excepcion."
- name: out_path
desc: "ruta del PNG RGB de salida; si None se escribe junto al original con sufijo '_flat' antes de la extension. Se crea el directorio destino si falta. keyword-only."
- name: color
desc: "color de fondo (r,g,b) en 0..255. Si llega (r,g,b,a) se ignora el alpha entrante (el fondo siempre es opaco). Por defecto blanco (255,255,255). keyword-only."
- name: size
desc: "si no es None, reescala el resultado a size x size (cuadrado), para encajar el tamano nativo del modelo 3D (SV3D 576, Zero123 256). keyword-only."
- name: resample
desc: "filtro de reescalado: 'lanczos' (por defecto), 'nearest', 'bilinear', 'bicubic', 'area'. String desconocido -> LANCZOS. keyword-only."
output: "dict con ok (bool), out_path (str, ruta del PNG RGB; vacio si error), size ([w,h] final), error (str, vacio si OK)."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_flatten_alpha_on_color.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_flatten_alpha_on_color import comfyui_flatten_alpha_on_color
# Sprite recortado (fondo transparente) -> base limpia sobre BLANCO a 576x576,
# lista como entrada de SV3D / Stable Zero123 / Hunyuan3D.
res = comfyui_flatten_alpha_on_color(
os.path.expanduser("~/ComfyUI/output/goblin_cutout.png"),
out_path="/tmp/goblin_flat.png",
color=(255, 255, 255),
size=576,
)
# {'ok': True, 'out_path': '/tmp/goblin_flat.png', 'size': [576, 576], 'error': ''}
# Sin out_path: escribe junto al original con sufijo _flat (goblin_cutout_flat.png)
comfyui_flatten_alpha_on_color("~/ComfyUI/output/goblin_cutout.png", size=256)
```
## Cuando usarla
Justo ANTES de meter un sprite recortado (PNG con alpha, p.ej. salida de rembg o de
`comfyui_matting_luma_to_alpha`) en un workflow image-to-3D / multivista (SV3D
turntable, Stable Zero123, Hunyuan3D). Esos modelos asumen el sujeto sobre un fondo
SOLIDO y opaco, no sobre alpha 0; si les pasas el PNG con transparencia tal cual, el
`LoadImage` de ComfyUI tira el alpha y reconstruye sobre los RGB basura que habia
debajo. Tambien para "poner fondo blanco" a cualquier asset recortado antes de un
catalogo o un thumbnail. Usa `size` para dejar la base ya al tamano nativo del modelo.
## Gotchas
- **Impura**: lee y escribe disco. Crea el directorio destino si no existe. No borra
el original.
- **El `LoadImage` de ComfyUI hace `convert("RGB")`** y descarta el alpha: por eso hay
que aplanar ANTES, no confiar en que el nodo respete la transparencia.
- Todo error es **dict `ok=False`** (no excepcion): path inexistente, fallo de I/O o
de decodificado de Pillow -> `error` lo explica. No crashea.
- **Fondo blanco por defecto** `(255,255,255)`: es lo que esperan SV3D/Zero123. Cambia
`color` solo si el modelo concreto pide otro fondo (algunos preferen gris medio).
- Si la imagen ya es RGB (sin alpha) se compone igual (alpha implicito 255): el
resultado es la misma imagen sobre el color, idempotente respecto al fondo.
- Reescalado con **LANCZOS por defecto** (suave); usa `resample="nearest"` si la base
es pixelart y no quieres interpolar el grid.
- `color` con 4 componentes `(r,g,b,a)`: el `a` se ignora, el fondo es siempre opaco.
@@ -0,0 +1,124 @@
"""comfyui_flatten_alpha_on_color — aplana un PNG con alpha sobre un fondo solido.
Compone una imagen con canal alpha (tipicamente un sprite ya recortado, fondo
transparente) sobre un fondo de color SOLIDO y opaco, produciendo un PNG RGB sin
transparencia. Sirve para preparar la entrada de modelos 3D / multivista (SV3D,
Stable Zero123, Hunyuan3D), que esperan el sujeto sobre un fondo limpio y opaco
(normalmente blanco), no sobre alpha 0.
Por que importa: el `LoadImage` de ComfyUI hace `convert("RGB")` y DESCARTA el
canal alpha, mostrando los RGB crudos que habia debajo de la transparencia. Esos
RGB suelen ser basura (negro residual, halos, restos del fondo original) -> la
reconstruccion 3D sale mala. Aplanar antes garantiza un fondo controlado.
La funcion es impura solo por la lectura/escritura de disco. Sin red, sin GPU,
sin servidor ComfyUI. Solo Pillow.
"""
import os
_RESAMPLE = {
"lanczos": "LANCZOS",
"nearest": "NEAREST",
"bilinear": "BILINEAR",
"bicubic": "BICUBIC",
"area": "BOX",
}
def comfyui_flatten_alpha_on_color(
image_path: str,
*,
out_path: str | None = None,
color: tuple = (255, 255, 255),
size: int | None = None,
resample: str = "lanczos",
) -> dict:
"""Aplana un PNG con alpha sobre un fondo de color solido -> PNG RGB.
Args:
image_path: ruta del PNG de entrada (idealmente con alpha; un sprite ya
recortado). Se expande `~`. Si no existe devuelve ok=False sin lanzar.
out_path: ruta del PNG RGB de salida; si None se escribe junto al original
con sufijo "_flat" antes de la extension. Se crea el directorio
destino si falta. keyword-only.
color: color de fondo (r, g, b) en 0..255. Si llega (r, g, b, a) se ignora
el alpha entrante (el fondo siempre es opaco). Por defecto blanco.
keyword-only.
size: si no es None, redimensiona el resultado a size x size (cuadrado),
util para encajar la base al tamano nativo del modelo 3D (SV3D 576,
Zero123 256). keyword-only.
resample: filtro de redimension ("lanczos" por defecto, "nearest",
"bilinear", "bicubic", "area"). String desconocido -> LANCZOS.
keyword-only.
Returns:
dict con:
- ok (bool): True si se compuso y guardo.
- out_path (str): ruta del PNG RGB generado (vacio si error).
- size (list[int]): [w, h] final del resultado (ausente si error temprano).
- error (str): mensaje de error; cadena vacia si todo OK.
"""
out = {"ok": False, "out_path": "", "size": [0, 0], "error": ""}
try:
from PIL import Image
except ImportError as exc:
out["error"] = f"falta dependencia (PIL): {exc}"
return out
src_path = os.path.expanduser(image_path)
if not os.path.isfile(src_path):
out["error"] = f"imagen no existe: {src_path!r}"
return out
# Color de fondo opaco: usa solo (r, g, b), descarta cualquier alpha entrante.
rgb = tuple(int(c) for c in color[:3])
if len(rgb) != 3:
out["error"] = f"color debe tener al menos 3 componentes (r,g,b), recibido {color!r}"
return out
bg_rgba = rgb + (255,)
resample_const = getattr(Image, _RESAMPLE.get(str(resample).lower(), "LANCZOS"))
try:
with Image.open(src_path) as src:
img = src.convert("RGBA")
canvas = Image.new("RGBA", img.size, bg_rgba)
flat = Image.alpha_composite(canvas, img).convert("RGB")
if size is not None:
flat = flat.resize((int(size), int(size)), resample_const)
if out_path is None:
base, ext = os.path.splitext(src_path)
dst_path = base + "_flat" + (ext if ext.lower() == ".png" else ".png")
else:
dst_path = os.path.expanduser(out_path)
dst_dir = os.path.dirname(os.path.abspath(dst_path))
os.makedirs(dst_dir, exist_ok=True)
flat.save(dst_path, "PNG")
except (OSError, ValueError) as exc:
out["error"] = str(exc)
return out
except Exception as exc: # pragma: no cover - Pillow puede lanzar tipos varios
out["error"] = str(exc)
return out
out.update(ok=True, out_path=dst_path, size=list(flat.size), error="")
return out
if __name__ == "__main__":
import json
import sys
if len(sys.argv) < 2:
print("uso: comfyui_flatten_alpha_on_color.py <sprite_con_alpha> [out] [size]",
file=sys.stderr)
sys.exit(2)
src = sys.argv[1]
dst = sys.argv[2] if len(sys.argv) > 2 else None
sz = int(sys.argv[3]) if len(sys.argv) > 3 else None
print(json.dumps(comfyui_flatten_alpha_on_color(src, out_path=dst, size=sz), indent=2))