wandres.dev
FÍSICA · Rapier y el mundo simulado

Cuerpos rígidos: dinámico, cinemático y fijo

Los cuatro tipos de cuerpo rígido, cuándo usar cada uno, cómo se les da movimiento sin romper la simulación, y qué hacen la masa, el amortiguamiento, el sueño y la detección continua.

⏱ 19 min

Elegir el tipo de cuerpo es la decisión de diseño más importante de una escena con física, y se toma antes de escribir la primera línea de lógica. Un cuerpo dinámico obedece al mundo; un cinemático obedece a tu código; uno fijo no obedece a nadie. Confundirlos produce los dos síntomas clásicos de la física mal integrada: plataformas que atraviesan paredes y personajes que se caen al suelo cuando les empujas un poco.

🎯 Al terminar esta lección sabrás
  • Elegir entre dinámico, fijo, cinemático por posición y cinemático por velocidad.
  • Mover cada tipo con el método correcto y saber por qué los demás están mal.
  • Configurar masa, amortiguamiento y bloqueos de grados de libertad.
  • Diagnosticar cuerpos dormidos y activar la detección continua cuando hace falta.

Los cuatro tipos

const d = RAPIER.RigidBodyDesc.dynamic();
const f = RAPIER.RigidBodyDesc.fixed();
const kp = RAPIER.RigidBodyDesc.kinematicPositionBased();
const kv = RAPIER.RigidBodyDesc.kinematicVelocityBased();

Dinámico. Lo mueve el motor. Recibe gravedad, fuerzas, impulsos y contactos; su trayectoria es el resultado de la simulación. Es lo que quieres para cualquier cosa que deba comportarse como un objeto físico: cajas, escombros, pelotas, ragdolls.

Fijo. No se mueve nunca. Masa infinita a efectos prácticos. Colisiona con dinámicos pero no con otros fijos ni con cinemáticos. Suelos, paredes, terreno, cualquier cosa inmóvil. Si un objeto no va a moverse jamás, ni siquiera necesita cuerpo: un collider suelto ya es fijo y consume menos.

Cinemático por posición. Tú decides dónde está en cada paso; el motor deduce su velocidad de la diferencia entre la posición actual y la siguiente, y usa esa velocidad para empujar correctamente a los dinámicos que toca. Plataformas móviles, ascensores, puertas, cualquier cosa cuya trayectoria esté guionizada.

Cinemático por velocidad. Igual, pero tú das la velocidad y el motor deduce la posición. La elección entre los dos es de preferencia: si tu movimiento se describe naturalmente como una trayectoria, usa el de posición; si se describe como “avanza a dos metros por segundo”, usa el de velocidad.

La propiedad que define a los dos cinemáticos y que hay que interiorizar: ignoran completamente los contactos. Una plataforma cinemática atraviesa una pared sin inmutarse, porque su trayectoria la decides tú y el motor no la va a corregir. Es una característica, no un defecto: el control total es exactamente lo que quieres en una plataforma. Pero significa que la responsabilidad de no atravesar nada es tuya, y que si quieres un personaje que respete las paredes necesitas o bien un cuerpo dinámico o bien el controlador de personaje que trae Rapier, que hace las consultas de colisión a mano.

Mover cada tipo correctamente

Aquí está el 90 % de los errores de integración.

Un dinámico se mueve con fuerzas, impulsos o velocidad. Nunca con posición.

// Fuerza: persistente entre pasos hasta que la reinicies. Afecta a la aceleracion.
cuerpo.addForce( { x: 0, y: 20, z: 0 }, true );
cuerpo.resetForces( true );

// Impulso: instantaneo, de una vez. Afecta a la velocidad.
cuerpo.applyImpulse( { x: 0, y: 5, z: 0 }, true );
cuerpo.applyImpulseAtPoint( { x: 0, y: 5, z: 0 }, { x: 1, y: 0, z: 0 }, true );

// Velocidad directa: util para control de personaje.
cuerpo.setLinvel( { x: 3, y: cuerpo.linvel().y, z: 0 }, true );

// Torque e impulso de torque, sus equivalentes angulares.
cuerpo.addTorque( { x: 0, y: 2, z: 0 }, true );
cuerpo.applyTorqueImpulse( { x: 0, y: 1, z: 0 }, true );

Ese true final es el wakeUp: despierta al cuerpo si estaba dormido. Ponlo casi siempre. La excepción documentada es simular una gravedad propia con addForce( f, false ), donde te interesa que el cuerpo pueda dormirse al alcanzar el equilibrio.

Las fuerzas son acumulativas y persistentes: se suman entre llamadas y siguen aplicándose paso tras paso hasta que llames a resetForces. Los impulsos son de una sola vez. Confundirlos da personajes que aceleran indefinidamente.

Un cinemático por posición se mueve con setNextKinematicTranslation.

// CORRECTO: el motor calcula la velocidad ficticia y empuja bien a los dinamicos.
plataforma.setNextKinematicTranslation( { x: 0, y: 2 + Math.sin( t ) * 3, z: 0 } );
plataforma.setNextKinematicRotation( q );

// MAL: teletransporta. Los dinamicos encima no se enteran y se quedan atras.
plataforma.setTranslation( { x: 0, y: 2 + Math.sin( t ) * 3, z: 0 }, true );

La diferencia es exactamente el motivo por el que existen esos métodos. setNextKinematic* no modifica la posición inmediatamente: guarda el destino, y durante el siguiente step() el motor calcula la velocidad implícita —destino menos origen, dividido por el paso— y la usa al resolver contactos. Por eso una caja apoyada sobre una plataforma que sube acompaña el movimiento en lugar de quedarse flotando. Con setTranslation la plataforma aparece de golpe en el destino y la caja se entera un paso tarde, lo que se ve como temblor, hundimiento o expulsión violenta.

Un fijo no se mueve. Si necesitas mover algo que creías fijo, era cinemático.

⚠️
setTranslation es teletransporte, no movimiento

La documentación de Rapier lo dice con todas las letras y conviene repetirlo: cambiar directamente la posición de un cuerpo es equivalente a teletransportarlo, y no es una acción físicamente realista. Sobre un dinámico produce interpenetraciones que el solver tiene que corregir con violencia. Úsalo solo para colocar cuerpos al inicio, para reiniciar una escena, o cuando la teletransportación es literalmente lo que quieres representar.

Masa, amortiguamiento y grados de libertad

La masa no se pone a mano casi nunca: sale de los colliders. Cada collider aporta masa e inercia calculadas desde su forma y su densidad, y el cuerpo suma las contribuciones de todos los suyos.

// Densidad por defecto 1. Una esfera de radio 1 pesa unos 4.19.
const col = RAPIER.ColliderDesc.ball( 1 ).setDensity( 2 );
mundo.createCollider( col, cuerpo );

Si quieres fijar la masa explícitamente, pon a cero la densidad de los colliders y usa setAdditionalMass o setAdditionalMassProperties en el descriptor; de lo contrario los dos valores se suman y acabas con el doble de lo que esperabas.

Una masa de cero significa masa infinita, no masa nula. Un cuerpo dinámico sin colliders y sin masa adicional no cae, y esa es la explicación de “mi objeto no responde a la gravedad” en la mayoría de los casos.

El amortiguamiento frena de forma continua y es la herramienta para simular resistencia del aire o para calmar una escena nerviosa:

const desc = RAPIER.RigidBodyDesc.dynamic()
	.setLinearDamping( 0.5 )
	.setAngularDamping( 1.0 );

Los bloqueos de grados de libertad son más eficientes y más estables que conseguir lo mismo con juntas:

// Un personaje que no debe volcar.
const personaje = RAPIER.RigidBodyDesc.dynamic()
	.setTranslation( 0, 2, 0 )
	.lockRotations();

// Rotacion permitida solo en Y: una puerta, una torreta.
cuerpo.setEnabledRotations( false, true, false, true );

lockRotations() en el descriptor bloquea las tres. setEnabledRotations( x, y, z, wakeUp ) permite un control fino después de crear el cuerpo. El caso del personaje que no vuelca es tan común que conviene tenerlo memorizado: sin bloquear rotaciones, una cápsula dinámica se cae en cuanto choca con algo.

Sueño y detección continua

Rapier duerme automáticamente los cuerpos que llevan un rato quietos. Un cuerpo dormido sale de la simulación y no consume nada, lo que permite escenas con miles de objetos donde solo unas decenas están activas. Se despierta solo cuando otro cuerpo activo lo toca, cuando una junta tira de él, o cuando tú lo despiertas.

cuerpo.wakeUp();
console.log( cuerpo.isSleeping() );

// Un cuerpo que nunca debe dormirse.
const desc = RAPIER.RigidBodyDesc.dynamic().setCanSleep( false );

El síntoma de un cuerpo dormido es desconcertante la primera vez: aplicas una fuerza y no pasa nada. Por eso casi todos los métodos llevan ese argumento wakeUp y por eso conviene ponerlo a true.

La detección continua evita el túnel: que un objeto rápido pase de un lado de una pared al otro entre dos pasos sin haber tocado nunca la pared.

const bala = RAPIER.RigidBodyDesc.dynamic()
	.setTranslation( 0, 1, 0 )
	.setLinvel( 200, 0, 0 )
	.setCcdEnabled( true );

// O despues de crearlo:
cuerpo.enableCcd( true );

Está desactivada por defecto porque cuesta. Actívala solo en los cuerpos que de verdad son rápidos: proyectiles, vehículos a alta velocidad, objetos lanzados con fuerza. Activarla en todos multiplica el coste de la fase estrecha sin beneficio, y activarla en cuerpos fijos no hace absolutamente nada.

La alternativa barata al CCD, cuando el objeto rápido es tuyo y controlado, es hacer un raycast desde la posición anterior a la nueva y resolver el impacto tú. Para una bala que solo necesita saber qué ha tocado, es mucho más barato que simularla.

La tentación de convertir todo en cinemático, y por qué se paga

Cuando la física da problemas —un objeto que tiembla, un personaje que se queda enganchado, una plataforma que empuja mal— la salida más tentadora es pasar el objeto conflictivo a cinemático y controlarlo a mano. De repente todo obedece, nada tiembla, y la escena hace exactamente lo que le dices. Esa sensación de control es real y es una trampa, porque acabas de mover el problema de un sitio donde estaba resuelto a un sitio donde tienes que resolverlo tú. Lo que un cuerpo cinemático te quita no es solo la gravedad: te quita la resolución de contactos, que es la parte difícil. Un dinámico que se acerca a una pared se detiene porque el solver calcula el impulso exacto que anula la componente normal de su velocidad, teniendo en cuenta la masa, la fricción, y todos los demás contactos simultáneos del mismo cuerpo. Todo eso, gratis. Con un cinemático tienes que hacerlo tú: detectar el contacto, calcular cuánto retroceder, decidir qué pasa si toca dos paredes a la vez en una esquina, evitar que el retroceso lo meta dentro de otra cosa. Ese es exactamente el conjunto de problemas que resuelve un character controller, y es el motivo por el que Rapier trae uno hecho con mundo.createCharacterController( offset ) en lugar de esperar que lo escribas. La regla práctica, entonces, tiene tres escalones y merece la pena recorrerlos en orden. Si el objeto debe reaccionar al mundo, es dinámico y sus problemas se arreglan con masa, amortiguamiento y bloqueo de rotaciones. Si el objeto debe imponer su trayectoria al mundo pero no ser afectado por él, es cinemático y su trayectoria la garantizas tú. Y si el objeto debe imponer su trayectoria respetando obstáculos —que es el caso de casi cualquier personaje jugable— no es ninguna de las dos cosas: es un controlador de personaje, una tercera categoría que usa consultas de colisión sin ser un cuerpo simulado. Saltarse ese tercer escalón y quedarse en el cinemático es el camino directo a reimplementar mal un solver de contactos.

⚔️ Cuatro cuerpos, cuatro comportamientos
  1. Crea uno de cada tipo en la misma escena y observa cómo responde cada uno a un empujón.
  2. Monta una plataforma que suba y baje con setNextKinematicTranslation y pon una caja encima.
  3. Cámbiala a setTranslation y documenta exactamente qué le pasa a la caja.
  4. Crea una cápsula dinámica como personaje y comprueba que se cae; arréglalo con lockRotations.
  5. Lanza un proyectil a 300 unidades por segundo contra una pared fina, con y sin setCcdEnabled( true ).