Pure Rigid-Body Scenes
libuipc represents a rigid/stiff object with Affine Body Dynamics (ABD). An
affine body has 12 degrees of freedom: translation plus a full 3x3 affine
transform. AffineBodyConstitution adds a shape-preservation energy that keeps
the affine transform close to rigid motion.
This is not an exact six-DOF rigid-body formulation. The stiffness kappa
controls how strongly non-rigid affine deformation is suppressed. For
rigid-looking behavior, use a physically scaled positive stiffness and verify
that visible shape change is negligible; the constitution reference suggests
roughly 100 MPa to 100 GPa as the normal working range.
Anatomy of one ABD body
| Part | API | Meaning |
|---|---|---|
| Rest geometry | tetmesh(...) or a closed surface mesh |
Local material points used for mass properties and collision surface. |
| Collision labels | label_surface, label_triangle_orient |
Extract the boundary used by contact and give tetrahedral boundary faces an orientation. |
| Physics | AffineBodyConstitution.apply_to(mesh, kappa, mass_density) |
Creates ABD mass, stiffness, DOF, transform, velocity, and fixed-state attributes. |
| Initial pose | mesh.transforms() |
Per-instance 4x4 transform. Do not translate local rest vertices to animate body state. |
| Boundary condition | mesh.instances()/is_fixed |
1 fixes that entire affine-body instance. |
| Contact material | ContactElement.apply_to(mesh) |
Selects a row/column in the scene contact table. Untagged geometry uses the default element. |
Complete minimal scene
This asset-free program drops one affine tetrahedron onto an implicit plane. There are no FEM constitutions in the scene, so every dynamic explicit body is ABD.
#include <cmath>
#include <filesystem>
#include <iostream>
#include <uipc/uipc.h>
#include <uipc/constitution/affine_body_constitution.h>
int main()
{
using namespace uipc;
using namespace uipc::constitution;
using namespace uipc::core;
using namespace uipc::geometry;
const std::string workspace = "output/docs/rigid_body_cpp";
std::filesystem::create_directories(workspace);
Engine engine{"cuda", workspace};
World world{engine};
auto config = Scene::default_config();
config["dt"] = 0.01;
config["gravity"] = Vector3{0.0, -9.8, 0.0};
Scene scene{config};
scene.contact_tabular().default_model(0.4, 1.0_GPa);
auto default_contact = scene.contact_tabular().default_element();
const Float s3 = std::sqrt(3.0) / 2.0;
vector<Vector3> vertices = {Vector3{0.0, 1.0, 0.0},
Vector3{0.0, 0.0, 1.0},
Vector3{-s3, 0.0, -0.5},
Vector3{s3, 0.0, -0.5}};
for(auto& p : vertices)
p *= 0.3;
vector<Vector4i> tetrahedra = {Vector4i{0, 1, 2, 3}};
auto body = tetmesh(vertices, tetrahedra);
label_surface(body);
label_triangle_orient(body);
AffineBodyConstitution abd;
abd.apply_to(body, 100.0_MPa, 1000.0);
default_contact.apply_to(body);
Transform transform = Transform::Identity();
transform.translation() = Vector3{0.0, 1.2, 0.0};
view(body.transforms())[0] = transform.matrix();
scene.objects().create("falling_body")->geometries().create(body);
auto floor = ground(0.0);
default_contact.apply_to(floor);
scene.objects().create("floor")->geometries().create(floor);
world.init(scene);
if(!world.is_valid())
return 1;
while(world.frame() < 10)
{
world.advance();
world.retrieve();
}
std::cout << "rigid-body scene reached frame " << world.frame() << '\n';
return world.is_valid() ? 0 : 1;
}
"""Minimal pure-affine-body scene used by the rigid-body tutorial."""
from pathlib import Path
import numpy as np
from uipc import Logger, Transform, Vector3, view
from uipc.core import Engine, Scene, World
from uipc.geometry import ground, label_surface, label_triangle_orient, tetmesh
from uipc.constitution import AffineBodyConstitution
from uipc.unit import GPa, MPa
Logger.set_level(Logger.Level.Warn)
workspace = Path("output/docs/rigid_body")
workspace.mkdir(parents=True, exist_ok=True)
engine = Engine("cuda", str(workspace))
world = World(engine)
config = Scene.default_config()
config["dt"] = 0.01
config["gravity"] = [[0.0], [-9.8], [0.0]]
scene = Scene(config)
# Contact is global, while friction/resistance live in the pairwise table.
scene.contact_tabular().default_model(0.4, 1.0 * GPa)
default_contact = scene.contact_tabular().default_element()
# A closed tetrahedral volume is one affine body instance.
s3 = np.sqrt(3.0) / 2.0
vertices = np.array(
[[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-s3, 0.0, -0.5], [s3, 0.0, -0.5]]
) * 0.3
tetrahedra = np.array([[0, 1, 2, 3]])
body = tetmesh(vertices, tetrahedra)
label_surface(body)
label_triangle_orient(body)
abd = AffineBodyConstitution()
abd.apply_to(body, kappa=100.0 * MPa, mass_density=1000.0)
default_contact.apply_to(body)
# ABD state is per instance. Move the instance, not its local rest vertices.
transform = Transform.Identity()
transform.translate(Vector3.Values([0.0, 1.2, 0.0]))
view(body.transforms())[0] = transform.matrix()
scene.objects().create("falling_body").geometries().create(body)
floor = ground(0.0)
default_contact.apply_to(floor)
scene.objects().create("floor").geometries().create(floor)
world.init(scene)
assert world.is_valid()
for _ in range(10):
world.advance()
world.retrieve()
assert world.is_valid()
print(f"rigid-body scene reached frame {world.frame()}")
The order in the example is intentional:
- create and label topology;
- apply ABD so instance attributes exist;
- apply the contact element;
- write the per-instance transform; and
- add the finished geometry to the scene.
Fixing a body
ABD fixed state is per instance, not per vertex. Set it after applying the constitution:
Setting selected vertex flags on a normal ABD mesh does not create a partially fixed rigid body; vertex-level boundary conditions belong to FEM/cloth.
Sharing one rest mesh across instances
ABD instances share topology and rest-space vertices but own independent transforms, velocities, stiffness values, and fixed flags. Resize the instance collection before applying ABD so all generated instance attributes have the right size:
cube.instances().resize(20);
abd.apply_to(cube, 100.0_MPa, 1000.0);
auto transforms = view(cube.transforms());
auto fixed = view(*cube.instances().find<IndexT>(builtin::is_fixed));
for(SizeT i = 0; i < transforms.size(); ++i)
{
Transform t = Transform::Identity();
t.translation() = Vector3{0.0, 0.4 * i, 0.0};
transforms[i] = t.matrix();
fixed[i] = 0;
}
cube.instances().resize(20)
abd.apply_to(cube, 100.0 * MPa, 1000.0)
transforms = view(cube.transforms())
fixed = view(cube.instances().find(builtin.is_fixed))
for i in range(len(transforms)):
t = Transform.Identity()
t.translate(Vector3.Values([0.0, 0.4 * i, 0.0]))
transforms[i] = t.matrix()
fixed[i] = 0
Instancing is preferable to duplicating identical meshes when only pose and per-body state differ.
Mass and stiffness
The standard overload computes mass properties from mesh geometry and
mass_density (default 1000 kg/m^3). A closed, consistently oriented shape is
therefore important. The public API also provides overloads for an explicit
12x12 ABD mass matrix and create_proxy(...) helpers for bodies whose mass
properties come from another source; proxy bodies have no collision geometry
until a collision shape is supplied separately.
kappa is shape-preservation stiffness, not contact resistance. Contact
resistance is the second argument of ContactTabular.default_model(...) or
insert(...). Changing one does not change the other.
Contact and self-collision
AffineBodyConstitution creates self_collision = 0 by default. That is
usually correct for a single closed rigid body: triangles from the same body
should not collide with one another. Contact between different ABD instances
or geometries still follows the contact table.
For material-dependent friction, assign contact elements before adding the geometry. See Rigid-Soft Coupling and Contact for a full pairwise table.
Deeper examples and implementation evidence
- Python starting point:
1_hello_libuipc - larger ABD systems:
6_wrecking_balls - material-dependent sliding:
10_ramp_sliding - C++ regression baselines:
0_abd_gravity.cppand8_abd_multi_contact_model.cpp - public constitution API:
affine_body_constitution.h