Semestre 02, 2026
Todo el motor se construye a partir de estos cuatro números. Cambiarlos no cambia las fórmulas.
Se calculan una sola vez al arrancar, nunca dentro del bucle de rayos.
(0,0) (W-1,0)
┌──────────────────────────────┐
│ │
y │ │
crece│- - - - - y = H/2 - - - - - - │ horizonte
hacia│ │
abajo│ │
▼ │
└──────────────────────────────┘
(0,H-1) (W-1,H-1)
x ──────────►
framebuffer.point(x, y)
// → buffer[y * ancho + x]
La memoria de video guarda los píxeles fila por fila, y la primera fila que se guarda es la que se dibuja hasta arriba.
y = 0 ┌───┐
│ │ ← fondo
y = arriba ├───┤
│███│
y = H/2 │███│ ← horizonte
│███│
y = abajo ├───┤
│ │ ← fondo
y = H-1 └───┘
↑
un píxel de ancho
La estaca es un solo segmento continuo, no dos partes. Arriba y abajo son sus dos extremos, y siempre está centrada en el horizonte.
columna 0 columna W-1
╲ │ ╱
╲ β < 0 │ β > 0 ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ a - FOV/2 ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱ ← dirección de vista a
╲ │ ╱
╲ │ ╱
╲│╱
jugador
La columna \(i\) recorre de \(0\) a \(W-1\). Esa fracción va de 0 en la orilla izquierda a 1 en la derecha.
$$ t = \frac{i}{W-1} $$Se arranca en la orilla izquierda del cono y se avanza \(FOV\) grados a lo largo de toda la pantalla.
$$ \beta_i = -\frac{FOV}{2} + FOV \cdot t $$Con \(W = 1300\) y \(FOV = 60°\), el paso entre columnas vecinas es \(\Delta\beta = 0.046189°\).
columna i t = i / 1299 β = -30° + 0.046189° · i cos β
0 0.000000 -30.0000° 0.866025
325 0.250192 -14.9885° 0.965978
650 0.500385 +0.0231° 1.000000
975 0.750577 +15.0346° 0.965769
1299 1.000000 +30.0000° 0.866025
El desvío es negativo en la mitad izquierda, cruza por cero cerca del centro y es positivo en la derecha. Con \(W\) par ninguna columna cae exactamente en el eje: la 650 queda a 0.0231 grados del centro.
plano de proyección
┌──────────────┬──────────────┐
│ │ │
│◄──── W/2 ────► │
│ │ │
└──────────────┴──────────────┘
╲ │ ╱
╲ │ ╱
╲ FOV/2 ╱
╲ │ ╱ d_plano
╲ │ ╱
╲│╱
● ojo
tan(FOV/2) = (W/2) / d_plano
El rayo devuelve una distancia en unidades del mundo. La pantalla se mide en píxeles. Hace falta un puente entre las dos.
ojo plano pared
│◄─ d_plano ─►│ │
│ ┤ ─┐ ┌────┤ ─┐
│ │ │ altura │ │ │ BLOCK_SIZE
●─────────────┼──┼───────────────┼────┤ │
│ │ │ │ │ │
│ ┤ ─┘ └────┤ ─┘
│ │
│◄─────────── corregida ─────────────►│
La pared y su imagen forman dos triángulos con el mismo vértice en el ojo, así que sus lados guardan la misma proporción.
W FOV d_plano = (W/2) / tan(FOV/2) Δβ por columna
1300 60° 1125.83 0.046189°
800 60° 692.82 0.075094°
1920 60° 1662.77 0.031266°
1300 90° 650.00 0.069284°
1300 45° 1569.24 0.034642°
y = 0 ┌───┐
│ │
arriba = 262 ──────├───┤ ─┐
│███│ │ 187.64
horizonte = 450 ───┼███┤ ─┤
│███│ │ 187.64
abajo = 637 ───────├───┤ ─┘
│ │
y = 899 └───┘
altura = 375.28 = 187.64 + 187.64
pared plana
════════════════════════════
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲│╱
jugador
rayo diagonal rayo del centro
recorre MÁS recorre MENOS
→ estaca baja → estaca alta
El coseno se toma del desvío \(\beta\) de la columna, no del ángulo absoluto del rayo. Por eso conviene guardar \(\beta\) desde que se calcula.
En las columnas del centro, la distancia cruda y la corregida traen prácticamente el mismo número. ¿Es un error del programa?
No. \(\cos 0° = 1\), así que el rayo que va en la dirección de vista nunca cambia. La corrección solo afecta a los rayos que se desvían, y cada vez más mientras más se desvían.
El jugador mira de frente a una pared plana que está a \(300\) unidades de distancia perpendicular. Al ser plana, todas sus estacas deberían salir de la misma altura.
columna β d cruda cos β corregida altura arriba abajo
0 -30.000° 346.410 0.866025 300.000 375.28 262 637
325 -14.988° 310.566 0.965978 300.000 375.28 262 637
650 +0.023° 300.000 1.000000 300.000 375.28 262 637
975 +15.035° 310.633 0.965769 300.000 375.28 262 637
1299 +30.000° 346.410 0.866025 300.000 375.28 262 637
La corrección deshace exactamente lo que agregó la diagonal: \((300/\cos\beta)\cdot\cos\beta = 300\). Las 1300 estacas salen idénticas y la pared se ve recta.
columna β d cruda altura sin corregir contra el centro
0 -30.000° 346.410 325.00 -13.4%
325 -14.988° 310.566 362.51 -3.4%
650 +0.023° 300.000 375.28 0.0%
975 +15.035° 310.633 362.43 -3.4%
1299 +30.000° 346.410 325.00 -13.4%
columna: 0 325 650 975 1299
corregida (pared recta) sin corregir (curveada)
┌────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ ┌──┬───┬───┬───┬──┐ │ │ ┌───┬───┐ │
│ │ │ │ │ │ │ │ │ ┌───┤ │ ├───┐ │
│ │ │ │ │ │ │ │ │ ┌─┤ │ │ │ ├─┐ │
│──┼──┼───┼───┼───┼──┼───│ │─┼─┼───┼───┼───┼───┼─┼──│ horizonte
│ │ │ │ │ │ │ │ │ └─┤ │ │ │ ├─┘ │
│ │ │ │ │ │ │ │ │ └───┤ │ ├───┘ │
│ └──┴───┴───┴───┴──┘ │ │ └───┴───┘ │
│ │ │ │
└────────────────────────┘ └────────────────────────┘
375 px parejos 325 en las orillas,
375 en el centro
Ahora en una escena real, donde cada rayo choca a una distancia distinta.
Columna → desvío → distancia cruda → corregida → altura → orillas → recorte. Nada de esto cambia si cambia la pantalla, el FOV o la escena.
Es la etapa anterior del motor: aquí solo se usa el resultado.
jugador
●·····●·····●·····●·····█ pared
0 100 200 300 415
↑
cruda = 415.0
La columna 325 se pinta de \(y = 309\) a \(y = 590\). Arriba y abajo queda el fondo.
La columna 975 se pinta de \(y = 0\) a \(y = 900\). La pared está tan cerca que llena la columna entera.
La estaca desborda cuando su altura pasa de \(H\):
$$ \frac{BLOCK\_SIZE}{corregida} \cdot d_{plano} > H $$ $$ corregida < \frac{BLOCK\_SIZE \cdot d_{plano}}{H} $$Toda columna cuya distancia corregida baje de 125.09 llena la pantalla completa.
Antes del bucle, una sola vez.