Cortometrajes low-poly PS1
Todo es código: Claude escribe un script de Blender por corto, lo renderiza en la máquina del usuario, revisa los fotogramas, los corrige, y luego construye el sonido y los subtítulos. Sin generador de video.
Probado en examples/lift-rocket/ ("¿Qué pasaría si los ascensores fueran tan rápidos como cohetes?", 34 s).
Lee ese ejemplo antes de escribir un nuevo corto: es la plantilla.
Requisitos (verifica primero, instala lo que falte)
- Blender 4.2+ (probado en 5.3). Búscalo:
blenderen PATH, si no,C:\Program Files\Blender Foundation\<ver>\blender.exe//Applications/Blender.app/Contents/MacOS/Blender. - Python 3.10+ con
numpy pillow soundfile faster-whisper;ffmpegen PATH. - Personajes: Quaternius "Ultimate Modular Men" (CC0, gratis, quaternius.com). Descomprime en
assets/quaternius-modular-men/o estableceLOWPOLY_PACKen su carpetaIndividual Characters/Blends. - Una voz TTS (ElevenLabs o cualquier herramienta que tenga el usuario). Opcional: el MCP
earshotpara verificar el sonido.
El flujo de trabajo (una carpeta por corto, copia examples/lift-rocket/ como inicio)
| # | Paso | Archivo / comando | Hecho cuando |
|---|---|---|---|
| 1 | Guión | script.md | Pregunta gancho + cadena de consecuencias, cada una peor y más divertida + números reales + remate seco. 60-110 palabras para 30-40 s. Cada número verificado con una fuente y escrito en la lista de Hechos. |
| 2 | Voz | vo/takeA.mp3 | 2 tomas, elige la que tenga tiempos de palabra limpios. Voz enérgica de creador, no calmada (calmado = retroalimentación "aburrido"). |
| 3 | Tiempos de palabras | python ../../scripts/words.py vo/takeA.mp3 | takeA_words.json no tiene palabras de longitud cero. |
| 4 | Lista de tomas | tabla en script.md | Una toma por frase de VO, una imagen nueva cada 1-3 s, cada toma vinculada a un tiempo de palabra. |
| 5 | Escena | build.py (Blender) | blender -b -P build.py -- frames --still 30,120,... renderiza imágenes fijas; mira cada imagen fija antes del render completo. |
| 6 | Vista previa + notas | python ../../scripts/preview.py . render luego serve | El usuario no tiene notas abiertas (ver bucle de Vista previa). |
| 7 | Renderizar | blender -b -P build.py -- frames | PNGs EEVEE 360x640, f_0001.png... |
| 8 | Sonido | python build_mix.py | SFX colocados en fotogramas de imagen, VO encima, cama lo-fi debajo, ducking. Escribe mix_raw.wav + cues.json. |
| 9 | Postproducción | python post.py | Dither PS1, efectos, subtítulos con palabra resaltada, llamadas de estadísticas -> out/ a 1080x1920. |
| 10 | Mezcla | ver abajo | short.mp4, luego medir y verificar sincronización. |
Regla de fotogramas en todas partes: 24 fps, fotograma 1 = t 0, F = lambda t: int(round(t * 24)) + 1.
Mezcla:
ffmpeg -y -framerate 24 -i out/f_%04d.png -i mix_raw.wav -c:v libx264 -crf 16 -pix_fmt yuv420p \
-af "loudnorm=I=-15:TP=-1.5:LRA=11,aresample=48000" -c:a aac -b:a 192k -shortest short.mp4
Cambia la voz más tarde sin volver a cortar la imagen:
python ../../scripts/words.py vo/takeB.mp3 luego python ../../scripts/align.py vo/takeA_words.json vo/takeB.mp3 --end 34 (--end = donde termina la cama de VO; la salida tiene una duración de end + 2 s. Sin él: última palabra + 1.5 s, lo que da un archivo más corto que la imagen)
-> takeB_aligned.wav + takeB_aligned_words.json. Apunta build_mix.py (variable de entorno VO) y post.py hacia ellos.
Bucle de vista previa (antes del render completo)
Vista previa rápida de baja resolución en el navegador, el usuario deja notas en la línea de tiempo, Claude corrige y vuelve a renderizar solo las tomas cuyo contenido cambió. Tomas = los marcadores de cámara en build.py.
| Haz | Comando (desde la carpeta del corto) |
|---|---|
| Renderizar la vista previa | python ../../scripts/preview.py . render (primera ejecución: cada toma; luego: solo tomas cambiadas) |
| Abrir la página de notas | python ../../scripts/preview.py . serve -> http://127.0.0.1:8765 |
| Leer las notas | python ../../scripts/preview.py . notes (agrupadas por toma, con números de fotograma) |
| Después de corregir | edita build.py, ejecuta render de nuevo, luego python ../../scripts/preview.py . done 3 5 --reply "lo que cambiaste" |
| Forzar tomas | render --shots c_press,c_slam o render --all |
build.pydebe aceptar--blend-only(construir la escena, guardar, no renderizar). Ver el ejemplo.- Detección de cambios: cada fotograma de vista previa obtiene una huella digital (cámara activa, transformación de cada objeto visible, pose de armadura, intercambios de caras, materiales, luces, datos de malla). Una toma se vuelve a renderizar solo si uno de sus fotogramas cambió. Las tomas sin cambios mantienen sus fotogramas antiguos.
- Configuración en
preview.json(opcional):scale50 (%),step2 (cada 2º fotograma),samples1,audio(por defectomix_raw.wav, si no la VO),final= comandos del flujo completo. El botón "Aprobar y renderizar final" de la página los ejecuta;notesimprime APROBADO cuando el usuario lo ha pulsado. - Corrige las notas una por una, responde a cada una, vuelve a renderizar y dile al usuario que no refresque nada: la página recarga el video por sí sola. Las notas repetidas del mismo tipo se convierten en una regla en este archivo.
- Medido en lift-rocket (RTX 3070 Ti laptop, 820 fotogramas): primera vista previa 76 s, una toma cambiada 13 s, nada cambiado 12 s. La mayor parte del tiempo de render de un fotograma es sobrecarga fija, por lo que una resolución más baja ayuda menos que renderizar menos fotogramas.
API del kit (scripts/lowpoly_kit.py, importar dentro de Blender)
load_character(scn, "Suit")->(armature, meshes, actions). Personajes: Adventurer, Beach, Casual_2, Casual_Hoodie, Farmer, King, Punk, Spacesuit, Suit, Swat, Worker.- Acciones (en el lugar, 24): Death, HitRecieve, HitRecieve_2, Idle, Idle_Neutral, Interact, Kick_Left/Right, Punch_Left/Right, Roll, Run, Run_Back/Left/Right, Walk, Wave, Gun_Shoot, Idle_Gun*, Idle_Sword, Sword_Slash, Run_Shoot.
add_faces(scn, arm, head, tag, kinds, spec, drop)antes de mover la armadura. Tipos: neutral, blink, side, shock, grimace, squint, dazed, xeyes.dropelimina los ojos/cejas 3D del paquete.show_faces(faces, [(frame, kind), ...]): cortes duros, cada cara se mantiene hasta la siguiente.nla(arm, [(action, start, end, blend[, influence]), ...]): entradas posteriores en pistas más altas.drive_root(arm, [(frame, x, y, yaw, ease), ...], last): yaw 0 mira hacia -Y (la cámara por defecto).new_cam,aim,yaw_to,ps1_render_settings(scn, outdir).- Sets:
scripts/set_lift_lobby.py(ejecutar conBUILD_ONLY=True, devuelvebox,uv_world,mat). Los nuevos sets siguen el mismo patrón: cajas con texturas de píxeles (interpolation = "Closest").
Reglas de aspecto visual
- Renderizar 360x640,
filter_size 0, AgX Punchy, sombreado plano; postproducción: color de 15 bits + dither Bayer, x3 nearest. - Las caras son decals planos de 32x28 píxeles intercambiados con cortes duros; nunca animar una cara suavemente.
- Aplastar bajo fuerza keyframing la escala de la armadura (z hacia abajo, xy hacia arriba). Golpes de comedia: rápido de entrada (2-3 fotogramas), mantener.
- Subtítulos: 1-3 palabras, sans serif gruesa, blanco con borde negro, palabra actual en amarillo, tercio inferior (y 1300 de 1920).
- Llamadas de estadísticas: cuadro rojo redondeado, tercio superior, aparición en 4 fotogramas, una por número real.
Reglas de sonido
- Cada SFX se coloca de modo que su pico caiga en el fotograma de imagen (
at(name, frame, db, peak=...)). - La voz debe ganar: VO normalizada a 0 dBFS, bus SFX -11 dB, música -7 dB, más ducking sidechain mientras la voz habla (SFX -5 dB, música -9 dB). La v1 del corto lift tenía SFX a -3 y música a 0: "No puedo oír la voz en off".
- Cortar la música justo en el golpe de parada grande, traerla de vuelta después. El silencio vende el gag.
- Master: loudnorm -15 LUFS, pico verdadero -1.5 dBTP.
- Verificar: earshot
timelineen el mp4 vscues.json(objetivo: cada cue dentro de 100 ms),measurepara LUFS.
Errores ya encontrados (no repetir)
- Las mallas del paquete vienen ocultas de render:
hide_render = False(el kit lo hace). - Las mallas de puerta ya están en su lugar cerrado: animar solo el desplazamiento, o se cierran con un espacio de 0.7 m.
- Un techo más bajo de ~2.75 m oculta las pantallas de pared de la cámara dentro de un coche tamaño ascensor.
- Un personaje que no debe ser visto todavía: key
location.z = -50hasta su toma. - NLA de una sola toma pasando a una mantenida: establecer
blend_out = 0en el clip o la pose aparece un fotograma. - Las tomas inversas salen negras (techo sin iluminar): añadir una luz frontal y un poco de emisión en el techo. Renderizar el ángulo inverso temprano.
- faster-whisper en mp3 puede romperse con un desajuste de PyAV:
words.pydecodifica con ffmpeg primero. - Una frase que comienza en -0.04 s da un segmento vacío en la alineación: sujetar con
max(0.0, ...)(hecho).
Costo (corto lift, RTX 3070 Ti laptop)
Renderizar 820 fotogramas en 92 s, postproducción 17 s, $0. Personajes, SFX, música: $0 (CC0 + sintetizados en código). Voz: ElevenLabs aproximadamente $0.10 por toma, 2 tomas por corto.
Antes de entregarlo
- Mira una hoja de contactos de los fotogramas finales (cada ~2 s), no solo las imágenes fijas.
- Mide la sonoridad, verifica la sincronización, revisa los subtítulos contra el texto de VO (las correcciones ortográficas van en post.py
FIX). - Dile al usuario qué se midió y qué no fue escuchado por un humano.