Feature lines are among the most expressive descriptors of surface geometry. They reveal sharp bends, concentrated curvature, and the paths the eye naturally follows across a shape. In scientific visualization, CAD inspection, and non-photorealistic rendering, they offer a compact, meaningful summary of the underlying surface.
This article builds on the curvature-estimation pipeline developed in the previous installment. Using the MeshExplicit representation, we computed per-vertex principal curvatures and principal directions, producing a local geometric field that describes how the surface bends.
Here, we use that field to extract three important classes of feature lines:
Together, these feature types cover both smooth and piecewise-smooth geometry. Ridges and valleys appear naturally on anatomical models, scanned objects, and sculpted meshes, while creases capture the hard edges common in CAD models, mechanical parts, and stylized assets.
We continue using MeshExplicit, extended with a lightweight adjacency module. This keeps the implementation approachable and consistent with the curvature article. Later, when we introduce half-edge meshes and segmentation, we will revisit feature lines with more advanced tools; for now, MeshExplicit is more than sufficient.
By the end of this article, you will have a complete C++ implementation for extracting feature lines from any triangle mesh, ready for visualization or integration into downstream tools.
Feature lines are surface curves that reveal meaningful geometric behavior. Rather than arbitrary polylines, they are defined by specific mathematical conditions that identify curvature extremes, sharp transitions, or visually important structure.
Ridges and valleys are derived from the
A
A
For discrete triangle meshes, the article uses a robust and easy-to-implement criterion:
Once candidate vertices are identified, the extractor traces polylines by walking along the principal direction \(d_{max}\).
This simplified approach is robust, efficient, and well suited to practical geometry-processing workflows.
Sharp creases are detected independently of curvature. They occur where two adjacent faces meet at a large
\[\theta =\arccos\left(\left\langle n_{0}^{\prime },\; n_{1}^{\prime }\right\rangle \right)\]
Here, the face normals are projected onto the plane orthogonal to the shared edge before the angle is measured. This makes the crease test more stable on irregular meshes.
If the angle \(\theta\) exceeds a user-defined threshold, such as 35°, the edge is classified as a crease. Connected crease edges are then assembled into crease polylines.
This method is especially effective for CAD models, mechanical parts, and meshes with deliberately hard edges.
Feature lines provide a compact, intuitive description of mesh structure. They are useful in several common workflows:
In short, feature lines turn dense surface data into a readable geometric summary that supports analysis, visualization, and downstream processing.
The MeshExplicit structure keeps the mesh data direct and easy to inspect. It stores the core geometric information needed by the feature-line extractor:
This layout is intentionally simple: it is readable, easy to debug, and well suited to teaching, experimentation, and incremental development.
Feature-line extraction depends on fast local neighborhood queries. Different feature types need different forms of connectivity:
MeshExplicit was enhanced to include adjacency. We added
The adjacency data is built once, then reused throughout ridge, valley, and crease extraction, or any other calculation.
The adjacency builder constructs the required connectivity in a single pass over the triangle faces. Its main responsibilities are:
With this functionality in place, MeshExplicit remains lightweight while still supporting all neighborhood operations required for feature-line extraction.
Feature lines are stored as polylines: ordered sequences of mesh vertices and corresponding 3D points. To keep the implementation modular, configurable, and easy to extend, we organize the data into three small structures.
A FeatureLine represents one extracted ridge, valley, or crease. It stores the line in two complementary forms:
Keeping both representations gives the extractor flexibility: the indices preserve topology, while the points make visualization and post-processing straightforward.
Feature-line extraction depends on a small set of user-controlled thresholds and limits. Grouping them into one parameter structure keeps the extractor clean and makes experiments easier to reproduce.
Together, these controls make it easy to tune the extractor for smooth scans, CAD-like models, or stylized assets without changing the core algorithm.
FeatureLineExtractor is the central class in this article. It combines the mesh, curvature data, and extraction parameters into one workflow.
The class exposes four public extraction methods:
Internally, the extractor separates the main algorithmic steps into focused helper routines:
This design keeps the public interface small while making the implementation easier to read, test, and extend in later sections.
Ridges and valleys are the primary feature lines on smooth surfaces. They show where curvature concentrates and how the surface bends along its principal directions. This section builds a practical extraction pipeline using the curvature fields from the previous article and the adjacency data introduced earlier.
The pipeline has three main stages:
Each stage is simple in isolation, but together they produce a robust algorithm that works well on real triangle meshes.
At each vertex, we use the maximum principal curvature \(k_{max}\) and its associated direction \(d_{max}\). The sign and magnitude of \(k_{max}\) determine whether the vertex is a ridge or valley candidate:
The ridge and valley thresholds are user-controlled parameters. They help the extractor:
On smooth geometry, \(k_{max}\) varies continuously; on triangle meshes, however, discretization can introduce local fluctuations. Thresholding keeps the extractor conservative and prevents noisy features from becoming lines.
In code, these criteria become two small boolean checks:
bool FeatureLineExtractor::isRidgeVertex(int v) const
{
double k = m_curvature.kmax[v];
return std::abs(k) > m_params.ridgeKappaThreshold && k > 0.0;
}
bool FeatureLineExtractor::isValleyVertex(int v) const
{
double k = m_curvature.kmax[v];
return std::abs(k) > m_params.valleyKappaThreshold && k < 0.0;
}
This intentionally conservative test traces only through vertices where the curvature signal is clearly significant.
After a ridge or valley seed is found, the extractor grows a polyline by walking through neighboring vertices in the principal direction d₁. This directional walk is the core of the ridge/valley algorithm.
For the current vertex and its principal direction, the extractor examines all 1-ring neighbors and selects the neighbor whose edge direction aligns best with d₁.
For each neighboring vertex, the test proceeds as follows:
The neighbor with the highest positive alignment score becomes the next vertex. Negative scores are ignored because they point behind the current tracing direction.
Although simple, this test is effective: it keeps the polyline moving with the surface’s curvature flow rather than jumping across unrelated mesh edges.
Because a feature line extends in two directions from its seed, the extractor traces both halves independently:
Tracing on either side stops when any termination condition is reached:
A compact helper lambda keeps this repeated logic localized:
auto traceOneSide = [&](int startVertex, bool forward) -> std::vector<int>
{
std::vector<int> path;
int current = startVertex;
int steps = 0;
while (steps < m_params.maxTraceSteps)
{
if (!isRidgeVertex(current)) break; // or isValleyVertex for valleys
path.push_back(current);
// Find the next vertex in the principal direction
int next = findNextVertex(current, forward);
if (next == -1) break; // No valid next vertex
current = next;
++steps;
}
return path;
};
This keeps the tracing code compact without obscuring the algorithm.
After both directions are traced, the extractor combines them into one ordered polyline:
The result is a clean, ordered ridge or valley polyline.
The final stage turns traced segments into a usable set of feature lines. It removes duplicates, filters out tiny fragments, and stores each accepted result as a FeatureLine.
Tracing from every candidate vertex would create many overlapping polylines. To prevent this, the extractor maintains a visited mask:
This simple bookkeeping step keeps the output clean and avoids redundant work.
Very short polylines are usually noise rather than meaningful features, so the extractor discards any line with
fewer vertices than
This user-controlled value is typically set between 3 and 5, depending on mesh resolution and noise level.
The complete ridge extraction routine combines candidate selection, tracing, duplicate suppression, and line storage:
std::vector<FeatureLine> FeatureLineExtractor::extractRidges()
{
std::vector<FeatureLine> lines;
std::vector<bool> visited(V, false);
for (int v = 0; v < V; ++v)
{
if (!isRidgeVertex(v) || visited[v])
continue;
FeatureLine line = traceCurvatureLine(v, true);
if (!line.empty())
{
for (int idx : line.vertices)
visited[idx] = true;
lines.push_back(std::move(line));
}
}
return lines;
}
Valley extraction uses the same structure, replacing the ridge predicate with the valley predicate.
Ridge and valley extraction combines curvature thresholding with directional tracing. The algorithm proceeds as follows:
This method is conservative, easy to implement, and effective on both smooth and moderately noisy meshes.
Ridges and valleys describe curvature-driven features on smooth surfaces. Many meshes, however, also contain
To detect these discontinuities, crease extraction uses the
For an edge shared by two faces, the dihedral angle measures how sharply those faces meet along the edge. The calculation starts from the two adjacent face normals and produces an angle θ that describes crease sharpness.
A simple normal-to-normal comparison would compute:
\[\theta =\arccos\left(\left\langle n_{0},\; n_{1}\right\rangle \right)\]
That direct comparison can be unstable because it does not isolate rotation around the shared edge. To measure crease sharpness more consistently, we first project both normals onto the plane orthogonal to the edge direction.
Let the edge be defined by its two endpoint vertices:
\[e=p_{1}-p_{0},\ \widehat{e}=\frac{e}{\parallel e\parallel }\]
Next, remove from each normal the component parallel to the edge direction:
\[n_{0}^{\prime }=n_{0}-\widehat{e}\text{\,}\left\langle n_{0},\; \widehat{e}\right\rangle\]
\[n_{1}^{\prime }=n_{1}-\widehat{e}\text{\,}\left\langle n_{1},\; \widehat{e}\right\rangle\]
Normalize the projected normals:
\[{\widehat{n}}_{0}^{\prime }=\frac{n_{0}^{\prime }}{\parallel n_{0}^{\prime }\parallel },\ {\widehat{n}}_{1}^{\prime }=\frac{n_{1}^{\prime }}{\parallel n_{1}^{\prime }\parallel }\]
Finally, compute the projected angle:
\[\theta =\arccos\left(\left\langle {\widehat{n}}_{0}^{\prime },\; {\widehat{n}}_{1}^{\prime }\right\rangle \right)\]
The resulting angle is expressed in degrees and remains stable even on irregular meshes.
If an edge has only one adjacent face, it lies on the mesh boundary. In this implementation, boundary edges are assigned a dihedral angle of zero and are not classified as creases.
A user-defined threshold determines whether an edge is sharp enough to count as a crease:
\[\theta \geq {\tau }_{\theta }\ \Rightarrow \ {crease\ edge}\]
Typical threshold ranges are:
The extractor computes these angles using the adjacency structure built earlier, so no additional topology pass is required.
Once each dihedral angle is available, crease detection becomes a simple filtering pass over the edge list:
The marked edges form a graph on the mesh. The next step is to convert that graph into ordered polylines.
Crease edges usually form connected chains. To make them useful for rendering and downstream processing, the
extractor assembles them into
First, the extractor builds a map from each vertex to the crease edges incident on it. This local lookup makes it efficient to continue a polyline from one edge to the next.
Starting from an unused crease edge, the extractor grows a line in both directions:
This process produces two partial vertex sequences:
The extractor reverses the left side and appends the right side, producing one ordered crease polyline.
Some meshes contain T-junctions, where more than two crease edges meet at the same vertex. The algorithm handles these cases conservatively:
This behavior is desirable because T-junctions usually represent multiple distinct crease paths rather than a single continuous feature.
Very short crease polylines, such as one- or two-edge fragments, are often noise or insignificant detail. The extractor
discards any polyline shorter than
Each accepted polyline is converted into a FeatureLine object containing:
The result is a clean set of crease lines ready for visualization or further processing.
Crease extraction is a direct and robust process:
Unlike ridge and valley extraction, crease detection does not depend on curvature fields. It works directly from connectivity and face normals, making it broadly applicable and especially effective for CAD models, mechanical parts, and stylized assets.
So far, we have handled ridges, valleys, and creases as separate feature types, each with its own detection criteria and traversal strategy. In practice, these lines are most useful when combined. A unified feature-line set gives a compact summary of the mesh by bringing together smooth curvature-driven features and sharp normal discontinuities.
The FeatureLineExtractor class supports this workflow directly. It exposes dedicated methods for each feature type
and a unified
A unified interface makes the extractor easier to use, easier to maintain, and easier to extend:
Visualization modules, segmentation algorithms, and analysis tools often need every feature line at once. A single extraction call reduces boilerplate and gives downstream code one consistent entry point.
All feature types—ridges, valleys, and creases—are returned as
Each extraction method keeps its own focused logic:
The unified method does not replace those routines; it simply orchestrates them and returns their results together.
Future feature types, such as suggestive contours, apparent ridges, or view-dependent lines, can be added without
changing the public workflow. Each new method can produce FeatureLine objects and contribute them to
The unified method is intentionally small. It calls each extractor, reserves enough space for the combined output, and appends the results in sequence:
std::vector<FeatureLine> FeatureLineExtractor::extractAll()
{
std::vector<FeatureLine> all;
auto ridges = extractRidges();
auto valleys = extractValleys();
auto creases = extractCreases();
all.reserve(ridges.size() + valleys.size() + creases.size());
all.insert(all.end(), ridges.begin(), ridges.end());
all.insert(all.end(), valleys.begin(), valleys.end());
all.insert(all.end(), creases.begin(), creases.end());
return all;
}
This method performs three simple steps:
No additional filtering or merging is applied here. Each feature type remains distinct, and the caller decides how to visualize, group, or post-process the results.
A typical workflow loads the mesh, prepares normals and adjacency, computes curvature, configures thresholds, and then runs unified extraction:
MeshExplicit mesh = loadMesh("model.obj");
mesh.buildAdjacency();
mesh.computeNormals();
CurvatureField curvature = computeCurvature(mesh);
FeatureLineParameters params;
params.ridgeKappaThreshold = 0.05;
params.valleyKappaThreshold = 0.05;
params.dihedralAngleThreshold = 35.0;
FeatureLineExtractor extractor(mesh, curvature, params);
auto lines = extractor.extractAll();
The resulting lines vector contains all three feature types:
They are ready for visualization, analysis, segmentation, or any other downstream processing step.
Feature-line extraction is lightweight compared with curvature estimation, remeshing, or segmentation, but its runtime and stability still matter for large meshes, interactive visualization, and downstream tools. This section summarizes the algorithm’s complexity, practical performance, memory use, and robustness on noisy or irregular input.
The full pipeline has three main computational stages:
Each stage has predictable behavior and scales linearly with mesh size.
The adjacency builder visits each triangle once and records the local connectivity needed by later stages:
Total complexity: \(O(F)\)
Memory use remains compact:
This construction is efficient enough for very large meshes, including models with millions of faces.
Ridge and valley extraction begins with a linear scan over the vertices. For each vertex, the extractor checks the curvature thresholds and, when appropriate, traces a polyline along the principal direction.
Tracing remains bounded by two safeguards:
Worst case complexity: \(O(V)\)
In practice, the workload is usually far below the worst case because:
As a result, ridge and valley extraction is fast enough for interactive workflows.
Crease detection performs one linear pass over the edge list. For each edge, the extractor computes a dihedral angle and marks the edge as a crease if it exceeds the configured threshold.
Each dihedral-angle test is constant time and uses only local data:
Polyline assembly is also linear in the number of crease edges, because each marked edge is visited at most once.
Total complexity: \(O(E)\)
Because \(E\) is approximately \(3F/2\) for triangle meshes, crease detection is effectively linear in the number of faces.
Combining the stages gives the total complexity: \(O(F)+O(V)+O(E)\approx O(F)\)
Overall, feature-line extraction runs in linear time and scales gracefully as mesh size increases.
Feature line extraction is sensitive to mesh quality, curvature noise, and threshold selection. Below are the main robustness factors and how the algorithm handles them.
Curvature estimation is inherently noisy on:
Ridge/valley extraction mitigates this by:
These simple rules eliminate most spurious lines.
High resolution meshes produce smoother curvature fields and cleaner feature lines. Low resolution meshes may produce:
Users can compensate by:
Dihedral angles are robust even on noisy meshes because they depend only on face normals. However, they may be unstable when:
The algorithm handles this by:
Crease lines may branch at vertices with multiple crease edges. The algorithm treats each branch as a separate polyline, which is the correct behavior for:
This ensures clean, predictable output.
Feature line extraction is:
Its simplicity and performance make it an ideal complement to curvature estimation and a natural precursor to more advanced topics such as half edge meshes, segmentation, and non photorealistic rendering.
Feature-line extraction is a natural continuation of the curvature-estimation pipeline developed earlier in this series. By combining curvature fields, adjacency information, and simple geometric criteria, we can analyze and visualize the structure of triangle meshes in a compact, expressive way.
Ridges and valleys reveal how a surface bends, while creases highlight sharp geometric transitions. Together, these feature types summarize both smooth and piecewise-smooth shape behavior.
The approach remains practical because each component is simple, efficient, and easy to reason about:
The result is a feature-line extractor that works across a wide range of meshes, from smooth scanned geometry to CAD models with hard edges, and fits naturally into TheMeshProject ecosystem.
This article also marks an architectural turning point. MeshExplicit remains useful for geometry storage, curvature estimation, and feature-line extraction, especially when paired with lightweight adjacency. However, more advanced operations require a richer representation.
Tasks such as mesh editing, segmentation, remeshing, and topological manipulation benefit from explicit edge orientation
and more efficient local traversal. That leads directly to the next major topic in the series:
The half-edge structure provides the connectivity tools needed for more advanced mesh-processing algorithms:
It is the natural next step in the evolution of this mesh-processing framework.
After introducing half-edge meshes, the series will move into mesh segmentation. Segmentation uses geometric cues—such as curvature, feature lines, and dihedral angles—to divide a mesh into meaningful regions. The feature lines extracted in this article will provide important boundary cues for that process.
This progression keeps the learning curve smooth while gradually introducing more advanced concepts, algorithms, and data structures.
Feature lines are more than polylines drawn on a surface. They are geometric fingerprints: they reveal structure, highlight transitions, and guide higher-level algorithms. With the tools developed in this article, readers now have a complete pipeline for extracting and visualizing ridges, valleys, and creases.
The next article will introduce the half-edge mesh, a key structure in modern geometry processing. From there, the series will move naturally into segmentation and other higher-level operations, continuing to build a cohesive, modular, and educational framework for mesh-based computation.