Tutorials
October 31, 2026
c++curvaturetutorial

Fast and Robust Principal Curvatures of a Mesh in C++

Compute principal curvatures, principal directions, Gaussian and mean curvature, and the shape index of a triangle mesh in C++. Header-only, C++17. 30.1 ms on 1M faces, 51× faster than libigl.

Žiga Sajovic, Polydera

Also available in Python and JavaScript.

Principal curvatures tell you how a surface bends at each point. k0 is the largest bend, k1 the smallest. From them you get Gaussian curvature, mean curvature, and the shape index. trueform computes all of it per vertex. It is header-only C++17. This tutorial covers principal curvatures, principal directions, the shape index, and how to read the signs.

trueform GitHub Documentation Try it live

Install

Header-only. It needs C++17 and oneTBB. Add it to your project via CMake FetchContent:

FetchContent_Declare(
  trueform
  GIT_REPOSITORY https://github.com/polydera/trueform
)
FetchContent_MakeAvailable(trueform)
target_link_libraries(my_app PRIVATE tf::trueform)

Also available via conan, nuget, and pip. See the installation guide for all options.

Loading the mesh

#include <trueform/trueform.hpp>

auto mesh_buffer = tf::read_stl("part.stl");
auto mesh = mesh_buffer.polygons();

tf::read_stl returns a tf::polygons_buffer that owns the data. .polygons() is a view on it. tf::read_obj works the same way. If your faces and points already live in your own arrays, wrap them with tf::make_polygons(tf::make_faces<3>(faces), tf::make_points<3>(points)). No copy is made.

To follow along without a file, make a sphere of radius 2:

auto sphere_buffer = tf::make_sphere_mesh(2.f, 100, 200);
auto mesh = sphere_buffer.polygons();

Principal curvatures

auto [k0, k1] = tf::make_principal_curvatures(mesh);
// k0, k1: tf::buffer<float>, one value per point; k0 >= k1

On the sphere both are close to 0.5, one over the radius. Each vertex fits a quadric to its neighborhood. The neighborhood is the k-ring, two rings by default. A larger ring smooths out noise:

auto [s0, s1] = tf::make_principal_curvatures(mesh, 3);

Gaussian and mean curvature follow directly:

tf::buffer<float> gaussian, mean;
gaussian.allocate(k0.size());
mean.allocate(k0.size());

for (auto [a, b, g, m] : tf::zip(k0, k1, gaussian, mean)) {
  g = a * b;
  m = (a + b) / 2;
}

On the sphere, Gaussian curvature is close to 0.25 and mean curvature close to 0.5.

For repeated runs into memory you already own, tf::compute_principal_curvatures(mesh, k0, k1) writes into pre-allocated buffers.

Principal directions

Ask for the directions too:

auto [c0, c1, d0, d1] = tf::make_principal_directions(mesh);
// d0, d1: tf::unit_vectors_buffer<float, 3>, one direction per point

d0 is the direction of k0, d1 the direction of k1. Both are unit vectors, perpendicular to each other and to the vertex normal. You can check:

auto normals = tf::compute_normals(mesh);
auto fm = tf::make_face_membership(mesh);
auto point_normals = tf::compute_point_normals(
    mesh | tf::tag(fm) | tf::tag_normals(normals.unit_vectors()));

float worst = 0.f;   // close to 0
for (auto [u, v, n] : tf::zip(d0, d1, point_normals.unit_vectors()))
  worst = std::max({worst, std::abs(tf::dot(u, v)), std::abs(tf::dot(u, n))});

Directions are useful for drawing hatching along a surface, for aligning tool paths, and for finding ridges and valleys.

Shape index

The shape index folds the two curvatures into one number in -1, 1. It describes the local shape, not its size. A small ball and a large ball have the same shape index.

auto si = tf::make_shape_index(mesh);      // one value per point
auto si3 = tf::make_shape_index(mesh, 3);  // larger neighborhood

RangeShape
[-1, -5/8)Cup
[-5/8, -3/8)Trough
[-3/8, 3/8)Saddle
[3/8, 5/8)Ridge
5/8, 1Cap

Classify every vertex:

std::array<int, 5> counts{};   // cup, trough, saddle, ridge, cap
for (float s : si) {
  int c = s < -5.f / 8 ? 0
        : s < -3.f / 8 ? 1
        : s <  3.f / 8 ? 2
        : s <  5.f / 8 ? 3 : 4;
  ++counts[c];
}

On the sphere every vertex is a cap.

Reading the signs

Signs follow the face orientation. With outward-facing normals, a convex surface has positive curvature. On the sphere k0 and k1 are positive and the shape index is near 1. Flip the faces and the signs flip with them:

auto flipped = sphere_buffer;   // a copy
tf::reverse_winding(flipped.faces());
auto si_flipped = tf::make_shape_index(flipped.polygons());   // near -1

If your results look inverted, check the winding first. tf::ensure_positive_orientation fixes it on closed meshes.

A height field shows all the shapes. Take a flat grid with +z normals and bend it:

auto plane_buffer = tf::make_plane_mesh(2.f, 2.f, 100, 100);

auto saddle = plane_buffer;   // shape index 0 at the center
for (auto pt : saddle.points()) pt[2] = pt[0] * pt[0] - pt[1] * pt[1];

auto ridge = plane_buffer;    // bends away from the normal: +0.5
for (auto pt : ridge.points()) pt[2] = -pt[0] * pt[0];

auto trough = plane_buffer;   // bends toward the normal: -0.5
for (auto pt : trough.points()) pt[2] = pt[0] * pt[0];

Working with results

All results are tf::buffers with one value per point, in the mesh's coordinate type. Use them as colors, as weights, or as fields for other tools.

Draw the lines where the surface changes from saddle to ridge with isocontours:

auto lines = tf::make_isocontours(mesh, tf::make_range(si), 3.f / 8);

Cut the mesh into regions by shape. The curves become edges and each face gets a band label:

std::array<float, 2> cuts{-3.f / 8, 3.f / 8};
auto [regions, labels, face_labels] =
    tf::embedded_isocurves(mesh, tf::make_range(si), tf::make_range(cuts));
// labels: 0 concave, 1 saddle, 2 convex

Flag sharp spots for inspection:

std::vector<int> sharp;
for (int i = 0; i < (int)k0.size(); ++i)
  if (std::max(std::abs(k0[i]), std::abs(k1[i])) > 10.f) sharp.push_back(i);

Combine curvature with geodesic distances to look only at the neighborhood of a point.

Performance

We measured principal curvatures on the Stanford dragon at 1M faces, against libigl's principal_curvature.

30.1msPrincipal curvatures1M faces, C++
51×Faster than libigl1M faces, C++ (1,520 ms)
27.7msFrom Python1M faces (libigl: 1,941 ms)

Apple M4 Max 16 threads

1M facestrueformlibiglSpeedupC++30.1 ms1,520 ms51×Python27.7 ms1,941 ms70×

Full benchmarks · Documentation

Cite as
@article{polydera:principal-curvatures-of-a-mesh-in-cpp,
  title={Fast and Robust Principal Curvatures of a Mesh in C++},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/principal-curvatures-of-a-mesh-in-cpp},
  organization={Polydera}
}