feat(gamedev): comfyui_build_trap_hazard_workflow — trampas/peligros de escenario
Builder ComfyUI puro (dict API format) para UNA trampa/peligro JUGABLE de nivel (pinchos, sierra giratoria, foso de lava, placa de presion, llamas, trampa de flechas, charco acido, descarga electrica): objeto de peligro aislado y centrado a perspectiva de juego (side/top-down/iso via view), fondo limpio recortable a alpha, estilo consistente para poblar niveles. Hermano de comfyui_build_prop_object/structure/foliage_set_workflow: mismo patron que compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (alpha si transparent). Diferenciado de prop_object (peligro con hitbox de dano vs decoracion inerte) y enemy_creature (trampa vs enemigo vivo); el negativo rechaza character/person/creature/multiple objects. Gotcha documentado: para hazards puramente etereos (llamas/electricidad/gas) usar transparent=False + comfyui_matting_luma_to_alpha (conserva el falloff), no Rembg. 12 tests offline en verde. Probado e2e en GPU con SD1.5 — spiked floor trap side 512x512 RGBA, mecanismo de peligro centrado recortado a alpha real (alpha extrema 0-255, prompt_id ab1b1560). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ VFX (ver `reports/0143`).
|
||||
| `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. |
|
||||
| `comfyui_build_structure_workflow_py_ml` | `(structure, *, view="isometric", style="game building", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN edificio/estructura de escenario (casa, torre, castillo, tienda, posada, ruina, muralla, puente, templo, faro): UN **building COMPLETO** y centrado a perspectiva de juego (`{view} view`, iso por defecto), fondo limpio recortable a alpha (`{structure}, {view} view, {style}, full building, complete structure, single building, centered, plain background, game asset, architecture…`) → txt2img cuadrado + LoRA estilo/iso opcional + Rembg (alpha). **EDIFICACIÓN grande que ocupa varios tiles y define el escenario**, no un objeto pequeño suelto (≠ `prop_object`, que es atrezzo que se deja sobre un tile); el negativo rechaza `small object / single item / prop / furniture`. `view` fija la perspectiva del mapa (iso/side/front/top-down/¾); LoRA iso fija mejor el ángulo 2:1. Set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `structure`. Probado e2e en GPU con SD1.5 — `medieval blacksmith shop` iso 512×512 RGBA, edificio centrado recortado a alpha (centroide 0.54/0.53, `reports/0164`). SD1.5. |
|
||||
| `comfyui_build_foliage_set_workflow_py_ml` | `(plant, *, view="side", style="game foliage, stylized", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN elemento de **vegetación/foliage** de escenario (árbol, arbusto, hierba alta, flores, helecho, hongo, cactus, tronco caído, juncos, hiedra): UN elemento de **naturaleza ORGÁNICA AISLADO** y centrado a perspectiva de juego (`{view} view`, `side` por defecto), fondo limpio recortable a alpha (`{plant}, {view} view, {style}, single plant element, centered, plain background, game nature asset, natural vegetation, organic, isolated plant…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Vegetación que viste el terreno**, distinta del **objeto MANUFACTURADO** suelto (≠ `prop_object`: barril/cofre/mueble) y del **EDIFICIO** (≠ `structure`: casa/torre); el negativo rechaza `building / manmade object / barrel / furniture / person` y `multiple plants / dense forest / jungle / landscape` (UN elemento, no un bosque) + `pot / planter / vase` (planta en maceta = `prop_object`). Recorte por **Rembg** (planta opaca de silueta definida), no luma→alpha. Set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `plant`. ⚠️ **dos gotchas reales SD1.5+Rembg**: (1) **plantas grandes (árbol) tienden a PAISAJE** (cielo+campo) en lugar de fondo plano → re-roll de seeds buscando fondo uniforme (`comfyui_batch_generate`); (2) **follaje verde claro sobre fondo claro → Rembg se come las hojas** y deja solo tronco/ramas → preferir elementos de **silueta compacta y color saturado** (hongo, arbusto denso) o `transparent=False` + matting manual. Probado e2e en GPU con SD1.5 — golden `a glowing mushroom` seed 7 512×512 RGBA, hongo centrado recortado a alpha limpio (centroide 0.51/0.58, opaco 19%, `prompt_id 8fb65a51`); evidencia del gotcha del roble en `reports/0170`. SD1.5. |
|
||||
| `comfyui_build_trap_hazard_workflow_py_ml` | `(hazard, *, view="side", style="game hazard trap", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UNA **trampa/peligro JUGABLE** de nivel (pinchos del suelo, sierra giratoria, foso de lava, placa de presión, columna de llamas, trampa de flechas, charco ácido, descarga eléctrica, prensa, estaca cayendo): UN objeto de **peligro AISLADO** y centrado a perspectiva de juego (`{view} view`, `side` por defecto), fondo limpio recortable a alpha (`{hazard}, {view} view, {style}, single hazard object, trap, dangerous, centered, plain background, game asset, high detail`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Peligro al que el motor asigna hitbox de daño + estado activo/inactivo**, distinto del **objeto INERTE de decoración** (≠ `prop_object`: barril/cofre que solo ambienta) y del **enemigo VIVO** (≠ `enemy_creature`); el negativo rechaza `character / person / creature / multiple objects` para que salga el mecanismo, no un enemigo ni una escena. Recorte por **Rembg** (trampa sólida de silueta definida: pinchos/sierra/placa); ⚠️ para hazards **puramente etéreos** (columna de llamas, arco eléctrico, gas) usar `transparent=False` + `comfyui_matting_luma_to_alpha` (conserva el falloff translúcido para blend aditivo), no Rembg. `view` fija la perspectiva del nivel (side/top-down/iso); set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `hazard`. Probado e2e en GPU con SD1.5 — `spiked floor trap` side seed 7 512×512 RGBA, mecanismo de peligro centrado recortado a alpha (alpha extrema 0–255, fondo transparente real, `prompt_id ab1b1560`, `reports/0174`). SD1.5. |
|
||||
| `comfyui_build_particle_texture_workflow_py_ml` | `(particle, *, soft=True, style="particle texture, soft glow", checkpoint="dreamshaper_8…", size=256, seed=0, lora=None, …) -> dict` | UNA textura de **partícula individual** reutilizable (chispa, humo, polvo, destello/flare, gota, copo, hoja, círculo de energía) — el "ladrillo" que el sistema de partículas del motor (Godot `GPUParticles2D`, Unity VFX Graph) instancia a **miles** y anima (spawn/fade/color over lifetime). Aislada y centrada **sobre fondo NEGRO** (`{particle} particle, {style}, isolated on pure black background, <soft|sharp> edges, single element, for game particle system…`) → txt2img cuadrado + LoRA estilo opcional. **`soft` controla el borde**: `soft=True` (defecto) → `soft glow, feathered edges` (humo/destello/gota); `soft=False` → `crisp sharp edges, high contrast` (chispa/copo/hoja). **NO inyecta Rembg** (rompería el falloff translúcido): insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, additive blend en el motor). **`size` por defecto pequeño (256)** porque se replica a miles. **DISTINTO de `vfx_spritesheet`** (ese es la SECUENCIA animada de un efecto; esto es UNA textura estática reutilizable) **y de `decal_overlay`** (ése es una mancha de desgaste estática para superponer; éste es un emisor de partículas). ⚠️ el `style` por defecto trae "soft glow" → si pides `soft=False` para algo nítido, usa un `style` sin connotación suave. Probado e2e en GPU con SD1.5 — `spark` 256×256 sobre negro plano (dark 85%) + luma→alpha RGBA con falloff preservado (`reports/0163`). SD1.5. |
|
||||
| `comfyui_build_rune_glyph_workflow_py_ml` | `(glyph, *, glow=True, style="arcane glowing rune", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UNA **runa / glifo / sigilo mágico** (glifos rúnicos, círculos mágicos, sigilos de invocación, inscripciones brillantes) para hechizos, portales, marcas de conjuro y efectos de magia: símbolo arcano **aislado** sobre fondo uniforme (`{glyph}, {style}, magic symbol, single isolated glyph, centered, glowing on a solid pure black background, occult sigil, arcane inscription, no scenery, game asset…`) → txt2img cuadrado + LoRA estilo opcional. **`glow` elige el camino a alpha**: `glow=True` (defecto) = runa BRILLANTE sobre **NEGRO puro**, **sin Rembg** (recortaría el halo del resplandor), insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, **blend aditivo** en el motor — conserva el glow); `glow=False` = runa MATE/grabada sobre fondo plano (el negativo rechaza `glow/neon/bloom`), recorte/inversión por el caller. El negativo rechaza `realistic text/readable words/latin alphabet` (un glifo arcano, **no letras reales**) + fondo texturizado/niebla. **DISTINTO de `status_effect_icon`** (símbolo SÓLIDO de UI, recorte Rembg, legible a 16-32 px en el HUD): la runa es una marca translúcida que **emite luz** e se inscribe en el mundo. Grimorio coherente = mismo `style`/`checkpoint`/`lora`, varía `glyph`/`seed`. ⚠️ luma Rec601 penaliza el rojo → para runas rojas (sigilo demoníaco) pasar `luma_weights` con más peso al rojo + subir `gamma`; runas blancas/azules/doradas van con pesos por defecto. Probado e2e en GPU con SD1.5 — `circular summoning rune` glow seed 11 512×512, círculo de invocación brillante sobre **negro puro** (esquinas luma 0.00, dark 83%, runa 3.4% brillante, max 255) apto luma→alpha (`prompt_id 701d149a`, `reports/0172`). SD1.5. |
|
||||
| `comfyui_build_title_lettering_workflow_py_ml` | `(text, *, letter_style="epic fantasy metallic", checkpoint="juggernaut_xl_v11…", width=1024, height=512, transparent=True, seed=0, lora=None, …) -> dict` | EL texto/logo de **título** de un juego (el nombre del juego o una palabra) renderizado con un **tratamiento de lettering** (metálico, tallado en fuego/piedra/madera, neón, cristal, oro), formato **apaisado** (`width>height`, 1024×512 por defecto), fondo plano recortable a alpha (`the word "{text}" as a game logo, {letter_style} lettering, stylized typography, centered, plain background…`) → txt2img apaisado + LoRA estilo opcional + Rembg (alpha). El **negativo NO rechaza texto** (el lettering es el sujeto) y empuja contra el ruido textual (`extra letters/jumbled text/deformed letters`). El VALOR es el ESTILO del lettering, **NO** la fidelidad tipográfica: ⚠️ la difusión renderiza texto de forma imperfecta — letras de más, deformadas o mal escritas; mitigar con palabras CORTAS en MAYÚSCULA, **re-roll de seeds** (`comfyui_batch_generate`), SDXL > SD1.5 para texto, o pintar el texto real con una fuente en el motor. **Una palabra que es un objeto concreto (DRAGON) → el modelo dibuja el objeto, no las letras** — usar palabras abstractas o reforzar `letter_style`. Marca coherente = mismo `letter_style`/`checkpoint`/`lora`, varía solo `text`. Recorte por **Rembg** (logo sólido), no luma→alpha. Probado e2e en GPU: `DRAGON`/`fire engraved` SD1.5 1024×512 → ilustró dragones rojos (alpha OK, confirma el gotcha de palabra-objeto, `prompt_id 6f3920b7`); `AETHER`/`epic fantasy metallic` SDXL 768×384 → **logo de texto metálico dorado** legible con ortografía imperfecta + alpha (`prompt_id 2a7fe8ba`, `reports/0165`). SD1.5/SDXL. |
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: comfyui_build_trap_hazard_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_trap_hazard_workflow(hazard: str, *, view: str = \"side\", style: str = \"game hazard trap\", 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 = \"trap_hazard\") -> dict"
|
||||
description: "Construye el dict (API format) del workflow de UNA trampa/peligro de escenario de juego 2D (pinchos del suelo, sierra giratoria, foso de lava, placa de presion, columna de llamas, trampa de flechas, charco acido, descarga electrica, prensa, estaca cayendo): UN objeto de peligro JUGABLE aislado y centrado a perspectiva de juego (lateral/cenital via view), fondo limpio uniforme recortable a alpha, estilo consistente para poblar niveles. Diferenciado de comfyui_build_prop_object (peligro jugable con hitbox de daño vs objeto inerte de decoracion) y de comfyui_build_enemy_creature (trampa vs enemigo vivo). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_prop_object/structure/foliage_set_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
|
||||
tags: [comfyui, ml, gamedev, gamedev-2d, trap, hazard, danger, scenery, environment, level, rembg, 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: hazard
|
||||
desc: "Descripcion del peligro (ej. 'spiked floor trap', 'spinning saw blade', 'lava pit', 'pressure plate', 'flame jet', 'arrow trap', 'acid pool', 'electric trap', 'falling spike', 'swinging blade', 'bear trap'). Se inserta en un prompt scaffold de trampa. No puede estar vacio."
|
||||
- name: view
|
||||
desc: "Perspectiva del juego con la que se muestra el peligro. Se inserta como '{view} view' en el prompt (ej. 'side', 'top-down', 'isometric', 'front', '3/4'). Por defecto 'side' (la mas comun para trampas de plataformas). Vacio/None -> sin clausula de vista. keyword-only."
|
||||
- name: style
|
||||
desc: "Descriptor de estilo que mantiene consistentes los peligros del set (ej. 'game hazard trap', 'pixel art hazard', 'low poly stylized trap', 'cartoon danger', 'dark fantasy trap'). Pasa el MISMO view + style + checkpoint + lora a todas las trampas del nivel para coherencia. 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 inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo para soltar sobre el nivel). False = peligro opaco sobre fondo plano, recortable luego (recomendado para hazards etereos: llamas/electricidad/gas + luma-to-alpha). keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. Misma seed + mismo hazard/view/style -> misma trampa. keyword-only."
|
||||
- name: lora
|
||||
desc: "LoRA de estilo opcional en models/loras (ej. 'pixel_art_sd15.safetensors', 'stylized_game_assets_xl.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. keyword-only."
|
||||
- name: negative
|
||||
desc: "Prompt negativo. None usa el negativo por defecto pensado para trampas (un peligro aislado y entero, sin personajes/criaturas vivas, sin multiples objetos, fondo limpio, sin texto/recorte). 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 trampa ('{hazard}, {view} view, {style}, single hazard object, trap, dangerous, centered, plain background, game asset, high detail') + LoRA de estilo opcional + Image Rembg (si transparent). UNA trampa; para poblar un nivel -> llamar por cada hazard con mismo view/style/checkpoint/lora; contact-sheet de los peligros -> montar los PNG con comfyui_build_grid."
|
||||
tested: true
|
||||
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; hazard + 'single hazard object' + 'trap' + 'centered' + 'game asset' + 'plain background' + 'side view' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge hazard al inicio del scaffold", "edge view reflejado como '{view} view'", "edge view vacio: sin clausula 'view' colgando", "edge style del set en prompt", "edge negativo anti personaje/criatura viva + multiple objects (diferenciado de enemy_creature/prop_object)", "edge lora: LoraLoader presente con strength", "edge transparent default True", "error hazard vacio -> ValueError", "determinismo"]
|
||||
test_file_path: "python/functions/ml/comfyui_build_trap_hazard_workflow_test.py"
|
||||
file_path: "python/functions/ml/comfyui_build_trap_hazard_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_trap_hazard_workflow import comfyui_build_trap_hazard_workflow
|
||||
|
||||
# Una trampa de escenario con fondo transparente (alpha), lista para soltar en el nivel.
|
||||
wf = comfyui_build_trap_hazard_workflow(
|
||||
"spiked floor trap",
|
||||
view="side",
|
||||
style="game hazard trap",
|
||||
transparent=True,
|
||||
seed=7,
|
||||
)
|
||||
# Poblar un nivel: variar `hazard` con el MISMO view/style/checkpoint/(lora) para coherencia.
|
||||
# for h in ["spinning saw blade", "lava pit", "pressure plate", "flame jet", "acid pool"]:
|
||||
# wf = comfyui_build_trap_hazard_workflow(h, view="side", style="game hazard trap", seed=7)
|
||||
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||
# Contact-sheet de los peligros: montar los PNG resultantes con comfyui_build_grid.
|
||||
```
|
||||
|
||||
O lanzable directo con: `python/.venv/bin/python python/functions/ml/comfyui_build_trap_hazard_workflow.py` (imprime nodos + class_types del ejemplo). Nota: `./fn run` no admite esta firma (parámetros keyword-only con `*`), igual que los builders hermanos; usa el `__main__` del `.py`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites trampas/peligros JUGABLES para poblar niveles de un juego (plataformas,
|
||||
mazmorras, roguelike, acción): pinchos del suelo, sierras giratorias, fosos de lava,
|
||||
placas de presión, columnas de llamas, trampas de flechas, charcos de ácido, descargas
|
||||
eléctricas, prensas, estacas cayendo. A diferencia de `comfyui_build_prop_object_workflow`
|
||||
(un objeto de decoración INERTE: barril, cofre, antorcha que solo ambienta), aquí el asset
|
||||
es un PELIGRO con el que el motor le asigna hitbox de daño, estado activo/inactivo y
|
||||
animación de disparo/golpe. Y a diferencia de `comfyui_build_enemy_creature_workflow`
|
||||
(enemigo VIVO), una trampa es un objeto/mecanismo, no una criatura — el negativo por
|
||||
defecto rechaza `character / person / creature` para que no salga un enemigo. Fija `view`
|
||||
(`side` por defecto) para la perspectiva del nivel y pasa el MISMO `view` + `style` +
|
||||
`checkpoint` + (`lora`) a todas las trampas del set para que combinen; varía solo
|
||||
`hazard`. `transparent` recorta el fondo (alpha) listo para soltar sobre el nivel. Para un
|
||||
atlas/contact-sheet de los peligros, genera cada uno y monta los PNG con
|
||||
`comfyui_build_grid`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Hazard (peligro jugable) != prop_object (decoración inerte) != enemy_creature
|
||||
(enemigo vivo)**: si lo que quieres es un objeto de escena que solo ambienta (barril,
|
||||
cofre, planta), usa `comfyui_build_prop_object_workflow`; si quieres una criatura
|
||||
enemiga, usa `comfyui_build_enemy_creature_workflow`. Este builder genera el MECANISMO
|
||||
de peligro (`single hazard object, trap, dangerous`); el negativo por defecto rechaza
|
||||
`character / person / creature / multiple objects` para no degradar a un enemigo ni a
|
||||
una escena con varios objetos.
|
||||
- **El recorte usa Rembg, NO luma-to-alpha por defecto**: la mayoría de trampas son masas
|
||||
sólidas con silueta definida (pinchos metálicos, sierra, placa de presión, foso); rembg
|
||||
las recorta limpio. Pero si la trampa es **puramente etérea** (columna de llamas, arco
|
||||
eléctrico, nube de gas) y quieres conservar el falloff translúcido para blend aditivo en
|
||||
el motor, pon `transparent=False` y recorta con `comfyui_matting_luma_to_alpha` (o
|
||||
on-black + luma) fuera de este builder, igual que hacen
|
||||
`comfyui_build_status_effect_icon` / `decal_overlay`. Con rembg un efecto puramente
|
||||
translúcido perdería los bordes suaves.
|
||||
- **`view` fija la perspectiva del nivel**: `side` (default) para trampas de plataformas;
|
||||
`top-down` para mazmorras cenitales; `isometric` para niveles iso; `front`/`3/4` para
|
||||
vistas frontales. Mantén el mismo `view` que el resto de assets del nivel.
|
||||
- **Coherencia del set = mismos parámetros**: si cambias `view`/`style`/`checkpoint`/`lora`/
|
||||
`seed` entre trampas, el nivel deja de combinar. Fija esos y varía solo `hazard`.
|
||||
- **SDXL pide más VRAM y resolución**: 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.
|
||||
- Si el modelo mete un personaje, varios objetos o una escena completa, el negativo por
|
||||
defecto ya empuja a "single hazard object / no people / no creature"; refuerza `style`
|
||||
con "single isolated trap mechanism" si insiste.
|
||||
- `transparent=False` deja el peligro opaco sobre fondo plano: útil si prefieres recortar
|
||||
fuera del workflow o el motor compone sobre un fondo sólido.
|
||||
- Es una función **pura**: solo arma el dict. La generación real (GPU) la hacen
|
||||
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`.
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Construye el workflow ComfyUI de UNA trampa/peligro de escenario de juego (API format).
|
||||
|
||||
Trampa o peligro de nivel (pinchos del suelo, sierra giratoria, foso de lava, placa de
|
||||
presion, columna de llamas, trampa de flechas, charco acido, descarga electrica, sierra
|
||||
de pared, prensa aplastante, estaca cayendo, brasero...): UN objeto de peligro AISLADO y
|
||||
centrado, con la perspectiva del juego (lateral o cenital via view), fondo limpio y
|
||||
uniforme recortable a alpha, estilo consistente para poblar niveles. Es el builder
|
||||
hermano de comfyui_build_prop_object_workflow / comfyui_build_structure_workflow: mismo
|
||||
patron (PURO, dict API format) que compone funciones existentes del registry, no reescribe
|
||||
el grafo.
|
||||
|
||||
Diferencia con prop_object (clave para no duplicar): un *prop* es un objeto de decoracion
|
||||
inerte (barril, cofre, antorcha, planta, roca) que ambienta la escena pero no afecta al
|
||||
jugador. Un *hazard* es un peligro JUGABLE que daña o mata: el motor le asigna una hitbox
|
||||
de daño, animacion de disparo/golpe y estado activo/inactivo. Por eso el scaffold empuja
|
||||
"hazard, trap, dangerous" y el negativo rechaza "character, person, creature" para que
|
||||
salga el peligro y no un enemigo (un enemigo vivo es comfyui_build_enemy_creature).
|
||||
|
||||
Cableado:
|
||||
|
||||
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
|
||||
-> CLIPTextEncode (prompt scaffold de hazard) ...
|
||||
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
|
||||
|
||||
Compone:
|
||||
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
|
||||
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia del set)
|
||||
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
|
||||
|
||||
Por que Rembg y NO comfyui_matting_luma_to_alpha por defecto: la mayoria de trampas son
|
||||
objetos SOLIDOS con silueta definida (pinchos metalicos, sierra, placa de presion, foso);
|
||||
rembg recorta limpio la silueta dejando alpha, listo para soltar sobre el nivel. La
|
||||
luma-to-alpha es para translucidos sobre negro (humo/fuego/magia). Si la trampa es
|
||||
puramente etérea (una columna de llamas, un arco electrico, una nube de gas) y quieres
|
||||
conservar el falloff translucido, pon transparent=False y recorta con
|
||||
comfyui_matting_luma_to_alpha (o un on-black + luma) fuera de este builder, igual que en
|
||||
comfyui_build_status_effect_icon / decal_overlay.
|
||||
|
||||
Por que un solo peligro centrado y fondo plano: una trampa se inserta como sprite/objeto
|
||||
en el motor; el scaffold empuja a "single hazard object, centered, plain background, game
|
||||
asset" y el negativo por defecto rechaza "multiple objects, character, person, creature,
|
||||
cropped, out of frame" para mantener UN peligro entero y recortable.
|
||||
|
||||
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 trampas/peligros: UN peligro completo y aislado, fondo
|
||||
# limpio, sin multiples objetos, sin personajes/criaturas vivas (eso es enemy_creature),
|
||||
# sin texto/marcas ni recortes. No filtra ningun tipo de trampa (pinchos, sierra, lava,
|
||||
# acido, electricidad... son validos).
|
||||
_HAZARD_NEGATIVE = (
|
||||
"character, person, people, creature, monster, enemy, hero, "
|
||||
"multiple objects, cluttered scene, full level, tileset, "
|
||||
"blurry, lowres, deformed, bad perspective, "
|
||||
"text, watermark, signature, logo, photo, photorealistic, "
|
||||
"cropped, cut off, out of frame, jpeg artifacts"
|
||||
)
|
||||
|
||||
|
||||
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_prop_object_workflow / structure: el nodo recorta
|
||||
la silueta del peligro 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_trap_hazard_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_trap_hazard_workflow(
|
||||
hazard: str,
|
||||
*,
|
||||
view: str = "side",
|
||||
style: str = "game hazard trap",
|
||||
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 = "trap_hazard",
|
||||
) -> dict:
|
||||
"""Construye el dict (API format) del workflow de una trampa/peligro de escenario.
|
||||
|
||||
Args:
|
||||
hazard: descripcion del peligro (ej. "spiked floor trap", "spinning saw blade",
|
||||
"lava pit", "pressure plate", "flame jet", "arrow trap", "acid pool",
|
||||
"electric trap", "falling spike", "swinging blade", "bear trap"). Se inserta
|
||||
en un prompt scaffold de trampa. No puede estar vacio.
|
||||
view: perspectiva del juego con la que se muestra el peligro. Se inserta como
|
||||
"{view} view" en el prompt (ej. "side", "top-down", "isometric", "front",
|
||||
"3/4"). Por defecto "side" (la mas comun para trampas de plataformas).
|
||||
Vacio/None -> sin clausula de vista. keyword-only.
|
||||
style: descriptor de estilo que mantiene consistentes los peligros del set (ej.
|
||||
"game hazard trap", "pixel art hazard", "low poly stylized trap", "cartoon
|
||||
danger", "dark fantasy trap"). Pasa el MISMO view + style + checkpoint +
|
||||
(lora) a todas las trampas del nivel 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 inyecta Image Rembg y el PNG sale con alpha (fondo recortado,
|
||||
listo para soltar sobre el nivel). Si False deja el peligro opaco sobre fondo
|
||||
plano, recortable luego por el caller/pipeline (recomendado para hazards
|
||||
etereos: llamas/electricidad/gas + luma-to-alpha). keyword-only.
|
||||
seed: semilla del KSampler. Misma seed + mismo hazard/view/style -> misma trampa.
|
||||
keyword-only.
|
||||
lora: LoRA de estilo opcional en models/loras (ej.
|
||||
'pixel_art_sd15.safetensors', 'stylized_game_assets_xl.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. keyword-only.
|
||||
negative: prompt negativo. None usa el negativo por defecto pensado para trampas
|
||||
(un peligro aislado y entero, sin personajes/criaturas vivas, sin multiples
|
||||
objetos, fondo limpio, sin texto/recorte). 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 trampa ('{hazard}, {view} view, {style}, single hazard object,
|
||||
centered, plain background, game asset, ...') + LoRA de estilo opcional + Image
|
||||
Rembg (si transparent). Es UNA trampa; para poblar un nivel -> llamar por cada
|
||||
hazard con el mismo view/style/checkpoint/(lora). Montar el set con
|
||||
comfyui_build_grid si se quiere un contact-sheet de los peligros.
|
||||
|
||||
Raises:
|
||||
ValueError: si hazard 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 hazard or not hazard.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_trap_hazard_workflow: 'hazard' no puede estar vacio"
|
||||
)
|
||||
|
||||
hazard = hazard.strip()
|
||||
view = (view or "").strip()
|
||||
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||
neg = _HAZARD_NEGATIVE if negative is None else negative
|
||||
|
||||
# Prompt scaffold de hazard: UN peligro aislado (no un personaje), centrado, fondo
|
||||
# plano, listo como asset de juego recortable. El "{view} view" fija la perspectiva
|
||||
# del nivel. "hazard, trap, dangerous" empuja al objeto de peligro, no a un enemigo.
|
||||
view_clause = f"{view} view, " if view else ""
|
||||
positive = (
|
||||
f"{hazard}, {view_clause}{style}, single hazard object, "
|
||||
"trap, dangerous, centered, plain background, game asset, "
|
||||
"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
|
||||
)
|
||||
|
||||
if transparent:
|
||||
wf = _inject_rembg(wf, rembg_model)
|
||||
|
||||
return wf
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_trap_hazard_workflow(
|
||||
"spiked floor trap",
|
||||
view="side",
|
||||
style="game hazard trap",
|
||||
transparent=True,
|
||||
seed=7,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"nodes": list(wf),
|
||||
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Tests offline de comfyui_build_trap_hazard_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_trap_hazard_workflow import ( # noqa: E402
|
||||
comfyui_build_trap_hazard_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_transparent_recipe():
|
||||
wf = comfyui_build_trap_hazard_workflow(
|
||||
"spiked floor trap", view="side", transparent=True, seed=7
|
||||
)
|
||||
cls = _classes(wf)
|
||||
# Cadena base txt2img + Rembg para alpha.
|
||||
assert "CheckpointLoaderSimple" in cls
|
||||
assert "KSampler" in cls
|
||||
assert "VAEDecode" in cls
|
||||
assert "SaveImage" in cls
|
||||
assert "Image Rembg (Remove Background)" in cls
|
||||
# Prompt scaffold de hazard.
|
||||
pos = _pos_with(wf, "spiked floor trap")
|
||||
text = pos["inputs"]["text"]
|
||||
assert text.startswith("spiked floor trap")
|
||||
assert "single hazard object" in text
|
||||
assert "trap" in text
|
||||
assert "centered" in text
|
||||
assert "plain background" in text
|
||||
assert "game asset" in text
|
||||
assert "side view" in text
|
||||
# SaveImage cuelga del Rembg, no del VAEDecode.
|
||||
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
|
||||
save = _by_class(wf, "SaveImage")[0]
|
||||
assert save["inputs"]["images"][0] == rembg_id
|
||||
# transparency activa en el nodo Rembg.
|
||||
rembg = _by_class(wf, "Image Rembg (Remove Background)")[0]
|
||||
assert rembg["inputs"]["transparency"] is True
|
||||
|
||||
|
||||
def test_edge_transparent_false_no_rembg():
|
||||
wf = comfyui_build_trap_hazard_workflow("lava pit", transparent=False)
|
||||
assert "Image Rembg (Remove Background)" not in _classes(wf)
|
||||
vae_id = _id_of(wf, "VAEDecode")
|
||||
save = _by_class(wf, "SaveImage")[0]
|
||||
assert save["inputs"]["images"][0] == vae_id
|
||||
|
||||
|
||||
def test_edge_size_square():
|
||||
wf = comfyui_build_trap_hazard_workflow("spinning saw blade", size=768)
|
||||
latent = _by_class(wf, "EmptyLatentImage")[0]
|
||||
assert latent["inputs"]["width"] == 768
|
||||
assert latent["inputs"]["height"] == 768
|
||||
|
||||
|
||||
def test_edge_hazard_at_start_of_scaffold():
|
||||
wf = comfyui_build_trap_hazard_workflow("acid pool")
|
||||
pos = _pos_with(wf, "acid pool")
|
||||
assert pos["inputs"]["text"].startswith("acid pool")
|
||||
|
||||
|
||||
def test_edge_view_reflected():
|
||||
wf = comfyui_build_trap_hazard_workflow("pressure plate", view="top-down")
|
||||
pos = _pos_with(wf, "pressure plate")
|
||||
assert "top-down view" in pos["inputs"]["text"]
|
||||
|
||||
|
||||
def test_edge_view_empty_no_dangling_clause():
|
||||
wf = comfyui_build_trap_hazard_workflow("flame jet", view="")
|
||||
pos = _pos_with(wf, "flame jet")
|
||||
text = pos["inputs"]["text"]
|
||||
# Sin clausula "view" colgando cuando view es vacio.
|
||||
assert " view," not in text
|
||||
assert "single hazard object" in text
|
||||
|
||||
|
||||
def test_edge_style_reflected():
|
||||
wf = comfyui_build_trap_hazard_workflow(
|
||||
"arrow trap", style="pixel art hazard"
|
||||
)
|
||||
pos = _pos_with(wf, "arrow trap")
|
||||
assert "pixel art hazard" in pos["inputs"]["text"]
|
||||
|
||||
|
||||
def test_edge_negative_rejects_living_character():
|
||||
wf = comfyui_build_trap_hazard_workflow("electric trap")
|
||||
neg = next(
|
||||
n for n in wf.values()
|
||||
if n["class_type"] == "CLIPTextEncode"
|
||||
and "electric trap" not in n["inputs"]["text"]
|
||||
)
|
||||
ntext = neg["inputs"]["text"]
|
||||
# Diferenciado de enemy_creature: rechaza personaje/criatura vivos.
|
||||
assert "character" in ntext
|
||||
assert "creature" in ntext
|
||||
assert "multiple objects" in ntext
|
||||
|
||||
|
||||
def test_edge_lora_injected():
|
||||
wf = comfyui_build_trap_hazard_workflow(
|
||||
"bear trap", lora="pixel_art_sd15.safetensors", lora_strength=0.8
|
||||
)
|
||||
loras = _by_class(wf, "LoraLoader")
|
||||
assert len(loras) == 1
|
||||
assert loras[0]["inputs"]["lora_name"] == "pixel_art_sd15.safetensors"
|
||||
assert loras[0]["inputs"]["strength_model"] == 0.8
|
||||
|
||||
|
||||
def test_edge_transparent_default_true():
|
||||
wf = comfyui_build_trap_hazard_workflow("swinging blade")
|
||||
assert "Image Rembg (Remove Background)" in _classes(wf)
|
||||
|
||||
|
||||
def test_error_empty_hazard():
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_trap_hazard_workflow(" ")
|
||||
|
||||
|
||||
def test_determinism():
|
||||
a = comfyui_build_trap_hazard_workflow("spiked floor trap", seed=7)
|
||||
b = comfyui_build_trap_hazard_workflow("spiked floor trap", seed=7)
|
||||
assert a == b
|
||||
Reference in New Issue
Block a user