miércoles, 15 de abril de 2026

FFMPEG: Creando un motor 3D???

Si os gustó mi último post sobre FFMPEG, este os hará replantearos mi nivel de salud mental. En esta ocasión trataré de implementar el motor Voxel Space que ya usé en mi demo de GameBoy Advance y en lenguaje LOGO, mediante filtros de video de FFMPEG, esto es sin ninguna línea de código.

Pues bien, la forma en la que funciona este motor voxel esta perfectamente documentada en este repositorio de GitHub: https://github.com/s-macke/voxelspace
Básicamente, y dicho de una forma que se entienda sin formulas ni código, debemos:
  1. Aplicar perspectiva en el eje X de forma que cuanto más cerca este un elemento del mapa de la cámara, más "zoom" haremos
  2. De la misma forma en el eje Y, cuanto más cerca este un elemento del mapa de la cámara, más se exagerará la elevación
  3. Trazar lineas verticales que alcancen esa altura calculada, empezando o dando prioridad a las más cercanas a la cámara
Bueno. ¡Empecemos! Usaremos el mismo mapa de siempre, dos archivos c.png y p.png
Lo primero que haremos en FFMPEG es unir el mapa de color y alturas (el cual pasará a ser el canal alpha) para poder trabajar con ellos de forma conjunta. Esto lo haremos con el filtro: alphamerge
A continuación recortaremos el mapa a un tamaño que sea el que tendrá nuestra salida de video. Esto lo haremos con el filtro: crop=340:256:9
Muy bien, ahora si queremos animarlo para que por ejemplo de vueltas, tendremos que añadir rotación antes de hacer el crop, y de paso añadiremos un poco de desplazamiento hacia los lados con: rotate=n/50:bilinear=0,crop=340:256:64+32*cos(n/25)
Aquí "n" es el número de frame, y usamos la opción bilinear=0 para que busque siempre el pixel más cercano en vez de calcular promedios, que afectarían al mapa de alturas.
Muy bien, ahora ¡empecemos a implementar nuestro motor! para aplicar perspectiva al eje X usaremos el filtro qeg que permite aplicar una formula a cada valor de pixel, en este caso para los colores la formula será p(floor(170+(X-170)*Y/256),Y)
Sobra decir que aquí 170 es la mitad del ancho y 256 es el alto. ¡De momento pinta bien! Para el canal alpha donde también hay que "exagerar" las alturas la formula será clip((height-alpha(floor(170+(X-170)*Y/256),Y))/Y*scale_h+horizon,0,255)
usamos clip para mantener los valores dentro de nuestro rango de altura 0-255.
Ahora, para trazar las lineas verticales en principio es necesario un bucle FOR iterando las distintas profundidades como se puede ver en el primer GIF. Esto en FFMPEG no es tan sencillo, aunque en principio existe una función "while" en la expresiones de filtros, me decantaré por usar varios IF-ELSEs encadenados para reducir la complejidad al máximo, aunque como resultado me quede un comando demasiado largo. Empecemos a partir de la profundidad 5 hasta el final en 255. Sería algo así:
if(gt(Y,alpha(X,5)),p(X,5), if(gt(Y,alpha(X,6)),p(X,6), if(gt(Y,alpha(X,7)),p(X,7), if(gt(Y,alpha(X,8)),p(X,8), if(gt(Y,alpha(X,9)),p(X,9), if(gt(Y,alpha(X,10)),p(X,10),
...
Y este sería el resultado final: ¡Funcionó!
El resultado animado que nos devuelve FFMPEG sería el siguiente:
ffmpeg -loop 1 -i c.png -loop 1 -i p.png -filter_complex "[0][1]alphamerge,rotate=n/50:bilinear=0,crop=340:256:64+32*cos(n/25),geq='p(floor(170+(X-170)*Y/256),Y)':a='clip((120-alpha(floor(170+(X-170)*Y/256),Y))/Y*280+0,0,255)',geq=' if(gt(Y,alpha(X,5)),p(X,5), if(gt(Y,alpha(X,6)),p(X,6), if(gt(Y,alpha(X,7)),p(X,7), if(gt(Y,alpha(X,8)),p(X,8), if(gt(Y,alpha(X,9)),p(X,9), if(gt(Y,alpha(X,10)),p(X,10), if(gt(Y,alpha(X,11)),p(X,11), if(gt(Y,alpha(X,12)),p(X,12), if(gt(Y,alpha(X,13)),p(X,13), if(gt(Y,alpha(X,15)),p(X,15), if(gt(Y,alpha(X,16)),p(X,16), if(gt(Y,alpha(X,17)),p(X,17), if(gt(Y,alpha(X,19)),p(X,19), if(gt(Y,alpha(X,20)),p(X,20), if(gt(Y,alpha(X,22)),p(X,22), if(gt(Y,alpha(X,23)),p(X,23), if(gt(Y,alpha(X,25)),p(X,25), if(gt(Y,alpha(X,26)),p(X,26), if(gt(Y,alpha(X,28)),p(X,28), if(gt(Y,alpha(X,29)),p(X,29), if(gt(Y,alpha(X,31)),p(X,31), if(gt(Y,alpha(X,33)),p(X,33), if(gt(Y,alpha(X,34)),p(X,34), if(gt(Y,alpha(X,36)),p(X,36), if(gt(Y,alpha(X,38)),p(X,38), if(gt(Y,alpha(X,40)),p(X,40), if(gt(Y,alpha(X,41)),p(X,41), if(gt(Y,alpha(X,43)),p(X,43), if(gt(Y,alpha(X,45)),p(X,45), if(gt(Y,alpha(X,47)),p(X,47), if(gt(Y,alpha(X,49)),p(X,49), if(gt(Y,alpha(X,51)),p(X,51), if(gt(Y,alpha(X,53)),p(X,53), if(gt(Y,alpha(X,55)),p(X,55), if(gt(Y,alpha(X,57)),p(X,57), if(gt(Y,alpha(X,59)),p(X,59), if(gt(Y,alpha(X,62)),p(X,62), if(gt(Y,alpha(X,64)),p(X,64), if(gt(Y,alpha(X,66)),p(X,66), if(gt(Y,alpha(X,68)),p(X,68), if(gt(Y,alpha(X,71)),p(X,71), if(gt(Y,alpha(X,73)),p(X,73), if(gt(Y,alpha(X,75)),p(X,75), if(gt(Y,alpha(X,78)),p(X,78), if(gt(Y,alpha(X,80)),p(X,80), if(gt(Y,alpha(X,83)),p(X,83), if(gt(Y,alpha(X,85)),p(X,85), if(gt(Y,alpha(X,88)),p(X,88), if(gt(Y,alpha(X,90)),p(X,90), if(gt(Y,alpha(X,93)),p(X,93), if(gt(Y,alpha(X,96)),p(X,96), if(gt(Y,alpha(X,98)),p(X,98), if(gt(Y,alpha(X,101)),p(X,101), if(gt(Y,alpha(X,104)),p(X,104), if(gt(Y,alpha(X,106)),p(X,106), if(gt(Y,alpha(X,109)),p(X,109), if(gt(Y,alpha(X,112)),p(X,112), if(gt(Y,alpha(X,115)),p(X,115), if(gt(Y,alpha(X,118)),p(X,118), if(gt(Y,alpha(X,121)),p(X,121), if(gt(Y,alpha(X,124)),p(X,124), if(gt(Y,alpha(X,127)),p(X,127), if(gt(Y,alpha(X,130)),p(X,130), if(gt(Y,alpha(X,133)),p(X,133), if(gt(Y,alpha(X,136)),p(X,136), if(gt(Y,alpha(X,139)),p(X,139), if(gt(Y,alpha(X,142)),p(X,142), if(gt(Y,alpha(X,146)),p(X,146), if(gt(Y,alpha(X,149)),p(X,149), if(gt(Y,alpha(X,152)),p(X,152), if(gt(Y,alpha(X,155)),p(X,155), if(gt(Y,alpha(X,159)),p(X,159), if(gt(Y,alpha(X,162)),p(X,162), if(gt(Y,alpha(X,166)),p(X,166), if(gt(Y,alpha(X,169)),p(X,169), if(gt(Y,alpha(X,172)),p(X,172), if(gt(Y,alpha(X,176)),p(X,176), if(gt(Y,alpha(X,180)),p(X,180), if(gt(Y,alpha(X,183)),p(X,183), if(gt(Y,alpha(X,187)),p(X,187), if(gt(Y,alpha(X,190)),p(X,190), if(gt(Y,alpha(X,194)),p(X,194), if(gt(Y,alpha(X,198)),p(X,198), if(gt(Y,alpha(X,202)),p(X,202), if(gt(Y,alpha(X,205)),p(X,205), if(gt(Y,alpha(X,209)),p(X,209), if(gt(Y,alpha(X,213)),p(X,213), if(gt(Y,alpha(X,217)),p(X,217), if(gt(Y,alpha(X,221)),p(X,221), if(gt(Y,alpha(X,225)),p(X,225), if(gt(Y,alpha(X,229)),p(X,229), if(gt(Y,alpha(X,233)),p(X,233), if(gt(Y,alpha(X,237)),p(X,237), if(gt(Y,alpha(X,241)),p(X,241), if(gt(Y,alpha(X,245)),p(X,245), if(gt(Y,alpha(X,249)),p(X,249), if(gt(Y,alpha(X,253)),p(X,253),145)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))':a=255" -r 25 -qp 0 salida.mp4

¿Preguntas? ¿Dudas?