feat(gamedev): comfyui_build_particle_texture_workflow — texturas de partícula individuales (chispa/humo/polvo/destello, sobre negro, luma→alpha, size 256)
Builder PURO (dict API format) del grupo gamedev-2d/gamedev-vfx: UNA textura de partícula reutilizable que el sistema de partículas del motor (Godot GPUParticles2D, Unity VFX Graph) instancia a miles. Aislada sobre fondo negro puro, pensada para luma→alpha (comfyui_matting_luma_to_alpha, additive blend); soft controla el borde (glow difuso vs nítido); NO inyecta Rembg (rompería el falloff); size 256 por defecto. Diferenciada de vfx_spritesheet (secuencia animada) y decal_overlay (mancha estática). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora. 9 tests offline verdes. Generación real verificada e2e en GPU (spark sobre negro plano + luma→alpha RGBA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `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. |
|
| `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_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. |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_build_particle_texture_workflow
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def comfyui_build_particle_texture_workflow(particle: str, *, soft: bool = True, style: str = \"particle texture, soft glow\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 256, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, negative: str | None = None, steps: int = 24, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"particle_texture\") -> dict"
|
||||||
|
description: "Construye el dict (API format) del workflow de UNA textura de particula individual con alpha: chispa, humo, polvo, destello/flare, gota, copo, hoja, circulo de energia — el 'ladrillo' que el sistema de particulas del motor (Godot GPUParticles2D, Unity VFX Graph) instancia a miles. Se genera aislada y centrada sobre fondo NEGRO puro, pensada para extraer alpha por luminancia con comfyui_matting_luma_to_alpha (translucido con falloff: additive blend). soft controla el borde (True=glow difuso/feathered; False=nitido/cristalino). NO inyecta Rembg (el matting de un translucido es luma-to-alpha, no un nodo). Size pequeño (256) por defecto: se replica a miles, no necesita resolucion alta. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora. Hermano de comfyui_build_decal_overlay/vfx_spritesheet_workflow. DISTINTO de vfx_spritesheet (ese es la SECUENCIA animada de un efecto; esto es UNA textura reutilizable estatica). Pura, sin red ni I/O. class_types verificados contra /object_info."
|
||||||
|
tags: [comfyui, ml, gamedev, gamedev-2d, gamedev-vfx, particle, texture, alpha, luma, spark, smoke, dust, flare, vfx, 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: particle
|
||||||
|
desc: "Descripcion del elemento de particula (ej. 'spark', 'smoke puff', 'dust mote', 'glowing flare', 'water droplet', 'snowflake', 'falling leaf', 'energy ring'). Se inserta en un prompt scaffold de particula aislada sobre negro. No puede estar vacio."
|
||||||
|
- name: soft
|
||||||
|
desc: "Si True (defecto) genera la particula con bordes suaves y glow difuso ('soft glow, soft falloff, feathered edges': humo, polvo, destello, circulo de energia, gota); si False con bordes nitidos y alto contraste ('crisp sharp edges, high contrast': chispa, copo de nieve, hoja con silueta). En ambos casos el fondo es negro plano. keyword-only."
|
||||||
|
- name: style
|
||||||
|
desc: "Descriptor de estilo de la particula (ej. 'particle texture, soft glow', 'stylized vfx, painterly', 'photoreal smoke', 'anime sparkle'). Pasa el MISMO style + checkpoint + lora a todas las particulas 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 512/768). keyword-only."
|
||||||
|
- name: size
|
||||||
|
desc: "Lado del cuadrado en px (width = height = size). 256 por defecto: una textura de particula es pequeña y se replica a miles, no necesita resolucion alta. keyword-only."
|
||||||
|
- name: seed
|
||||||
|
desc: "Semilla del KSampler. Misma seed + mismos particle/style -> misma textura; variar seed da variantes del mismo tipo de particula. keyword-only."
|
||||||
|
- name: lora
|
||||||
|
desc: "LoRA de estilo opcional en models/loras. 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 una particula aislada (un solo elemento sobre negro plano, sin escena/objeto solido/multiples elementos/fondo texturizado). 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 particula ('{particle} particle, {style}, isolated on pure black background, <soft|sharp> edges, single element, centered, for game particle system, ...') + el negativo refuerza UN solo elemento sobre fondo negro plano (rechaza escena/objeto solido/multiples particulas/fondo texturizado). NO lleva Rembg: el PNG resultante se convierte a RGBA con comfyui_matting_luma_to_alpha (luma=alpha) en un paso posterior. UNA particula; variar seed da variantes del mismo tipo."
|
||||||
|
file_path: python/functions/ml/comfyui_build_particle_texture_workflow.py
|
||||||
|
tested: true
|
||||||
|
test_file_path: python/functions/ml/comfyui_build_particle_texture_workflow_test.py
|
||||||
|
tests: [test_golden_particle_on_black_recipe, test_edge_soft_toggles_edge_character, test_edge_particle_reflected, test_edge_style_in_prompt, test_edge_size_reflected_and_small_default, test_edge_negative_isolates_single_particle, test_edge_lora_reflected, test_error_empty_particle, test_determinism]
|
||||||
|
---
|
||||||
|
|
||||||
|
Construye el dict (API format) del workflow de UNA textura de partícula individual con
|
||||||
|
alpha 2D: el "ladrillo" reutilizable que el sistema de partículas del motor (Godot
|
||||||
|
`GPUParticles2D`/`GPUParticles3D`, Unity VFX Graph / Shuriken) instancia a miles y anima
|
||||||
|
(spawn, velocidad, fade, color over lifetime). Chispa, humo, polvo, destello/flare,
|
||||||
|
gota, copo, hoja, círculo de energía. La pieza se genera AISLADA y centrada sobre fondo
|
||||||
|
NEGRO puro para poder extraer luego el canal alpha por luminancia. Es el builder hermano
|
||||||
|
de `comfyui_build_decal_overlay_workflow` / `comfyui_build_vfx_spritesheet_workflow`:
|
||||||
|
mismo patrón PURO (dict API format) que compone funciones existentes del registry sin
|
||||||
|
reescribir el grafo.
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites UNA textura pequeña y reutilizable para alimentar un sistema de
|
||||||
|
partículas: la chispa que el motor emite a centenares al golpear metal, el puff de humo
|
||||||
|
que sube de una antorcha, el mote de polvo de un ambiente, el destello de un pickup, la
|
||||||
|
gota de una fuente, el copo de una nevada, la hoja de un árbol al viento, el anillo de
|
||||||
|
energía de un hechizo. Generas UNA textura limpia y el motor hace el resto (instancia,
|
||||||
|
mueve, fade). Pasa el MISMO `style` + `checkpoint` + (`lora`) a todas las partículas de
|
||||||
|
un set para que combinen; varía `seed` para sacar variantes del mismo tipo (varias
|
||||||
|
chispas distintas).
|
||||||
|
|
||||||
|
Flujo típico después de generar:
|
||||||
|
|
||||||
|
1. `comfyui_build_particle_texture_workflow("spark", soft=False)` -> dict.
|
||||||
|
2. `comfyui_submit_workflow` -> `comfyui_wait_result` -> `comfyui_fetch_output_image`
|
||||||
|
(PNG de la partícula 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 con
|
||||||
|
additive blend.
|
||||||
|
|
||||||
|
**Elige este builder y NO `comfyui_build_vfx_spritesheet_workflow` cuando** quieres UNA
|
||||||
|
textura estática reutilizable, no una secuencia animada. El spritesheet es la animación
|
||||||
|
completa de un efecto (humo que arde en bucle, N frames vía AnimateDiff); la particle
|
||||||
|
texture es el ladrillo individual que el sistema de partículas replica y anima por su
|
||||||
|
cuenta. **Elige este builder y NO `comfyui_build_decal_overlay_workflow` cuando** lo que
|
||||||
|
quieres es un elemento de partícula (chispa/destello/gota) para emitir, no una mancha de
|
||||||
|
desgaste (sangre/óxido/grieta) para superponer estática sobre una superficie.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from ml.comfyui_build_particle_texture_workflow import comfyui_build_particle_texture_workflow
|
||||||
|
|
||||||
|
# Una chispa nítida sobre fondo negro, lista para submit + luma-to-alpha.
|
||||||
|
wf = comfyui_build_particle_texture_workflow(
|
||||||
|
"spark",
|
||||||
|
soft=False, # chispa = bordes nítidos / alto contraste
|
||||||
|
style="particle texture, glowing ember",
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
# Pipeline completo de una partícula con alpha:
|
||||||
|
# r = comfyui_submit_workflow(wf); comfyui_wait_result(r["prompt_id"])
|
||||||
|
# png = comfyui_fetch_output_image(...) # partícula sobre negro
|
||||||
|
# rgba = comfyui_matting_luma_to_alpha(png, gamma=1.2, black_point=0.04) # luma=alpha
|
||||||
|
# Variantes del mismo tipo: misma particle/style, cambia seed.
|
||||||
|
# for s in range(4):
|
||||||
|
# wf = comfyui_build_particle_texture_workflow("smoke puff", soft=True, seed=s)
|
||||||
|
```
|
||||||
|
|
||||||
|
O lanzable directo con: `./fn run comfyui_build_particle_texture_workflow` (imprime nodos
|
||||||
|
+ class_types del ejemplo).
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Siempre sobre negro, a propósito**: la partícula se genera sobre NEGRO puro para que
|
||||||
|
`comfyui_matting_luma_to_alpha` mapee la luminancia a alpha y conserve el degradado de
|
||||||
|
los bordes (glow, falloff). Es la técnica gamedev correcta para translúcidos: en el
|
||||||
|
motor se compone con additive blend y el solape de partículas brilla de forma natural.
|
||||||
|
Un matting binario (rembg) destruiría ese falloff y dejaría un borde duro recortado.
|
||||||
|
- **NO inyecta Rembg a propósito**: a diferencia de los builders de sprite/prop/item,
|
||||||
|
este NO lleva 'Image Rembg (Remove Background)'. El SaveImage toma directo del VAEDecode
|
||||||
|
(partícula sobre negro) y el matting es un paso posterior de disco (luma-to-alpha).
|
||||||
|
- **`soft` cambia el borde, no el fondo**: `soft=True` -> "soft glow, feathered edges"
|
||||||
|
(humo, destello, gota, círculo de energía); `soft=False` -> "crisp sharp edges, high
|
||||||
|
contrast" (chispa, copo, hoja). El fondo es negro plano en ambos casos. OJO: el `style`
|
||||||
|
por defecto incluye "soft glow"; si pides `soft=False` para algo nítido, pasa también un
|
||||||
|
`style` sin connotación suave (ej. "particle texture, glowing ember") para que no se
|
||||||
|
contradigan.
|
||||||
|
- **luma->alpha penaliza el rojo/azul saturado**: la luminancia Rec601 pesa el rojo a
|
||||||
|
0.299 y el azul a 0.114, así que una partícula muy ROJA o muy AZUL sobre negro sale
|
||||||
|
semi-transparente con los pesos por defecto. Para subir su opacidad, pasa a
|
||||||
|
`comfyui_matting_luma_to_alpha` unos `luma_weights` con más peso a ese canal (ej.
|
||||||
|
(0.6, 0.25, 0.15) para rojo) y sube `gamma`. Ajuste del paso de matting (caller), no del
|
||||||
|
builder. Para partículas BRILLANTES/blancas (chispa, destello, humo claro) los pesos por
|
||||||
|
defecto van perfectos.
|
||||||
|
- **El fondo debe quedar PLANO**: si el modelo mete escena/profundidad/múltiples
|
||||||
|
partículas detrás, el alpha por luminancia recogerá basura. El positivo fuerza "flat
|
||||||
|
solid black backdrop, single element" y el negativo rechaza 'scene, multiple particles,
|
||||||
|
textured background, frame'. Si aún así sale ruido de fondo, sube `cfg` o haz reroll de
|
||||||
|
`seed`.
|
||||||
|
- **Size pequeño por diseño**: 256 por defecto porque la textura se replica a miles; subir
|
||||||
|
resolución no aporta y gasta VRAM. SDXL pide más (subir a 512/768); con dreamshaper_8
|
||||||
|
(SD1.5) 256 va holgado en 8GB lowvram. Si hay OOM, baja `size` o usa SD1.5.
|
||||||
|
- **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`; el
|
||||||
|
alpha lo hace `comfyui_matting_luma_to_alpha`.
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
"""Construye el workflow ComfyUI de UNA textura de particula con alpha (API format).
|
||||||
|
|
||||||
|
Una textura de particula es UN elemento pequeño y reutilizable que alimenta el
|
||||||
|
sistema de particulas del motor (Godot GPUParticles2D / GPUParticles3D, Unity VFX
|
||||||
|
Graph / Shuriken): chispa, humo, polvo, destello/flare, gota, copo, hoja, circulo
|
||||||
|
de energia. El motor instancia MILES de copias de esta unica textura y las anima
|
||||||
|
(spawn, velocidad, fade, color over lifetime). Por eso se quiere UN solo elemento
|
||||||
|
limpio, centrado y aislado, NO una escena ni una secuencia animada.
|
||||||
|
|
||||||
|
La tecnica gamedev correcta para una particula translucida es generarla sobre
|
||||||
|
fondo NEGRO puro y convertir la luminancia en alpha con
|
||||||
|
`comfyui_matting_luma_to_alpha`: brillante -> opaco, negro -> transparente. Eso
|
||||||
|
preserva el degradado de los bordes (glow, falloff) que el additive blend del
|
||||||
|
motor necesita para que el solape de particulas brille de forma natural (un
|
||||||
|
matting binario tipo rembg destruiria ese falloff). Por eso esta textura SIEMPRE
|
||||||
|
se genera sobre negro y el scaffold empuja "isolated on pure black background": el
|
||||||
|
PNG resultante esta pensado para pasar por luma-to-alpha en un paso aparte.
|
||||||
|
|
||||||
|
`soft` controla el caracter del borde: True (defecto) -> "soft glow, soft falloff,
|
||||||
|
feathered edges" (humo, polvo, destello, circulo de energia, gota difusa); False ->
|
||||||
|
"crisp sharp edges, high contrast" (chispa nitida, copo de nieve cristalino, hoja
|
||||||
|
con silueta definida). En ambos casos el fondo sigue siendo negro plano.
|
||||||
|
|
||||||
|
Es el builder hermano de comfyui_build_decal_overlay_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 una
|
||||||
|
particula translucida es luma-to-alpha (post-proceso de disco), no un nodo.
|
||||||
|
|
||||||
|
Diferencia con comfyui_build_vfx_spritesheet_workflow: aquel genera una SECUENCIA
|
||||||
|
animada de N frames de un efecto (humo en bucle, fuego que arde) via AnimateDiff;
|
||||||
|
este genera UNA textura estatica reutilizable que el motor anima a miles. Un
|
||||||
|
spritesheet es la animacion completa; una particle texture es el "ladrillo" que el
|
||||||
|
sistema de particulas replica.
|
||||||
|
|
||||||
|
Cableado:
|
||||||
|
|
||||||
|
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
|
||||||
|
-> CLIPTextEncode (prompt scaffold particula aislada) ...
|
||||||
|
-> VAEDecode -> SaveImage (particula sobre negro)
|
||||||
|
|
||||||
|
Compone:
|
||||||
|
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
|
||||||
|
- comfyui_inject_lora -> LoRA de estilo opcional
|
||||||
|
|
||||||
|
Pipeline despues de generar (no en este builder):
|
||||||
|
comfyui_submit_workflow -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||||
|
-> comfyui_matting_luma_to_alpha -> PNG RGBA listo para el sistema de particulas
|
||||||
|
|
||||||
|
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 textura de particula: UN solo elemento limpio sobre
|
||||||
|
# fondo negro plano, SIN escena, objeto 3D solido, multiples elementos, profundidad
|
||||||
|
# ni marco/borde que delaten una composicion o arruinen el alpha por luminancia.
|
||||||
|
# El fondo DEBE quedar negro plano para que el luma-to-alpha no recoja basura.
|
||||||
|
_PARTICLE_NEGATIVE_COMMON = (
|
||||||
|
"3d render, solid object, scene, landscape, character, person, creature, "
|
||||||
|
"multiple particles, particle system, tiled pattern, grid, collage, "
|
||||||
|
"depth of field, perspective, vignette, frame, border, drop shadow, "
|
||||||
|
"textured background, gray background, white background, busy background, "
|
||||||
|
"background pattern, gradient background, "
|
||||||
|
"text, watermark, signature, logo, blurry, low quality, jpeg artifacts"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_build_particle_texture_workflow(
|
||||||
|
particle: str,
|
||||||
|
*,
|
||||||
|
soft: bool = True,
|
||||||
|
style: str = "particle texture, soft glow",
|
||||||
|
checkpoint: str = "dreamshaper_8.safetensors",
|
||||||
|
size: int = 256,
|
||||||
|
seed: int = 0,
|
||||||
|
lora: str | None = None,
|
||||||
|
lora_strength: float = 1.0,
|
||||||
|
negative: str | None = None,
|
||||||
|
steps: int = 24,
|
||||||
|
cfg: float = 7.0,
|
||||||
|
sampler_name: str = "dpmpp_2m",
|
||||||
|
scheduler: str = "karras",
|
||||||
|
filename_prefix: str = "particle_texture",
|
||||||
|
) -> dict:
|
||||||
|
"""Construye el dict (API format) del workflow de UNA textura de particula.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
particle: descripcion del elemento de particula (ej. "spark", "smoke
|
||||||
|
puff", "dust mote", "glowing flare", "water droplet", "snowflake",
|
||||||
|
"falling leaf", "energy ring"). Se inserta en un prompt scaffold de
|
||||||
|
particula aislada sobre negro. No puede estar vacio.
|
||||||
|
soft: si True (defecto) la particula se genera con bordes suaves y glow
|
||||||
|
difuso ("soft glow, soft falloff, feathered edges": humo, polvo,
|
||||||
|
destello, circulo de energia, gota); si False con bordes nitidos y
|
||||||
|
alto contraste ("crisp sharp edges, high contrast": chispa, copo de
|
||||||
|
nieve, hoja con silueta). En ambos casos el fondo es negro plano.
|
||||||
|
keyword-only.
|
||||||
|
style: descriptor de estilo de la particula (ej. "particle texture, soft
|
||||||
|
glow", "stylized vfx, painterly", "photoreal smoke", "anime sparkle").
|
||||||
|
Pasa el MISMO style + checkpoint + (lora) a todas las particulas 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 512/768). keyword-only.
|
||||||
|
size: lado del cuadrado en px (width = height = size). 256 por defecto:
|
||||||
|
una textura de particula es pequeña y se replica a miles, no necesita
|
||||||
|
resolucion alta. keyword-only.
|
||||||
|
seed: semilla del KSampler. Misma seed + mismos particle/style -> misma
|
||||||
|
textura; variar seed da variantes del mismo tipo de particula.
|
||||||
|
keyword-only.
|
||||||
|
lora: LoRA de estilo opcional en models/loras. 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
|
||||||
|
una particula aislada (un solo elemento sobre negro plano, sin escena
|
||||||
|
ni multiples elementos ni fondo texturizado). 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 particula ('{particle} particle, {style},
|
||||||
|
isolated on pure black background, <soft|sharp> edges, single element, for
|
||||||
|
game particle system, ...') + LoRA de estilo opcional. NO lleva Rembg: el
|
||||||
|
PNG resultante se convierte a RGBA con comfyui_matting_luma_to_alpha
|
||||||
|
(luma=alpha) en un paso posterior. UNA particula; variar seed da variantes.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si particle esta vacio.
|
||||||
|
"""
|
||||||
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||||
|
|
||||||
|
if not particle or not particle.strip():
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_particle_texture_workflow: 'particle' no puede estar vacio"
|
||||||
|
)
|
||||||
|
|
||||||
|
particle = particle.strip()
|
||||||
|
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||||
|
neg = _PARTICLE_NEGATIVE_COMMON if negative is None else negative
|
||||||
|
|
||||||
|
# Caracter del borde segun soft: suave/glow difuso para translucidos (humo,
|
||||||
|
# destello, circulo de energia) o nitido/cristalino para particulas con
|
||||||
|
# silueta (chispa, copo). En ambos casos el fondo permanece negro plano.
|
||||||
|
edge = (
|
||||||
|
"soft edges, soft glow, gentle falloff, feathered"
|
||||||
|
if soft
|
||||||
|
else "crisp sharp edges, high contrast, well defined silhouette"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prompt scaffold de particula aislada: UN solo elemento, centrado, sobre fondo
|
||||||
|
# NEGRO puro y plano (insumo de luma-to-alpha despues), listo como ladrillo del
|
||||||
|
# sistema de particulas del motor. El fondo se refuerza con "solid/flat" para
|
||||||
|
# que no salga texturizado y el alpha por luminancia no recoja basura.
|
||||||
|
positive = (
|
||||||
|
f"{particle} particle, {style}, isolated on pure black background, "
|
||||||
|
f"{edge}, single element, centered, for game particle system, "
|
||||||
|
"flat solid black backdrop, vfx sprite, glowing, 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
|
||||||
|
)
|
||||||
|
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
wf = comfyui_build_particle_texture_workflow(
|
||||||
|
"spark",
|
||||||
|
soft=False,
|
||||||
|
style="particle texture, glowing ember",
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"nodes": list(wf),
|
||||||
|
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Tests offline de comfyui_build_particle_texture_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_particle_texture_workflow import ( # noqa: E402
|
||||||
|
comfyui_build_particle_texture_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 _neg(wf):
|
||||||
|
return next(
|
||||||
|
n["inputs"]["text"]
|
||||||
|
for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and "3d render" in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_particle_on_black_recipe():
|
||||||
|
wf = comfyui_build_particle_texture_workflow("spark", soft=False, seed=7)
|
||||||
|
cls = _classes(wf)
|
||||||
|
# Cadena base txt2img pura: NO lleva Rembg (el matting es luma-to-alpha aparte,
|
||||||
|
# para preservar el falloff translucido que el additive blend del motor usa).
|
||||||
|
assert "CheckpointLoaderSimple" in cls
|
||||||
|
assert "KSampler" in cls
|
||||||
|
assert "VAEDecode" in cls
|
||||||
|
assert "SaveImage" in cls
|
||||||
|
assert "Image Rembg (Remove Background)" not in cls
|
||||||
|
# La particula + el aislamiento + el fondo negro aparecen en el prompt positivo.
|
||||||
|
pos = _pos_with(wf, "spark")
|
||||||
|
txt = pos["inputs"]["text"]
|
||||||
|
assert "isolated on pure black background" in txt
|
||||||
|
assert "single element" in txt
|
||||||
|
assert "game particle system" 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_soft_toggles_edge_character():
|
||||||
|
# soft=True -> bordes suaves / glow difuso (humo, destello, gota).
|
||||||
|
wf_soft = comfyui_build_particle_texture_workflow("smoke puff", soft=True)
|
||||||
|
pos_soft = _pos_with(wf_soft, "smoke puff")["inputs"]["text"]
|
||||||
|
assert "soft edges" in pos_soft
|
||||||
|
assert "soft glow" in pos_soft
|
||||||
|
assert "feathered" in pos_soft
|
||||||
|
assert "crisp sharp edges" not in pos_soft
|
||||||
|
# soft=False -> bordes nitidos / alto contraste (chispa, copo, hoja).
|
||||||
|
# style neutro para aislar el segmento de borde (el style default trae "soft glow").
|
||||||
|
wf_hard = comfyui_build_particle_texture_workflow(
|
||||||
|
"snowflake", soft=False, style="particle texture"
|
||||||
|
)
|
||||||
|
pos_hard = _pos_with(wf_hard, "snowflake")["inputs"]["text"]
|
||||||
|
assert "crisp sharp edges" in pos_hard
|
||||||
|
assert "high contrast" in pos_hard
|
||||||
|
assert "feathered" not in pos_hard # el segmento de borde suave no aparece
|
||||||
|
# En ambos casos el fondo es negro plano (insumo de luma->alpha).
|
||||||
|
assert "pure black background" in pos_soft
|
||||||
|
assert "pure black background" in pos_hard
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_particle_reflected():
|
||||||
|
for p in ["dust mote", "glowing flare", "water droplet", "energy ring", "falling leaf"]:
|
||||||
|
wf = comfyui_build_particle_texture_workflow(p)
|
||||||
|
pos = _pos_with(wf, p)
|
||||||
|
assert p in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_style_in_prompt():
|
||||||
|
wf = comfyui_build_particle_texture_workflow(
|
||||||
|
"glowing flare", style="anime sparkle, vibrant"
|
||||||
|
)
|
||||||
|
pos = _pos_with(wf, "glowing flare")
|
||||||
|
assert "anime sparkle, vibrant" in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_size_reflected_and_small_default():
|
||||||
|
# Tamaño explicito se refleja en el latente cuadrado.
|
||||||
|
wf = comfyui_build_particle_texture_workflow("spark", size=512)
|
||||||
|
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent["width"] == 512
|
||||||
|
assert latent["height"] == 512 # cuadrado
|
||||||
|
# Por defecto la textura de particula es pequeña (256): se replica a miles.
|
||||||
|
wf_def = comfyui_build_particle_texture_workflow("spark")
|
||||||
|
latent_def = _by_class(wf_def, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent_def["width"] == 256
|
||||||
|
assert latent_def["height"] == 256
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_negative_isolates_single_particle():
|
||||||
|
# El negativo por defecto rechaza escena/objeto solido/multiples particulas/marco
|
||||||
|
# y fondo texturizado: queremos UN solo elemento limpio sobre negro plano.
|
||||||
|
wf = comfyui_build_particle_texture_workflow("dust mote")
|
||||||
|
neg = _neg(wf)
|
||||||
|
assert "3d render" in neg
|
||||||
|
assert "scene" in neg
|
||||||
|
assert "multiple particles" in neg
|
||||||
|
assert "vignette" in neg
|
||||||
|
assert "frame" in neg
|
||||||
|
assert "textured background" in neg # fondo negro plano forzado
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_lora_reflected():
|
||||||
|
wf = comfyui_build_particle_texture_workflow(
|
||||||
|
"glowing flare", lora="vfx_sd15.safetensors", lora_strength=0.8
|
||||||
|
)
|
||||||
|
loras = _by_class(wf, "LoraLoader")
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]["inputs"]["lora_name"] == "vfx_sd15.safetensors"
|
||||||
|
assert loras[0]["inputs"]["strength_model"] == 0.8
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_empty_particle():
|
||||||
|
try:
|
||||||
|
comfyui_build_particle_texture_workflow(" ")
|
||||||
|
assert False
|
||||||
|
except ValueError as e:
|
||||||
|
assert "particle" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determinism():
|
||||||
|
a = comfyui_build_particle_texture_workflow(
|
||||||
|
"spark", soft=False, lora="vfx_sd15.safetensors", seed=7
|
||||||
|
)
|
||||||
|
b = comfyui_build_particle_texture_workflow(
|
||||||
|
"spark", soft=False, lora="vfx_sd15.safetensors", seed=7
|
||||||
|
)
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user