Skip to content

Pure Volumetric FEM Scenes

A volumetric FEM body is a tetrahedral SimplicialComplex whose vertices own the dynamic degrees of freedom. This page uses StableNeoHookean, the standard large-deformation solid model, without contact so that the constitutive setup is isolated from collision behavior.

What defines a deformable solid

Part API Meaning
Rest mesh tetmesh(vertices, tetrahedra) Tetrahedral material domain and rest positions.
Boundary surface label_surface, label_triangle_orient Extracts and orients boundary triangles for contact and export.
Elastic material ElasticModuli.youngs_poisson(E, nu) Converts Young's modulus and Poisson ratio to Lame parameters.
Primary constitution StableNeoHookean.apply_to(mesh, moduli, mass_density) Creates vertex state, mass, and per-tetrahedron material attributes.
Boundary condition mesh.vertices()/is_fixed 1 fixes one vertex; 0 leaves it dynamic.
Contact material ContactElement.apply_to(mesh) Optional pairwise material identity when contact is enabled.

The initial vertex positions are the material rest state. Unlike ABD, there is no per-body transform that moves a normal FEM body without changing that rest state. Place or transform the mesh before applying the constitution.

Complete contact-free scene

The following asset-free program drops one tetrahedron under gravity. Contact is explicitly disabled, so no collision surface, ground, or contact table is needed for the physics. The surface labels are retained because they are the normal preparation for later contact or surface export.

#include <cmath>
#include <filesystem>
#include <iostream>

#include <uipc/uipc.h>
#include <uipc/constitution/stable_neo_hookean.h>

int main()
{
    using namespace uipc;
    using namespace uipc::constitution;
    using namespace uipc::core;
    using namespace uipc::geometry;

    const std::string workspace = "output/docs/fem_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};
    config["contact"]["enable"] = false;
    Scene scene{config};

    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}};
    vector<Vector4i> tetrahedra = {Vector4i{0, 1, 2, 3}};

    auto solid = tetmesh(vertices, tetrahedra);
    label_surface(solid);
    label_triangle_orient(solid);

    StableNeoHookean material;
    auto             moduli = ElasticModuli::youngs_poisson(50.0_kPa, 0.499);
    material.apply_to(solid, moduli, 1000.0);

    scene.objects().create("soft_tet")->geometries().create(solid);

    world.init(scene);
    if(!world.is_valid())
        return 1;
    while(world.frame() < 10)
    {
        world.advance();
        world.retrieve();
    }

    std::cout << "FEM scene reached frame " << world.frame() << '\n';
    return world.is_valid() ? 0 : 1;
}
"""Minimal contact-free tetrahedral FEM scene used by the FEM tutorial."""

from pathlib import Path

import numpy as np

from uipc import Logger
from uipc.core import Engine, Scene, World
from uipc.geometry import label_surface, label_triangle_orient, tetmesh
from uipc.constitution import ElasticModuli, StableNeoHookean
from uipc.unit import kPa


Logger.set_level(Logger.Level.Warn)
workspace = Path("output/docs/fem")
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]]
config["contact"]["enable"] = False  # isolate the volumetric FEM model
scene = Scene(config)

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]]
)
tetrahedra = np.array([[0, 1, 2, 3]])
solid = tetmesh(vertices, tetrahedra)
label_surface(solid)
label_triangle_orient(solid)

material = ElasticModuli.youngs_poisson(50.0 * kPa, 0.499)
StableNeoHookean().apply_to(solid, material, mass_density=1000.0)

scene.objects().create("soft_tet").geometries().create(solid)

world.init(scene)
assert world.is_valid()
for _ in range(10):
    world.advance()
    world.retrieve()
assert world.is_valid()
print(f"FEM scene reached frame {world.frame()}")

The material in this example uses E = 50 kPa, nu = 0.499, and density 1000 kg/m^3. The public Stable Neo-Hookean overload defaults to E = 120 kPa, nu = 0.49, and density 1000 kg/m^3; supplying values explicitly is recommended because stiffness and scale are scene dependent.

For an ordinary isotropic 3D elastic material, use E > 0 and -1 < nu < 0.5. Values near 0.5 model near-incompressibility and usually make the linear system harder. The API rejects the singular endpoints but does not otherwise enforce a physically meaningful material range.

Fixing vertices

Apply the FEM constitution first because it creates the is_fixed attribute, then mark selected vertices:

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

is_fixed = solid.vertices().find(builtin.is_fixed)
fixed = view(is_fixed)
fixed[0] = 1
fixed[1] = 1

Fixed vertices remain part of the deformable mesh and still participate in surface contact. To prescribe time-dependent motion rather than a permanent pin, use a soft position constraint and update its animation state; see the animation tutorial and the 92_twisting_bar sample.

Adding ground contact

Starting from the complete program, remove the line that sets config["contact"]["enable"] = false, then configure and apply a material:

scene.contact_tabular().default_model(0.4, 100.0_MPa);
auto contact = scene.contact_tabular().default_element();
contact.apply_to(solid);

auto floor = ground(0.0);
contact.apply_to(floor);
scene.objects().create("floor")->geometries().create(floor);
from uipc.geometry import ground
from uipc.unit import MPa

scene.contact_tabular().default_model(0.4, 100.0 * MPa)
contact = scene.contact_tabular().default_element()
contact.apply_to(solid)

floor = ground(0.0)
contact.apply_to(floor)
scene.objects().create("floor").geometries().create(floor)

Do this before adding solid to the scene. The tetrahedral mesh already has a labeled surface; omitting label_surface(solid) is the common reason a solid does not collide.

Mesh and solver considerations

  • Tetrahedra must be non-degenerate and consistently oriented. Inspect input mesh quality before compensating with solver settings.
  • The FEM state is per vertex, so duplicating boundary vertices creates disconnected material unless a constraint is added intentionally.
  • Material modulus, density, time step, and mesh scale interact. If Newton or PCG convergence degrades, first verify units and element quality, then tune the documented solver tolerances.
  • linear_system/fem_preconditioner = "mas" enables the MAS preconditioner for every non-empty FEM mesh in the scene. It is a scene-wide performance choice, not a material model, and defaults to "diag".
  • FEM self-collision is enabled by default by the FEM base constitution. Pair masks and the global contact switch still determine whether candidates are active.

See Scene Configuration for exact defaults and operational domains, and Contact and Collision for activation distances and material stiffness.

Deeper examples and implementation evidence