Directional is a C++ geometry processing library focused on directional-field processing. The functionality is based on the definitions and taxonomy surveyed theoretically in [Vaxman et al. 2016] and [de Goes et al. 2016], and in many newer papers in the literature, cited within context in this tutorial and the code. Directional contains tools to edit, analyze, and visualize directional fields of various degrees, orders, and symmetries.
Discretization in Directional is abstracted by general discrete tangent bundles that can represent a rich class of directional fields. As a result, many of the library's algorithms can now work seamlessly on multiple representations without modification (for example, power fields can be computed on either vertex-based or face-based fields using the same function). The library comprises two basic elements:
-
Classes representing the basic tangent bundle and field structures. They are built with inheritance so that functions can be written polymorphically and consequently algorithms can be applied to several representations that have the minimal set of required properties.
-
Standalone functions, implementing directional-field algorithms, that take these classes as parameters.
-
Algebraic structures, such as cochain complexes.
Our paradigm avoids buffed classes with a complicated nested hierarchy; instead, the member functions in the classes are minimal, and only used to implement the essential properties of a geometric object (for instance, the connection between tangent spaces). Nevertheless, Directional strives to minimize the number of cumbersome parameters in functions and therefore relies considerably on (passive) data classes aggregating information about specific algorithms.
The library is header only, where each header contains a set of functions closely related (for instance, the precomputation and computation of some directional quantity over a mesh). For the most part, one header contains only one function. The atomic data structures are, for the most part, simple matrices in Eigen,
The visualization is done through a specialized class DirectionalViewer, which is a wrapper around PolyScope, with many extended options that facilitate the rendering of directional fields.
The header files contain documentation of the parameters to each function and their required composition; in this tutorial, we mostly tie the functionality of Directional to the theoretical concepts of directional fields and the methods to process and visualize them.
This tutorial comprises an exhaustive set of examples that demonstrate the capabilities of Directional, where every subchapter entails a single concept. The tutorial code can be installed by going into the tutorial folder from the main Directional folder, and typing the following instructions in a terminal:
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ../
makeThis will build all tutorial chapters in the bin folder. The necessary dependencies will be appended and built automatically. To build in Windows, use the cmake-gui .. options instead of the last two commands, and create the project using Visual Studio, with the proper tutorial subchapter as the "startup project".
To access a single example, say 202_Sampling, go to the bin subfolder, and the executable will be there. Command-line arguments are never required; the data is read from the data folder directly for each example. Some examples save output data in the output folder.
Most examples require a modest amount of user interaction; the GUI should usually be clear on how to do this.
In the continuum, directional fields are represented as elements (formally: sections) of tangent spaces, where a tangent space is a linear space attached to each point of a manifold. The set of all such tangent spaces is called a tangent bundle.
In the discrete setting, a tangent bundle is a finite graph TangentBundle. Discrete tangent bundles supply one or more of the following properties:
-
Intrinsic parameterization: vectors in a single tangent space are represented with coordinates
$\left(x_1,\cdots,x_d\right)$ in an arbitrary, but fixed basis of dimension$d$ . The basis itself does not need to be specified for many algorithms; it can stay abstract. -
Adjacency: a tangent bundle is represented as a graph where the tangent spaces are nodes, and the graph edges are adjacency relations that encode the local environment of any tangent space. This, in fact, encodes the combinatorial and topological structure of the underlying discrete manifold, akin to the smooth structure in the continuum.
-
Connection: a connection defines the notion of parallelity in two adjacent tangent spaces, which encodes a metric structure on the underlying manifold, and allows for computing derivatives and curvature. Specifically, a connection is a directed-edge-based quantity on the tangent-bundle graph, given as a change of coordinates between the bases of the source tangent space to the target tangent space. This can be represented as an orthogonal matrix.
-
metric The metric on the bundle is supplied in two quantities:
connectionMassis the weight of each edge$E_{TB}$ , andtangentSpaceMasspacks the weights of an inner product on vectors on the bundle. That is, it has either$V_{TB}$ or$V_{TB}+E_{TB}$ that are the non-zero components of the vector mass matrix. -
Cycles:
$G_{TB}$ can be equipped with a notion of cycles$F_{TB}$ that are simply-connected closed chains of spaces. Given a connection, the holonomy of a cycle is the failure of a vector to return to itself following a set of parallel transports around the cycle. Concretely, it is encoded in the logarithm of the product of connection matrices. In$d=2$ , holonomy, which is then a single angle, is equivalent to the curvature of the cycle modulo$2\pi$ . There are two types of cycles, which define the topology of the underlying manifold: local cycles (akin to "faces" in$G_{TB}$ ), which are small closed loops, and global cycles, which can be homological generators and boundary loops. The singularities of fields are defined on the local cycles.
Oftentimes, the above intrinsic quantities are enough for all algorithms; nevertheless, for reasons of input, output, and visualization, a TangentBundle will contain the following, embedding-based extrinsic quantities:
-
Sources and normals: point locations and their normals (the codimensional directions of the manifold), which define the tangent planes of the manifold in the Euclidean space. Note that this doesn't mean it's a watertight mesh; that could also encode a point cloud, for instance.
-
extrinsic to intrinsic and back: functionality that projects extrinsic vectors into intrinsic space (might lose information), or produces the extrinsic representation of an intrinsic directional object.
-
Cycles sources and normals. Like sources and normals, but for the cycles themselves. These quantities mark the locations of the singularities in space, for visualization purposes.
Two main types are currently implemented in directional: PCFaceTangentBundle implements face-based tangent spaces for 2D triangle meshes, where the fields are tangent to the natural plane supporting the triangles. IntrinsicVertexTangentBundle implements vertex-based intrinsic tangent spaces, which parameterize the cone environment of the
For example, this is how PCFaceTangentBundle implements the above quantities:
- Intrinsic parameterization: a local basis in every face.
- Adjacency: dual (inner) edges between any two faces.
- Connection: the rotation matrix between the bases of any two adjacent faces.
- Cycles: the local cycles are around vertex
$1$ -rings, where singularities are then defined as (dual) vertex values, and global cycles are dual loops of generators and boundaries. - Inner product: the natural face-based mass matrix (just a diagonal matrix of face areas).
- Sources are face barycenters, and normals are just face normals.
- The projection to the supporting plane of the face and encoding in local coordinates.
- Vertices and vertex normals (area-weighted from adjacent faces) are the cycle quantities.
Some of the choices above can be varied to different flavors of face-based fields (for instance, the metric culminating in the mass weights). PCFaceTangentBundle wraps around a an orientable input triangle mesh in a single connected-component. There are no other limitations on its genus or boundaries. If your input comprises several connected components altogether, you should use several tangent bundles.
The representation of a directional field is its encoding in each discrete tangent plane. The most important element is the number of vectors in each tangent plane, which we denote as the degree of the field CartesianField. Directional currently supports the following variants of Cartesian fields [Vaxman et al. 2016].
-
Raw - a vector of
$d\times N$ entries represents an intrinsic$1^N$ -vector (a directional with$N$ independent vectors in each tangent space) in dimension-dominant ordering:$(X_{1,1},\ldots, X_{1,d}),(X_{1,2},\ldots,X_{2,d}),\ldots (X_{N,1},\ldots, X_{N,d})$ per space (for instance, for$d=3$ and$N=4$ it would be$xyzxyzxyzxyz$ ordering with 12 components per tangent space). Vectors are assumed to be ordered in counterclockwise order in most Directional functions that process raw fields. the memory complexity is then$dN|V_{TB}|$ for the entire directional field. A Cartesian Field indicates being a raw field by settingCartesianField::fieldTypetodirectional::RAW_FIELD. -
Power Field - This is a unique type to
$d=2$ . It encodes an$N$ -rotational-symmetric ($N$ -RoSy) object as a single complex number$y=u^N$ relative to the local basis in the tangent space, where the$N$ -RoSy is the set of roots$u \cdot e^{\frac{2\pi i k}{N}}, k \in [0,N-1]$ . The magnitude is also encoded this way, though it may be neglected in some applications. The memory complexity is then$2|V_{TB}|$ . -
PolyVector - Also unique to
$d=2$ , this is a generalization of power fields that represents an$N$ -directional object in a tangent space as the coefficients$a$ of a monic complex polynomial$f(z)=z^N+\sum_{i=0}^{N-1}{a_iz^i}$ , which roots$u$ are the encoded$1^N$ -vector field. In the case where the field is an$N$ -RoSy, all coefficients but$a_0$ are zero. Note: A PolyVector that represents a perfect$N$ -RoSy would have all$a_i=0,\ \forall i>0$ , but$a_0$ would have the opposite sign from the power-field representation of the same$N$ -RoSy. This is since the power field represents$u^N$ directly, whereas a PolyVector represents the coefficients of$z^N-u^N$ in this case. The memory complexity is$2N|V_{TB}|$ .
Directional provides a number of conversion functions to switch between different representations. Each of the functions is of the form rep1_to_rep2, where rep1 and rep2 are the representation names in the above list. e.g., polyvector_to_raw(). Some possible combinations are given by composing two functions in sequence. However, note that not every conversion is possible; for instance, it is not possible to convert from PolyVectors to power fields, as they do not possess the same power of expression. Converting into the more explicit raw representation is often needed for I/O and visualization, but not only.
Directional uses a specialized class called directional::DirectionalViewer which wraps around PolyScope, augmenting it with functionality that pertains to directional fields. A mesh is then stored with its accompanying geometric quantities: the field, edge, vertex, and face-based scalar data, isolines, and more, that we detail in the tutorial per chapter in context. Like PolyScope, Directional supports independent multiple meshes, each with its own set of quantities. The viewer also returns the corresponding PolyScope quantities (for instance, the PolyScope::SurfaceMesh), so that one can use the entire functionality of PolyScope independently.
The most basic operation on directional fields is reading them from a file and drawing them in the most explicit way. In this example, a mesh and a field are read from a file and visualized as follows:
directional::readOFF(TUTORIAL_DATA_PATH "/bumpy.off",mesh);
ftb.init(mesh);
directional::read_raw_field(TUTORIAL_DATA_PATH "/bumpy.rawfield", ftb, N, field);
directional::read_singularities(TUTORIAL_DATA_PATH "/bumpy.sings", field);
viewer.init();
viewer.set_surface_mesh(mesh);
viewer.set_cartesian_field(field);
viewer.launch();The field is read in raw format (see File Formats), which is detailed in the Introduction. The field is face-based, and the singularities are consequently vertex-based,
The singularities and glyphs (and most other properties) can be toggled directly from the common PolyScope GUI. The field (and its singularities) are named field 0 and singularities 0, unless a custom name is provided by the user.
Glyph Rendering on a mesh with singularities.
This example shows a Cartesian field computed (with the power-field method described in Example 301) on either a vertex-based tangent bundle or a face-based tangent bundle. This highlights the flexibility of choosing a discretization. The relevant code segments are:
directional::IntrinsicVertexTangentBundle vtb;
directional::PCFaceTangentBundle ftb;
...
void callbackFunc()
{
ImGui::PushItemWidth(100); // Make ui elements 100 pixels wide,
if (viewingMode==FACE_FIELD)
if (ImGui::Button("Toggle Vertex Field"))
viewingMode=VERTEX_FIELD;
if (viewingMode==VERTEX_FIELD)
if (ImGui::Button("Toggle Face Field"))
viewingMode=FACE_FIELD;
viewer.toggle_singularities(viewingMode==FACE_FIELD, 0);
viewer.toggle_singularities(viewingMode==VERTEX_FIELD, 1);
viewer.toggle_cartesian_field(viewingMode==FACE_FIELD,0);
viewer.toggle_cartesian_field(viewingMode==VERTEX_FIELD,1);
ImGui::PopItemWidth();
}
...
directional::readOBJ(TUTORIAL_SHARED_PATH "/elephant.obj", mesh);
viewer.init();
viewer.set_callback(&callbackFunc);
ftb.init(mesh);
vtb.init(mesh);
...
viewer.set_surface_mesh(mesh);
viewer.set_cartesian_field(rawFaceField, "Face-Based Field", 0);
viewer.set_cartesian_field(rawVertexField, "Vertex-Based Field", 1);
viewer.launch();One can see the stages of computing a field: first reading a mesh (readOBJ()), then initializing the approxiate tangent bundle with the mesh (ftb/vtb.init()), and after computing the fields and converting it to raw format, setting the two fields (with appropriate names and ordinal numbers) to the scene. Note that a visual Cartesian field is a separate entity from a surface mesh; the visual quantities needed for Cartesian field are taken from its inner tangent bundle class (you can infact show the field "floating" without the underlying mesh. viewer.toggle_X() functions are used to control what's shown.
Power fields on a face-based tangent bundle (left) and vertex-based (right)
Vector fields on surfaces are commonly visualized by tracing [streamlines] (https://en.wikipedia.org/wiki/Streamlines,_streaklines,_and_pathlines). Directional supports the seeding and tracing of streamlines, for all types of directionals. The seeds for the streamlines are initialized using DirectionalViewer::init_streamlines(), and the lines are traced using DirectionalViewer::streamlines_next(). Each call to DirectionalViewer::advance_streamlines() extends each line by one triangle, allowing interactive rendering of the traced lines. The streamline have the same colors as the initial glyphs, where the colors fade into white as the streamline advance.
Tracing the original field (left) into streamlines (right)
It is possible to set and visualize scalar quantities on meshes at different discretization locations: either face based quantities that appear as flat colors per face, vertex-based (pointwise) quantities that interpolate linearly on faces, appearing smooth, and edge-based (integrated) quantities, that appear as flat quantities on a diamond mesh associates with each edge (taking a DirectionalViewer::set_X_data() functions, which also allow the setting of the viewable range of the function (the rest is clipped). The code generating the image below is:
viewer.set_surface_face_data(faceData, "x of normal");
viewer.set_surface_vertex_data(vertexData, "sin(y)");
viewer.set_surface_edge_data(edgeData, "principal effort");Edge-, Vertex- and face-based data on a mesh, with the field that induced the matching (Chapter 2).
On big meshes, it might appear cumbersome to view all glyphs on every face. It is possible to only view the glyphs on a subsample of faces, by using the sparsity parameter in DirectionalViewer::set_cartesian_field(). This is an integer parameter that controls the density of the sampling, in terms of face distance. Note the setting of the unitToAvgLengthRatio parameter, which controls the length of a vector of unit magnitude, relative to the average edge length.
Dense (sparsity 0) and Sparse (sparsity 5) views of a field as glyphs
Principal directions, the directions of minimum and maximum normal curvature on a mesh, are important quantities for many applications. They are shown in the example below. The new code part is viewer.set_raw_field(), which allows setting a raw field without the entire data structure of a Cartesian field.
The minimum (left) and maximum (right) principal directions, computed on the vertices. The respective normal curvatures are color-coded.
In the following sections, we show some effects of working with different representations and converting between them.
One of the fundamental operations in directional-field processing is matching. That is, defining which vectors in tangent space
Given a raw field (in assumed CCW order in every tangent space), and a matching, one defines the (sum of) rotation angles
Principal matching is done through the function principal_matching(), which accepts a Cartesian field as a parameter and computes the following:
- The matching on each (directed) TB-graph edge. This is stored in the
matchingmember of the field class. - The indices of the cycles. The singular local cycles are stored in the corresponding
singLocalCyclesandsingIndicesof the field class.
Singularities are computed as the index of each local cycle from the effort around it. The index of a cycle is the sum of efforts around a cycle. A directional must return to itself after a cycle, and therefore, the index is an integer principal_matching() does not update singularities around boundary or generator loops.
Note that the callback function in this example shows how to pick and select faces on a mesh.
A field with singularities is shown, with a selected face illustrating principal matching via colored vectors
This is an educational example that demonstrates the loss of information when generating a Cartesian field from rotation angles, and then trying to retrieve them by principal matching. This causes low valence cycles and undersampling causes aliasing in the perceived field. There are three modes seen in the example:
-
Polar mode: The user can prescribe the index of a singularity directly, and compute the field with index prescription (see example 401). With this, the rotation angles between adjacent faces can be arbitrarily large, and appear as noise in the low-valence cycles.
-
Principal-matching mode: The rotations are reconstructed from the field, without prior knowledge of the prescribed singularity from the polar mode. The large rotations between adjacent faces are aliased, giving rise to a "singularity party": many perceived singularities or a lower index.
-
In the interpolation mode, the field is interpolated from the constrained faces (red) to the free faces (white), keeping the red band fixed from the polar mode. We see a field that is smooth in the Cartesian sense, with more uniformly-placed singularities.
Left to right: the polar mode, the principal-matching mode, and the Cartesian mode.
Given a matching (in this case, principal matching), it is possible to "comb" the field. That is, re-index the vectors in each tangent space (keeping the CCW order), so that the vector indexing aligns perfectly with the matching to the neighbors---then, the new matching on the dual edges becomes trivially zero. This operation is important in order to prepare a directional field for integration. In the presence of singularities, the field can only be combed up to a forest of paths that connect between singularities, also known as seams. Note that such paths do not necessarily cut the mesh into a simply-connected patch, but may only connect subgroups of singularities with indices adding up to an integer; as a trivial example, a 1-vector field is always trivially combed, even in the presence of integral singularities, and the set of seams is empty. The combing is done through the function directional::combing(). The matching in the output combedField is already set to the trivial matching in the combed regions, and the correct matching across the seam.
Colored indices of an uncombed field (left), and a combed one (right). Seams are in black
The Cartesian representation is a meta-category for representation of vectors in explicit coordinates, either
This representation is offered in [Knöppel et al. 2013], but they did not give it a specific name (the method in general is called "globally optimal"). We use the name "power fields" which is coined in [Azencot et al. 2017]. A power field representation uses a complex basis in each tangent plane of a discrete tangent bundle, and represents an
By prescribing constraints
where PCFaceTangentBundle, directional::power_field(). It is possible to softly prescribe the constraints
where
void recompute_field()
{
directional::power_field(vtb, constVertices, constVectors, Eigen::VectorXd::Constant(constVertices.size(),-1.0), N,powerFieldHard, normalizeField);
directional::power_field(vtb, constVertices, constVectors, alignWeights, N,powerFieldSoft, normalizeField);
}The alignWeights parameter can either express a soft constraint
If the set
Hard (left) and soft (right) aligned constraints (yellow on red faces) interpolated to the rest of the mesh. Note the singularities that are discovered through principal matching.
A Polyvector field [^diamanti_2014] is a generalization of power fields that allows for representing independent vectors in each tangent space, invariant to their order. The representation is as the coefficient set
where the roots
With the function polyvector_field() one can solve the linear Polyvector problem in its full capacity; the input is a set of prescribed hard constraints
So that the set power_field() is implemented by calling polyvector_field(). This tutorial examples allows interacting with the alignment weights.
The algorithm is controled by the data structure PolyVectorData, with the important following fields:
struct PolyVectorData{
public:
//User parameters
Eigen::VectorXi constSpaces; // List of tangent spaces where there are (partial) constraints. The faces can repeat to constrain more vectors
Eigen::MatrixXd constVectors; // Corresponding to constSpaces.
...
int N; // Degree of field
const TangentBundle* tb; //The tangent bundle on which the field is defined
bool verbose; //whether to output anything
bool signSymmetry; // Whether field enforces a ssign symmetry (only when N is even, otherwise by default set to false)
double wSmooth; // Weight of smoothness
double wRoSy; // Weight of rotational-symmetry. "-1" means a perfect RoSy field (power field)
Eigen::VectorXd wAlignment; // Weight of alignment per each of the constfaces. "-1" means a fixed vector
...
};wSmooth is wRosy is wAlignment is verbose should be flagged when output is expected (false by default), and signSymmetry is flagged when the algorithm should enforce that for every vector true by default.
Top: Sharp-edge constraints (left; note sometimes more than one per face), Hard (middle) and soft (right) solution. Bottom: dominant-weighted alignment (left), and rotational symmetry (right).
Extension: PolyVector Iterations: The polyvector algorithm allows for alternate smooth-and-project iterations, which are commonplace in many algorithms. The next two subchapters of Chapter 3 are examples.
We demonstrate how to compute fields that minimize the so-called "Ginzburg-Landau functional" [Viertel and Osting 2017]:
$$
\int{|\nabla v|^2} + \frac{1}{\epsilon}\int{(|v|^2-1)^2},
$$
for some penalty
//Computing a regular PolyVector field without iterations
directional::polyvector_field(pvData, pvFieldOrig);
directional::polyvector_to_raw(pvFieldOrig, rawFieldOrig, N%2==0);
directional::principal_matching(rawFieldOrig);
//Iterating for a smoothest perfect-RoSy field
pvData.iterationMode = true;
int numIterations = 100;
std::vector<directional::PvIterationFunction> iterationFunctions;
iterationFunctions.push_back(directional::hard_rosy);
directional::polyvector_field(pvData, pvFieldGL);
directional::polyvector_iterate(pvData, pvFieldGL, iterationFunctions, numIterations);
directional::polyvector_to_raw(pvFieldGL, rawFieldGL, N%2==0);
directional::principal_matching(rawFieldGL);The new parts are the function polyvector_iterate that runs an iterative algorithm comprising:
-
An implicit step reducing the PolyVector energy.
-
Projection steps controlled by all the
pvIterationfunctions in the order they are input.
The basic version is highly configurable, including switching steps (1) and (2), and is controlled by the PolyVectorData structure. This includes, for instance, the ability to slow the implicit step (equivalent to playing with hard_rosy() function.
Left: The original PolyVector Fields. Right: After MBO iterations. Note the improved singularity in the zoom-in.
This functionality only works with face-based fields via IntrinsicFaceTangentBundle.
Vector-field guided surface parameterization is based on the idea of designing the candidate gradients
of the parameterization functions (which are tangent vector fields on the surface) instead of the functions themselves. Thus, vector-set fields (
In modern methods, this is usually achieved by a smooth-and-reduce-curl approach, which is again doable by the PolyVector Iterations, and using the following projection functions:
iterationFunctions.push_back(directional::soft_rosy); iterationFunctions.push_back(directional::curl_projection);soft_rosy() works by only taking the PolyVector part-way towards a perfect RoSy, and curl_projection() does principal matching, and then projects out the curl of the field. Following the iterations, the field is by design curl-free numerically, and the curl_matching() retrieves it.
Left: The original PolyVector Fields. Right: An Integrable field (the curl is color-coded with maximum of 0.09 at yellow).
Polar fields are represented using angles. These angles may encode the rotation from some given basis on a tangent space (and so it is a "logarithmic" representation, when compared to Cartesian methods), or an angle difference between two neighboring tangent spaces (in the sense of deviation from parallel transport). The former usually requires integer variables for directional-field design. The latter does not, but state-of-the-art methods require the prescription of indices around independent dual cycles in the mesh. Currently, Directional supports the latter.
The notion of encoding rotation angles on dual edges, as a means to encode deviation from parallel transport between adjacent tangent planes, appeared in several formats in the literature [Ray et al. 2008]. The formulation and notation we use in Directional is that of Trivial Connections [Crane et al. 2010]. Trivial connection solves for a single rotation angle
The basis cycles form the cycles around which curvatures (and singularities) have to be prescribed on the mesh. The sum on basis cycles is described in a sparse matrix directional::index_prescription(), which can also accept a solver for precomputation, for the purpose of prefactoring
Indices are prescribed on several vertex singularities, and on a generator loop, to match the index theorem.
The full details of the method implemented in this chapter can be found in a technical report [Vaxman 2021]. Many of the therotical ideas for general IntrinsicFaceTangentBundle.
Consider a seam edge
where
Seamless
In this example, we demonstrate the computation of such a integration, both permutationally, and fully seamless. The computed function is a
directional::IntegrationData intData(N);
...
directional::setup_integration(rawField, intData, meshCut, combedField);
intData.integralSeamless=false;
...
directional::integrate(combedField, intData, meshCut, cutUVRot ,cornerWholeUV);
...
intData.integralSeamless = true;
directional::integrate(combedField, intData, meshCut, cutUVFull,cornerWholeUV);The data structure containing all relevant information about the integration is IntegrationData. It contains some parameters that can be tuned to control the integration. Several relevant ones are:
double lengthRatio; //global scaling of functions
//Flags
bool integralSeamless; //Whether to do full translational seamless.
bool roundSeams; //Whether to round seams or round singularities
bool verbose; //output the integration log.lengthRatio encodes a global scale for the Poisson problem (scaling the fields uniformly), where the ratio is measured against the bounding box diagonal. Some of the other parameters are demonstrated in the other examples in this chapter. The integrator takes the original (whole) mesh, and generates a cut-mesh (in VMeshCut,FMeshCut) of disc-topology. The singularities are on the boundary of this mesh, and the function can consequently be defined without branching ambiguity on its vertices, with the appropriate permutation and translation across the cut seams.
Left: directional field. Center: permutationally-seamless integration. Right: fully-seamless integration.
Directional can handle integration for every
In this example, we demonstrate the results for lengthRatio. All are fully seamless. Note that the density of the isolines increases with
Left to right: $N=2,4,7,11$. Top: field. Bottom: integer isolines.
It is possible to choose whether to round the seam jumps
Left to right (bottom is zoom-in of top): Field, rounding only seams (leading to the top singularity being offset), and rounding singularity function values directly.]
It is possible to constrain the functions to have linear relations between them, which reduce the degrees of freedom. This is done by inputting a matrix intData through the field linRed, and there are pre-made funtcions to set it, such as set_triangular_symmetry(int N).
Top: A $6$-directional fields with a singularity. Bottom left: $N=6$ only sign symmetry is enforced (the three lines don't always meet at all intersections). Right: the same with added triangular symmetry, where all lines intersect.
This subchapter demonstrates how we can create an actual polygonal mesh from the arrangement of isolines on the surface. This creates an arrangement of lines (in exact numbers) on every triangle, and stitches them together across triangles to get a complete conforming mesh. The new mesh is given in the libhedra format of
The meshing unit is independent from the integration unit, and can be potentially used with external functions. However, it is easy to pipeline iit with the integration as follows:
//setting up mesh data from integration data
directional::MesherData mData;
directional::setup_mesher(meshCut[i], intData, mData);
//meshing and saving
mData.verbose = false;
std::cout<<"Meshing N="<<N[i]<<std::endl;
directional::mesher(meshWhole, mData, VPolyMesh[i], DPolyMesh[i], FPolyMesh[i]);
std::cout<<"Done!"<<std::endl;This reads the integrated information from intData. Conversely to previous version, the mesher does not necessarily need an external dependency. Nevertheless, it can optionally be sped up (considerably) by including GMP. This is controlled by CMake within the compilation of the tutorial.
Left to right: polygonal meshes of the arrangements of isolines from the N=4,7,11 examples ($N=2$ is not yet supported) in Example 502.
Directional fields, and differential forms, are objects of differential geometry where the underlying manifold is equipped with notions of gradient, curl, divergence, and where gradient fields are curl free and cogradient fields (or just "curl fields") are divergence free. These are special cases of the more abstract algebraic notion of cochain complexes. Without going into the full formality, such a complex is defined by a series of spaces
Example: All gradient fields are curl free, but there are only curl-free fields that are not also gradient in topologies like annuli or tori. the torus has genus
In the discrete setting, the spaces are represented as arrays of nodal values in some finite space, the
Although abstract, the notion has multiple concrete manifestations in directional field processing, which we explore through the chapter’s examples.
What is called "face-based FEM" is in fact just the structure with face-based vectors depicted in the following figure:

Top: the primal cochain complex, taking conforming piecewise linear (PL) functions to face-based piecewise-constant fields, and the curl operator takes the latter to edge-based diamond regions. In the bottom dual structure (right to left), the rotated cogradient of non-conforming piecewise-linear functions are also piecewise-constant fields. their divergence is defined on dual vertex voronoi areas. Going from PL functions to dual regions is done by the mass matrices $M_v$ and $M_e$. The face rotation operator $J$ simply rotates a vector by $\frac{\pi}{2}$ in a face. The cochain structure is actually such that $CG_v=0$, and the matching dual cochain structure $DJG_e=0$
Which is popular in geometry processing (for instance, studied extensively in [Wardetzky 2007]. This is exemplified in the following code:
Eigen::SparseMatrix<double> Gv = directional::conf_gradient_matrix_2D<double>(mesh);
Eigen::SparseMatrix<double> Ge = directional::non_conf_gradient_matrix_2D<double>(mesh);
Eigen::SparseMatrix<double> J = directional::face_vector_rotation_matrix_2D<double>(mesh);
◊
Eigen::SparseMatrix<double> C = directional::curl_matrix_2D<double>(mesh);
Eigen::SparseMatrix<double> D = directional::div_matrix_2D<double>(mesh);Ge is the non-conforming (Edge-based) cogradient, and J is the in-face rotation matrix. The code proceeds by verifying the cochain relations (discrete curl of gradient and divergence of rotated cogradient):
std::cout<<"max abs curl of exact field (should be numerically zero): "<<(C*Gv*confVec).cwiseAbs().maxCoeff()<<std::endl;
std::cout<<"max abs divergence of coexact field (should be numerically zero): "<<(D*J*Ge*nonConfVec).cwiseAbs().maxCoeff()<<std::endl;Left: a conforming PL function with its gradient. Right: a non-conforming PL function with its rotated cogradient.
DEC [Desbrun et al. 2005] is primarily working with differential forms rather than vector fields. As such, it decouples metric from differentials. In the discrete setting,
Top: the primal complex, where $d_0$ is the differential (EQuivalent of gradient), $d_1$ is the equivalent of curl, and their adjoints (up to Hodge duality) are the equivalents of rotated cogradient and divergence.
The sharp operator converts the discrete forms into their continuous counterparts, which in the PL case are Whitney forms (for instance, for
Eigen::SparseMatrix<double> d0 = directional::d0_matrix<double>(mesh);
Eigen::SparseMatrix<double> d1 = directional::d1_matrix<double>(mesh);
Eigen::SparseMatrix<double> hodgeStar, invHodgeStar;
directional::hodge_star_1_matrix(mesh, hodgeStar, invHodgeStar);
Eigen::SparseMatrix<double> M1;
directional::linear_whitney_mass_matrix(mesh, M1);
Eigen::SparseMatrix<double> L1 = d0.adjoint()*M1*d0;
Eigen::SparseMatrix<double> L2 = d0.adjoint()*hodgeStar*d0;
Eigen::SparseMatrix<double> diff = L1-L2;
double maxAbsValue = 0.0;
for (int k = 0; k < diff.outerSize(); ++k)
for (Eigen::SparseMatrix<double>::InnerIterator it(diff, k); it; ++it)
maxAbsValue = std::max(maxAbsValue, std::abs(it.value()));
std::cout<<"Exact laplacian identity (should be numerically zero): "<<maxAbsValue<<std::endl;
directional::project_exact(d1, hodgeStar, z2, z1Diag, z2Exact, true);
std::cout<<"Reproducing the original curl (z2Diag, should be numerically zero): "<<(z2-z2Exact).cwiseAbs().maxCoeff()<<std::endl;
directional::project_exact(d1, M1, z2, z1, z2Exact, true);
std::cout<<"Reproducing the original curl (z2, should be numerically zero): "<<(z2-z2Exact).cwiseAbs().maxCoeff()<<std::endl;
std::cout<<"Difference between z1 and z1Diag (small, not zero): "<<(z1-z1Diag).cwiseAbs().maxCoeff()<<std::endl;The differences will only be culminated in the last z1 vs. z1Diag. Other than the functions to construct the operators, this also introduces the project_exact() function which we discuss in the next tutorial example.
Left: The reconstructed field with the Whitney $1$-form mass matrix. Right: with the diagonal Hodge star. The visual difference in this case is mild.
The combination of a cochain complex and metric introduces the Hodge decomposition, where we can also define the codifferential
Hodge decomposition in directional is done by the function Hodge_decomposition(). To understand how it works, we need to examine its implementation:
project_exact(d, M, cochain, prevCochain, exactCochain);
project_coexact(dNext, M, MNext, cochain, k, coexactCochain, nextCochain);
harmCochain = cochain - exactCochain - coexactCochain;project_exact() finds
In case
In terms of code arguments, d is prevCochain is exactCochain is GagueFixing flag and is by default False.
project_coexact() is simpler, as it just computes:
$$
z_\text{coexact} = \text{argmin}|z_\text{coexact}|^2\ \ s.t.\ \ d_iz_\text{coexact} = d_iz_{i}
$$
This amount to an orthogonal projection of
Top: a piecewise-constant face-based vector field. Bottom: its decomposition into exact, coexact, and harmonic forms. The colors indicate the potential fields (left $f$ and middle $g$).
In the case of boundaries,
template<typename NumberType>
Eigen::SparseMatrix<NumberType> d0_matrix(const TriMesh& mesh,
const bool isDirichlet=false)Currently only Neumann conditions are implemeneted for DEC; that means that the exact component of the Hodge decomposition is free, whereas the coexact and the harmonic components are tangent the the boundaries.
Top: a $1$-form. Bottom: its decomposition into exact, coexact, and harmonic forms with Neumann boundary conditions (coexact and harmonic are tangent to the boundaries). The colors indicate the potential field (left $f$ and middle $g$).
it is possible to enumerate the harmonic fields as the null-space independent vectors (cohmology) of the harmonic operator. This is obtained through cohomology_basis().
Two example of harmonic fields. They are naturally associated to homologies (handles) of the surface.
Directional is a an ever-evolving project, and there are many algorithms in the state-of-the-art that we look forward to implement, with the help of volunteer researchers and practitioners from the field. Prominent examples of desired implementations are:
-
Support for 3D fields, particularly Octahedral fields [^solomon_2017], both in tet meshes and with the boundary-element method.
-
Support for tensor fields.
-
Higher-order fields.
Azencot et al., 2017 : Omri Azencot, Etienne Corman, Mirela Ben-Chen, Maks Ovsjanikov, Consistent Functional Cross Field Design for Mesh Quadrangulation.
Bommes et al., 2009 : David Bommes, Henrik Zimmer, Leif Kobbelt, Mixed-integer quadrangulation.
Bommes et al., 2012 : David Bommes, Henrik Zimmer, Leif Kobbelt, Practical Mixed-Integer Optimization for Geometry Processing.
Bouaziz et al., 2012 : Sofien Bouaziz, Mario Deuss, Yuliy Schwartzburg, Thibaut Weise, Mark Pauly, Shape-Up: Shaping Discrete Geometry with Projections.
Brandt et al., 2018 : Christopher Brandt, Leonardo Scandolo, Elmar Eisemann, Klaus Hildebrandt, Modeling n-Symmetry Vector Fields using Higher-Order Energies.
Crane et al., 2010 : Keenan Crane, Mathieu Desbrun, Peter Schröder, Trivial Connections on Discrete Surfaces.
Cignoni et al., Meshlab : Paolo Cignoni, Marco Callieri, Massimiliano Corsini, Matteo Dellepiane, Fabio Ganovelli, Guido Ranzuglia, MeshLab: an Open-Source Mesh Processing Tool.
Custers & Vaxman, 2020 : Bram Custers, Amir Vaxman, Subdivision Directional Fields.
Desbrun et al., 2005 : Discrete Differential Forms for Computational Modeling.
Diamanti et al., 2014 : Olga Diamanti, Amir Vaxman, Daniele Panozzo, Olga Sorkine-Hornung, Designing N-PolyVector Fields with Complex Polynomials.
Diamanti et al., 2015 : Olga Diamanti, Amir Vaxman, Daniele Panozzo, Olga Sorkine-Hornung, Integrable PolyVector Fields.
de Goes et al., 2016 : Fernando de Goes, Mathieu Desbrun, Yiying Tong, Vector Field Processing on Triangle Meshes.
Kälberer et al., 2007 : Felix Kälberer, Matthias Nieser, Konrad Polthier, QuadCover - Surface Parameterization using Branched Coverings.
Knöppel et al., 2013 : Felix Knöppel, Keenan Crane, Ulrich Pinkall, Peter Schröder, Globally Optimal Direction Fields.
Liu et al., 2008 : Yang Liu, Weiwei Xu, Jun Wang, Lifeng Zhu, Baining Guo, Falai Chen, Guoping Wang, General Planar Quadrilateral Mesh Design Using Conjugate Direction Field.
Myles et al., 2014 : Ashish Myles, Nico Pietroni, Denis Zorin, Robust Field-aligned Global Parametrization.
Meekes & Vaxman, 2021 : Merel Meekes, Amir Vaxman, Unconventional Patterns on Surfaces.
Panozzo et al., 2014 : Daniele Panozzo, Enrico Puppo, Marco Tarini, Olga Sorkine-Hornung, Frame Fields: Anisotropic and Non-Orthogonal Cross Fields.
Ray et al., 2008 : Nicolas Ray, Bruno Vallet, Wan Chiu Li, Bruno Lévy, N-symmetry direction field design.
Solomon et al., 2017 : Justin Solomon, Amir Vaxman, David Bommes, Boundary Element Octahedral Fields in Volumes.
Vaxman et al., 2016 : Amir Vaxman, Marcel Campen, Olga Diamanti, Daniele Panozzo, David Bommes, Klaus Hildebrandt, Mirela Ben-Chen, Directional Field Synthesis, Design, and Processing.
Vaxman, 2021 : Amir Vaxman, Directional Technical Reports: Seamless Integration.
Viertel and Osting, 2017 : An Approach to Quad Meshing Based on Harmonic Cross-Valued Maps and the Ginzburg-Landau Theory.
Wang et al., 2023 : Stephanie Wang, Mohammad Sina Nabizadeh, Albert Chern, Exterior Calculus in Graphics.
Wardetzky, 2007 : Max Wardetzky, Discrete differential operators on polyhedral surfaces-convergence and approximation.
























