feat(gamedev): comfyui_build_directional_sprite_workflow — sprite multi-direccional 2.5D (SV3D turntable / Zero123)

Builder puro (dict API format) que a partir del sprite frontal de un personaje
construye el workflow ComfyUI de N vistas direccionales consistentes (8-way
N/NE/E/SE/S/SW/W/NW o 4-way) rotando la figura en 3D. SV3D (orbit turntable) por
defecto, Stable Zero123 (batch por azimuth) como fallback de menor VRAM. Es el
puente 2.5D del catalogo gamedev-2d: consistencia rotacional real (el mismo
modelo rotado) frente a sprite_sheet (OpenPose 2D re-poza, identidad inconsistente).

Helper directional_sprite_view_order(directions) mapea frame i -> direccion i.
Funcion pura: solo construye el grafo; coste GPU al enviar con comfyui_submit_workflow.

Probado e2e en GPU: goblin enemy_creature_00001_ -> SV3D 8 direcciones elevation 15,
8 frames 576x576 en 75s, pico 7145/8192 MiB (prompt_id 8b9f75de). Consistencia
rotacional medida: MAE adyacentes 27 < frente-espalda 29.6, spread de paleta 3.83.

Report: reports/0187.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 07:36:38 +02:00
parent d08667df9b
commit 2bab120d7c
3 changed files with 457 additions and 3 deletions
+13 -3
View File
@@ -109,6 +109,12 @@ ruido), estos parten de una **imagen de entrada** y la transforman. Cuatro sub-e
varios lados; el nodo `ImagePadForOutpaint` extiende el canvas **y genera** la máscara feathered de varios lados; el nodo `ImagePadForOutpaint` extiende el canvas **y genera** la máscara feathered de
la franja nueva (no la recibe el caller), y el sampler genera ahí contenido coherente. Cambia el la franja nueva (no la recibe el caller), y el sampler genera ahí contenido coherente. Cambia el
**tamaño** del asset (recortar/extender un fondo o parallax a otra resolución/aspect), no lo de dentro. **tamaño** del asset (recortar/extender un fondo o parallax a otra resolución/aspect), no lo de dentro.
- **multi-vista 3D / 2.5D** (`directional_sprite`): parte del sprite **frontal** de un personaje y lo
**rota en 3D** (SV3D turntable u Stable Zero123 órbita) para producir N vistas direccionales del MISMO
personaje (8-way N/NE/E/SE/S/SW/W/NW o 4-way). A diferencia de `sprite_sheet` (re-poza con OpenPose 2D,
re-dibuja la silueta → identidad inconsistente entre ángulos), aquí la difusión 3D gira la figura sobre
su eje, así casco/arma/paleta son los mismos en cada dirección (**consistencia rotacional**). Cambia el
**ángulo de cámara**, no la pose ni el material.
Cubren el eje que el critic de generación (`reports/0178`) no exploró: derivar de un asset o Cubren el eje que el critic de generación (`reports/0178`) no exploró: derivar de un asset o
del dibujo del dev, no inventar un tipo nuevo desde texto. del dibujo del dev, no inventar un tipo nuevo desde texto.
@@ -118,6 +124,7 @@ del dibujo del dev, no inventar un tipo nuevo desde texto.
| `comfyui_build_asset_variant_workflow_py_ml` | `(input_image, variant, *, checkpoint="dreamshaper_8…", denoise=0.5, style="game asset", size=512, seed=0, lora=None, …) -> dict` | UNA **variante coherente de un asset 2D ya generado** (img2img): parte del sprite/icono que existe en `input_image` y produce su versión de **otro material/paleta/tier/estado** (`ice element`, `fire element`, `battle-damaged`, `golden tier 2`, `corrupted`) manteniendo **silueta, pose y composición** del original. Compone `comfyui_build_img2img_workflow` (LoadImage → VAEEncode → KSampler con `denoise`) + `comfyui_inject_lora` (estilo opcional) + `ImageScale` opcional (`size` normaliza la base a size×size; `size=None` preserva las dimensiones exactas sin deformar). El prompt es `{variant}, {style}, same composition, same pose, same silhouette, …`. **`denoise` es la palanca**: ~0.3 invisible, **0.45-0.6 recomendado** (cambia material/paleta, conserva forma), ~0.8 deriva la pose y se acerca a txt2img. Set de variantes del MISMO asset = mismo `input_image`/`style`/`seed`, varía solo `variant`. **DISTINTO de los builders txt2img** (`enemy_creature`, `item_icon`…): esos generan un tipo desde cero; éste transforma uno concreto. **NO inyecta Rembg** (img2img preserva el fondo/alpha del original según la base). ⚠️ la imagen base debe existir en `input/` del server (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); asset NO cuadrado + `size` fijo + `crop="disabled"` deforma → `size=None` o `crop="center"`. Probado e2e en GPU con SD1.5 — variante `ice element, frozen` del goblin `enemy_creature_00001_.png` denoise 0.5 seed 7 512×512 (`prompt_id 5e4a5d3d`): silueta conservada (luminance corr 0.63) + paleta a frío (blueness BR 1.6→+1.9), `reports/0181`. SD1.5. | | `comfyui_build_asset_variant_workflow_py_ml` | `(input_image, variant, *, checkpoint="dreamshaper_8…", denoise=0.5, style="game asset", size=512, seed=0, lora=None, …) -> dict` | UNA **variante coherente de un asset 2D ya generado** (img2img): parte del sprite/icono que existe en `input_image` y produce su versión de **otro material/paleta/tier/estado** (`ice element`, `fire element`, `battle-damaged`, `golden tier 2`, `corrupted`) manteniendo **silueta, pose y composición** del original. Compone `comfyui_build_img2img_workflow` (LoadImage → VAEEncode → KSampler con `denoise`) + `comfyui_inject_lora` (estilo opcional) + `ImageScale` opcional (`size` normaliza la base a size×size; `size=None` preserva las dimensiones exactas sin deformar). El prompt es `{variant}, {style}, same composition, same pose, same silhouette, …`. **`denoise` es la palanca**: ~0.3 invisible, **0.45-0.6 recomendado** (cambia material/paleta, conserva forma), ~0.8 deriva la pose y se acerca a txt2img. Set de variantes del MISMO asset = mismo `input_image`/`style`/`seed`, varía solo `variant`. **DISTINTO de los builders txt2img** (`enemy_creature`, `item_icon`…): esos generan un tipo desde cero; éste transforma uno concreto. **NO inyecta Rembg** (img2img preserva el fondo/alpha del original según la base). ⚠️ la imagen base debe existir en `input/` del server (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); asset NO cuadrado + `size` fijo + `crop="disabled"` deforma → `size=None` o `crop="center"`. Probado e2e en GPU con SD1.5 — variante `ice element, frozen` del goblin `enemy_creature_00001_.png` denoise 0.5 seed 7 512×512 (`prompt_id 5e4a5d3d`): silueta conservada (luminance corr 0.63) + paleta a frío (blueness BR 1.6→+1.9), `reports/0181`. SD1.5. |
| `comfyui_build_sprite_from_sketch_workflow_py_ml` | `(sketch_image, subject, *, control_type="lineart", checkpoint="dreamshaper_8…", style="game asset, clean, centered", strength=0.8, size=512, seed=0, lora=None, preprocess=True, controlnet_name=None, …) -> dict` | UN **sprite pintado a partir del BOCETO del dev**, guiado por **ControlNet** (sub-eje sketch→ControlNet, **NO img2img**). Recibe el dibujo tosco que existe en `sketch_image` (boceto/lineart/garabato) + `subject` (qué es), y genera un sprite en estilo de juego que **conserva la forma dibujada**: el dev marca la silueta, la IA pone material/color/acabado. Mecanismo: `txt2img` base (ruido, `EmptyLatentImage`, `denoise 1.0`) cuyo positivo pasa por `ControlNetApply` atado al mapa de líneas del boceto. `control_type` elige el **preprocesador** (`LineArtPreprocessor` / `ScribblePreprocessor` / `CannyEdgePreprocessor`, interpuesto entre el boceto y el ControlNet por un helper) y, por defecto, el **modelo CN emparejado**. Compone `comfyui_build_txt2img_workflow` + `comfyui_inject_controlnet` + `comfyui_inject_lora` (estilo opcional). **`strength` es la palanca**: 0 = ignora el dibujo (txt2img puro), ~0.8 recomendado (respeta forma dejando limpiar a la IA), 1.0 = se ciñe estricto. **DISTINTO de `asset_variant`** (img2img conserva forma+color de una imagen ya pintada) y de los txt2img (`enemy_creature`…, inventan la forma desde texto): éste conserva **solo la forma** del dibujo. ⚠️ el boceto debe existir en `input/` (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); `preprocess=False` solo si el sketch ya es un lineart limpio. **GOTCHA del server 8GB: solo `canny`/`depth`/`openpose` SD1.5 instalados** — para `lineart`/`scribble` pasa `controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` u usa `control_type="canny"` (out-of-the-box); pendiente humano descargar `control_v11p_sd15_lineart_fp16`/`scribble`. Probado e2e en GPU con SD1.5 — boceto del goblin `enemy_creature_00001_.png``CannyEdgePreprocessor` → ControlNet canny, `subject="dark fantasy goblin warrior"` strength 0.85 seed 123 512×512 (`prompt_id ea6fc372`): pose/orejas/hombrera/lanza dentada/espada del dibujo conservadas, repintado en estilo de juego, `reports/0182`. SD1.5. | | `comfyui_build_sprite_from_sketch_workflow_py_ml` | `(sketch_image, subject, *, control_type="lineart", checkpoint="dreamshaper_8…", style="game asset, clean, centered", strength=0.8, size=512, seed=0, lora=None, preprocess=True, controlnet_name=None, …) -> dict` | UN **sprite pintado a partir del BOCETO del dev**, guiado por **ControlNet** (sub-eje sketch→ControlNet, **NO img2img**). Recibe el dibujo tosco que existe en `sketch_image` (boceto/lineart/garabato) + `subject` (qué es), y genera un sprite en estilo de juego que **conserva la forma dibujada**: el dev marca la silueta, la IA pone material/color/acabado. Mecanismo: `txt2img` base (ruido, `EmptyLatentImage`, `denoise 1.0`) cuyo positivo pasa por `ControlNetApply` atado al mapa de líneas del boceto. `control_type` elige el **preprocesador** (`LineArtPreprocessor` / `ScribblePreprocessor` / `CannyEdgePreprocessor`, interpuesto entre el boceto y el ControlNet por un helper) y, por defecto, el **modelo CN emparejado**. Compone `comfyui_build_txt2img_workflow` + `comfyui_inject_controlnet` + `comfyui_inject_lora` (estilo opcional). **`strength` es la palanca**: 0 = ignora el dibujo (txt2img puro), ~0.8 recomendado (respeta forma dejando limpiar a la IA), 1.0 = se ciñe estricto. **DISTINTO de `asset_variant`** (img2img conserva forma+color de una imagen ya pintada) y de los txt2img (`enemy_creature`…, inventan la forma desde texto): éste conserva **solo la forma** del dibujo. ⚠️ el boceto debe existir en `input/` (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); `preprocess=False` solo si el sketch ya es un lineart limpio. **GOTCHA del server 8GB: solo `canny`/`depth`/`openpose` SD1.5 instalados** — para `lineart`/`scribble` pasa `controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` u usa `control_type="canny"` (out-of-the-box); pendiente humano descargar `control_v11p_sd15_lineart_fp16`/`scribble`. Probado e2e en GPU con SD1.5 — boceto del goblin `enemy_creature_00001_.png``CannyEdgePreprocessor` → ControlNet canny, `subject="dark fantasy goblin warrior"` strength 0.85 seed 123 512×512 (`prompt_id ea6fc372`): pose/orejas/hombrera/lanza dentada/espada del dibujo conservadas, repintado en estilo de juego, `reports/0182`. SD1.5. |
| `comfyui_build_inpaint_asset_workflow_py_ml` | `(input_image, mask_image, prompt, *, checkpoint="dreamshaper_8…", denoise=1.0, style="game asset", grow_mask=6, size=None, seed=0, lora=None, mode="vae_encode", …) -> dict` | EDITA **solo una región** de un asset 2D ya pintado (**inpaint**, sub-eje propio). Recibe el asset en `input_image` + una **máscara** `mask_image` (BLANCO = editar, NEGRO = conservar) + `prompt` de qué poner ahí, y repinta **únicamente** la zona enmascarada dejando el resto del sprite intacto (cambiar/añadir un arma, quitar un casco, poner un escudo, reparar una zona dañada). Mecanismo (`mode="vae_encode"`): `VAEEncodeForInpaint` codifica el latente respetando la máscara y dilata su borde `grow_mask` px para difuminar la costura; `KSampler` (`denoise` alto) regenera solo esa región con `{prompt}, {style}, seamless blend…`. Compone `comfyui_build_inpaint_workflow` (base) + `comfyui_inject_lora` (estilo opcional); `size` escala imagen **Y** máscara de forma consistente (escalar solo una las desalinea). **`grow_mask` es la palanca de costura** (6-10 px difumina el borde lo/nuevo); `denoise` 1.0 reescribe entero, ~0.5-0.7 repara suave. **DISTINTO de `asset_variant`** (img2img reescribe TODO el asset) y de `sprite_from_sketch` (ControlNet parte de un dibujo de líneas para un sprite nuevo): éste edita **un trozo** delimitado por la máscara. **ERROR-PATH**: si el server no expone `VAEEncodeForInpaint`, pasar `mode="noise_mask"` → degrada a `VAEEncode` + `SetLatentNoiseMask` (+ `GrowMask`); `mask_image` vacío lanza `ValueError`. ⚠️ asset y máscara deben existir en `input/` (subir con `POST /upload/image`) y compartir resolución (o usar `size`); `ImageScale` aquí NO ofrece `lanczos` (válidos `bilinear`/`nearest-exact`/`area`/`bicubic`); pura, no valida. Probado e2e en GPU con SD1.5 — máscara circular (R70) sobre la mano del goblin `enemy_creature_00001_.png`, `prompt="a glowing blue magic orb"` grow_mask 8 denoise 1.0 seed 7 (`prompt_id 88b52c66`): orbe azul en la región, **resto idéntico** (diff medio dentro 40.3 vs fuera 1.97 → ratio 20.4×; 44.6% px cambiados dentro vs 1.7% fuera), `reports/0183`. SD1.5. | | `comfyui_build_inpaint_asset_workflow_py_ml` | `(input_image, mask_image, prompt, *, checkpoint="dreamshaper_8…", denoise=1.0, style="game asset", grow_mask=6, size=None, seed=0, lora=None, mode="vae_encode", …) -> dict` | EDITA **solo una región** de un asset 2D ya pintado (**inpaint**, sub-eje propio). Recibe el asset en `input_image` + una **máscara** `mask_image` (BLANCO = editar, NEGRO = conservar) + `prompt` de qué poner ahí, y repinta **únicamente** la zona enmascarada dejando el resto del sprite intacto (cambiar/añadir un arma, quitar un casco, poner un escudo, reparar una zona dañada). Mecanismo (`mode="vae_encode"`): `VAEEncodeForInpaint` codifica el latente respetando la máscara y dilata su borde `grow_mask` px para difuminar la costura; `KSampler` (`denoise` alto) regenera solo esa región con `{prompt}, {style}, seamless blend…`. Compone `comfyui_build_inpaint_workflow` (base) + `comfyui_inject_lora` (estilo opcional); `size` escala imagen **Y** máscara de forma consistente (escalar solo una las desalinea). **`grow_mask` es la palanca de costura** (6-10 px difumina el borde lo/nuevo); `denoise` 1.0 reescribe entero, ~0.5-0.7 repara suave. **DISTINTO de `asset_variant`** (img2img reescribe TODO el asset) y de `sprite_from_sketch` (ControlNet parte de un dibujo de líneas para un sprite nuevo): éste edita **un trozo** delimitado por la máscara. **ERROR-PATH**: si el server no expone `VAEEncodeForInpaint`, pasar `mode="noise_mask"` → degrada a `VAEEncode` + `SetLatentNoiseMask` (+ `GrowMask`); `mask_image` vacío lanza `ValueError`. ⚠️ asset y máscara deben existir en `input/` (subir con `POST /upload/image`) y compartir resolución (o usar `size`); `ImageScale` aquí NO ofrece `lanczos` (válidos `bilinear`/`nearest-exact`/`area`/`bicubic`); pura, no valida. Probado e2e en GPU con SD1.5 — máscara circular (R70) sobre la mano del goblin `enemy_creature_00001_.png`, `prompt="a glowing blue magic orb"` grow_mask 8 denoise 1.0 seed 7 (`prompt_id 88b52c66`): orbe azul en la región, **resto idéntico** (diff medio dentro 40.3 vs fuera 1.97 → ratio 20.4×; 44.6% px cambiados dentro vs 1.7% fuera), `reports/0183`. SD1.5. |
| `comfyui_build_directional_sprite_workflow_py_ml` | `(input_image, *, directions=8, model="sv3d", elevation=0.0, size=None, orbit_frames=None, seed=0, ckpt=None, …) -> dict` | UN **sprite MULTI-DIRECCIONAL** del MISMO personaje rotado en 3D (**multi-vista 2.5D**, sub-eje propio): parte de la imagen **frontal** del personaje (fondo limpio, en `input/`) y construye el workflow que genera N vistas direccionales CONSISTENTES (8-way N/NE/E/SE/S/SW/W/NW o 4-way) para top-down/iso/shooter 8-way. `model="sv3d"` (default) = `SV3D_Conditioning` produce un **orbit turntable** de N frames equiespaciados en 360° en una pasada (mejor consistencia, `sv3d_p.safetensors`, nativo 576²); `model="zero123"` = `StableZero123_Conditioning_Batched` da un **batch** de N vistas por azimuth (fallback menor VRAM, `stable_zero123.ckpt`, nativo 256²). `elevation` (~15-30) da picado para cámara cenital; `orbit_frames` (SOLO sv3d) densifica el orbit (21 nativo) para submuestrear; el módulo expone `directional_sprite_view_order(directions)` (frame i = dirección i). **DISTINTO de `sprite_sheet`** (OpenPose 2D re-poza la silueta → identidad inconsistente): aquí la difusión 3D ROTA la figura sobre su eje → casco/arma/paleta idénticos en cada dirección (rotación 3D real, no re-dibujo). Construye, NO genera (el coste GPU es el `submit`); **pura, no valida** (la imagen frontal debe existir en `input/`). Hermana **pura** de `comfyui_generate_views_from_image` (orquestador impuro para recon 3D, 4 cardinales). ⚠️ VRAM RTX 3070 8GB: SV3D es modelo de vídeo, pesa — 8 frames@576² → pico **7145 MiB**; limpiar GPU antes (`POST /free`); OOM → baja `size`/`directions` o cae a zero123, NO matar procesos; `comfyui_wait_result` lanza `TimeoutError` pero el job completa (sondear `/history`). Probado e2e en GPU con SV3D — goblin `enemy_creature_00001_.png` (compuesto sobre blanco 576²) → 8 direcciones elevation 15 seed 7, **8 frames** 576² en 75 s, consistencia rotacional medida (MAE adyacentes 27 < frente↔espalda 29.6, spread de paleta 3.83 = mismo personaje en las 8 vistas; `prompt_id 8b9f75de`, `reports/0187`). SV3D/Zero123. |
| `comfyui_build_outpaint_asset_workflow_py_ml` | `(input_image, prompt, *, left=0, right=0, top=0, bottom=0, feather=40, checkpoint="dreamshaper_8…", denoise=1.0, style="game background", grow_mask=0, seed=0, lora=None, …) -> dict` | EXTIENDE **el lienzo** de un asset 2D ya pintado (**outpaint**, sub-eje propio). Recibe el asset en `input_image` + cuánto extender por cada lado (`left`/`right`/`top`/`bottom` px) + `prompt` de qué generar fuera de los bordes, y **agranda el canvas** generando contenido coherente con el original más allá de sus bordes (recortar/extender un fondo, parallax, card_art o splash a otra resolución/aspect ratio). Mecanismo: el nodo nativo `ImagePadForOutpaint` amplía el lienzo y **EMITE** a la vez la imagen extendida **y** la máscara feathered de la franja nueva (la genera el grafo, **NO** la recibe el caller); `VAEEncodeForInpaint` codifica respetando esa máscara y `KSampler` (`denoise` alto) genera lo nuevo con `{prompt}, {style}, seamless extension…`. Compone `comfyui_build_inpaint_workflow` (base; su `LoadImageMask` se elimina y `VAEEncodeForInpaint` se reconecta a las dos salidas del pad) + `comfyui_inject_lora` (estilo opcional). **`feather` difumina la costura** (40 px por defecto, no debe pasarse de la extensión); `grow_mask` (0 por defecto) dilata adicionalmente el borde si aparece costura dura. **DISTINTO de `inpaint_asset`**: éste **no recibe máscara** (la genera el pad) y cambia el **tamaño** del asset extendiendo hacia fuera, mientras inpaint edita una región **interior** con máscara externa del mismo tamaño. **ERROR-PATH**: `input_image`/`prompt` vacíos o las cuatro extensiones en 0 tras redondear (`left=3`→0) lanzan `ValueError`; si el server no expone `ImagePadForOutpaint`, consultar `/object_info`. ⚠️ el asset debe existir en `input/` (subir con `POST /upload/image`); las extensiones se redondean a múltiplo de 8 (`250→248`); pura, no valida. Probado e2e en GPU con SD1.5 — fondo `seamless_00004_.png` 512×512 extendido `right=256` feather 40 denoise 1.0 seed 7 (`prompt_id aa33de05`): canvas **512→768×512** (+256), original conservado (diff medio 7.2 lejos del borde) + franja nueva con contenido coherente (std 28.9, dist de paleta 28.6), `reports/0185`. SD1.5. | | `comfyui_build_outpaint_asset_workflow_py_ml` | `(input_image, prompt, *, left=0, right=0, top=0, bottom=0, feather=40, checkpoint="dreamshaper_8…", denoise=1.0, style="game background", grow_mask=0, seed=0, lora=None, …) -> dict` | EXTIENDE **el lienzo** de un asset 2D ya pintado (**outpaint**, sub-eje propio). Recibe el asset en `input_image` + cuánto extender por cada lado (`left`/`right`/`top`/`bottom` px) + `prompt` de qué generar fuera de los bordes, y **agranda el canvas** generando contenido coherente con el original más allá de sus bordes (recortar/extender un fondo, parallax, card_art o splash a otra resolución/aspect ratio). Mecanismo: el nodo nativo `ImagePadForOutpaint` amplía el lienzo y **EMITE** a la vez la imagen extendida **y** la máscara feathered de la franja nueva (la genera el grafo, **NO** la recibe el caller); `VAEEncodeForInpaint` codifica respetando esa máscara y `KSampler` (`denoise` alto) genera lo nuevo con `{prompt}, {style}, seamless extension…`. Compone `comfyui_build_inpaint_workflow` (base; su `LoadImageMask` se elimina y `VAEEncodeForInpaint` se reconecta a las dos salidas del pad) + `comfyui_inject_lora` (estilo opcional). **`feather` difumina la costura** (40 px por defecto, no debe pasarse de la extensión); `grow_mask` (0 por defecto) dilata adicionalmente el borde si aparece costura dura. **DISTINTO de `inpaint_asset`**: éste **no recibe máscara** (la genera el pad) y cambia el **tamaño** del asset extendiendo hacia fuera, mientras inpaint edita una región **interior** con máscara externa del mismo tamaño. **ERROR-PATH**: `input_image`/`prompt` vacíos o las cuatro extensiones en 0 tras redondear (`left=3`→0) lanzan `ValueError`; si el server no expone `ImagePadForOutpaint`, consultar `/object_info`. ⚠️ el asset debe existir en `input/` (subir con `POST /upload/image`); las extensiones se redondean a múltiplo de 8 (`250→248`); pura, no valida. Probado e2e en GPU con SD1.5 — fondo `seamless_00004_.png` 512×512 extendido `right=256` feather 40 denoise 1.0 seed 7 (`prompt_id aa33de05`): canvas **512→768×512** (+256), original conservado (diff medio 7.2 lejos del borde) + franja nueva con contenido coherente (std 28.9, dist de paleta 28.6), `reports/0185`. SD1.5. |
## Funciones de post-proceso y puente (`gamedev-2d`, CPU) ## Funciones de post-proceso y puente (`gamedev-2d`, CPU)
@@ -201,9 +208,12 @@ comfyui_export_asset_to_godot(rgba["out_path"], "vfx", PROJ)
un pack entero compartiendo checkpoint/style/lora/seed (issue 0087, ver tabla de un pack entero compartiendo checkpoint/style/lora/seed (issue 0087, ver tabla de
pipelines arriba). One-shots por-asset individuales (pixelart/sprite/VFX) siguen pipelines arriba). One-shots por-asset individuales (pixelart/sprite/VFX) siguen
encadenándose a mano; candidatos a promoción cuando el patrón se repita. encadenándose a mano; candidatos a promoción cuando el patrón se repita.
- **Sprite turnaround multi-vista** (orquestar N poses con identidad fija + juez): - **Sprite turnaround multi-vista** (N direcciones del mismo personaje con identidad fija):
el builder `comfyui_build_sprite_sheet_workflow` produce UN frame; la orquestación cubierto por `comfyui_build_directional_sprite_workflow` (rotación 3D SV3D/Zero123,
multi-pose es pipeline pendiente (plan `reports/0137` T2). consistencia rotacional medida — `reports/0187`). Lo que sigue **pendiente** es la
orquestación multi-POSE 2D con juez (re-pozar un personaje en N acciones manteniendo
identidad, distinto de rotarlo): `comfyui_build_sprite_sheet_workflow` produce UN frame;
el pipeline multi-pose con juez sigue pendiente (plan `reports/0137` T2).
- **Paletas lospec por red** (`load_lospec_palette`): no incluido. `pixelize` usa - **Paletas lospec por red** (`load_lospec_palette`): no incluido. `pixelize` usa
paletas fijas embebidas (game-boy/pico-8/nes) o lista de hex, sin HTTP. paletas fijas embebidas (game-boy/pico-8/nes) o lista de hex, sin HTTP.
- **TileSet / SpriteFrames `.tres`**: Godot no los deriva solos; `export_asset_to_godot` - **TileSet / SpriteFrames `.tres`**: Godot no los deriva solos; `export_asset_to_godot`
@@ -0,0 +1,134 @@
---
name: comfyui_build_directional_sprite_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_directional_sprite_workflow(input_image: str, *, directions: int = 8, model: str = \"sv3d\", elevation: float = 0.0, size: int | None = None, orbit_frames: int | None = None, seed: int = 0, ckpt: str | None = None, steps: int = 20, cfg: float | None = None, min_cfg: float = 1.0, sampler_name: str = \"euler\", scheduler: str = \"karras\", filename_prefix: str = \"directional_sprite\") -> dict"
description: "Construye el dict (API format) del workflow ComfyUI de un sprite MULTI-DIRECCIONAL: a partir de UNA imagen frontal de un personaje (fondo limpio) genera N vistas direccionales CONSISTENTES (el mismo personaje rotado en 3D, no re-dibujado) para un juego top-down / isométrico / shooter 8-way. Dos modelos 3D nativos del server: SV3D (orbit turntable en una pasada, mejor consistencia, sv3d_p.safetensors) o Stable Zero123 (batch de vistas por azimuth, fallback de menor VRAM, stable_zero123.ckpt). Es el puente 2.5D que faltaba en gamedev-2d: a diferencia de sprite_sheet (re-poza con OpenPose 2D, identidad inconsistente entre angulos), aqui la difusion 3D ROTA la figura sobre su eje, asi casco/arma/paleta son los mismos en cada direccion. Funcion pura: solo construye el grafo (class_types/inputs verificados contra /object_info); el coste GPU esta al enviar con comfyui_submit_workflow. La imagen frontal debe existir ya en el input/ del servidor."
tags: [comfyui, ml, gamedev-2d, sprite, directional, sv3d, zero123, turntable, multiview, stable-diffusion, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: input_image
desc: "Nombre del archivo de la imagen frontal del personaje dentro del input/ del servidor ComfyUI (lo carga LoadImage). Debe existir ya (subir con POST /upload/image o copiar a ~/ComfyUI/input/). Idealmente personaje centrado, encuadrado entero, mirando a camara, sobre fondo limpio (SV3D/Zero123 rinden mucho mejor sin fondo). No puede estar vacio."
- name: directions
desc: "Numero de direcciones a generar. 8 (default) = 8-way N/NE/E/SE/S/SW/W/NW (shooter 8-direcciones, top-down/iso); 4 = N/E/S/W (RPG clasico). Cualquier N>=1 vale (vistas equiespaciadas a 360/N grados); 4 y 8 reciben nombres cardinales via directional_sprite_view_order. keyword-only."
- name: model
desc: "'sv3d' (default, orbita turntable en una pasada, mejor consistencia angular, checkpoint sv3d_p.safetensors) o 'zero123' (batch de vistas con control de azimuth, fallback de menor VRAM, checkpoint stable_zero123.ckpt, nativo 256px). keyword-only."
- name: elevation
desc: "Elevacion de camara en grados. 0 = orbita en el ecuador (vista lateral pura). Top-down/isometrico suele querer PICADO (mirar al personaje algo desde arriba): subir a ~15-30. keyword-only."
- name: size
desc: "Lado en px de cada vista. None (default) usa la resolucion nativa del modelo (576 sv3d / 256 zero123). Bajarlo reduce VRAM a costa de detalle (p.ej. 320 para SV3D si OOM). keyword-only."
- name: orbit_frames
desc: "SOLO sv3d. Numero de frames del orbit que sintetiza SV3D_Conditioning. None (default) = directions (una imagen por direccion, salida directa). Subirlo (p.ej. 21, el nativo de SV3D) da una orbita mas densa y consistente de la que el caller submuestrea las direcciones (frame mas cercano por azimuth); a mas frames, mas VRAM. Ignorado por zero123. keyword-only."
- name: seed
desc: "Semilla del KSampler (0 = determinista; cambiar para variar la orbita). keyword-only."
- name: ckpt
desc: "Nombre del checkpoint en el servidor. None (default) usa el del modelo (sv3d_p.safetensors / stable_zero123.ckpt). Pasar 'sv3d_u.safetensors' para la orbita uniforme en el ecuador. keyword-only."
- name: steps
desc: "Pasos de sampling del KSampler. keyword-only."
- name: cfg
desc: "Guidance scale. None (default) usa el del modelo (2.5 sv3d / 4.0 zero123). keyword-only."
- name: min_cfg
desc: "SOLO sv3d. CFG del primer frame para VideoLinearCFGGuidance (interpola de min_cfg a cfg a lo largo del orbit). keyword-only."
- name: sampler_name
desc: "Algoritmo del KSampler. Default 'euler'. keyword-only."
- name: scheduler
desc: "Scheduler del KSampler. Default 'karras'. keyword-only."
- name: filename_prefix
desc: "Prefijo del archivo de salida del SaveImage. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow (claves = node_ids string, valores = class_type + inputs). SV3D: ImageOnlyCheckpointLoader + LoadImage + SV3D_Conditioning + VideoLinearCFGGuidance + KSampler + VAEDecode + SaveImage (los N frames del orbit). Zero123: ImageOnlyCheckpointLoader + LoadImage + StableZero123_Conditioning_Batched + KSampler + VAEDecode + SaveImage (un batch de directions vistas). El frame i (i-esima imagen del SaveImage, azimuth creciente desde la frontal) = direccion i de directional_sprite_view_order(directions). El modulo expone ademas directional_sprite_view_order(directions) -> lista de nombres de direccion alineada por indice con los frames."
tested: false
file_path: "python/functions/ml/comfyui_build_directional_sprite_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_directional_sprite_workflow import (
comfyui_build_directional_sprite_workflow,
directional_sprite_view_order,
)
from ml.comfyui_submit_workflow import comfyui_submit_workflow
# 'goblin_front.png' = sprite frontal del personaje, fondo limpio, ya en input/ del server.
wf = comfyui_build_directional_sprite_workflow(
"goblin_front.png",
directions=8, # 8-way N/NE/E/SE/S/SW/W/NW
model="sv3d", # turntable consistente (default)
elevation=15.0, # algo de picado (top-down/iso)
size=576,
seed=7,
)
# wf["3"]["class_type"] == "SV3D_Conditioning"; wf["7"] == SaveImage (los 8 frames del orbit)
pid = comfyui_submit_workflow(wf)["prompt_id"]
# sondear /history/{pid} -> comfyui_fetch_output_image por frame.
# El frame i corresponde a la direccion i:
print(directional_sprite_view_order(8)) # ['S','SE','E','NE','N','NW','W','SW']
```
Probado end-to-end (27/06/2026): goblin `enemy_creature_00001_.png` (compuesto sobre blanco
576×576) → SV3D `sv3d_p.safetensors` 8 direcciones elevation 15 seed 7. `prompt_id
8b9f75de-9dcb-41f6-ae4c-7d52d34ed238`, 8 frames `dir_sprite_goblin_0000{1..8}_.png`, 75 s,
VRAM pico 7145/8192 MiB. Consistencia rotacional medida: MAE adyacentes 27 (rotación
gradual) < frente↔espalda 29.6 (la espalda difiere más = rotación 3D real), spread de paleta
entre frames 3.83 (identidad de color consistente — el mismo goblin en las 8 vistas).
## Cuando usarla
- Cuando necesites un personaje visto desde **varias direcciones de movimiento** con
**identidad consistente**: enemigos de un 8-way shooter, NPCs de un top-down, unidades de
un RTS isométrico, sprite del jugador en un roguelike cenital.
- Tienes **UN sprite frontal** (lo generaste con `enemy_creature`/`topdown_sprite`, o lo
dibujaste) y quieres las otras direcciones SIN re-dibujarlas a mano.
- Elige `directions`: 8 para movimiento diagonal completo, 4 para RPG clásico.
- Sube algo de `elevation` (~15-30) para juegos con cámara picada (iso/top-down); déjalo en
0 para un turntable lateral puro.
- **No** la uses si quieres re-pozar al personaje (correr, atacar, saltar) en una sola vista
— eso es `comfyui_build_sprite_sheet_workflow` (OpenPose). Esto **rota**, no anima la pose.
## Gotchas
- **La imagen frontal debe existir ya en `input/`** del servidor (subir con
`POST /upload/image` o copiar a `~/ComfyUI/input/`). La función es pura y NO valida que
exista ni que el checkpoint esté instalado; valida con `comfyui_validate_workflow` antes
de enviar a un server desconocido.
- **Fondo limpio obligatorio para buen resultado.** SV3D/Zero123 rinden mucho mejor con el
personaje aislado sobre fondo plano (blanco o transparente compuesto sobre blanco). Un
fondo con escena confunde la rotación 3D. Recorta/compón el sprite antes (Rembg o
`Image.alpha_composite` sobre blanco).
- **VRAM (RTX 3070 8GB):** SV3D es un modelo de vídeo; aún en lowvram pesa. Medido: 8 frames
a 576² → pico **7145 MiB**, margen estrecho. Limpia la GPU antes
(`POST /free {"unload_models":true,"free_memory":true}`) — no convive con un juego AAA
abierto (~2.7 GB). Si **OOM**: baja `size` (576→320), baja `directions` (8→4), o cae a
`model="zero123"` (vistas independientes, batch más barato). NO hace falta matar procesos.
- **`comfyui_wait_result` lanza `TimeoutError`** si el orbit no termina dentro del timeout,
pero el job sigue en GPU y completa — recupera los frames sondeando `/history/{prompt_id}`
por su cuenta (envolver el wait en try/except o sondear directamente). El golden tardó 75 s,
pero con `orbit_frames=21` o `size` alto puede irse a minutos.
- **SV3D entrena a 21 frames.** Con `orbit_frames` distinto de 21 (p.ej. el default
`=directions=8`) el orbit es más espaciado pero funciona (probado a 8). Para máxima
fidelidad angular usa `orbit_frames=21` y submuestrea las `directions` vistas (frame más
cercano por azimuth) — a costa de más VRAM y tiempo.
- **Mapeo dirección↔frame:** el frame 0 es la vista frontal (personaje de frente); el frame
i es el azimuth i·360/N. `directional_sprite_view_order(directions)` da la lista de nombres
alineada por índice con los frames. La etiqueta cardinal ("S" = front/mirando abajo) es
convención gamedev top-down; ajústala a tu motor si difiere.
- **Distinto de `comfyui_generate_views_from_image`** (impura): esa orquesta
`/object_info` + submit + wait + fetch para reconstrucción 3D multi-vista (4 cardinales
front/right/back/left). Este builder es el equivalente **puro** orientado a sprite
direccional gamedev, hermano de `comfyui_build_img2vid_workflow` (builder puro frente al
orquestador). Para el flujo completo build→submit→wait→fetch, compón este builder con esas
funciones del registry.
## Capability growth log
(sin cambios desde v1.0.0)
@@ -0,0 +1,310 @@
"""Construye el workflow ComfyUI de un sprite MULTI-DIRECCIONAL (vistas 3D consistentes).
A partir de UNA imagen frontal de un personaje (fondo limpio), construye el dict (API
format) de un workflow que genera N vistas direccionales del MISMO personaje rotando en
3D: las 8 direcciones N/NE/E/SE/S/SW/W/NW de un shooter 8-way / juego top-down /
isométrico, o las 4 direcciones N/E/S/W de un RPG clásico. El valor de este builder es la
CONSISTENCIA ROTACIONAL: es el mismo personaje girado en 3D, no re-dibujado por dirección.
Es el puente 2.5D que faltaba en el catálogo `gamedev-2d`. Los builders 2D de ese grupo
(`enemy_creature`, `topdown_sprite`, `sprite_sheet`) producen UNA vista; `sprite_sheet`
re-poza con OpenPose 2D (re-dibuja la silueta para cada pose, así que la identidad del
personaje deriva entre ángulos). Aquí, en cambio, un modelo de difusión 3D (SV3D turntable
u Stable Zero123 órbita) rota la figura alrededor de su eje vertical, de modo que el casco,
el arma y la paleta son los mismos en cada dirección. Es la diferencia entre "dibujar al
goblin 8 veces" (inconsistente) y "fotografiar al goblin desde 8 ángulos" (consistente).
Dos modelos, ambos confirmados en /object_info del server 8GB:
- SV3D (model="sv3d", default): el nodo `SV3D_Conditioning` produce una ÓRBITA de
`video_frames` vistas equiespaciadas en 360° en una sola pasada (reutiliza la
maquinaria img2vid de Stable Video Diffusion: `ImageOnlyCheckpointLoader` →
`SV3D_Conditioning` → `VideoLinearCFGGuidance` → `KSampler` → `VAEDecode` → `SaveImage`,
los N frames). Mejor consistencia angular; checkpoint `sv3d_p.safetensors` (~9.4 GB en
disco, ~2-4 GB en VRAM porque es un modelo de vídeo en lowvram). Frame 0 = la vista de
entrada; el frame i corresponde al azimuth i·360/N.
- Stable Zero123 (model="zero123"): el nodo `StableZero123_Conditioning_Batched` genera
un BATCH de N vistas a azimuths equiespaciados (control directo del ángulo por vista).
Útil como fallback si SV3D no cabe en VRAM: cada vista es una imagen independiente, así
que se puede bajar `directions` o `size` con más holgura. Checkpoint
`stable_zero123.ckpt`. width/height nativos 256.
Función PURA: sin red, sin I/O. Solo construye el dict del grafo (los class_types y sus
inputs verificados contra /object_info). El coste GPU está al enviar con
`comfyui_submit_workflow`. La imagen de entrada debe existir ya en el `input/` del servidor
(subir antes con POST /upload/image). Hermana impura: `comfyui_generate_views_from_image`
(orquesta /object_info + submit + wait + fetch para reconstrucción 3D multi-vista, 4
cardinales); este builder es el equivalente PURO orientado a sprite direccional gamedev,
igual que `comfyui_build_img2vid_workflow` es el builder puro frente al orquestador.
"""
from __future__ import annotations
# Modelos soportados -> checkpoint por defecto que el nodo carga vía ImageOnlyCheckpointLoader.
_MODEL_CKPT = {
"sv3d": "sv3d_p.safetensors",
"zero123": "stable_zero123.ckpt",
}
# Resolución nativa por modelo cuando el caller no fija `size`. SV3D entrena a 576²;
# Stable Zero123 a 256². Usar la nativa evita degradación y picos de VRAM innecesarios.
_MODEL_NATIVE_SIZE = {"sv3d": 576, "zero123": 256}
# CFG por defecto por modelo cuando el caller no lo fija. SV3D usa guía de vídeo baja
# interpolada (VideoLinearCFGGuidance de min_cfg a cfg); Zero123 usa una CFG algo mayor.
_MODEL_DEFAULT_CFG = {"sv3d": 2.5, "zero123": 4.0}
# Nombres de dirección por nº de direcciones, en el orden del orbit empezando por la vista
# frontal (frame 0 = personaje de frente, mirando a cámara). Convención gamedev top-down:
# "front" = el personaje mirando hacia ABAJO/al jugador (south). Cada paso avanza el azimuth
# 360/N grados. El orden es solo una etiqueta de conveniencia para el caller al ensamblar el
# grid direccional; el grafo no depende de él.
_DIRECTION_NAMES = {
4: ["S", "E", "N", "W"],
8: ["S", "SE", "E", "NE", "N", "NW", "W", "SW"],
}
def directional_sprite_view_order(directions: int) -> list:
"""Lista ordenada de nombres de dirección que produce el orbit, frame 0..N-1.
El frame i del workflow (i-ésima imagen del SaveImage, en orden de azimuth creciente
desde la vista frontal) corresponde a la dirección devuelta en la posición i. Para
4 u 8 direcciones devuelve los nombres cardinales canónicos; para cualquier otro N
devuelve etiquetas genéricas por azimuth (``az0``, ``az45``, ...). Pura.
Args:
directions: número de direcciones del orbit (>= 1).
Returns:
lista de `directions` nombres de dirección, alineada por índice con los frames.
"""
directions = int(directions)
if directions in _DIRECTION_NAMES:
return list(_DIRECTION_NAMES[directions])
return [f"az{round(i * 360 / directions)}" for i in range(directions)]
def comfyui_build_directional_sprite_workflow(
input_image: str,
*,
directions: int = 8,
model: str = "sv3d",
elevation: float = 0.0,
size: int | None = None,
orbit_frames: int | None = None,
seed: int = 0,
ckpt: str | None = None,
steps: int = 20,
cfg: float | None = None,
min_cfg: float = 1.0,
sampler_name: str = "euler",
scheduler: str = "karras",
filename_prefix: str = "directional_sprite",
) -> dict:
"""Construye el dict (API format) del workflow de sprite multi-direccional.
Recibe el sprite frontal de UN personaje (imagen en el `input/` del servidor, fondo
limpio) y construye un workflow que produce N vistas direccionales CONSISTENTES (el
mismo personaje rotado en 3D), una por dirección de movimiento del juego.
Args:
input_image: nombre del archivo de imagen frontal del personaje dentro del `input/`
del servidor ComfyUI (lo carga el nodo LoadImage). Debe existir ya (subir con
POST /upload/image o copiar a ~/ComfyUI/input/). Idealmente con fondo limpio y el
personaje centrado y encuadrado entero, mirando a cámara. No puede estar vacío.
directions: número de direcciones a generar. 8 (default) = 8-way N/NE/E/SE/S/SW/W/NW
(shooter 8-direcciones, top-down/iso); 4 = N/E/S/W (RPG clásico). Cualquier N>=1
es válido (vistas equiespaciadas a 360/N grados); 4 y 8 reciben nombres cardinales
(ver `directional_sprite_view_order`). keyword-only.
model: "sv3d" (default, órbita turntable, mejor consistencia angular) o "zero123"
(batch de vistas con control de azimuth, fallback de menor VRAM). keyword-only.
elevation: elevación de cámara en grados. 0 = órbita en el ecuador (vista lateral
pura). Un juego top-down/isométrico suele querer algo de PICADO (mirar al
personaje desde arriba): subir a ~15-30. keyword-only.
size: lado en px de cada vista. None (default) usa la resolución nativa del modelo
(576 para sv3d, 256 para zero123). Bajarlo reduce VRAM a costa de detalle.
keyword-only.
orbit_frames: SOLO sv3d. Número de frames del orbit que sintetiza SV3D_Conditioning.
None (default) = `directions` (una imagen por dirección, salida directa). Subirlo
(p. ej. 21, el nativo de SV3D) da una órbita más densa y consistente de la que el
caller submuestrea las `directions` vistas (frame más cercano por azimuth); a más
frames, más VRAM. Ignorado por zero123 (su batch es exactamente `directions`).
keyword-only.
seed: semilla del KSampler (0 = determinista; cambiar para variar la órbita).
keyword-only.
ckpt: nombre del checkpoint en el servidor. None (default) usa el del modelo
(`sv3d_p.safetensors` / `stable_zero123.ckpt`). keyword-only.
steps: pasos de sampling del KSampler. keyword-only.
cfg: guidance scale. None (default) usa el del modelo (2.5 sv3d / 4.0 zero123).
keyword-only.
min_cfg: SOLO sv3d. CFG del primer frame para VideoLinearCFGGuidance (interpola de
min_cfg a cfg a lo largo del orbit). keyword-only.
sampler_name: algoritmo del KSampler. Default "euler". keyword-only.
scheduler: scheduler del KSampler. Default "karras". keyword-only.
filename_prefix: prefijo del archivo de salida del SaveImage. keyword-only.
Returns:
dict en API format listo para `comfyui_submit_workflow`. Las claves son node_ids
(string) y cada valor tiene class_type + inputs. SV3D devuelve 6 nodos
(ImageOnlyCheckpointLoader, LoadImage, SV3D_Conditioning, VideoLinearCFGGuidance,
KSampler, VAEDecode, SaveImage → los N frames del orbit). Zero123 devuelve
ImageOnlyCheckpointLoader + LoadImage + StableZero123_Conditioning_Batched + KSampler
+ VAEDecode + SaveImage (un batch de `directions` vistas). El frame i (i-ésima imagen
del SaveImage, azimuth creciente desde la frontal) = dirección i de
`directional_sprite_view_order(directions)`.
Raises:
ValueError: si input_image está vacío, model no es "sv3d"/"zero123", o directions < 1.
"""
if not input_image or not input_image.strip():
raise ValueError(
"comfyui_build_directional_sprite_workflow: 'input_image' no puede estar vacío "
"(el modelo 3D necesita la vista frontal en input/)."
)
if model not in _MODEL_CKPT:
raise ValueError(
"comfyui_build_directional_sprite_workflow: model "
f"'{model}' inválido; usa uno de {sorted(_MODEL_CKPT)}."
)
directions = int(directions)
if directions < 1:
raise ValueError(
"comfyui_build_directional_sprite_workflow: 'directions' debe ser >= 1 "
f"(recibido {directions}); 4 u 8 son los valores canónicos de gamedev."
)
input_image = input_image.strip()
ckpt_name = ckpt or _MODEL_CKPT[model]
px = int(size) if size is not None else _MODEL_NATIVE_SIZE[model]
guidance = float(cfg) if cfg is not None else _MODEL_DEFAULT_CFG[model]
if model == "sv3d":
return _build_sv3d(
input_image, ckpt_name, directions, elevation, px, orbit_frames, seed,
steps, guidance, min_cfg, sampler_name, scheduler, filename_prefix,
)
return _build_zero123(
input_image, ckpt_name, directions, elevation, px, seed,
steps, guidance, sampler_name, scheduler, filename_prefix,
)
def _build_sv3d(input_image, ckpt_name, directions, elevation, size, orbit_frames,
seed, steps, cfg, min_cfg, sampler_name, scheduler, filename_prefix) -> dict:
"""Workflow SV3D: 1 imagen frontal -> orbit de N vistas en una pasada.
SV3D reutiliza la cadena img2vid de Stable Video Diffusion: el checkpoint sv3d_p se
carga con ImageOnlyCheckpointLoader (MODEL + CLIP_VISION + VAE), SV3D_Conditioning
produce el conditioning del orbit y la latente, se muestrea como vídeo con guía CFG
lineal y el SaveImage emite los `video_frames` frames del turntable.
"""
video_frames = int(orbit_frames) if orbit_frames else directions
return {
"1": {"class_type": "LoadImage", "inputs": {"image": input_image}},
"2": {"class_type": "ImageOnlyCheckpointLoader", "inputs": {"ckpt_name": ckpt_name}},
"3": {
"class_type": "SV3D_Conditioning",
"inputs": {
"clip_vision": ["2", 1],
"init_image": ["1", 0],
"vae": ["2", 2],
"width": size,
"height": size,
"video_frames": video_frames,
"elevation": float(elevation),
},
},
"4": {
"class_type": "VideoLinearCFGGuidance",
"inputs": {"model": ["2", 0], "min_cfg": float(min_cfg)},
},
"5": {
"class_type": "KSampler",
"inputs": {
"seed": int(seed),
"steps": int(steps),
"cfg": float(cfg),
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": 1.0,
"model": ["4", 0],
"positive": ["3", 0],
"negative": ["3", 1],
"latent_image": ["3", 2],
},
},
"6": {"class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["2", 2]}},
"7": {
"class_type": "SaveImage",
"inputs": {"images": ["6", 0], "filename_prefix": filename_prefix},
},
}
def _build_zero123(input_image, ckpt_name, directions, elevation, size, seed,
steps, cfg, sampler_name, scheduler, filename_prefix) -> dict:
"""Workflow Stable Zero123: batch de N vistas a azimuths equiespaciados.
StableZero123_Conditioning_Batched genera `directions` vistas en una pasada, empezando
en azimuth 0 (la frontal) y avanzando 360/N grados por vista. El KSampler las muestrea
como un batch y el SaveImage las emite en orden de azimuth creciente.
"""
increment = 360.0 / directions
return {
"1": {"class_type": "LoadImage", "inputs": {"image": input_image}},
"2": {"class_type": "ImageOnlyCheckpointLoader", "inputs": {"ckpt_name": ckpt_name}},
"3": {
"class_type": "StableZero123_Conditioning_Batched",
"inputs": {
"clip_vision": ["2", 1],
"init_image": ["1", 0],
"vae": ["2", 2],
"width": size,
"height": size,
"batch_size": directions,
"elevation": float(elevation),
"azimuth": 0.0,
"elevation_batch_increment": 0.0,
"azimuth_batch_increment": increment,
},
},
"4": {
"class_type": "KSampler",
"inputs": {
"seed": int(seed),
"steps": int(steps),
"cfg": float(cfg),
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": 1.0,
"model": ["2", 0],
"positive": ["3", 0],
"negative": ["3", 1],
"latent_image": ["3", 2],
},
},
"5": {"class_type": "VAEDecode", "inputs": {"samples": ["4", 0], "vae": ["2", 2]}},
"6": {
"class_type": "SaveImage",
"inputs": {"images": ["5", 0], "filename_prefix": filename_prefix},
},
}
if __name__ == "__main__":
import json
wf = comfyui_build_directional_sprite_workflow(
"goblin_front.png",
directions=8,
model="sv3d",
elevation=15.0,
seed=7,
)
print(json.dumps({
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
"video_frames": wf["3"]["inputs"]["video_frames"],
"size": wf["3"]["inputs"]["width"],
"view_order": directional_sprite_view_order(8),
}, indent=2))