Construir un mundo abierto en el navegador, parte 30: Una cámara que respeta las paredes
Por Oleg Sidorkin, CTO y cofundador de Cinevva
¿Acabas de llegar? Consulta la guía de la serie. Explica qué es un prototipo exploratorio y enlaza todas las partes.
La parte 29 nos dio un único controlador capaz de manejar cualquier cuerpo. Ahora el cuerpo se mueve correctamente. Camina, se desliza, nada, planea, trepa por cuevas y se agacha bajo salientes. El problema es lo que lo observa. Durante veintinueve partes, la cámara fue un sistema orbital estándar que seguía al jugador y hacía exactamente una cosa ingeniosa para evitar situaciones embarazosas: se negaba a inclinarse por debajo del horizonte para no deslizarse bajo un suelo plano. Esa limitación lo delata todo. Existe porque la cámara no tenía ni idea de dónde estaba realmente la geometría del mundo, así que la única defensa contra el atravesamiento era prohibir los ángulos en los que resultaba más probable. Al acercarte a una colina, la cámara se metía dentro de ella. Al entrar en una de las cuevas de marching cubes de la parte 7, acababas mirando el interior de una roca. Si construías una casa con las herramientas de creación de la parte 16 y te quedabas de pie en una habitación, la cámara flotaba fuera de la pared mirando el revestimiento exterior. Esta parte consigue que la cámara respete la geometría tanto como ya lo hace el cuerpo.
La forma del problema y la forma de la solución
Una cámara en tercera persona tiene una tarea difícil y una docena de tareas fáciles. Las fáciles son seguir al personaje, suavizar el movimiento y gestionar los controles orbitales, y eso ya lo teníamos. La difícil es lo que la literatura académica denomina una restricción de visibilidad, y el estudio que todo el mundo cita, Camera Control in Computer Graphics, de Christie y Olivier, plantea todo el campo en torno a ella: mantener al sujeto encuadrado y sin oclusiones mientras se respeta el mundo. En tiempo de ejecución, esto se reduce a una pregunta engañosamente sencilla que se plantea en cada fotograma. El jugador es el pivote. El usuario ha rotado y ajustado el zoom hasta una posición deseada de la cámara, situada a cierta distancia detrás de él. ¿Hasta dónde puede colocarse realmente la cámara a lo largo de esa línea antes de introducirse en algo sólido? Si respondes con precisión, la cámara se recoge delante de la colina, se desliza por el brazo al retroceder hacia una esquina y se detiene ante el techo de la cueva en lugar de atravesarlo.
El patrón que resuelve esto es antiguo y está más que probado. Unreal lo llama spring arm, Godot incluye un nodo SpringArm3D y Cinemachine de Unity lo divide entre un sistema de seguimiento en tercera persona y una extensión para evitar oclusiones. La idea siempre es la misma. La cámara cuelga del extremo de un brazo anclado al pivote. Se mantiene a la longitud deseada cuando el trayecto está despejado, se retrae hacia el pivote cuando hay algo en medio y vuelve a extenderse con un efecto de resorte cuando el camino queda libre. Real-Time Cameras, de Mark Haigh-Hutchinson, responsable de cámaras de Metroid Prime, dedica capítulos enteros a los modos de fallo que convierten una implementación ingenua en algo capaz de marear a los jugadores. Tomamos ese patrón y construimos el nuestro, lo bastante pequeño como para leerlo de una sentada, en public/world/src/camera-rig.mjs.
Un brazo que solo controla su longitud
La regla de diseño que mantuvo pequeño el sistema procede del trabajo sobre el controlador de la parte 29: controlar una sola cosa por completo y rechazar todo lo demás. El sistema controla la longitud del brazo y nada más. El ángulo horizontal, la inclinación, el amortiguamiento de la órbita y los gestos táctiles y con la rueda del ratón permanecen en OrbitControls, que ya los resuelve bien y que no tenemos ningún interés en reescribir. Por tanto, este sistema no es un controlador de cámara. Es un posprocesado que se ejecuta después de los cálculos orbitales y corrige exactamente un número: la distancia entre el pivote y la cámara.
Esa decisión parece impecable, pero estuvo a punto de fallar de inmediato debido a cómo piensa OrbitControls. Al principio de cada actualización, lee la posición actual de la cámara y deriva de ella el radio de la órbita. Normalmente eso resulta invisible. Pero en cuanto nuestro sistema acorta la distancia de la cámara para esquivar una pared, en el siguiente fotograma OrbitControls lee esa posición acortada, concluye que el usuario debe haber acercado el zoom e incorpora ese acortamiento al zoom deseado por el usuario. Después de unos cuantos fotogramas, la cámara se ha desplomado sobre la cabeza del jugador y ya no vuelve a alejarse. La solución consiste en dos llamadas que enmarcan la actualización de los controles orbitales y constituyen toda la integración. Antes de que se ejecute OrbitControls, beforeControls() restaura la cámara a la distancia completa y sin acortar del fotograma anterior, para que los cálculos orbitales siempre lean el zoom real del usuario. Después de que se ejecute OrbitControls, afterControls(dt) lee esa posición deseada recién orbitada, resuelve la colisión, amortigua la longitud y coloca la cámara donde realmente debe renderizarse. La intención del usuario y la corrección de colisiones nunca interfieren entre sí, y el desplazamiento del zoom conserva exactamente la misma capacidad de respuesta que tenía antes de que existiera el sistema. La prueba sin interfaz gráfica que escribimos para el sistema verifica precisamente esto: empuja el brazo contra una pared durante sesenta fotogramas, despeja la pared y comprueba que la longitud vuelve con efecto de resorte al zoom completo de diez metros del usuario, en lugar de quedarse atascada en la distancia de colisión.
Un contrato, cualquier colisionador
El sistema nunca pregunta de qué está hecho el mundo. Un colisionador es simplemente un objeto con un método probe: el sistema le entrega un rayo, una distancia máxima y el radio de sondeo de la cámara, y recibe un número, la distancia máxima que puede recorrer la cámara antes de que ese colisionador la bloquee. El sistema consulta todos los colisionadores registrados y se queda con el impacto más cercano. Ese es todo el contrato, y es el mismo enfoque que utilizó el controlador del personaje al convertir la locomoción en comportamientos intercambiables. El terreno se conecta, los objetos se conectan y las estructuras de los edificios se conectan, cada uno detrás del mismo probe, mientras que el sistema permanece ajeno a todos ellos. Añadir un nuevo tipo de obstáculo significa añadir un colisionador a una lista, no editar la cámara.
Hay un detalle del contrato que demuestra su valor: el radio de sondeo. No lanzamos un rayo fino desde el pivote hasta la cámara, sino una esfera lo bastante grande como para contener el plano cercano de la cámara. Un único rayo detiene el centro de la cámara en la pared, pero el plano cercano tiene anchura, por lo que sus esquinas ya estarían hundidas en la pared antes de que el rayo central informara de un impacto. Barrer una esfera pequeña en lugar de un rayo es lo que hacen todas las implementaciones publicadas; Unreal lo expone como el tamaño de la sonda, y eso marca la diferencia entre una cámara que descansa limpiamente contra una superficie y otra que permite ver a través de ella en los bordes de la pantalla.
No estimamos ese radio, sino que lo derivamos. El punto del plano cercano más alejado de la cámara es una de sus esquinas, y la distancia hasta él se obtiene directamente de la proyección. Con un plano cercano
El radio de sondeo es esa distancia a la esquina multiplicada por un pequeño margen de seguridad, con un mínimo fijo para que nunca caiga por debajo de un valor razonable en un frustum muy estrecho. Cada vez que cambian el campo de visión, la relación de aspecto o el plano cercano, el radio se vuelve a calcular, de modo que un cambio de tamaño de la ventana o un zoom que afecte a la proyección no pueda dejar discretamente la sonda demasiado pequeña para cubrir las esquinas que debe proteger.
El colisionador que sabe de cuevas
Aquí es donde el colisionador del terreno se vuelve interesante, porque el terreno de nuestro motor no es un mapa de alturas. Desde la parte 7 es un campo de distancia con signo, una función que indica a qué distancia está cualquier punto del espacio de la superficie sólida más cercana y si se encuentra dentro o fuera de la roca. Un valor positivo es aire; uno negativo, roca. Ese único hecho es la razón por la que nuestra cámara puede hacer algo que una cámara basada en mapas de alturas no puede hacer por su propia estructura. Un mapa de alturas conoce la altura del suelo en unas coordenadas x y z. No tiene concepto de techo, porque solo puede haber una superficie sobre cada punto. Por eso, una cámara basada en mapas de alturas puede impedir que entres en una colina, pero no tiene ni idea de que el borde de un saliente o el techo de una cueva cuelgan sobre el pivote, y atraviesa ambos sin más. Un campo de distancia conoce todas las superficies en tres dimensiones, así que la misma sonda que detiene la cámara contra una ladera también la detiene contra el techo de una cueva sin un solo caso especial.
Desplazar la sonda por el campo es una técnica con nombre propio y un artículo científico que la respalda. Sphere Tracing, de John Hart, publicado en 1996, es el método estándar para hacer avanzar un rayo por un campo de distancia. El truco consiste en que el campo no solo indica si has golpeado algo, sino también una distancia segura que puedes avanzar sin chocar con nada. Así que, en lugar de avanzar poco a poco con pasos fijos diminutos, se muestrea el campo, se avanza según el margen que indica y se repite el proceso, dando grandes pasos por el aire libre y pasos cortos y cuidadosos al acercarse a una superficie. Sin embargo, hay una salvedad que nos impone el terreno. Un campo de distancias verdadero indica la distancia euclídea real hasta la superficie más cercana, y avanzar de una vez todo ese margen siempre es seguro. Pero donde el terreno sigue siendo un mapa de alturas en lugar de vóxeles esculpidos, el campo que podemos muestrear de forma económica no representa una distancia real, sino la separación vertical, el hueco en línea recta hacia abajo hasta el suelo. En una pendiente, ese valor exagera cuánto puede moverse realmente la cámara, porque la roca más cercana está a un lado, no justo debajo. La distancia real es menor por un factor que crece con el gradiente,
Expresado formalmente, el brazo es un rayo
Aquí,
El colisionador incorpora un método adicional como red de seguridad: la despenetración. En principio, la colisión debería impedir que la cámara entre en un sólido, pero algunas situaciones se le escapan: un pivote que atraviesa una pared tan fina que el brazo comienza dentro de ella, una cueva recién esculpida con las herramientas de terreno mientras la cámara estaba en la roca que acaba de desaparecer, o una pieza de construcción colocada alrededor de la cámara. En esos casos, el colisionador comprueba si la cámara terminó el fotograma dentro de un sólido, donde
Unas pocas iteraciones convergen en la isosuperficie de valor
Entrar de golpe, salir con suavidad y no sobresaltarse ante los postes de una valla
Un brazo que simplemente salta cada fotograma a la distancia de colisión es peor que no tener brazo, porque el mundo está lleno de objetos delgados tras los que la cámara pasa durante un solo fotograma —un poste de valla, una farola, el tronco de un árbol—, y una cámara que se abalanza para esquivar cada uno y vuelve a abalanzarse en sentido contrario provoca mareo. Tanto el libro de Haigh-Hutchinson como la muy apreciada charla de Itay Keren sobre el movimiento de cámara llegan a la misma intuición: la cámara debe reaccionar ante las amenazas y el peligro más rápido de lo que se relaja cuando desaparecen. Por eso la amortiguación es deliberadamente asimétrica. Cuando aparece un oclusor y el brazo tiene que acortarse, entra casi al instante, porque un fotograma de intersección queda feo y el jugador tolera un repliegue rápido. Cuando el oclusor desaparece y el brazo quiere alargarse, sale lentamente, y solo después de que transcurra un breve temporizador de espera con espacio libre continuo. Esa espera es la histéresis que elimina los sobresaltos. Si giras la cámara rápidamente junto a un poste delgado, el poste nunca permanece despejado el tiempo suficiente como para activar la extensión lenta, así que la cámara se desliza junto a él como si no estuviera ahí, que es exactamente lo que espera la vista. Cinemachine presenta la misma idea mediante valores separados de amortiguación al entrar y al salir de una colisión, y esa asimetría es lo que hace que parezca un operador de cámara en lugar de un muelle.
En código, es una sola línea de suavizado exponencial cuya velocidad cambia según el signo de la variación. Si
y la rama de salida suave solo se ejecuta cuando el espacio libre se ha mantenido durante el tiempo de espera
El último comportamiento está pensado para los interiores estrechos que dieron origen a toda esta parte. Cuando el brazo se acorta tanto que la cámara queda justo encima del jugador, ocultamos su propio avatar y dejamos que la vista se sitúe cerca de la primera persona. Es lo que hace Breath of the Wild en un santuario estrecho y el recurso al que recurren la mayoría de los juegos en tercera persona al quedar arrinconados, porque la alternativa —una cámara encajada contra una pared y mirando la nuca de un personaje— no sirve de nada. El rig expone una sola bandera para ello, y el bucle del mundo la consulta y alterna la visibilidad del avatar local. Con menos de un metro de brazo estás, en la práctica, en primera persona, se respetan las paredes y, en cuanto retrocedes hasta una sala con espacio, el avatar reaparece gradualmente y el brazo se extiende.
Lo siguiente que se conecta
El colisionador del terreno se publica hoy y es la mitad difícil, porque el terreno está por todas partes y un campo de distancias es algo incómodo de sondear. Los objetos y las estructuras de edificios procedentes de los prototipos de creación son la mitad fácil, y el contrato ya está preparado para ellos. Un segundo colisionador, uno de trazado de rayos, lanza un rayo desde el pivote hacia la cámara contra una lista de mallas e informa del impacto más cercano del mismo modo que el colisionador del terreno. La versión económica lanza un solo rayo, lo cual basta hasta que aumenta la cantidad de objetos, y la mejora consiste en sustituir el rayo por una esfera barrida mediante three-mesh-bvh, la biblioteca de Garrett Johnson que envuelve una malla en una jerarquía de volúmenes delimitadores para que las consultas espaciales se ejecuten en tiempo logarítmico en lugar de recurrir a la fuerza bruta. En cualquier caso, el rig no cambia. Consulta una lista más larga de colisionadores y toma el impacto más cercano, que es precisamente la razón por la que construimos primero el contrato y después los colisionadores.
Tecnología mencionada en este capítulo
Un brazo que posee un único valor. El rig de cámara es un posprocesado sobre OrbitControls, no su sustituto. Es dueño de la longitud del brazo y deja el giro horizontal, la inclinación, el zoom y la gestión de gestos en manos del controlador orbital en el que ya confiábamos. La integración consta de dos llamadas que enmarcan la actualización: beforeControls() restaura la distancia completa del fotograma anterior para que los cálculos orbitales lean el zoom real del usuario en lugar de confundir el acortamiento causado por una colisión con un acercamiento de cámara, y afterControls(dt) resuelve la colisión y escribe la posición que se renderizará. Sin ese par, la cámara se desploma sobre el jugador en unos pocos fotogramas.
Un contrato de colisionadores conectables. Un colisionador es cualquier objeto con un probe que responde a la pregunta «¿qué distancia puede recorrer la cámara desde el pivote a lo largo de este rayo antes de que la bloquees?». El rig consulta todos los colisionadores y toma el impacto más cercano, sin saber si el obstáculo es terreno, un objeto o una pared, siguiendo la misma disciplina de poseer una sola cosa que el controlador de personaje conectable empleaba para la locomoción. La sonda es una esfera barrida dimensionada para contener el plano cercano, no un rayo fino, de modo que las esquinas del frustum nunca atraviesen una superficie que el rayo central no detectaría. Su radio se deriva de la proyección: la distancia hasta una esquina del plano cercano,
Una sonda de terreno basada en distancias con signo que respeta salientes y cuevas. Como el terreno es un campo de distancias con signo en lugar de un mapa de alturas, la misma sonda que detiene la cámara contra una ladera también la detiene ante el techo de una cueva, un caso que un trazado de rayos sobre un mapa de alturas no puede detectar por su propia estructura. La marcha es un trazado de esferas al estilo de Hart: avanza una fracción subrelajada del margen indicado por el campo y queda limitada por ambos extremos, porque las regiones de mapa de alturas indican separación vertical en lugar de distancia verdadera y exageran cuánto puede moverse la cámara por una pendiente, por lo que un paso completo saltaría una cresta. Una pasada de despenetración guiada por el gradiente actúa como red de seguridad y recupera la vista cuando se esculpe el suelo bajo la cámara.
Amortiguación asimétrica con temporizador de espera. El brazo entra rápidamente cuando aparece un oclusor y sale con suavidad cuando desaparece, y solo después de un breve periodo de espacio libre continuo, por lo que pasar rápidamente junto a un poste de valla nunca hace que la cámara se abalance. Por debajo de un umbral de contracción, el rig activa el estado de casi primera persona y el bucle del mundo oculta el avatar local, la solución habitual para interiores estrechos en lugar de dejar la cámara enterrada en una pared.
Referencias
El planteamiento del control de cámara como una restricción de visibilidad procede de Marc Christie y Patrick Olivier, Control de cámara en gráficos por computadora (Computer Graphics Forum, 2008). La marcha por campos de distancias procede de John C. Hart, Trazado de esferas: un método geométrico para el trazado de rayos con antialiasing de superficies implícitas (The Visual Computer, 1996). El patrón de brazo con muelle y su colisión mediante una esfera de sondeo están documentados en Componente Spring Arm de Epic y en Desoclusor de Cinemachine y Seguimiento en tercera persona de Unity. La intuición sobre el movimiento y la amortiguación procede de Mark Haigh-Hutchinson, Cámaras en tiempo real (Morgan Kaufmann, 2009), y de Itay Keren, Desplazarse hacia atrás: teoría y práctica de las cámaras en juegos de desplazamiento lateral (GDC 2015). La vía de mejora para el colisionador de mallas es three-mesh-bvh, de Garrett Johnson.
Parte 30 de 30. Anterior: Parte 29 - Un controlador, cualquier cuerpo Guía de la serie: /es/blog/2026-02-25-open-world-browser-series-guide