Saltar al contenido

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.

Los cuatro tipos

JS
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.

JS
// 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.

JS
// 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.

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.

JS
// 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:

JS
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:

JS
// 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.

JS
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.

JS
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.