Saltar al contenido

MapControls y FlyControls: recorrer en lugar de orbitar

Qué cambia MapControls respecto a OrbitControls y cómo hace que el punto bajo el cursor no se mueva, y cómo funciona el vuelo libre de FlyControls con su delta obligatorio.

MapControls: el mismo motor, otro contrato

MapControls extiende OrbitControls y en r184 solo cambia tres propiedades y dos métodos internos. Las propiedades son estas:

JS
// Lo que MapControls sobrescribe respecto a OrbitControlsscreenSpacePanning = false;                                     // en Orbit es truemouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.ROTATE };touches = { ONE: TOUCH.PAN, TWO: TOUCH.DOLLY_ROTATE };

Eso es todo el cambio de configuración: el botón izquierdo panea en lugar de rotar, y el paneo es horizontal respecto al vector up en lugar de estar en el plano de la pantalla. Con eso, arrastrar mueve el suelo bajo tus pies en vez de girar la escena, que es la convención de cualquier mapa desde hace veinte años.

JS
import * as THREE from 'three';import { MapControls } from 'three/addons/controls/MapControls.js';const controls = new MapControls( camera, renderer.domElement );controls.enableDamping = true;controls.dampingFactor = 0.08;controls.maxPolarAngle = Math.PI / 2 - 0.1;   // que no se meta bajo el terrenocontrols.minDistance = 5;controls.maxDistance = 400;renderer.setAnimationLoop( () => {  controls.update();  renderer.render( scene, camera );} );

Todo lo demás —los límites, saveState, reset, zoomToCursor, los eventos, la obligación de llamar a update() con damping— se hereda tal cual y funciona igual. Si conoces OrbitControls, ya conoces MapControls.

Una diferencia menor en la firma: el constructor de MapControls es ( object, domElement ), sin el valor por defecto null que sí tiene el de OrbitControls. En la práctica da igual, pero si construyes sin elemento DOM esperando conectar después, MapControls te lo pasará como undefined y el comportamiento no es el mismo.

El paneo en espacio de mundo

Esta es la parte interesante y es reciente. Con el paneo en espacio de pantalla —el de OrbitControls— el desplazamiento se calcula a partir de píxeles arrastrados y de la distancia al objetivo. Funciona perfectamente cuando miras un objeto de frente, pero al inclinar la cámara sobre un terreno aparece un artefacto molesto: el punto del suelo que agarraste se desliza bajo el cursor, hacia adelante o hacia atrás según la inclinación, porque la relación entre píxeles y unidades de mundo no es constante a lo largo de la pantalla cuando el plano está en escorzo.

r184 lo resuelve reescribiendo _handleMouseDownPan y _handleMouseMovePan en MapControls. Cuando screenSpacePanning es false, el control ya no traduce píxeles a unidades: lanza un rayo desde el cursor contra un Plane definido por el vector up del objeto y el punto target, guarda el punto de intersección al empezar el arrastre, y en cada movimiento calcula la nueva intersección y aplica la diferencia directamente en espacio de mundo. El resultado es que el punto que agarraste se queda exactamente bajo el cursor, con cualquier inclinación de cámara.

Dos consecuencias prácticas se derivan de eso. La primera es que el plano de paneo pasa por target, así que si tu terreno tiene relieve y target está a la altura del valle, arrastrar sobre una montaña se sentirá desplazado. Ajustar target.y a la altura del terreno bajo el cursor, si tienes esa información, mejora mucho la sensación. La segunda es que si pones screenSpacePanning = true en un MapControls, el nuevo comportamiento se desactiva y la implementación delega en la de OrbitControls: es la forma de recuperar el paneo antiguo si lo prefieres.

Este código consume propiedades internas de OrbitControls —el acumulador de desplazamiento y el manejador de la clase padre—, lo que significa en la práctica que un MapControls está acoplado a la versión de OrbitControls que trae la misma release. No mezcles addons de versiones distintas de Three.js: es un consejo general y aquí es especialmente literal.

FlyControls: seis grados de libertad

FlyControls no orbita nada. No tiene target, no privilegia ninguna vertical, y permite alabeo, así que la cámara puede acabar boca abajo. Es el modelo de una nave, no el de una persona.

JS
new FlyControls( object, domElement = null )

Sus propiedades públicas son exactamente cuatro, ni una más:

JS
import { FlyControls } from 'three/addons/controls/FlyControls.js';const controls = new FlyControls( camera, renderer.domElement );controls.movementSpeed = 10;    // unidades por segundocontrols.rollSpeed = 0.5;       // radianes por segundocontrols.dragToLook = true;     // mirar solo mientras se arrastracontrols.autoForward = false;   // avanzar siempre sin pulsar nada

Y la diferencia de contrato con OrbitControls: update() recibe un delta en segundos y es obligatorio. No es una optimización ni una mejora opcional, es cómo funciona: el control multiplica el vector de movimiento por delta * movementSpeed y el de rotación por delta * rollSpeed. Sin delta, no hay movimiento.

JS
import * as THREE from 'three';const reloj = new THREE.Timer();reloj.connect( document );renderer.setAnimationLoop( ( tiempo ) => {  reloj.update( tiempo );  const delta = Math.min( reloj.getDelta(), 0.1 );  controls.update( delta );  renderer.render( scene, camera );} );

El acotado del delta importa más aquí que en OrbitControls, porque el movimiento es proporcional al tiempo: al volver de una pestaña inactiva, un delta de tres segundos con movementSpeed a diez te teletransporta treinta unidades hacia adelante.

El mapa de teclas está fijado en el código y usa event.code, así que no depende de la distribución del teclado:

Tecla Acción
KeyW / KeyS Avanzar y retroceder
KeyA / KeyD Desplazarse a los lados
KeyR / KeyF Subir y bajar
ArrowUp / ArrowDown Cabecear
ArrowLeft / ArrowRight Guiñar
KeyQ / KeyE Alabear

Los escuchadores de teclado se instalan en window, no en el elemento DOM, así que FlyControls captura las teclas aunque el foco esté en otra parte de la página. Si tu página tiene campos de texto, eso es un problema y tendrás que desactivar el control mientras estén enfocados con controls.enabled = false.

Hay dos detalles del comportamiento del ratón que conviene saber. Con dragToLook en false, el movimiento del puntero rota la cámara siempre, sin necesidad de pulsar, y los botones izquierdo y derecho avanzan y retroceden. Con dragToLook en true, la cámara solo gira mientras arrastras y los botones dejan de mover. Para la mayoría de las aplicaciones web la segunda es la única aceptable, porque la primera secuestra el puntero de facto.

Y un hecho verificado en el código fuente de r184 que ahorra una tarde de depuración: las teclas de mayúsculas asignan una propiedad movementSpeedMultiplier que no está declarada en el constructor y no se usa en update(). Es código muerto. Mantener pulsado mayúsculas en FlyControls no acelera nada. Si necesitas un modificador de velocidad, tienes que implementarlo tú:

JS
addEventListener( 'keydown', ( e ) => {  if ( e.code === 'ShiftLeft' ) controls.movementSpeed = 40;} );addEventListener( 'keyup', ( e ) => {  if ( e.code === 'ShiftLeft' ) controls.movementSpeed = 10;} );

FlyControls emite un único evento, change, y solo cuando la posición o la rotación han cambiado de verdad. No emite start ni end, así que el patrón de bajar la calidad durante la interacción que funciona con OrbitControls aquí hay que montarlo sobre las pulsaciones de tecla.

Cuál de los dos

El criterio es qué representa la cámara. MapControls mantiene una vertical privilegiada y un punto de interés en el suelo: es el contrato de un observador que sobrevuela un plano. Sirve para terreno, para planos de edificio, para escenas urbanas, para cualquier cosa donde exista un “arriba” incuestionable y el usuario quiera cubrir distancia sin perder la orientación.

FlyControls no tiene vertical: la cámara puede acabar en cualquier orientación y no hay forma de “enderezarla” salvo que lo hagas tú. Eso es exactamente lo que quieres en el espacio, bajo el agua o dentro de una estructura compleja, y exactamente lo que no quieres en un terreno, porque el usuario acaba desorientado y sin manera de recuperarse. Si necesitas recorrido en primera persona sobre un suelo, el control adecuado es PointerLockControls, que sí bloquea el alabeo.