← CC2018

Sprites

Semestre 02, 2026

El mundo tiene algo más que paredes

  • El motor solo sabe dibujar paredes: cada columna es una estaca que sale de un rayo.
  • Un enemigo o un barril está en medio del pasillo y no ocupa una celda completa.
  • La solución de la época fue dibujarlos como una imagen plana que siempre mira al jugador.
Enemigos y objetos dibujados como imágenes planas dentro de un motor raycaster

El sprite es una imagen suelta en el mundo

Tiene una posición en el mundo: dos números, igual que el jugador.
No tiene orientación propia. No importa desde qué lado se le mire.
Se dibuja como un rectángulo de pantalla, no como una columna.
Su imagen trae zonas transparentes: solo se pinta la silueta.

Las tres preguntas de todo sprite

En qué columna de la pantalla queda su centro.
Cuántos píxeles mide, según qué tan lejos está.
Qué partes suyas quedan tapadas por una pared o por otro sprite.
Las dos primeras son trigonometría. La tercera es lo nuevo de esta lección.

La técnica del billboard

  • Un billboard es un rectángulo que siempre queda de frente a la cámara.
  • Como nunca se ve de canto, no se nota que es plano.
  • En el raycaster ni hace falta rotarlo: se dibuja directo en coordenadas de pantalla.
Rectángulos de nubes orientados siempre hacia la cámara

Los sprites direccionales

  • Un enemigo que siempre se ve de frente delata el truco: nunca se le ve la espalda.
  • La solución clásica fue guardar ocho imágenes, una cada 45 grados.
  • Solo cambia cuál imagen se muestrea; el resto del proceso queda igual.

        las ocho vistas de un enemigo

              atrás
                7
          6           0
       izq              der
        5      ●        1
                                 según el ángulo
          4           2          entre su frente
                3                y el jugador
             de frente

El vector del jugador al sprite

La diferencia de posiciones $$ d_x = sprite_x - jugador_x $$ $$ d_y = sprite_y - jugador_y $$

No importa dónde está el sprite, sino dónde está respecto al jugador.


                            ▓ sprite (600, 320)
                          ╱ │
                        ╱   │
                  d   ╱     │  dy = 70
                    ╱       │
                  ╱  θ      │
     jugador  ●───────────────
     (250,250)     dx = 350

La distancia y el ángulo

Las dos cantidades $$ d = \sqrt{d_x^2 + d_y^2} $$ $$ \theta = \operatorname{atan2}(d_y,\ d_x) $$
  • La distancia decide el tamaño en pantalla.
  • El ángulo decide la posición horizontal.

La distancia y el ángulo con números

Jugador en (250, 250), sprite en (600, 320)
dx = 600 - 250 = 350
dy = 320 - 250 = 70
d  = √(350² + 70²)
   = √127400 = 356.93
θ  = atan2(70, 350) = 11.3099°
Tiene que ser el arcotangente de dos argumentos. Con una sola división se pierde el cuadrante y los sprites de atrás salen al frente.

El desvío respecto a la mirada

El mismo beta de las paredes $$ \beta = \theta - a $$

Usar la misma cantidad que las paredes es lo que mantiene pegados los sprites a la escena.


              dirección de vista a
                      │
                      │      ▓ sprite
                      │    ╱
                      │  ╱  β
                      │╱
                      ●
                   jugador

        β < 0 a la izquierda, β > 0 a la derecha

La normalización del ángulo

  • Los ángulos dan la vuelta, así que la resta puede salirse del rango útil.
  • Un desvío de 340 grados y uno de menos 20 son el mismo ángulo.
  • Hay que traerlo al rango de menos 180 a 180.
La corrección $$ \beta \leftarrow \left((\beta + 180°) \bmod 360°\right) - 180° $$

El caso que delata la falta de normalización

El jugador mira casi al norte
jugador mira a  350°
sprite está en   10°
β crudo = 10 - 350 = -340°
β normalizado    = +20°
Con el valor crudo el sprite se descarta. Con el normalizado queda a 20 grados del centro, bien dentro de la vista.

El filtro de visibilidad

La prueba $$ |\beta| \le \frac{FOV}{2} + margen $$
  • El que no pasa se descarta antes de calcular nada más.
  • Es el primer ahorro grande del proceso.

El margen del filtro

Por qué hace falta

Un sprite con el centro apenas afuera del cono todavía puede tener media silueta adentro. Sin margen se corta de golpe en la orilla.

Cuánto

El semiancho angular del propio sprite. En el ejemplo son 160.83 columnas, que a 0.046189 grados por columna dan 7.43 grados.

Los otros descartes

Detrás del jugador: el desvío pasa de 90 grados y no hay nada que dibujar.
Distancia casi cero: el tamaño se dispararía al infinito.
Más lejos que el alcance útil: mediría menos de un píxel.
Cada descarte temprano ahorra el rectángulo completo, que es la parte cara.

La columna central del sprite

El mapeo del motor $$ i_{centro} = \frac{\beta + FOV/2}{\Delta\beta} $$
  • Es la fórmula del desvío por columna, despejada al revés.
  • Antes se iba de la columna al ángulo; ahora del ángulo a la columna.

La columna central con números

FOV de 60°, 1300 columnas
β      = 11.3099°
Δβ     = 0.046189°
centro = (11.3099 + 30) / 0.046189
        = 894.4
Hay que usar el mismo mapeo que las paredes. Mezclar dos convenciones despega los sprites del piso.

El mapeo angular contra el de tangente


       β        columna angular        columna con tangente      diferencia

    -30.0°                 0.0                        0.0             0.0
    -15.0°               324.8                      348.3            23.5
      0.0°               649.5                      650.0             0.5
    +15.0°               974.3                      951.7            22.6
    +30.0°              1299.0                     1300.0             1.0
Este motor lanza rayos a ángulos parejos, así que su mapeo es lineal. A media pantalla los dos se separan más de 20 píxeles.

El tamaño en pantalla

La misma fórmula de la estaca $$ corregida = d\cos\beta $$ $$ lado = \frac{SPRITE\_SIZE}{corregida}\cdot d_{plano} $$
  • Un sprite se proyecta igual que una pared.
  • Es un objeto de cierto alto en el mundo, visto a cierta distancia.

El tamaño con números

Sprite de 100 unidades de alto
d         = 356.93
cos β     = 0.98058
corregida = 350.00
lado      = (100 / 350) · 1125.83
          = 321.67 px
Sin corregir el ojo de pez el sprite crecería al caminar hacia las orillas de la pantalla, sin haberse acercado.

Las cuatro orillas del rectángulo

Horizontales $$ izq = i_{centro} - \frac{lado}{2} $$ $$ der = i_{centro} + \frac{lado}{2} $$
Verticales $$ arr = horizonte - \frac{lado}{2} $$ $$ aba = horizonte + \frac{lado}{2} $$
Igual que la estaca, el sprite queda centrado en el horizonte.

El rectángulo sobre la pantalla


                          centro = 894
                                │
        izq = 734               │              der = 1055
           ┌────────────────────┼────────────────────┐   arriba = 289
           │                    │                    │
           │        ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          │
   450 ────┼────────────────────●────────────────────┼──── horizonte
           │        ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓          │
           │                    │                    │
           └────────────────────┼────────────────────┘   abajo = 611
                          lado = 321.67

El muestreo de la imagen

Las dos coordenadas $$ u = \frac{k - izq}{lado} $$ $$ v = \frac{y - arr}{lado} $$
  • Cada columna del rectángulo aporta su avance horizontal.
  • Cada fila aporta el vertical.
  • Se convierten a texel igual que en las paredes.

La diferencia con una pared

En la pared

La coordenada horizontal sale del punto de impacto del rayo y es la misma para toda la columna.

En el sprite

Sale de la posición dentro del rectángulo y cambia en cada columna que se pinta.

El recorte tampoco entra en la fórmula

  • Un sprite cercano se sale de la pantalla por los cuatro lados.
  • Las orillas se acotan para no escribir fuera del framebuffer.
  • Pero la imagen se muestrea siempre con las orillas reales, sin acotar.

    orillas reales        lo que se pinta

    izq  = -120           de 0 a 1055
    der  = 1055
    arr  = -230           de 0 a 899
    aba  = 1130

    el sprite mide 1175 px de lado,
    pero solo se escribe lo que
    cae dentro de la pantalla

La transparencia

  • La imagen es un rectángulo, pero el objeto casi nunca lo es.
  • Los texels que no son parte de la silueta no se escriben.
  • Es la única diferencia real entre pintar una pared y pintar un sprite.

   la imagen           lo que se pinta

   ░░░░▓▓░░░░              ▓▓
   ░░░▓▓▓▓░░░             ▓▓▓▓
   ░░▓▓▓▓▓▓░░            ▓▓▓▓▓▓
   ░░░▓▓▓▓░░░             ▓▓▓▓
   ░░░░▓▓░░░░              ▓▓

   ░ transparente, no se escribe
   ▓ opaco, se escribe

Las dos maneras de marcar lo transparente

Color clave

Un color que nunca aparece en el arte, casi siempre magenta puro. Cualquier texel de ese color se salta. Es lo más barato y lo que usaba la época.

Canal alfa

Un cuarto canal dice qué tan opaco es cada texel. Permite bordes suaves, pero obliga a mezclar con el color que ya estaba.

El problema de la oclusión

  • Los sprites se dibujan después de las paredes, encima de lo ya pintado.
  • Uno que está detrás de una pared se vería a través de ella.
  • El motor no tiene una escena en 3D que consultar para saber qué tapa a qué.

     vista desde arriba

        pared
     ═══════════════
            │
            │   ▓ sprite (más lejos)
            │
            ●
         jugador

     el sprite no debería verse

El buffer de profundidad por columna

  • Es un arreglo con una distancia por cada columna de la pantalla.
  • Lo llena el bucle de paredes: cada columna guarda su distancia corregida.
  • Eso es todo lo que el motor sabe de profundidad, y alcanza.

   columna       0     1     2   ...  1299
              ┌─────┬─────┬─────┬───┬─────┐
   z-buffer   │ 412 │ 410 │ 408 │...│ 396 │
              └─────┴─────┴─────┴───┴─────┘
                 ▲
                 │
        una escritura por columna
        mientras se pintan las paredes

La prueba de profundidad

Antes de pintar la columna k

Se compara la distancia corregida del sprite con la que guardó la pared. Si el sprite está más lejos, esa columna entera se salta.

  • Una columna de pared es sólida de arriba abajo.
  • Por eso una sola distancia describe toda la columna.
  • Esa simplificación es la que hace barata la prueba.

La oclusión columna por columna


   columnas            734 ... 870      871 ... 1055

   pared (z-buffer)    300 ... 300      500 ...  500
   sprite              350 ... 350      350 ...  350

   comparación         350 ≥ 300        350 < 500
   resultado           tapado           se dibuja

   ┌──────────────────────────┬────────────────────────────┐
   │      ███ pared ███       │   ▓▓ mitad visible ▓▓      │
   └──────────────────────────┴────────────────────────────┘

Las dos distancias deben ser comparables

El buffer guarda distancias corregidas, proyectadas sobre la dirección de vista.
La del sprite también tiene que ir corregida, no la euclidiana cruda.
Mezclarlas hace que los sprites se asomen por las paredes cerca de las orillas.
Es el mismo número que ya se usó para el tamaño, así que sale gratis.

El orden entre varios sprites

  • El buffer solo guarda paredes: no dice nada de un sprite frente a otro.
  • En cualquier orden, uno lejano puede terminar tapando a uno cercano.
  • Se ordenan por distancia y se pintan del más lejano al más cercano.

   A está lejos            B está cerca

   de lejos a cerca        de cerca a lejos

   ┌──────────────┐        ┌──────────────┐
   │  AAAA        │        │  AAAA        │
   │  AABBBB      │        │  AAAABB      │
   │    BBBB      │        │    BBBB      │
   └──────────────┘        └──────────────┘

   B queda al frente       A tapa a B aunque
   como corresponde        está más lejos

El algoritmo del pintor

La idea

Es como trabaja un pintor: primero el fondo, después lo que está adelante. Cada capa cubre a la anterior.

El costo

Ordenar unas decenas de sprites es despreciable frente al pintado. Se rehace en cada cuadro, porque el orden depende de dónde está el jugador.

El recorrido de un cuadro con sprites


    dibujar todas las paredes ──► llenar el z-buffer, una distancia por columna
         │
         ▼   calcular la distancia de cada sprite
         ▼   ordenar de lejos a cerca
         │
    ── por cada sprite ──────────────────────────────────
         ▼   vector al sprite ────────────► dx · dy
         ▼   distancia y ángulo ──────────► d · θ
         ▼   desvío normalizado ──────────► β
         ▼   filtro de visibilidad ───────► dentro o descartado
         ▼   columna central y lado ──────► centro · lado
         ▼   rectángulo y recorte ────────► columnas y filas a pintar
         │
    ── por cada columna del rectángulo ──────────────────
         ▼   comparar con el z-buffer ────► tapada o visible
         ▼   por cada fila: texel → transparencia → pintar

Errores comunes

1No normalizar el desvío
  • Los sprites desaparecen cuando el jugador cruza el cero de su ángulo.
2Usar el arcotangente de un argumento
  • Se pierde el cuadrante y los de atrás salen reflejados al frente.

Errores comunes

3Dibujar los sprites antes que las paredes
  • El buffer de profundidad todavía está vacío.
4Comparar distancias de distinto tipo
  • Con la cruda, los sprites se asoman a través de las paredes.

Errores comunes

5No ordenar la lista en cada cuadro
  • Un sprite lejano tapa a uno cercano al cruzarse.
6Recortar antes de muestrear
  • El sprite se encoge justo cuando debería llenar la pantalla.

Hacia dónde sigue esta idea

La animación es cambiar de imagen con el tiempo: el resto del proceso no se entera.
Las partículas son sprites diminutos por centenares, con las mismas fórmulas.
En un motor con polígonos el billboard es un rectángulo al que se le cancela la rotación de la cámara.
El buffer por columna es la versión mínima del z-buffer por píxel de toda GPU.