- Es una implementación que logra reproducir Bad Apple!! en tiempo real sobre el lento motor de Minecraft, con la misma resolución de 512×384, 20 fps y escala de grises que el original
- La clave es hacer que un structure block en modo
LOADse reemplace a sí mismo por la estructura del siguiente fotograma, y superar el límite de 10 Hz de redstone usando relojes desfasados - Si se usara un bloque por píxel, habría que actualizar 768 chunks a 20 Hz, así que se redujo la carga haciendo que un bloque representara más información visual mediante texturas, modelos y blockstates personalizados del resource pack
- El cuello de botella no estaba en redstone ni en la iluminación, sino en
setBlocky el manejo de eventos, así que se redujo la cantidad de actualizaciones con codificación delta y reemplazos de sprites 4×4 que cambian con frecuencia - La calidad final se refinó con escala de grises de 6 colores, dithering de ruido azul y corrección del ruido de compresión con pérdida, aunque no se resolvió por completo el problema de reducir el original de 30 fps a 20 fps
Condiciones para una reproducción cercana al original
- El objetivo era reproducir Bad Apple!! dentro de Minecraft lo más parecido posible al original
- El video se reproduce a 20 fps
- La resolución es 512×384, igual que la animación original
- No es blanco y negro puro, sino escala de grises
- En CPU y GPU modernas puede verse a 20 fps reales, sin grabar primero ni acelerar la reproducción
- No usa command blocks
- Se excluyeron de las condiciones los atajos fáciles
- No se usan mods como método de implementación; solo se permiten mods de optimización para pruebas en hardware modesto
- No se usan command blocks,
/setblockni datapacks - Tampoco se usan texturas animadas
- En dispositivos de bajos recursos puede hacer falta VulkanMod o Sodium
- Conviene evitar C2ME porque su guardado automático provoca pérdida de rendimiento
Límites con los que chocaron las implementaciones previas
- La mayoría de las implementaciones anteriores de Bad Apple!! se quedaban en pantallas pequeñas o renderizado lento
- El intento de catlord5 logró 512×384 y 30 fps, pero renderizaba unas 40 veces más lento
- Algunas implementaciones en tiempo real basadas en redstone llegaban apenas a 5 fps o a resoluciones bajas
- Minecraft es lento no solo en su motor de simulación, sino también en el de renderizado, y la carga crece conforme aumenta el número de chunks
- Un chunk de 16×16×16 tiene un costo alto de rerenderizado sin importar su contenido
- Reducir la cantidad de chunks que cubre la pantalla es clave
- Redstone es difícil de usar tal cual para reproducir a 20 fps porque los generadores de reloj prácticos normalmente emiten una señal de 10 Hz
- El redstone dust es un componente principal sin retraso, pero tiene un costo alto en rendimiento
Experimentos con métodos de almacenamiento de datos
- El método de hopper line es la forma típica de almacenamiento: sacar ítems guardados en cofres con hoppers y leerlos con comparators
- Los hoppers transfieren ítems cada 0.4 segundos, así que el framerate máximo es de 2.5 fps
- Para 20 fps harían falta muchas líneas de hopper en paralelo, y eso trae problemas de mosaico y costo de simulación
- También se probó un método con jukebox y music discs para leer valores de 1 a 15 y almacenar casi 4 bits
- Si los bits se expanden sobre el eje temporal, se podría apuntar a 20 fps con 2 hoppers
- Pero la lógica de redstone y el dust necesarios para desplazar bits eran demasiado lentos y ni el prototipo rendía bien
- La repeater delay line es un método simple: poner una línea de repeaters por píxel y sacar el valor en el momento adecuado
- Permite un mosaico de píxel 1×1
- Pero incluso implementaciones previas mucho más pequeñas necesitaban una aceleración de 20 veces, así que no bastaba
Avanzar fotogramas con structure blocks
- Los structure blocks (structure block) permiten guardar un área con
SAVEy cargarla en otra ubicación conLOAD- No se pueden obtener en survival, pero tampoco sustituyen todo con un solo comando como un command block
- Pueden activarse con señal de redstone
- La clave es que un structure block en modo
LOADpuede cargar un área que se superpone consigo mismo- Si el structure block actual se reemplaza por el siguiente, cada activación carga el siguiente fotograma
- Si solo se reemplaza sin más, el nuevo structure block detecta de inmediato la señal de redstone cercana y se activa recursivamente
- Esa recursión continúa hasta chocar con el límite duro de Minecraft
- Incluso se detiene antes de que termine el apagado del redstone dust, dejando estados de energía anómalos
- Se evitó la activación recursiva añadiendo con repeaters un retraso de 1 redstone tick
- Las estructuras se crearon con
/tick freezepara guardar los repeaters en estado apagado - Después de cargarse, avanzan a la siguiente estructura un redstone tick más tarde
- Las estructuras se crearon con
Manejo de ticks para llegar a 20 fps
- En Minecraft existen game ticks y redstone ticks
- El motor del juego recalcula la física a 20 Hz
- Los componentes de redstone normalmente programan actualizaciones en unidades de 0.1 segundos, es decir, 10 Hz
- Los eventos reales se procesan cada 0.05 segundos, y según el momento en que entra la entrada del usuario, la respuesta de redstone también puede desplazarse a esa fase
- Para lograr 20 fps se usan cuatro estructuras
- La estructura roja y la amarilla forman un reloj de 10 Hz
- La azul y la verde forman otro reloj de 10 Hz
- Si ambos relojes arrancan con distinta fase, los cuatro colores se muestran alternadamente en el mismo lugar a 20 Hz
- Para iniciar ambos relojes con una diferencia de fase estable, se aprovechó un bug antiguo por el cual un pistón tarda 3 game ticks cuando empuja un redstone block mediante entrada directa del usuario
Técnicas con resource packs para reducir la cantidad de chunks
- Si se usa un bloque por píxel, una pantalla de 512×384 ocupa 24 chunks de alto y 32 de ancho
- Eso significa 768 chunks que tendrían que actualizarse constantemente a 20 Hz
- Además choca con la distancia máxima de renderizado de 32 chunks en vanilla, así que no es realista
- Con texturas personalizadas del resource pack se alteraron las texturas de varios bloques para meter varios subpíxeles dentro de un solo bloque
- 16 variantes de bloque equivalen a 4 bits
- Con 256 bloques y colores adicionales se puede representar escala de grises en vez de blanco y negro
- Con este método, la resolución en bloques se redujo 4 veces, quedando en 256×192 bloques
- La cantidad de chunks a actualizar baja a 192
- Aun así, seguir actualizando a 20 Hz sigue siendo una carga muy grande
Cola de renderizado y codificación delta
- El motor de renderizado de Minecraft prioriza las actualizaciones de chunks cerca del jugador
- Varios hilos generan chunks a la vez, y toman de la cola de actualizaciones empezando por los más cercanos al jugador
- Si los N chunks más cercanos se actualizan constantemente a 20 Hz, puede pasar que solo esos se procesen y los demás ni siquiera se rendericen
- Con Spark se confirmó que el cuello de botella eran las actualizaciones generales, más que redstone o la iluminación
- En particular,
setBlocky los event handlers eran el problema
- En particular,
- La solución fue reducir la cantidad de actualizaciones, aplicando codificación delta para cambiar solo los bloques distintos entre fotogramas
- Como la mayoría de los fotogramas no cambian tanto, en teoría había margen para mejorar el rendimiento
- Como un structure block solo puede cargar 48×48×48 bloques a la vez, la pantalla se dividió en 6×4 subpantallas de 48×48
- El prototipo inicial tardaba unos 7 minutos por ejecución, y requería
/tick freeze, 24 botones y/tick unfreeze, pero sí funcionaba- Solo con codificación delta seguía sin ser suficientemente rápido
Optimización con modelos y blockstate
- Los modelos de Minecraft definen la forma de los bloques como cuboids, y sus coordenadas pueden especificarse más allá del rango
(0,0,0)a(16,16,16), concretamente de -16 a 32- Si se configura bien, un bloque puede renderizarse como si fuera hasta 3 veces más grande, reemplazando un área de 9 bloques
- No hay suficientes bloques para cubrir todas las combinaciones, así que solo sirve en casos frecuentes como una zona 6×6 completamente negra
- De unos 600 bloques utilizables, 256 se usaron para la representación base de subpíxeles, y parte de los restantes se dedicó a optimizaciones
- El enfoque final fue dividir la pantalla en celdas de bloques 2×2 y tomar el sprite de 4×4 píxeles de cada celda como candidato para reemplazarlo con un solo bloque
- Se calcula la diferencia entre dos fotogramas consecutivos
- Se suma puntuación a las versiones antes y después de las celdas que cambian
- En escenas de movimiento rápido, se da mayor puntuación a las celdas donde cambian más píxeles
- A partir de los sprites con mejor puntuación se asignan bloques disponibles
- Para superar el límite del número base de bloques, se exploraron los
blockstates- Bloques como
oak_logpueden elegir modelos distintos según sus propiedades - Bloques como
grindstonepermiten usar varias combinaciones de propiedades como clave
- Bloques como
- Al extraer variantes de blockstate de los assets base y filtrar las propiedades que no se pueden controlar, la cantidad de modelos accesibles subió de unas 600 a 1700
- La cantidad de colores aumentó a 6
- La cantidad de bloques optimizados subió a 400
Audio y dispositivo de arranque
- La música se manejó reemplazando con un resource pack el sonido de un music disc
- La duración de reproducción del disco sigue siendo fija aunque se cambie el audio
- Se usó el disco Relic, que es el más cercano a la duración de “Bad Apple!!”
- También se modificó
assets/minecraft/lang/en_us.jsonpara que el subtítulo del juego mostrara “Now Playing: Bad Apple!!”
- Se conectaron button, dropper, hopper y jukebox para que, con una sola pulsación, el disco entrara en el jukebox y empezara a reproducirse
- Cuando termina, el hopper devuelve el disco al dropper para dejarlo listo para la siguiente reproducción
- Por culpa de la quasiconnectivity, la señal de redstone emitida por el jukebox durante la reproducción desordenaba el estado del hopper y del dropper
- Se hizo que el redstone dust actualizara el dropper al pulsar el botón
- Luego, un repeater vuelve a activar el dropper un redstone tick después para insertar el disco
- Para llevar la señal desde la posición de observación hasta el mecanismo detrás de la pantalla, se creó un cable instantáneo basado en structure blocks
- Un structure block carga un redstone torch energizado para el siguiente tramo, y en el siguiente tick se apaga por culpa de un redstone block
- Ese pulso activa el siguiente structure block y transmite la señal
- Como un structure block puede abarcar hasta 48 bloques, puede mandar la señal de inicio a la cuadrícula de subpantallas de 48×48
- Los aproximadamente 150 bloques desde el observador hasta la parte trasera de la pantalla se conectaron con una estructura aparte que puede reiniciarse
- Esta transmite la señal generando y borrando en cadena combinaciones de structure block y redstone block
- Como esa estructura era difícil de guardar manualmente en creative, los archivos de estructura se generaron con una librería de Python
- El mecanismo final quedó integrado en una caja de 4×2×3 que se opera con un botón exterior
Preprocesamiento de fotogramas y calidad de video
- Las tareas pendientes de preprocesamiento eran reducir un video full color a 6 colores y convertir un video de 30 fps a 20 fps
- Bad Apple!! no usa solo blanco y negro puro; en varias escenas también usa escala de grises
- Motion blur
- Objetos con distintos niveles de brillo
- Gradientes en transiciones de escena
- Efectos como fuego, sol, sombras y ondulaciones del agua
- Si simplemente se redondea al color más cercano, aparece banding
- El dithering lo mitiga convirtiendo colores intermedios no representables en patrones de colores representables adyacentes
- Un dithering global de alta calidad puede producir resultados muy distintos entre fotogramas
- El ojo humano nota fácilmente esas inconsistencias
- Y además genera demasiadas actualizaciones para que Minecraft las soporte
- Los dithering locales como Bayer dithering eran estables pero de baja calidad, así que el ordered dithering basado en blue noise fue el punto medio
ffmpegno soporta dithering de blue noise- Se recortó a 512×384 la textura de blue noise de Christoph Peters y se aplicó con un script en Rust
- El video original provenía de una subida a Niconico con compresión con pérdida, así que tenía ruido
- El ruido en las zonas negras y blancas se volvía más visible después del dithering
- Se corrigió redondeando a negro los casi negros, a blanco los casi blancos, y distribuyendo los tonos intermedios de forma que mantuvieran continuidad
- El problema de bajar de 30 fps a 20 fps no se resolvió por completo
- Si se descarta cada tercer fotograma, el intervalo del movimiento cambia entre fotogramas impares y pares, y eso se nota visualmente
- La cantidad de actualizaciones también varía con un patrón dentado
- Los videos de Bad Apple!! a 60 fps que hay en línea estaban escalados con IA o herramientas automáticas y tenían muchos artefactos en cambios de escena rápidos
Resultado y trabajo posterior
- La implementación empezó en resolución 48×36 y 2 colores, pasó por 128×96 y 10 colores, luego por 256×192, y finalmente llegó a 512×384 y 6 colores
- También hubo intentos de reproducir la música con note blocks, pero para lograr buena calidad habría hecho falta un proyecto aparte, así que se abandonó
- Se creó una técnica de structstone para usar structure blocks como si fueran redstone, y hasta se empezó un prototipo de computadora con esa idea
- Durante el desarrollo se usaron
ffmpeg,mpv, el image crate de Rust, código descompilado de Minecraft y técnicas para minimizar el tamaño del directorio del mundo - Todo el trabajo llevó más de un mes, y al hacerlo en colaboración con amigos terminó siendo una experiencia de resolver problemas de una forma distinta a la de un proyecto común
1 comentarios
Opiniones de Hacker News
Aprendí mucho más de gráficos por computadora de lo que esperaba, y mis elogios para el autor.
Una pequeña corrección: la imagen que el autor llamó “The sun” en realidad es una escena en la que Eirin [0] mira la luna. En esa escena [1], Eirin extiende la mano hacia la luna de la que fue exiliada, pero duda y la retira; en la escena siguiente, Kaguya [2] también extiende la mano hacia la luna, pero no duda. Según la wiki de Touhou, el plan de robarse la luna fue de Eirin, así que no estoy muy seguro de qué significa exactamente ese simbolismo.
[0] https://en.touhouwiki.net/wiki/Eirin_Yagokoro
[1] https://youtu.be/FtutLA63Cp8?t=99
[2] https://en.touhouwiki.net/wiki/Kaguya_Houraisan
Eirin eligió cortar deliberadamente la conexión con la luna para proteger a Kaguya.
No entiendo muy bien por qué Bad Apple se está convirtiendo de facto en el Hello World del renderizado gráfico, pero verlo en tiempo real es divertido.
También vi esta demo que muestra hipermedia de alta tasa de cuadros con Bad Apple: https://data-star.dev/examples/bad_apple
En muchos sentidos, Touhou, a diferencia de fandoms anteriores, se acercó al prototipo del fandom moderno de Internet, y los videos de Bad Apple no se bajan aunque usen el mismo audio.
Segundo, el formato de teatro de sombras es fácil de reconocer por más baja que sea la resolución. Incluso he visto un caso con una cuadrícula de 3x3. Además, al ser en blanco y negro, es decir, solo dos colores 1/0, con saber lo básico a nivel “Hello World” es muy fácil convertir los fotogramas a casi cualquier formato imaginable.
Funciona sobre una CPU completamente programable hecha con Redstone. Las especificaciones de IRIS Computer son: CPU personalizada de 16 bits, 8 kB de RAM, 64 kB de ROM, 1 kB de ROM de texturas, pantalla de 96x64 píxeles y 16 colores, unidad de punto flotante (add/sub/mult/div/sqrt), reloj de 173 ticks de Redstone, sin aceleración por hardware para gráficos 3D, ejecuta programas escritos en URCL y, gracias al servidor MCHPRS, corre a 1 millón de ticks por segundo, por lo que la velocidad de reloj es de 5.8 kHz.
Más que música con un compás y medidas constantes, tiene mucho más sentido si imaginas que, mientras MS-DOS arranca, conectas los bits pares de un bus de 16 bits a instrumentos y los escuchas. Como el desarrollador de los juegos Touhou componía mientras hacía solo juegos hardcore de disparos para PC-88/PC-98, sin formación formal en teoría musical, parece un resultado natural; por eso puede sentirse más familiar para un ingeniero de hardware embebido que la música común.
Otro factor fue la comunidad nicovideo.jp / nico-tech, que se desarrolló a partir de la cultura de 2ch/futaba. Usuarios con una pericia muy superior a cualquier recompensa o ambición monetaria, muchos de ellos estudiantes de STEM en esa época, volcaban tecnología en remixes por diversión. Magos de FPGA anónimos, expertos en drivers de motores y editores de video aparecían de repente, dejaban videos alucinantes y se iban; era realmente absurdo. En cierto momento, Maker Faire Tokyo, para quedar bien ante desarrolladores web con camiseta que querían guardar las apariencias, llegó a poner a las personas sospechosas de usar camisas de vestir de nico-tech en una zona aislada de un recinto separado. Eso fue irónico, llevó al nacimiento de los encuentros de nico-tech y no volvió a repetirse. Esa densidad absurda de calidad y cantidad de contenido creó la inercia del PV de Bad Apple!!.
El último factor clave fue que el PV era monocromo, estrictamente en escala de grises. Probablemente por eso terminó siendo este video y no algún otro de la época dorada de nicovideo.jp.
1: https://www.youtube.com/watch?v=Yw5HTeT_dis
Por sí mismo también es una obra de arte atractiva e impactante, así que creo que tiene muchas cualidades que les gustarían especialmente a la gente de la demoscene.
También hay otro clip de video con color: https://youtu.be/mgfwwqwxdxY
“¡Bad Apple sobre cualquier cosa!” es una de mis modas geek favoritas.
Cuando lo vi por primera vez en Genesis/Mega Drive, me sorprendió que fuera posible en un hardware tan limitado. Me gusta ver cómo hacen nuevos ports para cosas con poca potencia. No creo ser lo bastante listo para hacer uno yo mismo, porque no soy bueno en programación de bajo nivel, pero respeto muchísimo a quienes sí pueden.
La parte de “esta recursión termina cuando Minecraft llega a un límite duro y, por suerte, se genera un bloque amarillo en vez de uno rojo” me recuerda al viejo glitch de supresión de actualizaciones (https://mcdf.wiki.gg/wiki/Java_Edition:Update_Suppression)
La más complicada population suppression (https://mcdf.wiki.gg/wiki/Java_Edition:Population_Suppressio...) también puede, de forma similar, dejar el motor del juego en un estado con glitches y hacer que los bloques caigan de inmediato.
Mi algoritmo de dithering favorito para video es Yliluoma dithering: https://bisqwit.iki.fi/story/howto/dither/jy/
Es especialmente útil para contenido en escala de grises, porque encontrar la matriz de dithering óptima dentro de la paleta disponible es una operación exacta simple, y el resultado se puede poner en una tabla de consulta para usarlo en renderizado en tiempo real. Personalmente, creo que se ve mucho mejor que Bayer o el dithering aleatorio, sobre todo en gradientes.
Decir algo como “Redstone dust es prácticamente el único componente que no genera retrasos de ticks, pero es muy lento. Parece que en Mojang no hay nadie que sepa de algoritmos de grafos” es exagerado.
Desde el artículo que enlazaba el post original como fuente de información, ya se volvió mucho menos lento, y hubo muchas mejoras en los últimos 3 años, incluida una reciente. Mojang recibe muchísimas críticas de todos lados. La razón por la que tomó tanto tiempo hacer que Redstone fuera menos lento es que, si tocan Redstone aunque sea un poco, la comunidad grita, y si hacen algo que no sea agregar funciones nuevas, la comunidad también grita, así que el incentivo para hacerlo baja. Enojarse en internet y decir que no saben algoritmos de grafos no ayuda. Mojang ha contratado varias veces a talentos muy destacados de la comunidad de Minecraft, como Panda4994, Kingbdogz y Gnembon, y tiene la experiencia técnica para hacer lo que quiere. Lo que no tiene es tiempo y presupuesto infinitos. Mantener y sincronizar al mismo tiempo una base de código Java de 15 años y una enorme app C++ multiplataforma es realmente difícil, así que ojalá se les tuviera un poco de paciencia. Estoy cansado del odio que les cae de todas partes todo el día, y me gustaría que simplemente pudiéramos decir que Minecraft es genial.
Antes me molestaban más, pero después me di cuenta de que no eran tanto arrogancia como una ingenuidad propia de gente de 16 a 21 años con poca experiencia “profesional”.
Tampoco parece que les importe la compatibilidad entre versiones.
Desde la secundaria ya no me metí tanto en Minecraft como para construir mecanismos serios con Redstone.
Ahora juego con amigos unas cuantas veces al mes, cuando de repente me vuelven las ganas de construir algo y explorar. Al ver el ecosistema actual de Redstone, está tan cambiado que resulta irreconocible, y me pregunto si sentiré algo parecido mientras voy convirtiéndome poco a poco en ingeniero de software senior. Imagino que, con el paso del tiempo, al ver stacks que no he tocado en años de trabajo práctico, me sorprenderá lo rápido que cambia la tecnología y las cosas nuevas que la gente construye sobre ella.
No estoy de acuerdo con la reacción de “Y… ¿eso es todo? En retrospectiva, el resultado parece casi trivial de lograr, y uno se pregunta por qué nadie lo había hecho antes”.
Este es un gran registro de desarrollo y una pequeña lección sobre cómo dividir una tarea que parece abrumadora en partes casi imposibles, pero posibles. Me encantó. Como referencia, esta implementación renderiza Bad Apple en Minecraft vanilla a 20 fps con una sola textura personalizada y unas cuantas definiciones de objetos personalizadas cambiadas para permitir más texturas. El resto es muy exótico, pero vanilla.
Me parece curioso que se haya puesto tanto esfuerzo en el video en sí.
Cuando yo termino una implementación de Bad Apple, normalmente quedo tan agotado que no me queda margen para pensar en dithering o tasa de cuadros; simplemente la paso por ffmpeg y la doy por terminada.
También vale la pena ver Bad Apple hecho como mundo de Minecraft: https://www.youtube.com/watch?v=RN3QW9SVnds