Cargando...

WiiUCamera

0

WiiUCamera

Convertí el gamepad de WiiU en una cámara funcional


Teniendo contexto, la Wii U tenía una cámara integrada que se usaba en juegos como Game & Wario o Nintendo Land. Pero... por alguna razón, Nintendo nunca lanzó una aplicación nativa simplemente para hacer uso de ella de forma no recreativa.

Me refiero a una app sencilla que te permita tomar fotos, grabar videos y guardarlos en tu SD, sin tener que entrar a menús lentos como el Mii Maker. Durante mucho tiempo, esa cámara estuvo de adorno para la comunidad Homebrew... ¡Hasta hoy!

Después de meses de desarrollo, ingeniería inversa y peleas con la gestión de memoria, les presento la versión 1.4.0 de: WiiUCamera.

1. Como inicie, el proceso y una casi cancelación

Antes de iniciar con todo, quiero agradecerle mucho a Gemini, que fue mi gran soporte en cuanto a búsqueda de información, una buenísima capacidad de comprensión tanto en lo técnico como en lo moral y como su préstamo para hacer vibe coding en casos extremos.

Volviendo al tema, vi que todo proyecto de homebrew comienza con la configuración del entorno. Para ese momento, no sabia que programar directamente sobre el hardware de la Wii U (usando solo GX2, la API gráfica nativa) iba a ser una tarea prácticamente imposible xd 👻

Necesitaba una librería que actuara como intermediario, que fuera amigable y, sobre todo, que ya tuviera soporte para la consola; la elección que encontré fue: SDL2 

Como inicie con devkitPro y pacman

Afortunadamente, se podía programar código en lenguaje de C++, sin embargo para compilarlo con la condición que la Wii U entienda (ya que tenia arq en PowerPC), no bastaba con un compilador normal. Según internet, debía usar devkitPro, que era dedicada para desarrollo en consolas de Nintendo.

El primer paso fue abrir la terminal y descargar las librerías necesarias. No solo necesitábamos el núcleo, sino también soporte para imágenes (PNG/JPG) y fuentes (TTF):

pacman -S wiiu-dev wut-tools wut ppc-sdl2 ppc-sdl2_image ppc-sdl2_ttf

El Primer Código: Renderizando la Cámara

La primera vez que intente hacer ver una imagen en tiempo real en el gamepad no fue fácil, fue gestión de texturas. La lógica que utilizamos se basaba en el concepto de Texture Streaming.

A diferencia de una imagen estática (como un fondo), la cámara envía datos nuevos 30 o 60 veces por segundo. Para manejar esto, utilizamos una textura especial de SDL configurada como SDL_TEXTUREACCESS_STREAMING. Aquí está el fragmento de código real de esas primeras pruebas y qué significa cada parte: 

PD: Sorry, las primeras veces olvide de hacer backups esenciales, pero con ayuda de Gemini pude reconstruir lo que fue en su momento.

// 1. Inicialización de Video
// Esto prepara la Wii U para mostrar gráficos en el GamePad y TV
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    WHBLogPrintf("Error al iniciar SDL: %s", SDL_GetError());
    return -1;
}

// 2. Creación de la Ventana y el Renderizador
// El renderer es el "pincel" que dibuja en la pantalla
window = SDL_CreateWindow("WiiUCamera", 0, 0, 1280, 720, 0);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

// 3. La Textura de la Cámara (El paso clave)
// Creamos un espacio en memoria reservado para recibir los datos crudos.
// FORMATO: ARGB8888 (Alpha, Rojo, Verde, Azul)
// ACCESO: STREAMING (Permite actualizarla constantemente desde CPU)
SDL_Texture* camTexture = SDL_CreateTexture(renderer, 
                                            SDL_PIXELFORMAT_ARGB8888, 
                                            SDL_TEXTUREACCESS_STREAMING, 
                                            640, 480); // Resolución VGA nativa

Una vez configurado esto, el "bucle principal" (Main Loop) se encargaba de pedirle al hardware los nuevos datos de la cámara y "inyectarlos" en nuestra textura:

while (appRunning) {
    void* pixels;
    int pitch;

    // Bloqueamos la textura para escribir en ella (Lock)
    SDL_LockTexture(camTexture, NULL, &pixels, &pitch);
    
    // ... Aquí copiamos los datos del hardware (memcpy) ...
    
    // Desbloqueamos (Unlock) para que la GPU pueda usarla
    SDL_UnlockTexture(camTexture);

    // Dibujamos el resultado en pantalla
    SDL_RenderCopy(renderer, camTexture, NULL, NULL);
    SDL_RenderPresent(renderer);
}

Ver ese primer cuadro de video, aunque fuera con baja resolución y sin filtros, fue el momento donde podia confirmar que funcionaba bien. Confirmamos que podíamos manipular el feed de video píxel a píxel, lo que sentó las bases para lo que vendría después

2. Diseñando la Interfaz: Relación de imagen y el Táctil

Una vez que tuvimos la imagen, nos topamos con el primer problema de diseño: La Relación de Aspecto. Las caras parecían aplastadas y deformes, debía corregir eso en primer lugar

El dilema de la resolución (4:3 vs 16:9)

Técnicamente, esto sucede porque el sensor de la cámara entrega una imagen en resolución VGA (640x480), que es un formato cuadrado (4:3), similar a las teles viejas. Sin embargo, la pantalla del GamePad de Wii U tiene una resolución de 854x480, que es panorámica (aprox 16:9).

Si usaba SDL_RenderCopy(..., NULL), la librería intenta llenar todo el espacio, estirando esos 640 píxeles hasta llegar a 854. ¿Y que hice? Pues no mucho, decidí renderizar la cámara en su tamaño original pegada a la izquierda.

Matemáticas simples:
Ancho GamePad (854) - Ancho Cámara (640) = 214 píxeles sobrantes.

En lugar de dejar ese espacio de forma hueca, decidí convertir ese espacio sobrante a la derecha en una Barra de Herramientas. Así nació el diseño icónico de la app: la cámara limpia a la izquierda y los botones de control a la derecha.

Creando un Menú desde cero 

Hoy en día existen librerías para hacer menús, pero en ese momento, tuve que crear la lógica de botones "a mano" (porque no sabia, no me juzguen)

Aunque, con respecto a todo el menú fue lo mas fácil de este proyecto, lo que se me complico fue el hecho de implementar el táctil a la aplicación

(Gemini me ayudo a explicar mucho mejor esta parte)
🤖: La pantalla táctil de la Wii U no entrega coordenadas de píxeles (0 a 854), sino valores analógicos del digitalizador (0 a 4096 aprox). Tuve que implementar una función de mapeo para traducir "dónde tocó el dedo" a "en qué píxel está".

// Lógica primitiva de botones (Versiones antiguas)
// Si el toque está dentro del cuadrado del botón... ¡Acción!
if (touchX > 650 && touchX < 800) { 
    if (touchY > 50 && touchY < 150) {
        TomarFoto(); // Botón superior
    }
    else if (touchY > 200 && touchY < 300) {
        GrabarVideo(); // Botón medio
    }
}

Este sistema era rudimentario y a veces fallaba si no tocabas el centro exacto, pero funcionaba. Fue la base sobre la que construimos el menú moderno que ven hoy, mucho más pulido y responsivo.

3. Editor de fotos (💀)

Despues de que la gente me recomendara hacer una nueva version, ya que la primera a nivel global era buena pero poco intuitiva, tuve que hacer una uptade de emergencia para llenar esos errores.

En eso, tuve la idea de crear un Editor de Fotos. La idea era simple: dibujar en la pantalla y guardar el resultado. Pero cuando abrimos el archivo guardado en la PC, nos encontramos con un desastre:


Fig 1. Corrupción de datos por desalineación de memoria (Efecto raro).

¿Qué pasó? La GPU de la Wii U (AMD Latte) no almacena las texturas en la memoria VRAM de forma lineal (píxel 1, píxel 2...). Utiliza Tiling (bloques desordenados) y agrega Padding (relleno invisible). Al intentar leer eso como una línea recta para guardar el archivo, la imagen se rompía y los colores se invertían.

En lugar de pelear contra la GPU y sus formatos complejos, cambiamos el enfoque: "Si la GPU es complicada, usemos la RAM". Así nació la arquitectura de doble buffer, a la que prácticamente era como meter un minion ahi adentro xd

La idea es mantener dos lienzos existiendo simultáneamente en el código:

  • El Maestro (GPU): Una SDL_Texture que se muestra en la pantalla del GamePad. Es rápida, permite ver lo que haces a 60FPS.
  • El Minion (RAM): Una SDL_Surface oculta en la memoria del procesador. Es una copia matemática exacta, pixel por pixel, en formato lineal (RGBA8888).

(Otra vez Gemini ayudando con la redacción de aqui abajo)

Implementación del Trazado Dual

Modificamos el motor de dibujo. Ahora, cuando tocas la pantalla, el pincel pinta en dos lugares a la vez. Al usuario se le muestra la versión GPU, pero los datos reales se escriben en el "Minion" en la RAM.

// Función DibujarTrazoDual: Escribe en GPU y RAM simultáneamente
void DibujarTrazoDual(SDL_Renderer* renderer, SDL_Surface* surfaceRAM, int x, int y) {
    // 1. Dibujar en GPU (Para que el usuario lo vea)
    SDL_RenderDrawPoint(renderer, x, y);

    // 2. Escribir en RAM (El "Minion" toma nota para el guardado)
    // Accedemos directamente a la dirección de memoria lineal
    // Esto evita el problema del Tiling de la GPU
    Uint32* pixel = (Uint32*)((Uint8*)surfaceRAM->pixels + y * surfaceRAM->pitch + x * 4);
    *pixel = color_actual; 
}

Al momento de guardar, ignoramos por completo la pantalla. La función de guardado va directamente a la memoria RAM (el Minion), lee los bytes limpios y ordenados, y genera el archivo BMP. Resultado: Ya no hay más errores.

4. Próximamente mas bloques

(...)

Conclusión

El desarrollo de WiiUCamera ha sido una bestia en su creación. Pero con perseverancia pude hacer un proyecto relevante para cada uno de ustedes!
Espero que quienes lo tengan descargado, disfruten de estas funciones y muchas más que se vendrán! :)

Tal vez te interesen estas entradas

No hay comentarios