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.
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.

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

| Range | Shape |
|---|---|
| [-1, -5/8) | Cup |
| [-5/8, -3/8) | Trough |
| [-3/8, 3/8) | Saddle |
| [3/8, 5/8) | Ridge |
| 5/8, 1 | Cap |
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.
Related tutorials
- Fast and Accurate Geodesic Distances in C++ — distance along the surface from one seed or many.
- Fast and Robust Isocontours on a Triangle Mesh in C++ — curves and bands of equal value on a scalar field.
- Fast and Robust Mesh Slicing and Cross-Sections in C++ — plane sections, layers and slabs of a mesh.
- Fast STL and OBJ Reading in C++ — load and write meshes, the first step of every pipeline.
Performance
We measured principal curvatures on the Stanford dragon at 1M faces, against libigl's principal_curvature.
Apple M4 Max 16 threads

Full benchmarks · Documentation
@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}
}