Exportar vídeo 360° desde Godot: ocho días construyendo Godot360 Studio
Godot no tiene forma de renderizar un vídeo 360° en el que puedas mirar alrededor. Construí una: seis cámaras sincronizadas, un shader equirectangular, FFmpeg y metadatos esféricos, todo desde un panel inferior. Esto es lo que se rompía en las costuras del cubo y cómo lo arreglé.
Tenía una escena pequeña en Godot llamada UMBRAL: una sala oscura donde las cosas reaccionan cuando las miras. Un núcleo dorado que cambia si lo observas fijamente durante un segundo. Un eco a tu espalda que late cuando te giras hacia él. Solo funciona si estás dentro. Una grabación de pantalla normal lo aplasta todo en mi trayectoria de cámara, y la gracia era justo que fuese el espectador quien decidiera hacia dónde mirar.
YouTube admite exactamente eso desde 2015. Subes un MP4 equirectangular con los metadatos correctos y el reproductor te deja arrastrar por la esfera en el escritorio o girar el móvil. Godot no trae ningún exportador para ello. Así que construí uno.
Ocho días después, Godot360 Studio 1.0.0 está en GitHub con licencia MIT y cuatro películas terminadas en una lista de reproducción de YouTube. Este artículo va de las partes interesantes: cómo seis cámaras se convierten en una esfera, por qué el trabajo de verdad estaba en las costuras entre ellas y lo que cuesta renderizar un minuto a 8K.

Qué es en realidad un vídeo 360°
Un vídeo 360° es un MP4 corriente cuyos fotogramas son panoramas equirectangulares 2:1. El eje horizontal es la longitud, el vertical la latitud, y las filas superior e inferior se estiran hasta un único punto cada una, como en un mapamundi. Un reproductor compatible envuelve esa imagen alrededor del espectador y le muestra la parte hacia la que mira.


Lo único que hace que el reproductor lo trate como esférico son unos metadatos dentro del contenedor. Todo lo demás es un problema de renderizado: ¿cómo sacas una esfera completa de un motor que solo sabe dibujar frustums en perspectiva?
Seis cámaras, un solo mundo
La respuesta clásica es un cubemap. Renderizas seis vistas de 90° (derecha, izquierda, arriba, abajo, delante, detrás) desde el mismo punto, y un shader convierte cada píxel del panorama en una dirección, elige la cara que esa dirección atraviesa y la muestrea.
El rig es un Node que construye seis SubViewport. El detalle importante es que todos comparten el World3D de la cámara original, de modo que la escena avanza exactamente una vez por fotograma y todas las caras ven el mismo estado de la simulación:
const DIRECTIONS: Array[Vector3] = [Vector3.RIGHT, Vector3.LEFT, Vector3.UP,
Vector3.DOWN, Vector3.FORWARD, Vector3.BACK]
const UP_VECTORS: Array[Vector3] = [Vector3.UP, Vector3.UP, Vector3.BACK,
Vector3.FORWARD, Vector3.UP, Vector3.UP]
for i in range(6):
var viewport := SubViewport.new()
viewport.size = Vector2i(face_size, face_size)
viewport.world_3d = source.get_world_3d()
viewport.use_hdr_2d = false # las caras salen como sRGB SDR ya con tone mapping
for property in VIEWPORT_SETTINGS: # msaa, taa, fsr, atlas de sombras...
viewport.set(property, source.get_viewport().get(property))
add_child(viewport)
var view := Camera3D.new()
viewport.add_child(view)
material.set_shader_parameter("face_" + FACE_NAMES[i], viewport.get_texture())
En cada fotograma, la cámara de cada cara copia el environment, los atributos, el compositor y la máscara de culling de la cámara original, y después rota su base:
var local_basis := Basis.looking_at(DIRECTIONS[i], UP_VECTORS[i])
var transform := source.get_camera_transform()
view.global_transform = Transform3D(transform.basis.orthonormalized() * local_basis, transform.origin)
Un ColorRect en un CanvasLayer alto ejecuta el shader de ensamblado. La longitud cero mira hacia el eje -Z de Godot, y las matemáticas de dirección ocupan unas pocas líneas:
float longitude = (UV.x - 0.5) * TAU;
float latitude = (0.5 - UV.y) * PI;
vec3 d = vec3(sin(longitude) * cos(latitude), sin(latitude), -cos(longitude) * cos(latitude));
vec3 a = abs(d);
if (a.x >= a.y && a.x >= a.z) {
vec2 p = vec2(d.x > 0.0 ? d.z : -d.z, -d.y) / a.x;
result = texture(d.x > 0.0 ? face_right : face_left, p * 0.5 + 0.5);
}
// ...lo mismo para y y z
Esa es toda la proyección. Funcionaba el primer día. Los otros siete fueron para todo lo que hay alrededor.
El pipeline alrededor del rig
El panel del editor nunca renderiza nada por sí mismo. Escribe una receta job.json y lanza un coordinador headless, que a su vez arranca un worker de GPU independiente en el modo Movie Maker de Godot con --fixed-fps y --write-movie. Movie Maker me da un reloj de fotogramas determinista y, de regalo, graba el audio estéreo de la escena en un WAV.
En cada fotograma, el worker ensambla el panorama, lo lee de vuelta y escribe un PNG sin pérdida. El modo de almacenamiento “Fast” envía los fotogramas en crudo directamente a la entrada estándar de FFmpeg por una tubería bloqueante, así que un disco lento aplica contrapresión en lugar de acumular fotogramas en RAM. Después, el coordinador:
- Codifica H.264/AAC con una conversión real de sRGB a BT.709 de rango limitado, mezclando el audio de la escena con una banda sonora opcional, desfases y ganancia.
- Inyecta los metadatos esféricos. Es GDScript puro, sin depender de la herramienta spatial-media de Google en Python: al MP4 van tanto el bloque XMP
GSphericalheredado como las cajassv3d/st3d. - Ejecuta FFprobe y una serie de comprobaciones técnicas, y solo un archivo verificado recibe el nombre final
video-360.mp4.
La cancelación se detiene en un límite de fotograma y mata el codificador. Los trabajos parciales conservan sus fotogramas, así que una captura completa puede recodificarse con otra compresión u otro audio sin volver a renderizar la escena.
Prueba un segundo antes de renderizar cuarenta gigas
Lo que más uso es un botón llamado Test 1 second. Renderiza el primer segundo con la receta exacta (escena, cámara, tamaño de cara, FPS, modo de almacenamiento, CRF), ejecuta toda la codificación y verificación, y extrapola tiempo y disco para la duración completa.
Medí lo honestas que son esas estimaciones con cuatro exportaciones de producción de un minuto en una RTX 3060 Ti:
| Renderer / salida | Pipeline completo | Captura PNG conservada |
|---|---|---|
| Forward+ / 4K | 710 s | 13,96 GiB |
| Mobile / 4K | 704 s | 14,33 GiB |
| Forward+ / 8K | 1.531 s | 41,64 GiB |
| Mobile / 8K | 1.482 s | 42,37 GiB |
En esas cuatro ejecuciones el estimador sobrestimó el tiempo entre un 11 y un 22 % y clavó el almacenamiento con un margen de ±1,5 %. La regla que salió de ahí: reserva unos 14 GiB por minuto de 4K y 42 GiB por minuto de 8K, y cuenta con unos 12 y 25 minutos de reloj respectivamente en esa máquina.
De dónde salen las costuras
Si unes seis viewports de Godot sin más, obtienes una esfera con las aristas del cubo a la vista. Tres causas distintas, tres arreglos distintos.
La exposición automática es por cara. Cada SubViewport mide su propia luminancia, así que la cara que contiene el sol sale más oscura que sus vecinas y aparecen bloques de brillo. El arreglo es una exposición de captura explícita, Fixed (authored), que desactiva la medición automática en los CameraAttributes que controla el worker sin tocar la animación de exposición ni nada más de la escena. En el fixture de luz en movimiento redujo la discontinuidad medida en los bordes un 99,7 % y coincidió fotograma a fotograma con un oráculo creado de forma independiente.

El glow se corta en el frustum. El bloom es un efecto de espacio de pantalla; un objeto brillante justo fuera de una cara no puede aportar su halo a esa cara, así que el halo muere en seco en el borde. El arreglo son los bordes de captura: renderizar cada cara con un campo de visión más amplio, mantener la densidad de píxeles original en el núcleo de 90° y fundir las regiones solapadas en el shader.
# capture_projection.gd
var border := ceili(face_size * percent / 100.0)
var texture_size := face_size + border * 2
var scale := float(face_size) / float(texture_size)
return {"texture_size": texture_size, "uv_scale": scale,
"fov": rad_to_deg(2.0 * atan(1.0 / scale))}
En el shader, unos pesos con smoothstep solo suben dentro de los frustums ampliados y se normalizan entre las caras (hasta tres) que ven una dirección. Sin borde, el camino original de una sola cara es exacto. Con el 12,5 % por arista recomendado, un núcleo de 2048 píxeles pasa a un objetivo de 2560 (un 56 % más de píxeles por cara) y los cortes de glow en aristas ecuatoriales, esquinas y arista superior bajaron un 99,6 %, un 99,2 % y un 99,5 % en Forward+.

La cámara llegaba un fotograma tarde. Una cámara hija de un BoneAttachment3D en un esqueleto animado llegaba siempre un fotograma por detrás de la malla. Skeleton3D aplica su pose final y las transformaciones de los attachments en la cola diferida de Godot, y mi rig copiaba la transformación de la cámara antes de que eso ocurriera. El arreglo fue una línea:
func _process(_delta: float) -> void:
# Leer la cámara después de la actualización diferida de Skeleton3D,
# pero antes de que Godot vuelque las transformaciones Node3D de este dibujado.
sync_camera.call_deferred()

Las partículas tenían sus propios problemas de arranque (el worker antiguo forzaba la raíz de la escena a INHERIT, lo que reanudaba escenas que se habían pausado a sí mismas), y la solución fue respetar el modo de proceso definido en la escena y exigir al menos dos fotogramas de calentamiento antes del primer fotograma entregado.
Revisar sin salir de Godot
No quería que el bucle fuera “renderizar, abrir VLC, entrecerrar los ojos”. El panel muestra el primer fotograma entregado en una vista esférica que puedes arrastrar o mover con el teclado. Play video prepara una copia local en Theora, limitada a 2K / 30 FPS para que el editor siga respondiendo, con reproducción, pausa, seek y sonido. El MP4 a resolución completa no se toca. Las exportaciones recientes recuerdan los últimos doce trabajos, y cualquier captura completa puede reabrirse y recodificarse.

Cuatro películas como fixtures de prueba
Las capturas de documentación de un cubo de calibración no convencen a nadie. Así que el repositorio incluye cuatro escenas completas con sus recetas de exportación:
- THRESHOLD: sesenta segundos renderizados a 7680×3840, cuatro mundos procedurales unidos por transiciones de velo esférico. La banda sonora a 80 BPM se sintetiza de forma determinista en Python y NumPy, sin samples; el audio ya renderizado va en el repositorio, así que el addon nunca ejecuta Python.
- AFTERGLOW: una sala disco a 4K donde el suelo de cristal se levanta con el bajo. Las luces leen los ataques de nota interpretados desde el archivo de cues de la partitura, y todos los shaders animados reciben un reloj explícito para que un salto hacia atrás sea una función pura del tiempo de la película.
- LUMEN: un observatorio orbital de 24 segundos construido para poner a prueba estelas de partículas nativas y vapor ascendente.
- UMBRAL: la instalación que empezó todo. Interactiva en Godot, una secuencia fija de 12 segundos en vídeo.
Cada una tiene sus propias comprobaciones en GDScript, así que sirven también como tests de regresión para cortes de cámara, sincronización musical y límites de efectos.
Ocho días en cifras
| Día | Qué entró |
|---|---|
| 6 sep | Versiones 0.1 a 0.6: rig, almacenamiento PNG, mezcla de audio, metadatos esféricos, control de espacio en disco, recuperación |
| 7 sep | Empaquetado beta, THRESHOLD, cambio de nombre desde Umbral360, primera pasada de onboarding |
| 8 sep | Reproducción en el editor, exportaciones recientes, bordes de captura, exposición fija, arreglo de la cámara en hueso, timing de partículas |
| 9 sep | Personajes GLB importados, modificadores de mirada, revisión de apariencia combinada |
| 10 sep | Partículas complejas, LUMEN, AFTERGLOW, captura con TAA/FSR y LightmapGI |
| 11 sep | Efectos combinados, cuatro exportaciones de producción sostenidas a 4K/8K |
| 12 sep | Nada publicado |
| 13 sep | Pulido del Studio, política de soporte en Windows, RC1, RC2, 1.0.0 pública |
El addon tiene unas 5.000 líneas de GDScript repartidas en treinta scripts. Los tests son más grandes que el producto: unas 9.000 líneas de revisores en GDScript y Python que decodifican cada fotograma entregado y lo comparan con referencias analíticas. El paquete 1.0 pasa 6.070 comprobaciones en cinco combinaciones de motor y renderer en Windows, más 52 comprobaciones nativas del editor, todo registrado en el log de validación del repositorio.
Lo que no hace
Solo monoscópico. El ODS estereoscópico, el audio ambisónico y el HDR quedan fuera de la 1.0. Una exposición automática compartida por toda la esfera necesitaría medir la luminancia en HDR antes del tone mapping, así que está aplazada; usa una exposición fija definida en la escena. Los reflejos en espacio de pantalla, la profundidad de campo y el historial temporal todavía pueden diferir en las aristas del cubo incluso con bordes.
Windows es la plataforma con soporte. Linux y macOS son experimentales: Linux con renderizado Mobile por software tiene una discrepancia sin resolver en sus primeros cinco fotogramas de historial temporal, y la cobertura en Mac es solo CI headless.
Lo que aprendí
La proyección es la parte fácil. Seis cámaras y un shader se hacen en un día. El producto es todo lo que impide que se vean las costuras, y cada costura tenía una causa física distinta.
Estima antes de jugarte 42 GiB. Una sonda de un segundo que ejecuta el pipeline real valió más que cualquier barra de progreso.
Verifica el archivo, no el render. Un MP4 precioso sin metadatos sv3d es un vídeo plano en YouTube. La inyección de metadatos y las comprobaciones con FFprobe son parte de la exportación, no un paso posterior.
El orden de actualización del motor no es un detalle. El bug de la cámara en el hueso era invisible hasta un corte seco, y el arreglo consistió en entender cuándo vuelca Skeleton3D. Lee el código del árbol de escena.
Convierte las demos en tests. THRESHOLD y AFTERGLOW existen porque necesitaba algo que mereciera la pena ver, pero además ejercitan lo que los fixtures sintéticos no cubren: cortes secos, transiciones entre mundos enteros y música que tiene que seguir sincronizada después de un salto.
Pruébalo
Godot360 Studio está en GitHub con licencia MIT. Necesita Godot 4.7.2 Standard y un FFmpeg/FFprobe nativos; ni Python, ni .NET, ni plantillas de exportación. Copia la carpeta del addon, activa el plugin, elige una escena y una Camera3D, pulsa Test 1 second y luego Render 360 video.
Antes, mira las cuatro películas en 360°. Arrastra para mirar alrededor. De eso iba todo.
Artículos relacionados
23 de marzo de 2026
Reacciones a impactos tipo Euphoria para Godot: código abierto en seis días
14 de septiembre de 2026
Baza: la Cuatrola con sus reglas intactas, en medio megabyte
31 de agosto de 2026
Hice fine-tuning de un modelo de 9B hasta convertirlo en especialista en Godot 4, y el juez era el propio motor