feat(gamedev): comfyui_build_decal_overlay_workflow — decals/overlays con alpha (luma→alpha sobre negro)

Builder puro (dict API format) para texturas de superposicion (sangre, grietas,
suciedad, grunge, oxido, quemaduras, salpicaduras): genera el decal aislado sobre
fondo plano (negro por defecto), pensado para extraer alpha con
comfyui_matting_luma_to_alpha (luminancia=alpha, conserva el falloff de translucidos).
NO inyecta Rembg (el matting es luma->alpha de disco, no un nodo). Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora. 9 tests offline verdes;
generacion real verificada e2e en GPU (8GB lowvram, SD1.5, prompt_id 109907a4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 00:34:53 +02:00
parent 6add50311b
commit b88730b7cb
4 changed files with 495 additions and 0 deletions
+1
View File
@@ -46,6 +46,7 @@ VFX (ver `reports/0143`).
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. | | `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). 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_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. |
## Funciones de post-proceso y puente (`gamedev`, CPU) ## Funciones de post-proceso y puente (`gamedev`, CPU)
@@ -0,0 +1,146 @@
---
name: comfyui_build_decal_overlay_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_decal_overlay_workflow(decal: str, *, on_black: bool = True, style: str = \"grunge decal, high detail\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"decal_overlay\") -> dict"
description: "Construye el dict (API format) del workflow de UN decal / overlay con alpha 2D: textura aislada para superponer sobre superficies/sprites/paredes con blend mode del motor — sangre, grietas, suciedad, grunge, oxido, quemaduras, salpicaduras, arañazos, musgo. Se genera AISLADA sobre fondo uniforme; on_black=True (defecto) la pone sobre NEGRO puro, pensada para extraer alpha por luminancia con comfyui_matting_luma_to_alpha (translucido con falloff: la tecnica gamedev correcta para decals). NO inyecta Rembg (el matting de un decal es luma-to-alpha, no un nodo). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo grunge opcional). Hermano de comfyui_build_seamless_tile/vfx_spritesheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, decal, overlay, alpha, blood, grunge, rust, dirt, blend, luma, 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: decal
desc: "Descripcion de la textura a superponer (ej. 'blood splatter', 'wall crack', 'rust stain', 'mud splatter', 'burn mark', 'dirt grunge', 'scratch marks', 'moss patch'). Se inserta en un prompt scaffold de decal aislado. No puede estar vacio."
- name: on_black
desc: "Si True (defecto) genera el decal sobre fondo NEGRO puro, pensado para extraer alpha por luminancia con comfyui_matting_luma_to_alpha (translucido con falloff suave: sangre, humo, salpicadura). False = fondo BLANCO (decals oscuros sobre superficies claras, o recorte/inversion por el caller). keyword-only."
- name: style
desc: "Descriptor de estilo del decal (ej. 'grunge decal, high detail', 'stylized blood, painterly', 'photorealistic rust', 'cartoon crack'). Pasa el MISMO style + checkpoint + lora a todos los decals de un set 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 a 768/1024). keyword-only."
- name: size
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + mismos decal/style -> misma textura; variar seed da variantes del mismo tipo de decal. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. grunge/sangre/oxido). 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: negative
desc: "Prompt negativo. None usa el negativo por defecto pensado para un decal aislado (textura plana sin objetos/escena/profundidad/marco que delaten una composicion). 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 decal ('{decal}, {style}, single isolated decal, centered, on a solid pure black|white background, flat ... backdrop, sticker, ...') + el negativo refuerza fondo PLANO (rechaza fondo texturizado/grunge/del color contrario) + LoRA de estilo opcional. NO lleva Rembg: con on_black=True el PNG resultante se convierte a RGBA con comfyui_matting_luma_to_alpha (luma=alpha) en un paso posterior. UN decal; variar seed da variantes del mismo tipo."
file_path: python/functions/ml/comfyui_build_decal_overlay_workflow.py
tested: true
test_file_path: python/functions/ml/comfyui_build_decal_overlay_workflow_test.py
tests: [test_golden_decal_on_black_recipe, test_edge_on_black_toggles_background, test_edge_decal_reflected, test_edge_style_in_prompt, test_edge_size_reflected, test_edge_negative_isolates_decal, test_edge_lora_reflected, test_error_empty_decal, test_determinism]
---
Construye el dict (API format) del workflow de UN decal / overlay con alpha 2D: una
textura que se SUPERPONE sobre una superficie del juego con un blend mode del motor
(sangre, grietas, suciedad, grunge, oxido, quemaduras, salpicaduras, arañazos, musgo,
polvo). La pieza se genera AISLADA sobre un fondo uniforme para poder extraer luego el
canal alpha. Es el builder hermano de `comfyui_build_seamless_tile_workflow` /
`comfyui_build_vfx_spritesheet_workflow`: mismo patron PURO (dict API format) que
compone funciones existentes del registry sin reescribir el grafo.
## Cuando usarla
Cuando necesites texturas de superposicion para dar desgaste/daño/suciedad a un nivel:
manchas de sangre sobre el suelo o paredes, grietas en piedra, oxido en metal, barro en
una superficie, quemaduras, salpicaduras, arañazos. El decal se genera aislado y se
extrae su alpha para componerlo en el motor con un blend mode (multiply, additive,
overlay) encima del sprite/tile/pared base.
Flujo tipico despues de generar:
1. `comfyui_build_decal_overlay_workflow("blood splatter", on_black=True)` -> dict.
2. `comfyui_submit_workflow` -> `comfyui_wait_result` -> `comfyui_fetch_output_image`
(PNG del decal sobre negro).
3. `comfyui_matting_luma_to_alpha(png, gamma=..., black_point=...)` -> PNG RGBA donde
la luminancia ES el alpha: brillante=opaco, negro=transparente. Listo para el motor.
Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos los decals de un set para que
combinen; varia `seed` para sacar variantes del mismo tipo (varias manchas de sangre
distintas).
**Elige este builder y NO `comfyui_build_prop_object_workflow` / `item_icon` cuando** lo
que quieres es una TEXTURA que se superpone (con falloff translucido), no un objeto
solido con silueta. Un prop/item es un sujeto recortable con Rembg; un decal es una
mancha/grunge cuyo alpha sale de la luminancia.
## Gotchas
- **on_black=True esta pensado para luma->alpha, no es opcional de adorno**: el decal se
genera sobre NEGRO puro precisamente para que `comfyui_matting_luma_to_alpha` mapee la
luminancia a alpha y conserve el degradado de los bordes (sangre, salpicadura, humo).
Si extraes el alpha con un matting binario (rembg) pierdes el falloff y el decal queda
con borde duro recortado. Para decals oscuros sobre superficie clara, usa
`on_black=False` (fondo blanco) e invierte/recorta segun tu motor.
- **NO inyecta Rembg a proposito**: a diferencia de los builders de sprite/prop/item,
este NO lleva 'Image Rembg (Remove Background)'. El SaveImage toma directo del
VAEDecode (decal sobre fondo uniforme) y el matting es un paso posterior de disco.
- **Es una funcion pura**: solo arma el dict. La generacion real (GPU) la hacen
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`; el
alpha lo hace `comfyui_matting_luma_to_alpha`.
- **El fondo debe quedar PLANO**: si el modelo mete una escena/objeto/profundidad detras
del decal, el alpha por luminancia recogera basura. El positivo fuerza "solid pure
black/white background, flat backdrop, sticker, no scenery" y el negativo rechaza
'3d render, object, scene, depth, vignette, frame, border' MAS el fondo texturizado/del
color contrario (gray/grunge/white background cuando on_black=True). Si aun asi sale
ruido de fondo, sube `cfg`.
- **"grunge" en el `style` tiende a llenar el fondo (aprendido en la prueba real)**: con
SD1.5 (dreamshaper_8) un style que contenga "grunge" hace que el modelo pinte una
TEXTURA de fondo gris/disco que arruina el fondo negro plano. Para un decal limpio sobre
negro, prefiere un style que describa SOLO el decal sin connotacion de fondo (ej.
"splatter decal, glossy red blood, high detail" en vez de "grunge decal") y haz reroll de
`seed`: en el barrido de la prueba la seed 11 dio fondo negro plano (esquinas luma ~0.07,
63% de pixeles oscuros) mientras otras seeds metian un disco gris.
- **luma->alpha penaliza el rojo (sangre)**: la luminancia Rec601 pesa el rojo a 0.299, asi
que una salpicadura de sangre ROJA sobre negro sale semi-transparente con los pesos por
defecto. Para que la sangre quede mas opaca, pasa a `comfyui_matting_luma_to_alpha` unos
`luma_weights` con mas peso al rojo (ej. (0.6, 0.25, 0.15) o (1, 0, 0) = canal rojo puro)
y sube `gamma`. Es ajuste del paso de matting (caller), no del builder. Para efectos
BRILLANTES (humo blanco, fuego, destello) los pesos por defecto van perfectos.
- **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.
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_decal_overlay_workflow import comfyui_build_decal_overlay_workflow
# Un decal de sangre sobre fondo negro, listo para submit + luma-to-alpha.
wf = comfyui_build_decal_overlay_workflow(
"blood splatter",
on_black=True,
style="grunge decal, high detail",
seed=7,
)
# Pipeline completo de un decal con alpha:
# r = comfyui_submit_workflow(wf); comfyui_wait_result(r["prompt_id"])
# png = comfyui_fetch_output_image(...) # decal sobre negro
# rgba = comfyui_matting_luma_to_alpha(png, gamma=1.2, black_point=0.05) # luma=alpha
# Variantes del mismo tipo: misma decal/style, cambia seed.
# for s in range(4):
# wf = comfyui_build_decal_overlay_workflow("blood splatter", seed=s)
```
O lanzable directo con: `./fn run comfyui_build_decal_overlay_workflow` (imprime nodos + class_types del ejemplo).
@@ -0,0 +1,208 @@
"""Construye el workflow ComfyUI de UN decal / overlay con alpha (API format).
Un decal es una textura que se SUPERPONE sobre una superficie del juego con un
blend mode del motor: manchas de sangre, grietas, suciedad, grunge, oxido,
quemaduras, salpicaduras, arañazos, musgo, polvo. La pieza se genera AISLADA sobre
un fondo uniforme (negro por defecto) para poder extraer luego el canal alpha.
La tecnica gamedev correcta para un decal translucido (sangre, humo, salpicadura,
mancha con falloff suave) es generarlo sobre fondo NEGRO y convertir la luminancia
en alpha con `comfyui_matting_luma_to_alpha`: brillante -> opaco, negro ->
transparente. Eso preserva el degradado de los bordes (cosa que un matting binario
tipo rembg destruye). Por eso `on_black=True` es el modo por defecto y el scaffold
empuja "on pure black background": el PNG resultante esta pensado para pasar por
luma-to-alpha en un paso aparte. `on_black=False` (fondo blanco) sirve para decals
oscuros sobre superficies claras o cuando el caller prefiere invertir/recortar de
otra forma.
Es el builder hermano de comfyui_build_seamless_tile_workflow /
comfyui_build_vfx_spritesheet_workflow: mismo patron (PURO, dict API format) que
compone funciones existentes del registry, no reescribe el grafo. A diferencia de
los builders de sprite/prop/item, este NO inyecta Rembg: el matting de un decal es
luma-to-alpha (post-proceso de disco), no un nodo del workflow.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo grunge] -> KSampler
-> CLIPTextEncode (prompt scaffold decal aislado) ...
-> VAEDecode -> SaveImage (decal sobre fondo uniforme)
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo (grunge/sangre/oxido) opcional
Pipeline despues de generar (no en este builder):
comfyui_submit_workflow -> comfyui_wait_result -> comfyui_fetch_output_image
-> comfyui_matting_luma_to_alpha (con on_black=True) -> PNG RGBA listo para el motor
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode,
SaveImage, LoraLoader.
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Negativo comun a cualquier decal: una sola textura plana, plena de detalle, SIN
# objetos 3D, escena, personajes, profundidad ni marco/borde que delaten una
# composicion.
_DECAL_NEGATIVE_COMMON = (
"3d render, object, scene, landscape, character, person, creature, "
"depth of field, perspective, vignette, frame, border, drop shadow, "
"multiple decals, tiled pattern, text, watermark, signature, logo, "
"blurry, low quality, jpeg artifacts"
)
# Refuerzo de fondo segun on_black: el fondo DEBE quedar PLANO y del color esperado
# para que el matting (luma->alpha sobre negro, o recorte sobre blanco) no recoja
# basura. Sin esto, un style tipo "grunge" arrastra una textura gris de fondo que
# arruina el alpha. Rechaza explicitamente fondos texturizados/de otro color.
_DECAL_BG_NEG_ON_BLACK = (
"textured background, grunge background, gray background, white background, "
"busy background, background pattern, noisy background, gradient background"
)
_DECAL_BG_NEG_ON_WHITE = (
"textured background, grunge background, gray background, black background, "
"busy background, background pattern, noisy background, gradient background"
)
def comfyui_build_decal_overlay_workflow(
decal: str,
*,
on_black: bool = True,
style: str = "grunge decal, high detail",
checkpoint: str = "dreamshaper_8.safetensors",
size: int = 512,
seed: int = 0,
lora: str | None = None,
lora_strength: float = 1.0,
negative: str | None = None,
steps: int = 28,
cfg: float = 7.0,
sampler_name: str = "dpmpp_2m",
scheduler: str = "karras",
filename_prefix: str = "decal_overlay",
) -> dict:
"""Construye el dict (API format) del workflow de UN decal / overlay con alpha.
Args:
decal: descripcion de la textura a superponer (ej. "blood splatter",
"wall crack", "rust stain", "mud splatter", "burn mark", "dirt grunge",
"scratch marks", "moss patch"). Se inserta en un prompt scaffold de
decal aislado. No puede estar vacio.
on_black: si True (defecto) el decal se genera sobre fondo NEGRO puro,
pensado para extraer el alpha por luminancia con
comfyui_matting_luma_to_alpha (translucido con falloff suave: sangre,
humo, salpicadura). Si False se genera sobre fondo BLANCO (decals
oscuros sobre superficies claras, o recorte/inversion por el caller).
keyword-only.
style: descriptor de estilo del decal (ej. "grunge decal, high detail",
"stylized blood, painterly", "photorealistic rust", "cartoon crack").
Pasa el MISMO style + checkpoint + (lora) a todos los decals de un 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.
seed: semilla del KSampler. Misma seed + mismos decal/style -> misma
textura; variar seed da variantes del mismo tipo de decal. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej. grunge/sangre/oxido).
None = sin LoRA. keyword-only.
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
keyword-only.
negative: prompt negativo. None usa el negativo por defecto pensado para un
decal aislado (una textura plana sin objetos/escena/profundidad/marco
que delaten una composicion). 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 decal ('{decal}, {style}, isolated, on pure black|
white background, texture overlay, game asset, ...') + LoRA de estilo
opcional. NO lleva Rembg: con on_black=True el PNG resultante se convierte a
RGBA con comfyui_matting_luma_to_alpha (luma=alpha) en un paso posterior.
Raises:
ValueError: si decal esta vacio.
"""
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
if not decal or not decal.strip():
raise ValueError(
"comfyui_build_decal_overlay_workflow: 'decal' no puede estar vacio"
)
decal = decal.strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
if negative is None:
bg_neg = _DECAL_BG_NEG_ON_BLACK if on_black else _DECAL_BG_NEG_ON_WHITE
neg = f"{_DECAL_NEGATIVE_COMMON}, {bg_neg}"
else:
neg = negative
# Prompt scaffold de decal aislado: una textura plana, centrada, sobre fondo
# UNIFORME y PLANO (negro por defecto -> luma-to-alpha despues), lista como
# overlay recortable para superponer con blend mode en el motor. El fondo se
# refuerza con "solid/flat" + repeticion para que no salga texturizado (el
# style del decal, p.ej. "grunge", no debe contaminar el fondo).
bg = (
"on a solid pure black background, flat black backdrop"
if on_black
else "on a solid pure white background, flat white backdrop"
)
positive = (
f"{decal}, {style}, single isolated decal, centered, {bg}, "
"flat uniform background, plain backdrop, sticker, no scenery, "
"texture overlay, game asset, high detail, sharp edges"
)
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
)
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_decal_overlay_workflow(
"blood splatter",
on_black=True,
style="grunge decal, high detail",
seed=7,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,140 @@
"""Tests offline de comfyui_build_decal_overlay_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_decal_overlay_workflow import ( # noqa: E402
comfyui_build_decal_overlay_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_decal_on_black_recipe():
wf = comfyui_build_decal_overlay_workflow("blood splatter", on_black=True, seed=7)
cls = _classes(wf)
# Cadena base txt2img pura: NO lleva Rembg (el matting es luma-to-alpha aparte).
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" not in cls
# El decal + el aislamiento + el fondo negro aparecen en el prompt positivo.
pos = _pos_with(wf, "blood splatter")
txt = pos["inputs"]["text"]
assert "isolated" in txt
assert "solid pure black background" in txt
assert "texture overlay" in txt
assert "game asset" in txt
# SaveImage toma del VAEDecode directamente (sin recorte intermedio).
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_on_black_toggles_background():
# on_black=True -> fondo negro plano (pensado para luma->alpha) + el negativo
# rechaza fondos de otro color (gris/blanco/texturizado).
wf_black = comfyui_build_decal_overlay_workflow("rust stain", on_black=True)
pos_black = _pos_with(wf_black, "rust stain")["inputs"]["text"]
assert "solid pure black background" in pos_black
assert "white" not in pos_black
neg_black = next(
n["inputs"]["text"] for n in wf_black.values()
if n["class_type"] == "CLIPTextEncode" and "3d render" in n["inputs"]["text"]
)
assert "white background" in neg_black # rechaza fondo blanco cuando es negro
# on_black=False -> fondo blanco plano + el negativo rechaza fondo negro.
wf_white = comfyui_build_decal_overlay_workflow("rust stain", on_black=False)
pos_white = _pos_with(wf_white, "rust stain")["inputs"]["text"]
assert "solid pure white background" in pos_white
assert "black" not in pos_white
neg_white = next(
n["inputs"]["text"] for n in wf_white.values()
if n["class_type"] == "CLIPTextEncode" and "3d render" in n["inputs"]["text"]
)
assert "black background" in neg_white # rechaza fondo negro cuando es blanco
def test_edge_decal_reflected():
for d in ["wall crack", "mud splatter", "burn mark"]:
wf = comfyui_build_decal_overlay_workflow(d)
pos = _pos_with(wf, d)
assert d in pos["inputs"]["text"]
def test_edge_style_in_prompt():
wf = comfyui_build_decal_overlay_workflow(
"blood splatter", style="stylized blood, painterly"
)
pos = _pos_with(wf, "blood splatter")
assert "stylized blood, painterly" in pos["inputs"]["text"]
def test_edge_size_reflected():
wf = comfyui_build_decal_overlay_workflow("dirt grunge", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_negative_isolates_decal():
# El negativo por defecto rechaza objetos/escena/profundidad/marco que delaten
# una composicion (queremos una textura plana aislada).
wf = comfyui_build_decal_overlay_workflow("scratch marks")
neg = next(
n["inputs"]["text"]
for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "3d render" in n["inputs"]["text"]
)
assert "3d render" in neg
assert "scene" in neg
assert "vignette" in neg
assert "frame" in neg
assert "textured background" in neg # fondo plano forzado
def test_edge_lora_reflected():
wf = comfyui_build_decal_overlay_workflow(
"rust stain", lora="grunge_sd15.safetensors", lora_strength=0.8
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "grunge_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.8
def test_error_empty_decal():
try:
comfyui_build_decal_overlay_workflow(" ")
assert False
except ValueError as e:
assert "decal" in str(e)
def test_determinism():
a = comfyui_build_decal_overlay_workflow(
"blood splatter", on_black=True, lora="grunge_sd15.safetensors", seed=7
)
b = comfyui_build_decal_overlay_workflow(
"blood splatter", on_black=True, lora="grunge_sd15.safetensors", seed=7
)
assert a == b