← CC2018

Texturas

Semestre 02, 2026

De un color plano a una imagen

  • Hasta ahora cada estaca se pinta de un solo color.
  • Texturizar es darle a cada píxel de la estaca su propio color, sacado de una imagen.
  • La geometría no cambia: siguen siendo estacas verticales de un píxel de ancho.
Motor raycaster con paredes texturizadas

Lo único que cambia

Antes

El carácter de la pared da un color. La estaca se pinta de ese color, de arriba abajo.

Después

El carácter de la pared da una imagen. La estaca muestra una tira vertical de esa imagen.

Las dos preguntas por resolver

Qué columna de la textura le toca a esta columna de pantalla.
Qué fila de la textura le toca a cada píxel de la estaca.

La textura es un mapa de bits

  • Es una imagen en memoria, con un ancho \(T_w\) y un alto \(T_h\).
  • A cada píxel de la imagen se le dice texel.
  • Se lee igual que el framebuffer: dos enteros adentro, un color afuera.

    tx = 0                    tx = 127
       ┌─────────────────────────┐  ty = 0
       │ · · · · · · · · · · · · │
       │ · · · · ███ · · · · · · │
       │ · · · · ███ · · · · · · │
       │ · · · · · · · · · · · · │
       │ · · · · · · · · · · · · │
       └─────────────────────────┘  ty = 127

        textura[ty * 128 + tx]

Las coordenadas normalizadas

  • Usar texels directo ata el código al tamaño de la imagen.
  • En su lugar se usan dos números reales \(u\) y \(v\), siempre entre 0 y 1.
  • El origen queda arriba a la izquierda, igual que en la pantalla.

   (0, 0)          u →           (1, 0)
        ┌───────────────────────────┐  ty = 0
        │                           │
        │                           │
      v │                           │
      ↓ │                           │
        │                           │
        └───────────────────────────┘  ty = 127
   (0, 1)                        (1, 1)

     tx = 0                      tx = 127

De coordenada normalizada a texel

La conversión $$ t_x = \lfloor u \cdot T_w \rfloor $$ $$ t_y = \lfloor v \cdot T_h \rfloor $$
  • Se multiplica por el tamaño de la textura y se tira la parte decimal.
  • De dónde salen \(u\) y \(v\) se ve más adelante: los da el punto donde el rayo pega.

La conversión con números

Textura de 128×128
   u     u · 128     tx
0.000       0.00       0
0.375      48.00      48
0.507      64.90      64
0.999     127.87     127
Con \(u = 1\) el índice sería 128 y se sale del arreglo. Hay que acotar con \(\min(t_x,\ T_w - 1)\).

Dos coordenadas con dos ritmos distintos

La horizontal

Dice en qué punto de la pared pegó el rayo. Se calcula una vez por columna y no vuelve a cambiar.

La vertical

Dice a qué altura de la pared corresponde cada píxel. Cambia en cada fila.

Una columna es una tira vertical de la textura


        la textura                         la pantalla

  tx=0        tx=48      tx=127
    ┌───────────╥───────────┐            ┌─────────────────────┐
    │           ║           │ v=0.00     │        ┌─╥─┐        │ arriba
    │           ║           │            │        │ ║ │        │
    │           ║           │ v=0.50     │────────┼─╫─┼────────│ horizonte
    │           ║           │            │        │ ║ │        │
    │           ║           │ v=0.99     │        └─╨─┘        │ abajo
    └───────────╨───────────┘            └─────────────────────┘
                ▲                                   ▲
         u = 0.375 fija                    la columna i
       toda la tira se estira              un píxel de ancho
        sobre la estaca

El rayo tiene que devolver el punto de impacto

  • Hoy el rayo devuelve la distancia y el carácter de la celda.
  • Saber que pegó en una pared no dice en qué punto de la pared pegó.
  • La celda es un número redondeado: pierde justo lo que hace falta.
El punto exacto $$ h_x = jugador_x + d\cos\theta $$ $$ h_y = jugador_y + d\sin\theta $$

Es la misma cuenta que ya hace el rayo para avanzar. Solo hay que guardarla.

El paso del rayo se pasa de la cara

  • El rayo avanza a saltos y se detiene ya adentro de la pared.
  • Para un color plano eso no se nota; para la textura decide qué texel se lee.
  • Un texel mide \(100 / 128 = 0.781\) unidades, así que un paso de 1.0 se pasa de un texel entero.

  paso = 1.0

     ·   ·   ·   ·  █ ← se detiene aquí
                 ↑  ↑
            cara │  │ punto reportado
                 └──┘
                error hasta 1.0

  paso = 0.05

     ····························█
                                ↑↑
                          error hasta 0.05

El salto a la frontera


   marchado fijo        salto a frontera

   ┌───┬───┬───┐        ┌───┬───┬───┐
   │·· │···│···│        │   │   │   │
   │·· │···│···│        ●───●───●───●
   │·· │···│ ██│        │   │   │ ██│
   └───┴───┴───┘        └───┴───┴───┘

   cientos de pruebas   tres saltos exactos
  • Bajar el paso a 0.05 limpia la imagen, pero multiplica por veinte el trabajo.
  • La salida es saltar directo a la siguiente frontera de celda.
  • El salto cae exactamente sobre la cara, así que el impacto sale exacto y gratis.

Cada bloque tiene cuatro caras

  • En las caras verticales lo que varía a lo largo de la cara es \(y\).
  • En las horizontales lo que varía es \(x\).
  • La coordenada de la textura sale del eje que recorre la cara golpeada.

        x=400          x=500
          ┌──────────────┐  y=300
          │              │
   cara   │              │   cara
 vertical │   BLOQUE     │ vertical
  u ← y   │              │  u ← y
          │              │
          └──────────────┘  y=400
       cara horizontal  u ← x

La regla para saber qué cara se golpeó

Posición dentro de la celda $$ \ell_x = h_x \bmod B $$ $$ \ell_y = h_y \bmod B $$
Distancia a la frontera $$ b_x = \min(\ell_x,\ B - \ell_x) $$ $$ b_y = \min(\ell_y,\ B - \ell_y) $$
Gana la frontera más cercana, porque el rayo acaba de cruzarla. Si \(b_x < b_y\) la cara es vertical.

La coordenada horizontal

Cara vertical $$ u = \frac{h_y \bmod B}{B} $$
Cara horizontal $$ u = \frac{h_x \bmod B}{B} $$

Qué hace cada parte de la fórmula

  • El módulo borra en qué celda se está y deja la posición dentro de la celda.
  • Dividir entre \(BLOCK\_SIZE\) lleva ese número al rango de 0 a 1.
  • Al pasar a la celda siguiente vuelve a cero, así que la textura se repite sola.

La coordenada horizontal con números

Impacto en (400.0, 337.5)
ℓx = 400.0 mod 100 = 0.0
ℓy = 337.5 mod 100 = 37.5
bx = min(0.0, 100.0) = 0.0
by = min(37.5, 62.5) = 37.5
bx < by → cara vertical
u  = 37.5 / 100 = 0.375
tx = ⌊0.375 · 128⌋ = 48
La columna 48 se estira sobre toda la estaca. No depende de la distancia: solo del punto de impacto.

La coordenada vertical

La fórmula $$ v = \frac{y - arriba}{altura} $$
  • La estaca completa representa la pared completa.
  • Su fila de arriba es \(v = 0\) y la de abajo \(v \to 1\).

   pantalla              textura

   arriba ─── ┌───┐      ty = 0
              │███│
              │███│
   horizonte ─┼███┤      ty = 64
              │███│
              │███│
   abajo ──── └───┘      ty = 127

     altura px      ←→   T_h texels

La coordenada vertical con números

Columna 325: altura 280.84, arriba 309.58
fila 309 → v = 0.000 → ty = 0
fila 380 → v = 0.251 → ty = 32
fila 450 → v = 0.500 → ty = 64
fila 589 → v = 0.995 → ty = 127
281 filas de pantalla muestran 128 texels: cada texel ocupa dos filas y la imagen se ve estirada.

El recorte no debe entrar en la fórmula

  • Con la pared cerca, la estaca se sale de la pantalla y sus orillas se acotan.
  • Ese recorte es para no pintar fuera del framebuffer, no cambia la pared.
  • La fórmula usa siempre la orilla real, aunque sea negativa.
Las dos versiones de arriba
arriba_real     = -382.67  ← para v
arriba_recortada = 0        ← para pintar

Mezclarlas aplasta la textura dentro de la parte visible.

El paso incremental

Cuántos texels avanza cada fila $$ paso_{ty} = \frac{T_h}{altura} $$
Dónde arranca la primera fila visible $$ ty_0 = (y_{inicio} - arriba) \cdot paso_{ty} $$
Con esos dos números el bucle no divide: solo suma el paso en cada fila. Es el mismo truco de Bresenham.

El paso incremental con números

Columna 975, estaca recortada
altura   = 1665.34
arriba   = -382.67
paso_ty  = 128 / 1665.34 = 0.0769
y_inicio = 0
ty_0     = 382.67 · 0.0769 = 29.41
La textura no arranca en 0 porque los primeros 29 texels quedaron arriba de la pantalla.

El catálogo de texturas

  • El mapa sigue siendo el mismo archivo de texto.
  • Antes cada símbolo daba un color, ahora da una imagen.
  • El símbolo sin imagen cae en una textura por defecto.

   símbolo        antes            ahora

      +        color azul       textura de esquina
      -        color rojo       textura horizontal
      |        color rojo       textura vertical
      g        color verde      textura de meta
    otro       color claro      textura por defecto

La carga ocurre antes del bucle

Abrir y descomprimir un archivo es lento, del orden de milisegundos.
Hacerlo dentro del bucle sería repetirlo 1300 veces por cuadro.
Ya decodificada, pedir un color cuesta lo mismo que leer un arreglo.
Se guarda el ancho y el alto, que son los que convierten \(u\) y \(v\) en índices.

El recorrido de una columna


    columna i de la pantalla
         │
         ▼   lanzar el rayo ───────────► distancia · carácter · impacto
         │
         ▼   corregir el ojo de pez ───► distancia corregida
         │
         ▼   medir la estaca ──────────► altura · orillas reales
         │
         ├──► elegir la imagen del catálogo
         ├──► coordenada horizontal ───► columna de texel
         ├──► paso vertical y arranque
         │
         ▼   recortar a la pantalla ───► rango de filas visible
         │
         ▼   por cada fila: leer texel → atenuar → pintar

El sombreado de las caras

El factor por cara $$ f_{cara} = \begin{cases} 1.0 & \text{horizontal} \\ 0.7 & \text{vertical} \end{cases} $$
  • Sin esto, dos caras perpendiculares se ven iguales y la esquina desaparece.
  • No es iluminación real: es una convención que hace legible el laberinto.
  • La orientación ya se calculó para obtener \(u\), así que sale gratis.

El oscurecimiento por distancia

El factor por distancia $$ f_{dist} = \max\left(0.25,\ 1 - \frac{corregida}{d_{max}}\right) $$
  • Las paredes lejanas se apagan hacia el fondo.
  • El piso de 0.25 evita que la escena se vuelva negra.
  • Los dos factores se multiplican y el resultado va al color del texel.

La atenuación canal por canal

  • El color del texel viene empacado en un solo número.
  • Multiplicar ese número por el factor mezclaría los canales entre sí.
  • Hay que separar, atenuar cada canal y volver a empacar.

        texel original
   ┌────────┬────────┬────────┐
   │   R    │   G    │   B    │
   │  200   │  180   │  150   │
   └────────┴────────┴────────┘
        │        │        │
       × f      × f      × f      f = 0.70
        ▼        ▼        ▼
   ┌────────┬────────┬────────┐
   │  140   │  126   │  105   │
   └────────┴────────┴────────┘

Errores comunes

1Recortar antes de mapear
  • La pared se encoge al acercarse en vez de crecer.
2Usar la celda en vez del punto
  • La pared sale de un solo color, como antes de texturizar.

Errores comunes

3Tomar el eje equivocado
  • La textura se desliza en vez de quedarse pegada a la pared.
4No acotar el índice de texel
  • Una coordenada de 0.99999 se sale del arreglo.

Hacia dónde sigue esta idea

Los sprites son el mismo cálculo sobre un rectángulo que siempre encara al jugador.
El piso y el techo se texturizan al revés: por fila de pantalla, no por columna.
En rasterización el par \((u, v)\) viene en el modelo y el shader hace esta misma consulta.
Toda GPU trae hardware dedicado solo a esta operación.