feat(gamedev): comfyui_build_projectile_workflow — proyectiles/balas/hechizos orientados (glow→luma-alpha, sólido→rembg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `comfyui_build_topdown_sprite_workflow_py_ml` | `(subject, *, direction="south", style="top-down game sprite, RPG", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN sprite en **vista CENITAL (top-down)** estilo RPG clásico/roguelike (Zelda, juegos cenitales): personaje/objeto visto **desde arriba**, centrado, fondo limpio recortable a alpha (`{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas del MISMO personaje = misma `subject`/`style`/`seed`, varía solo `direction` → montar con `comfyui_build_grid`. **DISTINTO de `sprite_sheet` (vista lateral/frontal de plataformas)**: el negativo por defecto rechaza side/front/3-4/isometric/perspective para forzar la cenital. Con SD1.5 sin LoRA sale picado alto; cenital estricto pide LoRA top-down + cfg alto. Probado e2e en GPU con SD1.5 (`reports/0156`). SD1.5. |
|
| `comfyui_build_topdown_sprite_workflow_py_ml` | `(subject, *, direction="south", style="top-down game sprite, RPG", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN sprite en **vista CENITAL (top-down)** estilo RPG clásico/roguelike (Zelda, juegos cenitales): personaje/objeto visto **desde arriba**, centrado, fondo limpio recortable a alpha (`{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas del MISMO personaje = misma `subject`/`style`/`seed`, varía solo `direction` → montar con `comfyui_build_grid`. **DISTINTO de `sprite_sheet` (vista lateral/frontal de plataformas)**: el negativo por defecto rechaza side/front/3-4/isometric/perspective para forzar la cenital. Con SD1.5 sin LoRA sale picado alto; cenital estricto pide LoRA top-down + cfg alto. Probado e2e en GPU con SD1.5 (`reports/0156`). SD1.5. |
|
||||||
| `comfyui_build_splash_art_workflow_py_ml` | `(scene, *, mood="epic, cinematic", checkpoint="juggernaut_xl_v11…", width=1024, height=576, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración grande de UN splash / pantalla de carga / key art en formato **pantalla apaisado 16:9** (`width>height`, ~1024×576), composición cinematográfica (`{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`) para verse a pantalla completa; si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el título/logo/barra de carga los pone el motor/post (negativo rechaza `text/title/logo/UI/frame/watermark`), dejando aire para superponer el título. Set coherente = mismo `mood`/`checkpoint`/`lora`, varía solo `scene`. Probado e2e en GPU con SD1.5 + hires (1024×576 → 1536×864, 54s, `reports/0159`). SD1.5/SDXL. |
|
| `comfyui_build_splash_art_workflow_py_ml` | `(scene, *, mood="epic, cinematic", checkpoint="juggernaut_xl_v11…", width=1024, height=576, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración grande de UN splash / pantalla de carga / key art en formato **pantalla apaisado 16:9** (`width>height`, ~1024×576), composición cinematográfica (`{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`) para verse a pantalla completa; si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el título/logo/barra de carga los pone el motor/post (negativo rechaza `text/title/logo/UI/frame/watermark`), dejando aire para superponer el título. Set coherente = mismo `mood`/`checkpoint`/`lora`, varía solo `scene`. Probado e2e en GPU con SD1.5 + hires (1024×576 → 1536×864, 54s, `reports/0159`). SD1.5/SDXL. |
|
||||||
| `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. |
|
| `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. |
|
||||||
|
| `comfyui_build_projectile_workflow_py_ml` | `(projectile, *, direction="right", glow=False, style="game projectile, side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN proyectil orientado (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite pequeño con **orientación** (apunta a la derecha por defecto, ángulo 0 — el motor rota el sprite), aislado, listo para instanciar. **`glow` elige el camino a alpha**: `glow=False` (defecto) = proyectil SÓLIDO con silueta → `plain background` + **Rembg** (alpha por recorte, como `item_icon`/`topdown_sprite`); `glow=True` = brillante/mágico → `glowing, on black background` **sin Rembg** (recortaría el halo), insumo de **`comfyui_matting_luma_to_alpha`** que el caller aplica luego (como `vfx_spritesheet`/`decal_overlay`). `glow=True` ignora `transparent`/`rembg_model`; el negativo por defecto NO rechaza "black background". `direction` se inserta como `pointing {direction}` (`""`/None = sin orientación). Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `projectile`/`seed`. Probado e2e en GPU con SD1.5 — fireball glow sobre negro + luma→alpha RGBA (`reports/0161`). SD1.5. |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_build_projectile_workflow
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def comfyui_build_projectile_workflow(projectile: str, *, direction: str = \"right\", glow: bool = False, style: str = \"game projectile, side view\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, transparent: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, rembg_model: str = \"u2net\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"projectile\") -> dict"
|
||||||
|
description: "Construye el dict (API format) del workflow de UN proyectil orientado 2D: flecha, bala, bola de fuego, rayo, misil o hechizo — sprite pequeno con orientacion (horizontal por defecto, apuntando a la derecha), aislado sobre fondo recortable a alpha, listo para instanciar como proyectil en el motor (rotar segun el angulo de disparo). Opcion `direction` (right/left/up/down) y `glow`: glow=False (defecto) -> proyectil SOLIDO con Image Rembg para alpha; glow=True -> proyectil BRILLANTE/magico sobre fondo NEGRO sin Rembg, como insumo de comfyui_matting_luma_to_alpha (luminancia-como-alpha) que el caller aplica luego. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (si solido y transparent). Hermano de comfyui_build_item_icon/topdown_sprite/vfx_spritesheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
|
||||||
|
tags: [comfyui, ml, gamedev, gamedev-2d, projectile, bullet, fireball, spell, vfx, rembg, luma-to-alpha, workflow]
|
||||||
|
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_lora_py_ml]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: projectile
|
||||||
|
desc: "Nombre del proyectil (ej. 'arrow', 'bullet', 'fireball', 'lightning bolt', 'missile', 'magic spell'). Se inserta en un prompt scaffold de proyectil. No puede estar vacio."
|
||||||
|
- name: direction
|
||||||
|
desc: "Orientacion hacia la que apunta el proyectil. 'right' por defecto (apuntando a la derecha, angulo 0 — el convenio del motor que luego rota el sprite), 'left', 'up', 'down' (tambien diagonales como 'up-right'). None/'' = sin orientacion explicita. keyword-only."
|
||||||
|
- name: glow
|
||||||
|
desc: "Si True el proyectil es brillante/magico (bola de fuego, rayo, hechizo): scaffold 'glowing, on black background' y NO se inyecta Rembg (recortaria el halo translucido). La imagen queda sobre fondo negro como INSUMO de comfyui_matting_luma_to_alpha. glow=True ignora transparent/rembg_model. False (defecto) = proyectil solido por Rembg. keyword-only."
|
||||||
|
- name: style
|
||||||
|
desc: "Descriptor de estilo que mantiene consistentes los proyectiles del set (ej. 'game projectile, side view', 'hand-painted RPG projectile', 'pixel art bullet'). Pasa el MISMO style + checkpoint + lora a todos los proyectiles para coherencia visual. keyword-only."
|
||||||
|
- name: checkpoint
|
||||||
|
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas VRAM, subir size). keyword-only."
|
||||||
|
- name: size
|
||||||
|
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
|
||||||
|
- name: transparent
|
||||||
|
desc: "Si True (y glow=False) inyecta Image Rembg y el PNG sale con alpha (silueta recortada). False = proyectil opaco sobre fondo plano, recortable luego por el caller. Ignorado cuando glow=True. keyword-only."
|
||||||
|
- name: seed
|
||||||
|
desc: "Semilla del KSampler. keyword-only."
|
||||||
|
- name: lora
|
||||||
|
desc: "LoRA de estilo opcional en models/loras (ej. 'detail_tweaker_sd15.safetensors'). None = sin LoRA. keyword-only."
|
||||||
|
- name: lora_strength
|
||||||
|
desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only."
|
||||||
|
- name: rembg_model
|
||||||
|
desc: "Modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa si transparent=True y glow=False. keyword-only."
|
||||||
|
- name: negative
|
||||||
|
desc: "Prompt negativo. None usa el negativo por defecto pensado para proyectiles (un objeto aislado, sin escenario/personaje, sin texto/foto). No menciona 'black background' para no rechazar el fondo negro que glow necesita. keyword-only."
|
||||||
|
- name: steps
|
||||||
|
desc: "Pasos del KSampler. keyword-only."
|
||||||
|
- name: cfg
|
||||||
|
desc: "CFG del KSampler. keyword-only."
|
||||||
|
- name: sampler_name
|
||||||
|
desc: "Sampler del KSampler. keyword-only."
|
||||||
|
- name: scheduler
|
||||||
|
desc: "Scheduler del KSampler. keyword-only."
|
||||||
|
- name: filename_prefix
|
||||||
|
desc: "Prefijo del PNG en output/. keyword-only."
|
||||||
|
output: "dict en API format listo para comfyui_submit_workflow: base txt2img cuadrada con prompt scaffold de proyectil ('a {projectile}, {style}, pointing {direction}, isolated, single object, {plain background | glowing, on black background}, game asset, ...') + LoRA de estilo opcional. Si glow=False y transparent, ademas Image Rembg para alpha. Si glow=True, la imagen sale sobre fondo negro (insumo de comfyui_matting_luma_to_alpha). UN proyectil; un set -> llamar por proyectil con el mismo style/checkpoint/lora."
|
||||||
|
tested: true
|
||||||
|
tests: ["golden solido transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; projectile + 'game projectile, side view' + 'pointing right' + 'isolated' + 'plain background' + 'game asset' en prompt; SaveImage <- Rembg; transparency True", "golden glow sobre negro: 'glowing, on black background' en prompt, sin 'plain background', sin Rembg, SaveImage <- VAEDecode", "glow ignora transparent: glow=True + transparent=True no inyecta Rembg", "edge direction reflejada: left/up/down/up-right aparecen como 'pointing {d}'", "edge direction opcional: sin direction no hay 'pointing'", "edge solido opaco: glow=False + transparent=False sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge style en prompt", "edge lora: LoraLoader presente con strength", "negativo por defecto permite black background (glow lo necesita)", "error projectile vacio -> ValueError", "determinismo"]
|
||||||
|
test_file_path: "python/functions/ml/comfyui_build_projectile_workflow_test.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_build_projectile_workflow.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from ml.comfyui_build_projectile_workflow import comfyui_build_projectile_workflow
|
||||||
|
|
||||||
|
# Proyectil brillante (bola de fuego) apuntando a la derecha, sobre fondo negro
|
||||||
|
# como insumo de luma->alpha. Listo para submit.
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"fireball",
|
||||||
|
direction="right",
|
||||||
|
glow=True,
|
||||||
|
style="game projectile, side view",
|
||||||
|
seed=42,
|
||||||
|
)
|
||||||
|
# comfyui_submit_workflow(wf) -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||||
|
# -> comfyui_matting_luma_to_alpha (brillante=opaco, negro=transparente)
|
||||||
|
|
||||||
|
# Proyectil solido (flecha) con alpha directo via Rembg:
|
||||||
|
wf2 = comfyui_build_projectile_workflow("arrow", direction="right", glow=False, transparent=True, seed=5)
|
||||||
|
```
|
||||||
|
|
||||||
|
O lanzable directo con: `./fn run comfyui_build_projectile_workflow` (imprime nodos + class_types del ejemplo).
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites el sprite de un PROYECTIL de juego — flecha, bala, bola de fuego,
|
||||||
|
rayo, misil, hechizo, plasma — orientado (apuntando a la derecha por defecto, angulo
|
||||||
|
0) y aislado para instanciar en el motor y rotar segun el angulo de disparo. Elige el
|
||||||
|
camino a alpha segun la naturaleza del proyectil:
|
||||||
|
|
||||||
|
- **`glow=False` (proyectil solido: flecha, bala, misil, espada lanzada)**: la silueta
|
||||||
|
esta definida; con `transparent=True` se recorta limpio con Image Rembg dejando alpha.
|
||||||
|
- **`glow=True` (proyectil brillante/magico: bola de fuego, rayo, hechizo)**: el halo es
|
||||||
|
translucido; se genera sobre fondo NEGRO y NO se recorta con Rembg. Despues aplica
|
||||||
|
`comfyui_matting_luma_to_alpha` (luminancia-como-alpha) en un paso aparte para sacar
|
||||||
|
el halo con transparencia gradual.
|
||||||
|
|
||||||
|
Usa `direction` para fijar hacia donde apunta el sprite si el motor no rota desde
|
||||||
|
"right". Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos los proyectiles del
|
||||||
|
juego para que combinen.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **glow elige el camino a alpha**: `glow=True` NO inyecta Rembg (recortaria el halo) y
|
||||||
|
pone el proyectil sobre fondo negro como insumo de `comfyui_matting_luma_to_alpha`.
|
||||||
|
`glow=False` + `transparent=True` recorta la silueta solida con Rembg. No mezcles: un
|
||||||
|
proyectil brillante recortado por silueta pierde el halo; uno solido por luma-to-alpha
|
||||||
|
pierde las zonas oscuras del objeto.
|
||||||
|
- **glow=True ignora `transparent`/`rembg_model`**: el camino de un brillante es siempre
|
||||||
|
luma-to-alpha, no Rembg. Esos dos parametros solo aplican a `glow=False`.
|
||||||
|
- **El negativo por defecto NO rechaza "black background"**: es deliberado, para que el
|
||||||
|
fondo negro de `glow=True` salga. Si pasas un `negative` propio para un proyectil glow,
|
||||||
|
NO incluyas "black background" o anularas el insumo de luma-to-alpha.
|
||||||
|
- **`direction` se inserta como "pointing {direction}"**: `direction="up"` -> "pointing up"
|
||||||
|
en el prompt. Deja `direction=""`/None para un proyectil sin orientacion fija. El
|
||||||
|
convenio tipico del motor es apuntar a la derecha (angulo 0) y rotar el sprite.
|
||||||
|
- **El matting/recorte es un paso aparte**: esta funcion solo arma el dict. La generacion
|
||||||
|
real (GPU) la hacen `comfyui_submit_workflow` + `comfyui_wait_result` +
|
||||||
|
`comfyui_fetch_output_image`; el luma-to-alpha de un proyectil glow es
|
||||||
|
`comfyui_matting_luma_to_alpha` en un pipeline posterior.
|
||||||
|
- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"`
|
||||||
|
sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram). Si
|
||||||
|
hay OOM, baja `size` o usa SD1.5.
|
||||||
|
- Es una funcion **pura**: solo arma el dict, sin red ni I/O. Determinista para los mismos
|
||||||
|
argumentos.
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"""Construye el workflow ComfyUI de UN proyectil orientado (API format).
|
||||||
|
|
||||||
|
Proyectiles de juego (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite
|
||||||
|
pequeno con ORIENTACION (horizontal por defecto, apuntando a la derecha), aislado
|
||||||
|
sobre fondo recortable a alpha, listo para instanciar como proyectil en el motor
|
||||||
|
(rotar segun el angulo de disparo). Es el builder hermano de
|
||||||
|
comfyui_build_item_icon_workflow / comfyui_build_vfx_spritesheet_workflow /
|
||||||
|
comfyui_build_topdown_sprite_workflow: mismo patron (PURO, dict API format) que
|
||||||
|
compone funciones existentes del registry, no reescribe el grafo.
|
||||||
|
|
||||||
|
Cableado:
|
||||||
|
|
||||||
|
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
|
||||||
|
-> CLIPTextEncode (prompt scaffold de proyectil) ...
|
||||||
|
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
|
||||||
|
|
||||||
|
Compone:
|
||||||
|
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
|
||||||
|
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia)
|
||||||
|
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
|
||||||
|
|
||||||
|
Dos caminos a alpha segun la naturaleza del proyectil:
|
||||||
|
|
||||||
|
- glow=False (DEFECTO): proyectil SOLIDO (flecha, bala, misil, espada lanzada)
|
||||||
|
con silueta definida. El scaffold pide "plain background" y, si transparent,
|
||||||
|
se inyecta Rembg que recorta la silueta dejando alpha limpio. Es el camino
|
||||||
|
analogo a item_icon / topdown_sprite.
|
||||||
|
|
||||||
|
- glow=True: proyectil BRILLANTE/MAGICO (bola de fuego, rayo, hechizo, plasma)
|
||||||
|
cuyo halo es translucido. El scaffold pide "glowing, on black background" y NO
|
||||||
|
se inyecta Rembg (recortaria el halo). La imagen queda sobre fondo negro,
|
||||||
|
pensada como INSUMO de comfyui_matting_luma_to_alpha (luminancia-como-alpha:
|
||||||
|
brillante=opaco, negro=transparente), que el caller aplica luego en un
|
||||||
|
pipeline. Mismo motivo por el que comfyui_build_vfx_spritesheet_workflow genera
|
||||||
|
sobre fondo negro. Por eso glow=True ignora `transparent`/`rembg_model`.
|
||||||
|
|
||||||
|
Por que `direction` y no meterlo en `style`: separar la orientacion del estilo deja
|
||||||
|
fijar hacia donde "apunta" el proyectil (right/left/up/down) manteniendo el resto.
|
||||||
|
El convenio del motor suele ser apuntar a la derecha (angulo 0) y rotar el sprite;
|
||||||
|
por eso direction="right" por defecto. direction vacio = sin orientacion explicita.
|
||||||
|
|
||||||
|
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
|
||||||
|
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode,
|
||||||
|
SaveImage, LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN).
|
||||||
|
|
||||||
|
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el
|
||||||
|
helper de rembg). Determinista para los mismos argumentos.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
# Negativo por defecto pensado para proyectiles: UN objeto aislado, sin escenario ni
|
||||||
|
# personaje que lo sostenga, sin texto/marcas ni recortes. No menciona "black
|
||||||
|
# background" para no rechazar el fondo negro que glow=True necesita.
|
||||||
|
_PROJECTILE_NEGATIVE = (
|
||||||
|
"blurry, lowres, multiple objects, cluttered background, busy background, "
|
||||||
|
"scenery, landscape, character, person, hand holding, text, watermark, "
|
||||||
|
"signature, photo, photorealistic, jpeg artifacts, cropped, out of frame, "
|
||||||
|
"deformed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_rembg(workflow: dict, model: str) -> dict:
|
||||||
|
"""Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage.
|
||||||
|
|
||||||
|
Mismo helper que usan comfyui_build_item_icon_workflow / topdown_sprite: el nodo
|
||||||
|
recorta la silueta del proyectil solido dejando alpha. Repunta SaveImage.images
|
||||||
|
a la salida del Rembg.
|
||||||
|
"""
|
||||||
|
wf = copy.deepcopy(workflow)
|
||||||
|
vaedecode_id = next(
|
||||||
|
(nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None
|
||||||
|
)
|
||||||
|
save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None)
|
||||||
|
if vaedecode_id is None or save_id is None:
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_projectile_workflow: no se encontro VAEDecode/SaveImage para Rembg"
|
||||||
|
)
|
||||||
|
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
|
||||||
|
rembg_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
|
||||||
|
wf[rembg_id] = {
|
||||||
|
"class_type": "Image Rembg (Remove Background)",
|
||||||
|
"inputs": {
|
||||||
|
"images": [vaedecode_id, 0],
|
||||||
|
"transparency": True,
|
||||||
|
"model": model,
|
||||||
|
"post_processing": False,
|
||||||
|
"only_mask": False,
|
||||||
|
"alpha_matting": False,
|
||||||
|
"alpha_matting_foreground_threshold": 240,
|
||||||
|
"alpha_matting_background_threshold": 10,
|
||||||
|
"alpha_matting_erode_size": 10,
|
||||||
|
"background_color": "none",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wf[save_id]["inputs"]["images"] = [rembg_id, 0]
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_build_projectile_workflow(
|
||||||
|
projectile: str,
|
||||||
|
*,
|
||||||
|
direction: str = "right",
|
||||||
|
glow: bool = False,
|
||||||
|
style: str = "game projectile, side view",
|
||||||
|
checkpoint: str = "dreamshaper_8.safetensors",
|
||||||
|
size: int = 512,
|
||||||
|
transparent: bool = True,
|
||||||
|
seed: int = 0,
|
||||||
|
lora: str | None = None,
|
||||||
|
lora_strength: float = 1.0,
|
||||||
|
rembg_model: str = "u2net",
|
||||||
|
negative: str | None = None,
|
||||||
|
steps: int = 28,
|
||||||
|
cfg: float = 7.0,
|
||||||
|
sampler_name: str = "dpmpp_2m",
|
||||||
|
scheduler: str = "karras",
|
||||||
|
filename_prefix: str = "projectile",
|
||||||
|
) -> dict:
|
||||||
|
"""Construye el dict (API format) del workflow de UN proyectil orientado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
projectile: nombre del proyectil (ej. "arrow", "bullet", "fireball",
|
||||||
|
"lightning bolt", "missile", "magic spell"). Se inserta en un prompt
|
||||||
|
scaffold de proyectil. No puede estar vacio.
|
||||||
|
direction: orientacion hacia la que apunta el proyectil. Tipico "right"
|
||||||
|
(apuntando a la derecha, angulo 0 — el convenio del motor que luego
|
||||||
|
rota el sprite), "left", "up", "down" (tambien diagonales como
|
||||||
|
"up-right" si el motor las usa). None/"" = sin orientacion explicita.
|
||||||
|
keyword-only.
|
||||||
|
glow: si True el proyectil es brillante/magico (bola de fuego, rayo,
|
||||||
|
hechizo): el scaffold pide "glowing, on black background" y NO se
|
||||||
|
inyecta Rembg (recortaria el halo translucido). La imagen queda sobre
|
||||||
|
fondo negro como INSUMO de comfyui_matting_luma_to_alpha
|
||||||
|
(luminancia-como-alpha), que el caller aplica luego en un pipeline.
|
||||||
|
glow=True ignora `transparent`/`rembg_model`. Si False (defecto) el
|
||||||
|
proyectil es solido y va por Rembg. keyword-only.
|
||||||
|
style: descriptor de estilo que mantiene consistentes los proyectiles de un
|
||||||
|
set (ej. "game projectile, side view", "hand-painted RPG projectile",
|
||||||
|
"pixel art bullet"). Pasa el MISMO style + checkpoint + (lora) a todos
|
||||||
|
los proyectiles del set para coherencia visual. keyword-only.
|
||||||
|
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5,
|
||||||
|
holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors'
|
||||||
|
para SDXL (mas VRAM, subir size a 768/1024). keyword-only.
|
||||||
|
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por
|
||||||
|
defecto. keyword-only.
|
||||||
|
transparent: si True (y glow=False) inyecta Rembg y el PNG sale con alpha
|
||||||
|
(silueta recortada). Si False deja el proyectil opaco sobre fondo plano,
|
||||||
|
recortable luego por el caller/pipeline. Ignorado cuando glow=True.
|
||||||
|
keyword-only.
|
||||||
|
seed: semilla del KSampler. keyword-only.
|
||||||
|
lora: LoRA de estilo opcional en models/loras (ej.
|
||||||
|
'detail_tweaker_sd15.safetensors'). None = sin LoRA. keyword-only.
|
||||||
|
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
|
||||||
|
keyword-only.
|
||||||
|
rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo
|
||||||
|
se usa si transparent=True y glow=False. keyword-only.
|
||||||
|
negative: prompt negativo. None usa el negativo por defecto pensado para
|
||||||
|
proyectiles (un objeto aislado, sin escenario/personaje, sin texto/foto).
|
||||||
|
keyword-only.
|
||||||
|
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de
|
||||||
|
generacion. keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict en API format listo para comfyui_submit_workflow: txt2img base cuadrada
|
||||||
|
con prompt scaffold de proyectil ('a {projectile}, {style}, pointing
|
||||||
|
{direction}, isolated, ...') + LoRA de estilo opcional. Si glow=False y
|
||||||
|
transparent, ademas Image Rembg para alpha. Si glow=True, la imagen sale
|
||||||
|
sobre fondo negro (insumo de comfyui_matting_luma_to_alpha). Es UN proyectil;
|
||||||
|
un set -> llamar por proyectil con el mismo style/checkpoint/lora.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si projectile esta vacio, o si la base no tiene VAEDecode/SaveImage
|
||||||
|
donde inyectar el Rembg (propagado por el helper).
|
||||||
|
"""
|
||||||
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||||
|
|
||||||
|
if not projectile or not projectile.strip():
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_projectile_workflow: 'projectile' no puede estar vacio"
|
||||||
|
)
|
||||||
|
|
||||||
|
projectile = projectile.strip()
|
||||||
|
direction = (direction or "").strip()
|
||||||
|
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||||
|
neg = _PROJECTILE_NEGATIVE if negative is None else negative
|
||||||
|
|
||||||
|
# Fondo segun la naturaleza: brillante sobre negro (luma->alpha) vs solido sobre
|
||||||
|
# fondo plano (recorte por silueta).
|
||||||
|
background = "glowing, on black background" if glow else "plain background"
|
||||||
|
pointing = f"pointing {direction}, " if direction else ""
|
||||||
|
positive = (
|
||||||
|
f"a {projectile}, {style}, {pointing}isolated, single object, "
|
||||||
|
f"{background}, game asset, dynamic, high detail"
|
||||||
|
)
|
||||||
|
|
||||||
|
wf = comfyui_build_txt2img_workflow(
|
||||||
|
checkpoint,
|
||||||
|
positive,
|
||||||
|
neg,
|
||||||
|
steps=steps,
|
||||||
|
cfg=cfg,
|
||||||
|
width=size,
|
||||||
|
height=size,
|
||||||
|
seed=seed,
|
||||||
|
sampler_name=sampler_name,
|
||||||
|
scheduler=scheduler,
|
||||||
|
filename_prefix=filename_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
if lora:
|
||||||
|
from ml.comfyui_inject_lora import comfyui_inject_lora
|
||||||
|
|
||||||
|
wf = comfyui_inject_lora(
|
||||||
|
wf, lora, strength_model=lora_strength, strength_clip=lora_strength
|
||||||
|
)
|
||||||
|
|
||||||
|
# Solo el proyectil solido va por Rembg. El brillante va por luma->alpha (caller).
|
||||||
|
if transparent and not glow:
|
||||||
|
wf = _inject_rembg(wf, rembg_model)
|
||||||
|
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"fireball",
|
||||||
|
direction="right",
|
||||||
|
glow=True,
|
||||||
|
style="game projectile, side view",
|
||||||
|
seed=42,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"nodes": list(wf),
|
||||||
|
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""Tests offline de comfyui_build_projectile_workflow (estructura del dict, sin GPU)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ml.comfyui_build_projectile_workflow import ( # noqa: E402
|
||||||
|
comfyui_build_projectile_workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _classes(wf):
|
||||||
|
return sorted({n["class_type"] for n in wf.values()})
|
||||||
|
|
||||||
|
|
||||||
|
def _by_class(wf, cls):
|
||||||
|
return [n for n in wf.values() if n["class_type"] == cls]
|
||||||
|
|
||||||
|
|
||||||
|
def _id_of(wf, cls):
|
||||||
|
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
|
||||||
|
|
||||||
|
|
||||||
|
def _pos_with(wf, needle):
|
||||||
|
return next(
|
||||||
|
n for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_solid_transparent_recipe():
|
||||||
|
# Proyectil solido (glow=False) + transparent -> cadena base txt2img + Rembg.
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"arrow", direction="right", glow=False, transparent=True, seed=5
|
||||||
|
)
|
||||||
|
cls = _classes(wf)
|
||||||
|
assert "CheckpointLoaderSimple" in cls
|
||||||
|
assert "KSampler" in cls
|
||||||
|
assert "VAEDecode" in cls
|
||||||
|
assert "SaveImage" in cls
|
||||||
|
assert "Image Rembg (Remove Background)" in cls
|
||||||
|
# El proyectil + estilo + orientacion aparecen en el prompt positivo.
|
||||||
|
pos = _pos_with(wf, "arrow")
|
||||||
|
txt = pos["inputs"]["text"]
|
||||||
|
assert "game projectile, side view" in txt
|
||||||
|
assert "pointing right" in txt
|
||||||
|
assert "isolated" in txt
|
||||||
|
assert "plain background" in txt
|
||||||
|
assert "game asset" in txt
|
||||||
|
# SaveImage toma la imagen del Rembg (no del VAEDecode).
|
||||||
|
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [rembg_id, 0]
|
||||||
|
assert _by_class(wf, "Image Rembg (Remove Background)")[0]["inputs"]["transparency"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_glow_on_black_no_rembg():
|
||||||
|
# glow=True -> "glowing, on black background" + SIN Rembg (insumo luma->alpha).
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"fireball", direction="right", glow=True, seed=42
|
||||||
|
)
|
||||||
|
cls = _classes(wf)
|
||||||
|
assert "Image Rembg (Remove Background)" not in cls
|
||||||
|
pos = _pos_with(wf, "fireball")
|
||||||
|
txt = pos["inputs"]["text"]
|
||||||
|
assert "glowing, on black background" in txt
|
||||||
|
assert "plain background" not in txt
|
||||||
|
# SaveImage toma del VAEDecode directamente (no hay Rembg).
|
||||||
|
vd_id = _id_of(wf, "VAEDecode")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [vd_id, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_glow_ignores_transparent():
|
||||||
|
# Aunque transparent=True, glow=True NO inyecta Rembg.
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"lightning bolt", glow=True, transparent=True
|
||||||
|
)
|
||||||
|
assert "Image Rembg (Remove Background)" not in _classes(wf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_direction_reflected():
|
||||||
|
for d in ["left", "up", "down", "up-right"]:
|
||||||
|
wf = comfyui_build_projectile_workflow("bullet", direction=d, glow=False)
|
||||||
|
pos = _pos_with(wf, "bullet")
|
||||||
|
assert f"pointing {d}" in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_direction_optional():
|
||||||
|
# Sin orientacion, el prompt no inserta "pointing".
|
||||||
|
wf = comfyui_build_projectile_workflow("missile", direction="", glow=False, transparent=False)
|
||||||
|
pos = _pos_with(wf, "missile")
|
||||||
|
assert "pointing" not in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_solid_opaque_no_rembg():
|
||||||
|
# glow=False + transparent=False -> sin Rembg, SaveImage del VAEDecode.
|
||||||
|
wf = comfyui_build_projectile_workflow("bullet", glow=False, transparent=False)
|
||||||
|
assert "Image Rembg (Remove Background)" not in _classes(wf)
|
||||||
|
vd_id = _id_of(wf, "VAEDecode")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [vd_id, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_size_reflected():
|
||||||
|
wf = comfyui_build_projectile_workflow("arrow", size=768)
|
||||||
|
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent["width"] == 768
|
||||||
|
assert latent["height"] == 768 # cuadrado
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_style_reflected():
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"fireball", style="pixel art bullet", glow=True
|
||||||
|
)
|
||||||
|
pos = _pos_with(wf, "fireball")
|
||||||
|
assert "pixel art bullet" in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_lora_reflected():
|
||||||
|
wf = comfyui_build_projectile_workflow(
|
||||||
|
"arrow", lora="detail_tweaker_sd15.safetensors", lora_strength=0.9
|
||||||
|
)
|
||||||
|
loras = _by_class(wf, "LoraLoader")
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]["inputs"]["lora_name"] == "detail_tweaker_sd15.safetensors"
|
||||||
|
assert loras[0]["inputs"]["strength_model"] == 0.9
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_negative_allows_black_background():
|
||||||
|
# El negativo por defecto NO debe rechazar "black background" (glow lo necesita).
|
||||||
|
wf = comfyui_build_projectile_workflow("fireball", glow=True)
|
||||||
|
neg = next(
|
||||||
|
n["inputs"]["text"]
|
||||||
|
for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and "watermark" in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
assert "black background" not in neg
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_empty_projectile():
|
||||||
|
try:
|
||||||
|
comfyui_build_projectile_workflow(" ")
|
||||||
|
assert False
|
||||||
|
except ValueError as e:
|
||||||
|
assert "projectile" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determinism():
|
||||||
|
a = comfyui_build_projectile_workflow(
|
||||||
|
"fireball", direction="right", glow=True, lora="detail_tweaker_sd15.safetensors", seed=7
|
||||||
|
)
|
||||||
|
b = comfyui_build_projectile_workflow(
|
||||||
|
"fireball", direction="right", glow=True, lora="detail_tweaker_sd15.safetensors", seed=7
|
||||||
|
)
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user