Translated from the author's Korean original. Read the original (한국어)
FEM for Rigid Body Dynamics: Quaternion-based Rotation Handling and Inertia Tensors
A linear tet has 12 nodal dofs; the rigid constraint leaves 6. The explicit ceiling is dt under 2/omega_max, and in-between poses stay on the sphere only with SLERP.
Basic Equations of Rigid Body Dynamics#
In an FSI run or a DEM pile, a rigid body only has to keep distances fixed. No strain. The rotation model is what usually breaks that promise first.
Particle moves as translation of the center of mass plus a rotation.
is the center-of-mass velocity, the angular velocity, and the offset from the center of mass.
The axis slider and ω change which axis spins and how long one turn takes, even at the same angular-speed magnitude.
주황 화살표가 회전축. 사원수 q를 직접 곱해 매 프레임 큐브를 회전시킨다 — 짐벌락 없이 안정.
Raise ω and the body completes a turn sooner. Tilt the axis onto a body diagonal and check that no angle locks onto a single coordinate axis.
Linear Momentum and Angular Momentum#
Total linear momentum from Newton's second law is:
Where is the total mass. The acceleration of the center of mass is:
Total angular momentum is:
Where is the inertia tensor and is the total torque.
Inertia Tensor#
The inertia tensor relative to the center of mass is:
Extending to a continuum:
Where is the identity tensor, and is the outer product.
When an object rotates, the inertia tensor is transformed by the orientation matrix .
Where is the initial inertia tensor in the body frame, which only needs to be calculated once at the start of the simulation.
Rotation Representation Using Quaternions#
Representing rotation with Euler angles causes the gimbal lock problem. To avoid this, quaternions are used.
A quaternion consists of a scalar part and a vector part.
A rotation by an angle about a unit axis is:
The composite rotation of two quaternions and is expressed as a product.
Using this, the orientation matrix is explicitly expressed in terms of quaternion components.
The time derivative of a quaternion is linked to the angular velocity .
The diagonal of is . The off-diagonal is . Put all the mass on the axis, so , and while . There is no lever arm about that axis. is that sum, stored once in the body frame. In the world frame only has to be rebuilt. Multiply a body-frame tensor by a world-frame angular velocity and the torque axis tilts off. The same congruence shows up when a stiffness tensor changes coordinates, which is what the constitutive-tensor transformation tracks inside an element.
At , and , so . That is the quaternion the code starts with. At , and , the scalar dies, and the vector part is just the axis . At , , so . The rotation matches the identity. Only the sign flipped. A quaternion covers one full turn twice. The entry of is , which is 1 for this identity value, and the off-diagonal entries are 0. The in front of is the same factor as in the angle formula. Integrate it raw and walks away from 1. A linear tet has 4 nodes and 3 displacements each, 12 degrees of freedom. The rigid constraint cuts that to 3 translations plus 3 rotations, 6 in all. only keeps the coordinates consistent. It does not create those 6 by itself.
FEM Discretization and Tetrahedral Shape Functions#
When discretizing the displacement field of a rigid body in FEM, the shape functions of a tetrahedral element are:
Where are dimensionless barycentric coordinates and satisfy the condition .
In linear tetrahedral elements, displacement interpolation is:
Applying rigid body constraints reduces the degrees of freedom to 6 (3 linear + 3 rotational).
Python Implementation Example#
import numpy as np
class RigidBody:
def __init__(self, mass, I_body):
self.M = mass
self.I0 = I_body # Body frame inertia tensor (3x3)
self.x_cm = np.zeros(3) # Center of mass position
self.v_cm = np.zeros(3) # Center of mass velocity
self.q = np.array([1., 0., 0., 0.]) # Quaternion (w, x, y, z)
self.omega = np.zeros(3) # Angular velocity
def quaternion_to_rotation(self):
w, x, y, z = self.q
return np.array([
[1-2*(y**2+z**2), 2*(x*y - w*z), 2*(x*z + w*y)],
[2*(x*y + w*z), 1-2*(x**2+z**2), 2*(y*z - w*x)],
[2*(x*z - w*y), 2*(y*z + w*x), 1-2*(x**2+y**2)]
])
def inertia_world(self):
R = self.quaternion_to_rotation()
return R @ self.I0 @ R.T
def step(self, F_ext, tau_ext, dt):
# Linear motion integration
a_cm = F_ext / self.M
self.v_cm += a_cm * dt
self.x_cm += self.v_cm * dt
# Rotational motion integration
I_world = self.inertia_world()
alpha = np.linalg.solve(I_world, tau_ext - np.cross(self.omega, I_world @ self.omega))
self.omega += alpha * dt
# Quaternion update
omega_q = np.concatenate([[0.], self.omega])
q_dot = 0.5 * quaternion_multiply(omega_q, self.q)
self.q += q_dot * dt
self.q /= np.linalg.norm(self.q) # Normalize unit quaternion
def quaternion_multiply(q1, q2):
w1, x1, y1, z1 = q1
w2, x2, y2, z2 = q2
return np.array([
w1*w2 - x1*x2 - y1*y2 - z1*z2,
w1*x2 + x1*w2 + y1*z2 - z1*y2,
w1*y2 - x1*z2 + y1*w2 + z1*x2,
w1*z2 + x1*y2 - y1*x2 + z1*w2
])Skip the renormalization and R stops being a rotation#
Leave unscaled and the columns of lose unit length. The line q /= norm(q) is that scaling. Torque computed in the world frame, then multiplied by the stored , points along the wrong axis. The world tensor is before is consistent. The explicit ceiling is . A high means the step shrinks first.
A straight blend between two poses sinks inside the unit sphere. SLERP is the curve that hits at and at .
LERP does not stay on that sphere. Count rotational freedom together with element strain and the constraint eats the motion, the same way shear locking in a thin shell stiffens a plate that should bend. A rigid body is 6 degrees of freedom, not 12.
Related
Share if you found it helpful.