Skip to content

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:

  1. create and label topology;
  2. apply ABD so instance attributes exist;
  3. apply the contact element;
  4. write the per-instance transform; and
  5. 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:

auto is_fixed = body.instances().find<IndexT>(builtin::is_fixed);
view(*is_fixed)[0] = 1;
import uipc.builtin as builtin
from uipc import view

is_fixed = body.instances().find(builtin.is_fixed)
view(is_fixed)[0] = 1

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