CGAL 5.6 - 3D Isosurfacing
User Manual

Author
Julian Stahl and Daniel Zint

Figure 60.1 Different isovalues of a bunny.

Introduction

This package provides methods to compute a surface mesh representing an isosurface of a 3-dimensional scalar field. An isosurface is defined as the surface on which the value of this field is equal to a given constant, i.e. the isovalue. Depending on the isosurfacing method and the input data structure, the result is either a triangular, quadrilateral, or higher order polygonal indexed face set.

Algorithms

There are multiple algorithms to extract isosurfaces. This package contains Marching Cubes, topologically correct Marching Cubes, Dual Contouring, and Octree Marching. (References?)

Marching Cubes (MC)

MC processes all cells of the input domain individually. Each cell corner gets a sign (+/-) to indicate if it is above or below the isovalue. A new vertex is created on every cell edge where the sign changes, i.e. the isosurface is intersected. The vertex position is computed via linear interpolation of the scalar values of the incident corners. Depending on the configuration of signs at the corners the resulting vertices are connected to form triangles within the cell.

MC can process any input data structure that consists of hexahedral cells. In case of a conforming grid, MC produces a triangle mesh that is manifold in most scenarios. If the mesh is manifold and the isosurface does not intersect the domain boundaries, the mesh is also watertight. Compared to other approaches the algorithm often generates more and many thin triangles with acute angles. MC does not preserve sharp edges.

Figure 60.2 Some example cases of Marching Cubes.

Topologically correct Marching Cubes (TMC)

This algorithm is an extension to MC and provides additional guarantees. It generates a mesh that is homeomorphic to the trilinear interpolant of the input function inside each cube. Furthermore, the mesh is guaranteed to be manifold and watertight, as long as the isosurface does not intersect the domain boundaries.

TODO examples

Dual Contouring (DC)

DC creates one vertex in every cell that is intersected by the isosurface. Next, a face is created for each edge that intersects the isosurface, by connecting the vertices of the incident cells. For a uniform hexahedral grid, this results in a quadrilateral mesh.

The classical DC method requires the gradient of the scalar field. It is passed to the method as an additional parameter in the form of a functor. The default argument for this parameter assumes the gradient to be zero. Thus, for using the classical DC, the gradient has to be defined by the user.

Different versions of DC compute the vertex positions differently. Therefore, the vertex positioning is configurable with an optional parameter. Some of them do not require the gradient and therefore even work with the zero gradient.

Dual Contouring works on any domain but does not guarantee a manifold or watertight mesh. It creates less faces than Marching Cubes. The main advantage of DC over MC is the ability to represent sharp edges.

Figure 60.3 Isosurface of the IWP function generated by Dual Contouring.

Comparison

Algorithm Domains Faces Manifold Watertight* Topologically correct
MC Hexahedral Triangles no no no
TMC Hexahedral Triangles yes yes yes
DC All Polygons no no no

(* assuming the isosurface does not leave the given bounding box)

Figure 60.4 Comparison between a cube generated by Dual Contouring (left) and Marching Cubes (right).

Interface

Each algorithm is represented by a single functions. The function signature is the same for all algorithms:

template <typename Concurrency_tag = Sequential_tag, class Domain_, class PointRange, class PolygonRange>
void make_triangle_mesh_using_marching_cubes(const Domain_& domain, const typename Domain_::FT iso_value,
PointRange& points, PolygonRange& polygons);

The input is provided in the form of a domain (see Domains).

The iso_value parameter describes the grid value the isosurface should represent.

The output is in the form of an indexed face set that is written to the two collections points and polygons. The vertex positions are stored as Point_3 in points. Each face in polygons is a list of indices pointing into the points collection. Depending on the algorithm, the indexed face set may either store a polygon soup or a topological mesh.

Algorithms can run sequentially on one CPU core or in parallel. The Concurrency_tag is used to specify how the algorithm is executed and is either Sequential_tag or Parallel_tag. To enable parallelism, CGAL needs to be linked with the Intel TBB library. If the parallel version is not availible the sequential version will always be used as a fallback.

Domains

A domain is an object that provides functions to access the input data, its geometry, topology, and optionally its gradient. There are some predefined domain classes that wrap the input data and provide a generalized interface for the algorithm. Users can also define new domains by implementing the Isosurfacing_domain concept.

Implicit domain

The Implicit_domain represents the input function in an implicit form without storing any values. It takes a functor or lambda that computes the value of the function from the position of a vertex as parameter. Additionally, the bounding box and spacing between grid points have to be specified.

Cartesian grid domain

The Cartesian_grid_domain only takes a Cartesian_grid_3 as parameter. It represents a uniform grid of values that are either computed by the user or read from an Image_3. The constructor of Cartesian_grid_3 needs the number of grid points in each dimension and the bounding box. The values are read and written with value(x, y, z) where x, y, and z are the coordinates of a grid point. Alternatively, all required data can be copied from an Image_3.

Octree domain

The Octree_domain wraps an octree to be used by isosurfacing algorithms. The octree has to be already refined. ...

Performance

Figure 60.5 Scaling of Marching Cubes with more threads.

Examples

Implicit sphere

The following example shows the usage of the Marching Cubes algorithm to extract an isosurface. The domain is an Implicit_domain that describes a sphere by the distance to its origin as an implicit function.


File Isosurfacing_3/marching_cubes_implicit_sphere.cpp

#include <CGAL/Implicit_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Vector_3 Vector;
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
// distance to the origin
auto sphere_function = [](const Point& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
};
// create a domain with bounding box [-1, 1]^3 and grid spacing 0.02
{-1, -1, -1, 1, 1, 1}, Vector(0.02f, 0.02f, 0.02f), sphere_function); // TODO: this is ugly
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 0.8
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Cartesian_grid_3 cube comparison

The following example compares all provided algorithms to extract an isosurface. The domain is an Cartesian_grid_domain that describes a cube by storing the manhattan distance to its origin in a Cartesian_grid_3.


File Isosurfacing_3/all_cartesian_cube.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/TC_marching_cubes_3.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
FT sign(FT value) {
return (value > 0) - (value < 0);
}
int main() {
// create a cartesian grid with 100^3 grid points and the bounding box [-1, 1]^3
Grid grid(7, 7, 7, {-1, -1, -1, 1, 1, 1});
// calculate the value at all grid points
for (std::size_t x = 0; x < grid.xdim(); x++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t z = 0; z < grid.zdim(); z++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
// manhattan distance to the origin
grid.value(x, y, z) = std::max({std::abs(pos_x), std::abs(pos_y), std::abs(pos_z)});
}
}
}
auto cube_gradient = [](const Point& p) {
// the normal depends on the side of the cube
const FT max_value = std::max({std::abs(p.x()), std::abs(p.y()), std::abs(p.z())});
Vector g(0, 0, 0);
if (max_value == std::abs(p.x())) {
g += Vector(sign(p.x()), 0, 0);
}
if (max_value == std::abs(p.y())) {
g += Vector(0, sign(p.y()), 0);
}
if (max_value == std::abs(p.z())) {
g += Vector(0, 0, sign(p.z()));
}
const FT length_sq = g.squared_length();
if (length_sq > 0.00001) {
g /= CGAL::approximate_sqrt(length_sq);
}
return g;
};
// create a domain from the grid
// prepare collections for the results
Point_range points_mc, points_tmc, points_dc;
Polygon_range polygons_mc, polygons_tmc, polygons_dc;
// execute marching cubes, topologically correct marching cubes and dual contouring with an isovalue of 0.8
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, 0.88, points_mc, polygons_mc);
CGAL::Isosurfacing::make_triangle_mesh_using_tmc(domain, 0.88, points_tmc, polygons_tmc);
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(domain, 0.88, points_dc, polygons_dc);
// save the results in the OFF format
CGAL::IO::write_OFF("result_mc.off", points_mc, polygons_mc);
CGAL::IO::write_OFF("result_tmc.off", points_tmc, polygons_tmc);
CGAL::IO::write_OFF("result_dc.off", points_dc, polygons_dc);
}

Image_3

The following example shows how to load data from an Image_3.


File Isosurfacing_3/marching_cubes_inrimage.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
const std::string fname = "../../../data/skull_2.9.inr";
// load the image
CGAL::Image_3 image;
if (!image.read(fname)) {
std::cerr << "Error: Cannot read file " << fname << std::endl;
return EXIT_FAILURE;
}
// convert it to a cartesian grid
const Grid grid(image);
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 2.9
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Offset mesh

The following example shows how to compute an offset mesh. The original mesh is passed to an AABB_tree to allow fast distance queries. With the use of Side_of_triangle_mesh the sign of the distance function is flipped inside the mesh.


File Isosurfacing_3/marching_cubes_mesh_offset.cpp

#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef CGAL::Surface_mesh<Point> Mesh;
typedef CGAL::AABB_face_graph_triangle_primitive<Mesh> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> Traits;
typedef CGAL::AABB_tree<Traits> Tree;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
// computes the distance of a point p from the mesh with the use of a AABB_tree
inline Kernel::FT distance_to_mesh(const Tree& tree, const Point& p) {
const Point& x = tree.closest_point(p);
return std::sqrt((p - x).squared_length());
}
int main() {
const std::string input_name = "../../../data/bunny.off";
const int n_voxels = 20;
const FT offset_value = -0.03;
// load the original mesh
Mesh mesh_input;
if (!CGAL::IO::read_OFF(input_name, mesh_input)) {
std::cerr << "Could not read mesh" << std::endl;
exit(-1);
}
// compute the new bounding box
CGAL::Bbox_3 aabb_grid = CGAL::Polygon_mesh_processing::bbox(mesh_input);
Vector aabb_increase_vec = Vector(offset_value + 0.01, offset_value + 0.01, offset_value + 0.01);
aabb_grid += (Point(aabb_grid.xmax(), aabb_grid.ymax(), aabb_grid.zmax()) + aabb_increase_vec).bbox();
aabb_grid += (Point(aabb_grid.xmin(), aabb_grid.ymin(), aabb_grid.zmin()) - aabb_increase_vec).bbox();
// construct AABB tree
Tree tree(mesh_input.faces_begin(), mesh_input.faces_end(), mesh_input);
CGAL::Side_of_triangle_mesh<Mesh, CGAL::GetGeomTraits<Mesh>::type> sotm(mesh_input);
// create the grid
Grid grid(n_voxels, n_voxels, n_voxels, aabb_grid);
for (std::size_t z = 0; z < grid.zdim(); z++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t x = 0; x < grid.xdim(); x++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
const Point p(pos_x, pos_y, pos_z);
// compute the distance
grid.value(x, y, z) = distance_to_mesh(tree, p);
// flip the sign, so the distance is negative inside the mesh
const bool is_inside = (sotm(p) == CGAL::ON_BOUNDED_SIDE);
if (is_inside) {
grid.value(x, y, z) *= -1;
}
}
}
}
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue equal to the offset
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, offset_value, points, polygons);
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Octree Dual Contouring

The following example shows how to extract an isosurface from an octree using Dual Contouring. The domain is an Octree_domain that describes a sphere by the distance to its origin stored in an octree. The octree is highly refined in one octant and only coarse in the others.


File Isosurfacing_3/dual_contouring_octree.cpp

#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Octree_domain.h>
#include <CGAL/Octree_wrapper.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <math.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Vector_3 Vector_3;
typedef typename Kernel::Point_3 Point_3;
typedef std::vector<Point_3> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
Kernel::FT sphere_function(const Point_3& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
}
struct Refine_one_eighth {
std::size_t min_depth_;
std::size_t max_depth_;
std::size_t octree_dim_;
Octree_wrapper_::Uniform_coords uniform_coordinates(const Octree_wrapper_::Octree::Node& node) const {
auto coords = node.global_coordinates();
const std::size_t depth_factor = std::size_t(1) << (max_depth_ - node.depth());
for (int i = 0; i < Octree_wrapper_::Octree::Node::Dimension::value; ++i) {
coords[i] *= (uint32_t)depth_factor;
}
return coords;
}
Refine_one_eighth(std::size_t min_depth, std::size_t max_depth) : min_depth_(min_depth), max_depth_(max_depth) {
octree_dim_ = std::size_t(1) << max_depth_;
}
bool operator()(const Octree_wrapper_::Octree::Node& n) const {
// n.depth()
if (n.depth() < min_depth_) {
return true;
}
if (n.depth() == max_depth_) {
return false;
}
auto leaf_coords = uniform_coordinates(n);
if (leaf_coords[0] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[1] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[2] >= octree_dim_ / 2) {
// return false;
}
return true;
}
};
int main() {
Octree_wrapper_ octree_wrap({-1, -1, -1, 1, 1, 1});
Refine_one_eighth split_predicate(4, 6);
octree_wrap.refine(split_predicate);
Octree_domain_ octree_domain(octree_wrap);
auto lam = [&](const Octree_domain_::Vertex_handle& v) {
Point_3 p = octree_domain.position(v);
const auto val = sphere_function(p);
Vector_3 gradient = p - CGAL::ORIGIN;
gradient = gradient / std::sqrt(gradient.squared_length());
octree_wrap.value(v) = val;
octree_wrap.gradient(v) = gradient;
};
octree_domain.iterate_vertices(lam, CGAL::Sequential_tag());
Point_range points;
Polygon_range polygons;
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(octree_domain, 0.8, points, polygons);
CGAL::IO::write_OFF("result.off", points, polygons);
}
CGAL 5.6 - 3D Isosurfacing
User Manual

Author
Julian Stahl and Daniel Zint

Figure 60.1 Different isovalues of a bunny.

Introduction

This package provides methods to compute a surface mesh representing an isosurface of a 3-dimensional scalar field. An isosurface is defined as the surface on which the value of this field is equal to a given constant, i.e. the isovalue. Depending on the isosurfacing method and the input data structure, the result is either a triangular, quadrilateral, or higher order polygonal indexed face set.

Algorithms

There are multiple algorithms to extract isosurfaces. This package contains Marching Cubes, topologically correct Marching Cubes, Dual Contouring, and Octree Marching. (References?)

Marching Cubes (MC)

MC processes all cells of the input domain individually. Each cell corner gets a sign (+/-) to indicate if it is above or below the isovalue. A new vertex is created on every cell edge where the sign changes, i.e. the isosurface is intersected. The vertex position is computed via linear interpolation of the scalar values of the incident corners. Depending on the configuration of signs at the corners the resulting vertices are connected to form triangles within the cell.

MC can process any input data structure that consists of hexahedral cells. In case of a conforming grid, MC produces a triangle mesh that is manifold in most scenarios. If the mesh is manifold and the isosurface does not intersect the domain boundaries, the mesh is also watertight. Compared to other approaches the algorithm often generates more and many thin triangles with acute angles. MC does not preserve sharp edges.

Figure 60.2 Some example cases of Marching Cubes.

Topologically correct Marching Cubes (TMC)

This algorithm is an extension to MC and provides additional guarantees. It generates a mesh that is homeomorphic to the trilinear interpolant of the input function inside each cube. Furthermore, the mesh is guaranteed to be manifold and watertight, as long as the isosurface does not intersect the domain boundaries.

TODO examples

Dual Contouring (DC)

DC creates one vertex in every cell that is intersected by the isosurface. Next, a face is created for each edge that intersects the isosurface, by connecting the vertices of the incident cells. For a uniform hexahedral grid, this results in a quadrilateral mesh.

The classical DC method requires the gradient of the scalar field. It is passed to the method as an additional parameter in the form of a functor. The default argument for this parameter assumes the gradient to be zero. Thus, for using the classical DC, the gradient has to be defined by the user.

Different versions of DC compute the vertex positions differently. Therefore, the vertex positioning is configurable with an optional parameter. Some of them do not require the gradient and therefore even work with the zero gradient.

Dual Contouring works on any domain but does not guarantee a manifold or watertight mesh. It creates less faces than Marching Cubes. The main advantage of DC over MC is the ability to represent sharp edges.

Figure 60.3 Isosurface of the IWP function generated by Dual Contouring.

Comparison

Algorithm Domains Faces Manifold Watertight* Topologically correct
MC Hexahedral Triangles no no no
TMC Hexahedral Triangles yes yes yes
DC All Polygons no no no

(* assuming the isosurface does not leave the given bounding box)

Figure 60.4 Comparison between a cube generated by Dual Contouring (left) and Marching Cubes (right).

Interface

Each algorithm is represented by a single functions. The function signature is the same for all algorithms:

template <typename Concurrency_tag = Sequential_tag, class Domain_, class PointRange, class PolygonRange>
void make_triangle_mesh_using_marching_cubes(const Domain_& domain, const typename Domain_::FT iso_value,
PointRange& points, PolygonRange& polygons);

The input is provided in the form of a domain (see Domains).

The iso_value parameter describes the grid value the isosurface should represent.

The output is in the form of an indexed face set that is written to the two collections points and polygons. The vertex positions are stored as Point_3 in points. Each face in polygons is a list of indices pointing into the points collection. Depending on the algorithm, the indexed face set may either store a polygon soup or a topological mesh.

Algorithms can run sequentially on one CPU core or in parallel. The Concurrency_tag is used to specify how the algorithm is executed and is either Sequential_tag or Parallel_tag. To enable parallelism, CGAL needs to be linked with the Intel TBB library. If the parallel version is not availible the sequential version will always be used as a fallback.

Domains

A domain is an object that provides functions to access the input data, its geometry, topology, and optionally its gradient. There are some predefined domain classes that wrap the input data and provide a generalized interface for the algorithm. Users can also define new domains by implementing the Isosurfacing_domain concept.

Implicit domain

The Implicit_domain represents the input function in an implicit form without storing any values. It takes a functor or lambda that computes the value of the function from the position of a vertex as parameter. Additionally, the bounding box and spacing between grid points have to be specified.

Cartesian grid domain

The Cartesian_grid_domain only takes a Cartesian_grid_3 as parameter. It represents a uniform grid of values that are either computed by the user or read from an Image_3. The constructor of Cartesian_grid_3 needs the number of grid points in each dimension and the bounding box. The values are read and written with value(x, y, z) where x, y, and z are the coordinates of a grid point. Alternatively, all required data can be copied from an Image_3.

Octree domain

The Octree_domain wraps an octree to be used by isosurfacing algorithms. The octree has to be already refined. ...

Performance

Figure 60.5 Scaling of Marching Cubes with more threads.

Examples

Implicit sphere

The following example shows the usage of the Marching Cubes algorithm to extract an isosurface. The domain is an Implicit_domain that describes a sphere by the distance to its origin as an implicit function.


File Isosurfacing_3/marching_cubes_implicit_sphere.cpp

#include <CGAL/Implicit_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Vector_3 Vector;
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
// distance to the origin
auto sphere_function = [](const Point& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
};
// create a domain with bounding box [-1, 1]^3 and grid spacing 0.02
{-1, -1, -1, 1, 1, 1}, Vector(0.02f, 0.02f, 0.02f), sphere_function); // TODO: this is ugly
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 0.8
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Cartesian_grid_3 cube comparison

The following example compares all provided algorithms to extract an isosurface. The domain is an Cartesian_grid_domain that describes a cube by storing the manhattan distance to its origin in a Cartesian_grid_3.


File Isosurfacing_3/all_cartesian_cube.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/TC_marching_cubes_3.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
FT sign(FT value) {
return (value > 0) - (value < 0);
}
int main() {
// create a cartesian grid with 100^3 grid points and the bounding box [-1, 1]^3
Grid grid(7, 7, 7, {-1, -1, -1, 1, 1, 1});
// calculate the value at all grid points
for (std::size_t x = 0; x < grid.xdim(); x++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t z = 0; z < grid.zdim(); z++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
// manhattan distance to the origin
grid.value(x, y, z) = std::max({std::abs(pos_x), std::abs(pos_y), std::abs(pos_z)});
}
}
}
auto cube_gradient = [](const Point& p) {
// the normal depends on the side of the cube
const FT max_value = std::max({std::abs(p.x()), std::abs(p.y()), std::abs(p.z())});
Vector g(0, 0, 0);
if (max_value == std::abs(p.x())) {
g += Vector(sign(p.x()), 0, 0);
}
if (max_value == std::abs(p.y())) {
g += Vector(0, sign(p.y()), 0);
}
if (max_value == std::abs(p.z())) {
g += Vector(0, 0, sign(p.z()));
}
const FT length_sq = g.squared_length();
if (length_sq > 0.00001) {
g /= CGAL::approximate_sqrt(length_sq);
}
return g;
};
// create a domain from the grid
// prepare collections for the results
Point_range points_mc, points_tmc, points_dc;
Polygon_range polygons_mc, polygons_tmc, polygons_dc;
// execute marching cubes, topologically correct marching cubes and dual contouring with an isovalue of 0.8
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, 0.88, points_mc, polygons_mc);
CGAL::Isosurfacing::make_triangle_mesh_using_tmc(domain, 0.88, points_tmc, polygons_tmc);
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(domain, 0.88, points_dc, polygons_dc);
// save the results in the OFF format
CGAL::IO::write_OFF("result_mc.off", points_mc, polygons_mc);
CGAL::IO::write_OFF("result_tmc.off", points_tmc, polygons_tmc);
CGAL::IO::write_OFF("result_dc.off", points_dc, polygons_dc);
}

Image_3

The following example shows how to load data from an Image_3.


File Isosurfacing_3/marching_cubes_inrimage.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
const std::string fname = "../../../data/skull_2.9.inr";
// load the image
CGAL::Image_3 image;
if (!image.read(fname)) {
std::cerr << "Error: Cannot read file " << fname << std::endl;
return EXIT_FAILURE;
}
// convert it to a cartesian grid
const Grid grid(image);
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 2.9
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Offset mesh

The following example shows how to compute an offset mesh. The original mesh is passed to an AABB_tree to allow fast distance queries. With the use of Side_of_triangle_mesh the sign of the distance function is flipped inside the mesh.


File Isosurfacing_3/marching_cubes_mesh_offset.cpp

#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef CGAL::Surface_mesh<Point> Mesh;
typedef CGAL::AABB_face_graph_triangle_primitive<Mesh> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> Traits;
typedef CGAL::AABB_tree<Traits> Tree;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
// computes the distance of a point p from the mesh with the use of a AABB_tree
inline Kernel::FT distance_to_mesh(const Tree& tree, const Point& p) {
const Point& x = tree.closest_point(p);
return std::sqrt((p - x).squared_length());
}
int main() {
const std::string input_name = "../../../data/bunny.off";
const int n_voxels = 20;
const FT offset_value = -0.03;
// load the original mesh
Mesh mesh_input;
if (!CGAL::IO::read_OFF(input_name, mesh_input)) {
std::cerr << "Could not read mesh" << std::endl;
exit(-1);
}
// compute the new bounding box
CGAL::Bbox_3 aabb_grid = CGAL::Polygon_mesh_processing::bbox(mesh_input);
Vector aabb_increase_vec = Vector(offset_value + 0.01, offset_value + 0.01, offset_value + 0.01);
aabb_grid += (Point(aabb_grid.xmax(), aabb_grid.ymax(), aabb_grid.zmax()) + aabb_increase_vec).bbox();
aabb_grid += (Point(aabb_grid.xmin(), aabb_grid.ymin(), aabb_grid.zmin()) - aabb_increase_vec).bbox();
// construct AABB tree
Tree tree(mesh_input.faces_begin(), mesh_input.faces_end(), mesh_input);
CGAL::Side_of_triangle_mesh<Mesh, CGAL::GetGeomTraits<Mesh>::type> sotm(mesh_input);
// create the grid
Grid grid(n_voxels, n_voxels, n_voxels, aabb_grid);
for (std::size_t z = 0; z < grid.zdim(); z++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t x = 0; x < grid.xdim(); x++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
const Point p(pos_x, pos_y, pos_z);
// compute the distance
grid.value(x, y, z) = distance_to_mesh(tree, p);
// flip the sign, so the distance is negative inside the mesh
const bool is_inside = (sotm(p) == CGAL::ON_BOUNDED_SIDE);
if (is_inside) {
grid.value(x, y, z) *= -1;
}
}
}
}
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue equal to the offset
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, offset_value, points, polygons);
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Octree Dual Contouring

The following example shows how to extract an isosurface from an octree using Dual Contouring. The domain is an Octree_domain that describes a sphere by the distance to its origin stored in an octree. The octree is highly refined in one octant and only coarse in the others.


File Isosurfacing_3/dual_contouring_octree.cpp

#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Octree_domain.h>
#include <CGAL/Octree_wrapper.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <math.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Vector_3 Vector_3;
typedef typename Kernel::Point_3 Point_3;
typedef std::vector<Point_3> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
Kernel::FT sphere_function(const Point_3& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
}
struct Refine_one_eighth {
std::size_t min_depth_;
std::size_t max_depth_;
std::size_t octree_dim_;
Octree_wrapper_::Uniform_coords uniform_coordinates(const Octree_wrapper_::Octree::Node& node) const {
auto coords = node.global_coordinates();
const std::size_t depth_factor = std::size_t(1) << (max_depth_ - node.depth());
for (int i = 0; i < Octree_wrapper_::Octree::Node::Dimension::value; ++i) {
coords[i] *= (uint32_t)depth_factor;
}
return coords;
}
Refine_one_eighth(std::size_t min_depth, std::size_t max_depth) : min_depth_(min_depth), max_depth_(max_depth) {
octree_dim_ = std::size_t(1) << max_depth_;
}
bool operator()(const Octree_wrapper_::Octree::Node& n) const {
// n.depth()
if (n.depth() < min_depth_) {
return true;
}
if (n.depth() == max_depth_) {
return false;
}
auto leaf_coords = uniform_coordinates(n);
if (leaf_coords[0] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[1] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[2] >= octree_dim_ / 2) {
// return false;
}
return true;
}
};
int main() {
Octree_wrapper_ octree_wrap({-1, -1, -1, 1, 1, 1});
Refine_one_eighth split_predicate(4, 6);
octree_wrap.refine(split_predicate);
Octree_domain_ octree_domain(octree_wrap);
auto lam = [&](const Octree_domain_::Vertex_handle& v) {
Point_3 p = octree_domain.position(v);
const auto val = sphere_function(p);
Vector_3 gradient = p - CGAL::ORIGIN;
gradient = gradient / std::sqrt(gradient.squared_length());
octree_wrap.value(v) = val;
octree_wrap.gradient(v) = gradient;
};
octree_domain.iterate_vertices(lam, CGAL::Sequential_tag());
Point_range points;
Polygon_range polygons;
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(octree_domain, 0.8, points, polygons);
CGAL::IO::write_OFF("result.off", points, polygons);
}
CGAL 5.6 - 3D Isosurfacing
User Manual

Author
Julian Stahl and Daniel Zint

Figure 60.1 Different isovalues of a bunny.

Introduction

This package provides methods to compute a surface mesh representing an isosurface of a 3-dimensional scalar field. An isosurface is defined as the surface on which the value of this field is equal to a given constant, i.e. the isovalue. Depending on the isosurfacing method and the input data structure, the result is either a triangular, quadrilateral, or higher order polygonal indexed face set.

Algorithms

There are multiple algorithms to extract isosurfaces. This package contains Marching Cubes, topologically correct Marching Cubes, Dual Contouring, and Octree Marching. (References?)

Marching Cubes (MC)

MC processes all cells of the input domain individually. Each cell corner gets a sign (+/-) to indicate if it is above or below the isovalue. A new vertex is created on every cell edge where the sign changes, i.e. the isosurface is intersected. The vertex position is computed via linear interpolation of the scalar values of the incident corners. Depending on the configuration of signs at the corners the resulting vertices are connected to form triangles within the cell.

MC can process any input data structure that consists of hexahedral cells. In case of a conforming grid, MC produces a triangle mesh that is manifold in most scenarios. If the mesh is manifold and the isosurface does not intersect the domain boundaries, the mesh is also watertight. Compared to other approaches the algorithm often generates more and many thin triangles with acute angles. MC does not preserve sharp edges.

Figure 60.2 Some example cases of Marching Cubes.

Topologically correct Marching Cubes (TMC)

This algorithm is an extension to MC and provides additional guarantees. It generates a mesh that is homeomorphic to the trilinear interpolant of the input function inside each cube. Furthermore, the mesh is guaranteed to be manifold and watertight, as long as the isosurface does not intersect the domain boundaries.

TODO examples

Dual Contouring (DC)

DC creates one vertex in every cell that is intersected by the isosurface. Next, a face is created for each edge that intersects the isosurface, by connecting the vertices of the incident cells. For a uniform hexahedral grid, this results in a quadrilateral mesh.

The classical DC method requires the gradient of the scalar field. It is passed to the method as an additional parameter in the form of a functor. The default argument for this parameter assumes the gradient to be zero. Thus, for using the classical DC, the gradient has to be defined by the user.

Different versions of DC compute the vertex positions differently. Therefore, the vertex positioning is configurable with an optional parameter. Some of them do not require the gradient and therefore even work with the zero gradient.

Dual Contouring works on any domain but does not guarantee a manifold or watertight mesh. It creates less faces than Marching Cubes. The main advantage of DC over MC is the ability to represent sharp edges.

Figure 60.3 Isosurface of the IWP function generated by Dual Contouring.

Comparison

Algorithm Domains Faces Manifold Watertight* Topologically correct
MC Hexahedral Triangles no no no
TMC Hexahedral Triangles yes yes yes
DC All Polygons no no no

(* assuming the isosurface does not leave the given bounding box)

Figure 60.4 Comparison between a cube generated by Dual Contouring (left) and Marching Cubes (right).

Interface

Each algorithm is represented by a single functions. The function signature is the same for all algorithms:

template <typename Concurrency_tag = Sequential_tag, class Domain_, class PointRange, class PolygonRange>
void make_triangle_mesh_using_marching_cubes(const Domain_& domain, const typename Domain_::FT iso_value,
PointRange& points, PolygonRange& polygons);

The input is provided in the form of a domain (see Domains).

The iso_value parameter describes the grid value the isosurface should represent.

The output is in the form of an indexed face set that is written to the two collections points and polygons. The vertex positions are stored as Point_3 in points. Each face in polygons is a list of indices pointing into the points collection. Depending on the algorithm, the indexed face set may either store a polygon soup or a topological mesh.

Algorithms can run sequentially on one CPU core or in parallel. The Concurrency_tag is used to specify how the algorithm is executed and is either Sequential_tag or Parallel_tag. To enable parallelism, CGAL needs to be linked with the Intel TBB library. If the parallel version is not availible the sequential version will always be used as a fallback.

Domains

A domain is an object that provides functions to access the input data, its geometry, topology, and optionally its gradient. There are some predefined domain classes that wrap the input data and provide a generalized interface for the algorithm. Users can also define new domains by implementing the Isosurfacing_domain concept.

Implicit domain

The Implicit_domain represents the input function in an implicit form without storing any values. It takes a functor or lambda that computes the value of the function from the position of a vertex as parameter. Additionally, the bounding box and spacing between grid points have to be specified.

Cartesian grid domain

The Cartesian_grid_domain only takes a Cartesian_grid_3 as parameter. It represents a uniform grid of values that are either computed by the user or read from an Image_3. The constructor of Cartesian_grid_3 needs the number of grid points in each dimension and the bounding box. The values are read and written with value(x, y, z) where x, y, and z are the coordinates of a grid point. Alternatively, all required data can be copied from an Image_3.

Octree domain

The Octree_domain wraps an octree to be used by isosurfacing algorithms. The octree has to be already refined. ...

Performance

Figure 60.5 Scaling of Marching Cubes with more threads.

Examples

Implicit sphere

The following example shows the usage of the Marching Cubes algorithm to extract an isosurface. The domain is an Implicit_domain that describes a sphere by the distance to its origin as an implicit function.


File Isosurfacing_3/marching_cubes_implicit_sphere.cpp

#include <CGAL/Implicit_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Vector_3 Vector;
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
// distance to the origin
auto sphere_function = [](const Point& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
};
// create a domain with bounding box [-1, 1]^3 and grid spacing 0.02
{-1, -1, -1, 1, 1, 1}, Vector(0.02f, 0.02f, 0.02f), sphere_function); // TODO: this is ugly
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 0.8
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Cartesian_grid_3 cube comparison

The following example compares all provided algorithms to extract an isosurface. The domain is an Cartesian_grid_domain that describes a cube by storing the manhattan distance to its origin in a Cartesian_grid_3.


File Isosurfacing_3/all_cartesian_cube.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/TC_marching_cubes_3.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
FT sign(FT value) {
return (value > 0) - (value < 0);
}
int main() {
// create a cartesian grid with 100^3 grid points and the bounding box [-1, 1]^3
Grid grid(7, 7, 7, {-1, -1, -1, 1, 1, 1});
// calculate the value at all grid points
for (std::size_t x = 0; x < grid.xdim(); x++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t z = 0; z < grid.zdim(); z++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
// manhattan distance to the origin
grid.value(x, y, z) = std::max({std::abs(pos_x), std::abs(pos_y), std::abs(pos_z)});
}
}
}
auto cube_gradient = [](const Point& p) {
// the normal depends on the side of the cube
const FT max_value = std::max({std::abs(p.x()), std::abs(p.y()), std::abs(p.z())});
Vector g(0, 0, 0);
if (max_value == std::abs(p.x())) {
g += Vector(sign(p.x()), 0, 0);
}
if (max_value == std::abs(p.y())) {
g += Vector(0, sign(p.y()), 0);
}
if (max_value == std::abs(p.z())) {
g += Vector(0, 0, sign(p.z()));
}
const FT length_sq = g.squared_length();
if (length_sq > 0.00001) {
g /= CGAL::approximate_sqrt(length_sq);
}
return g;
};
// create a domain from the grid
// prepare collections for the results
Point_range points_mc, points_tmc, points_dc;
Polygon_range polygons_mc, polygons_tmc, polygons_dc;
// execute marching cubes, topologically correct marching cubes and dual contouring with an isovalue of 0.8
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, 0.88, points_mc, polygons_mc);
CGAL::Isosurfacing::make_triangle_mesh_using_tmc(domain, 0.88, points_tmc, polygons_tmc);
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(domain, 0.88, points_dc, polygons_dc);
// save the results in the OFF format
CGAL::IO::write_OFF("result_mc.off", points_mc, polygons_mc);
CGAL::IO::write_OFF("result_tmc.off", points_tmc, polygons_tmc);
CGAL::IO::write_OFF("result_dc.off", points_dc, polygons_dc);
}

Image_3

The following example shows how to load data from an Image_3.


File Isosurfacing_3/marching_cubes_inrimage.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
const std::string fname = "../../../data/skull_2.9.inr";
// load the image
CGAL::Image_3 image;
if (!image.read(fname)) {
std::cerr << "Error: Cannot read file " << fname << std::endl;
return EXIT_FAILURE;
}
// convert it to a cartesian grid
const Grid grid(image);
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 2.9
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Offset mesh

The following example shows how to compute an offset mesh. The original mesh is passed to an AABB_tree to allow fast distance queries. With the use of Side_of_triangle_mesh the sign of the distance function is flipped inside the mesh.


File Isosurfacing_3/marching_cubes_mesh_offset.cpp

#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef CGAL::Surface_mesh<Point> Mesh;
typedef CGAL::AABB_face_graph_triangle_primitive<Mesh> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> Traits;
typedef CGAL::AABB_tree<Traits> Tree;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
// computes the distance of a point p from the mesh with the use of a AABB_tree
inline Kernel::FT distance_to_mesh(const Tree& tree, const Point& p) {
const Point& x = tree.closest_point(p);
return std::sqrt((p - x).squared_length());
}
int main() {
const std::string input_name = "../../../data/bunny.off";
const int n_voxels = 20;
const FT offset_value = -0.03;
// load the original mesh
Mesh mesh_input;
if (!CGAL::IO::read_OFF(input_name, mesh_input)) {
std::cerr << "Could not read mesh" << std::endl;
exit(-1);
}
// compute the new bounding box
CGAL::Bbox_3 aabb_grid = CGAL::Polygon_mesh_processing::bbox(mesh_input);
Vector aabb_increase_vec = Vector(offset_value + 0.01, offset_value + 0.01, offset_value + 0.01);
aabb_grid += (Point(aabb_grid.xmax(), aabb_grid.ymax(), aabb_grid.zmax()) + aabb_increase_vec).bbox();
aabb_grid += (Point(aabb_grid.xmin(), aabb_grid.ymin(), aabb_grid.zmin()) - aabb_increase_vec).bbox();
// construct AABB tree
Tree tree(mesh_input.faces_begin(), mesh_input.faces_end(), mesh_input);
CGAL::Side_of_triangle_mesh<Mesh, CGAL::GetGeomTraits<Mesh>::type> sotm(mesh_input);
// create the grid
Grid grid(n_voxels, n_voxels, n_voxels, aabb_grid);
for (std::size_t z = 0; z < grid.zdim(); z++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t x = 0; x < grid.xdim(); x++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
const Point p(pos_x, pos_y, pos_z);
// compute the distance
grid.value(x, y, z) = distance_to_mesh(tree, p);
// flip the sign, so the distance is negative inside the mesh
const bool is_inside = (sotm(p) == CGAL::ON_BOUNDED_SIDE);
if (is_inside) {
grid.value(x, y, z) *= -1;
}
}
}
}
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue equal to the offset
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, offset_value, points, polygons);
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Octree Dual Contouring

The following example shows how to extract an isosurface from an octree using Dual Contouring. The domain is an Octree_domain that describes a sphere by the distance to its origin stored in an octree. The octree is highly refined in one octant and only coarse in the others.


File Isosurfacing_3/dual_contouring_octree.cpp

#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Octree_domain.h>
#include <CGAL/Octree_wrapper.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <math.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Vector_3 Vector_3;
typedef typename Kernel::Point_3 Point_3;
typedef std::vector<Point_3> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
Kernel::FT sphere_function(const Point_3& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
}
struct Refine_one_eighth {
std::size_t min_depth_;
std::size_t max_depth_;
std::size_t octree_dim_;
Octree_wrapper_::Uniform_coords uniform_coordinates(const Octree_wrapper_::Octree::Node& node) const {
auto coords = node.global_coordinates();
const std::size_t depth_factor = std::size_t(1) << (max_depth_ - node.depth());
for (int i = 0; i < Octree_wrapper_::Octree::Node::Dimension::value; ++i) {
coords[i] *= (uint32_t)depth_factor;
}
return coords;
}
Refine_one_eighth(std::size_t min_depth, std::size_t max_depth) : min_depth_(min_depth), max_depth_(max_depth) {
octree_dim_ = std::size_t(1) << max_depth_;
}
bool operator()(const Octree_wrapper_::Octree::Node& n) const {
// n.depth()
if (n.depth() < min_depth_) {
return true;
}
if (n.depth() == max_depth_) {
return false;
}
auto leaf_coords = uniform_coordinates(n);
if (leaf_coords[0] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[1] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[2] >= octree_dim_ / 2) {
// return false;
}
return true;
}
};
int main() {
Octree_wrapper_ octree_wrap({-1, -1, -1, 1, 1, 1});
Refine_one_eighth split_predicate(4, 6);
octree_wrap.refine(split_predicate);
Octree_domain_ octree_domain(octree_wrap);
auto lam = [&](const Octree_domain_::Vertex_handle& v) {
Point_3 p = octree_domain.position(v);
const auto val = sphere_function(p);
Vector_3 gradient = p - CGAL::ORIGIN;
gradient = gradient / std::sqrt(gradient.squared_length());
octree_wrap.value(v) = val;
octree_wrap.gradient(v) = gradient;
};
octree_domain.iterate_vertices(lam, CGAL::Sequential_tag());
Point_range points;
Polygon_range polygons;
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(octree_domain, 0.8, points, polygons);
CGAL::IO::write_OFF("result.off", points, polygons);
}
CGAL 5.6 - 3D Isosurfacing
User Manual

Author
Julian Stahl and Daniel Zint

Figure 60.1 Different isovalues of a bunny.

Introduction

This package provides methods to compute a surface mesh representing an isosurface of a 3-dimensional scalar field. An isosurface is defined as the surface on which the value of this field is equal to a given constant, i.e. the isovalue. Depending on the isosurfacing method and the input data structure, the result is either a triangular, quadrilateral, or higher order polygonal indexed face set.

Algorithms

There are multiple algorithms to extract isosurfaces. This package contains Marching Cubes, topologically correct Marching Cubes, Dual Contouring, and Octree Marching. (References?)

Marching Cubes (MC)

MC processes all cells of the input domain individually. Each cell corner gets a sign (+/-) to indicate if it is above or below the isovalue. A new vertex is created on every cell edge where the sign changes, i.e. the isosurface is intersected. The vertex position is computed via linear interpolation of the scalar values of the incident corners. Depending on the configuration of signs at the corners the resulting vertices are connected to form triangles within the cell.

MC can process any input data structure that consists of hexahedral cells. In case of a conforming grid, MC produces a triangle mesh that is manifold in most scenarios. If the mesh is manifold and the isosurface does not intersect the domain boundaries, the mesh is also watertight. Compared to other approaches the algorithm often generates more and many thin triangles with acute angles. MC does not preserve sharp edges.

Figure 60.2 Some example cases of Marching Cubes.

Topologically correct Marching Cubes (TMC)

This algorithm is an extension to MC and provides additional guarantees. It generates a mesh that is homeomorphic to the trilinear interpolant of the input function inside each cube. Furthermore, the mesh is guaranteed to be manifold and watertight, as long as the isosurface does not intersect the domain boundaries.

TODO examples

Dual Contouring (DC)

DC creates one vertex in every cell that is intersected by the isosurface. Next, a face is created for each edge that intersects the isosurface, by connecting the vertices of the incident cells. For a uniform hexahedral grid, this results in a quadrilateral mesh.

The classical DC method requires the gradient of the scalar field. It is passed to the method as an additional parameter in the form of a functor. The default argument for this parameter assumes the gradient to be zero. Thus, for using the classical DC, the gradient has to be defined by the user.

Different versions of DC compute the vertex positions differently. Therefore, the vertex positioning is configurable with an optional parameter. Some of them do not require the gradient and therefore even work with the zero gradient.

Dual Contouring works on any domain but does not guarantee a manifold or watertight mesh. It creates less faces than Marching Cubes. The main advantage of DC over MC is the ability to represent sharp edges.

Figure 60.3 Isosurface of the IWP function generated by Dual Contouring.

Comparison

Algorithm Domains Faces Manifold Watertight* Topologically correct
MC Hexahedral Triangles no no no
TMC Hexahedral Triangles yes yes yes
DC All Polygons no no no

(* assuming the isosurface does not leave the given bounding box)

Figure 60.4 Comparison between a cube generated by Dual Contouring (left) and Marching Cubes (right).

Interface

Each algorithm is represented by a single functions. The function signature is the same for all algorithms:

template <typename Concurrency_tag = Sequential_tag, class Domain_, class PointRange, class PolygonRange>
void make_triangle_mesh_using_marching_cubes(const Domain_& domain, const typename Domain_::FT iso_value,
PointRange& points, PolygonRange& polygons);

The input is provided in the form of a domain (see Domains).

The iso_value parameter describes the grid value the isosurface should represent.

The output is in the form of an indexed face set that is written to the two collections points and polygons. The vertex positions are stored as Point_3 in points. Each face in polygons is a list of indices pointing into the points collection. Depending on the algorithm, the indexed face set may either store a polygon soup or a topological mesh.

Algorithms can run sequentially on one CPU core or in parallel. The Concurrency_tag is used to specify how the algorithm is executed and is either Sequential_tag or Parallel_tag. To enable parallelism, CGAL needs to be linked with the Intel TBB library. If the parallel version is not availible the sequential version will always be used as a fallback.

Domains

A domain is an object that provides functions to access the input data, its geometry, topology, and optionally its gradient. There are some predefined domain classes that wrap the input data and provide a generalized interface for the algorithm. Users can also define new domains by implementing the Isosurfacing_domain concept.

Implicit domain

The Implicit_domain represents the input function in an implicit form without storing any values. It takes a functor or lambda that computes the value of the function from the position of a vertex as parameter. Additionally, the bounding box and spacing between grid points have to be specified.

Cartesian grid domain

The Cartesian_grid_domain only takes a Cartesian_grid_3 as parameter. It represents a uniform grid of values that are either computed by the user or read from an Image_3. The constructor of Cartesian_grid_3 needs the number of grid points in each dimension and the bounding box. The values are read and written with value(x, y, z) where x, y, and z are the coordinates of a grid point. Alternatively, all required data can be copied from an Image_3.

Octree domain

The Octree_domain wraps an octree to be used by isosurfacing algorithms. The octree has to be already refined. ...

Performance

Figure 60.5 Scaling of Marching Cubes with more threads.

Examples

Implicit sphere

The following example shows the usage of the Marching Cubes algorithm to extract an isosurface. The domain is an Implicit_domain that describes a sphere by the distance to its origin as an implicit function.


File Isosurfacing_3/marching_cubes_implicit_sphere.cpp

#include <CGAL/Implicit_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Vector_3 Vector;
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
// distance to the origin
auto sphere_function = [](const Point& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
};
// create a domain with bounding box [-1, 1]^3 and grid spacing 0.02
{-1, -1, -1, 1, 1, 1}, Vector(0.02f, 0.02f, 0.02f), sphere_function); // TODO: this is ugly
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 0.8
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Cartesian_grid_3 cube comparison

The following example compares all provided algorithms to extract an isosurface. The domain is an Cartesian_grid_domain that describes a cube by storing the manhattan distance to its origin in a Cartesian_grid_3.


File Isosurfacing_3/all_cartesian_cube.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/TC_marching_cubes_3.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
FT sign(FT value) {
return (value > 0) - (value < 0);
}
int main() {
// create a cartesian grid with 100^3 grid points and the bounding box [-1, 1]^3
Grid grid(7, 7, 7, {-1, -1, -1, 1, 1, 1});
// calculate the value at all grid points
for (std::size_t x = 0; x < grid.xdim(); x++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t z = 0; z < grid.zdim(); z++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
// manhattan distance to the origin
grid.value(x, y, z) = std::max({std::abs(pos_x), std::abs(pos_y), std::abs(pos_z)});
}
}
}
auto cube_gradient = [](const Point& p) {
// the normal depends on the side of the cube
const FT max_value = std::max({std::abs(p.x()), std::abs(p.y()), std::abs(p.z())});
Vector g(0, 0, 0);
if (max_value == std::abs(p.x())) {
g += Vector(sign(p.x()), 0, 0);
}
if (max_value == std::abs(p.y())) {
g += Vector(0, sign(p.y()), 0);
}
if (max_value == std::abs(p.z())) {
g += Vector(0, 0, sign(p.z()));
}
const FT length_sq = g.squared_length();
if (length_sq > 0.00001) {
g /= CGAL::approximate_sqrt(length_sq);
}
return g;
};
// create a domain from the grid
// prepare collections for the results
Point_range points_mc, points_tmc, points_dc;
Polygon_range polygons_mc, polygons_tmc, polygons_dc;
// execute marching cubes, topologically correct marching cubes and dual contouring with an isovalue of 0.8
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, 0.88, points_mc, polygons_mc);
CGAL::Isosurfacing::make_triangle_mesh_using_tmc(domain, 0.88, points_tmc, polygons_tmc);
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(domain, 0.88, points_dc, polygons_dc);
// save the results in the OFF format
CGAL::IO::write_OFF("result_mc.off", points_mc, polygons_mc);
CGAL::IO::write_OFF("result_tmc.off", points_tmc, polygons_tmc);
CGAL::IO::write_OFF("result_dc.off", points_dc, polygons_dc);
}

Image_3

The following example shows how to load data from an Image_3.


File Isosurfacing_3/marching_cubes_inrimage.cpp

#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
typedef typename Kernel::Point_3 Point;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
int main() {
const std::string fname = "../../../data/skull_2.9.inr";
// load the image
CGAL::Image_3 image;
if (!image.read(fname)) {
std::cerr << "Error: Cannot read file " << fname << std::endl;
return EXIT_FAILURE;
}
// convert it to a cartesian grid
const Grid grid(image);
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue of 2.9
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Offset mesh

The following example shows how to compute an offset mesh. The original mesh is passed to an AABB_tree to allow fast distance queries. With the use of Side_of_triangle_mesh the sign of the distance function is flipped inside the mesh.


File Isosurfacing_3/marching_cubes_mesh_offset.cpp

#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/Cartesian_grid_3.h>
#include <CGAL/Cartesian_grid_domain.h>
#include <CGAL/Marching_cubes_3.h>
#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Point_3 Point;
typedef typename Kernel::Vector_3 Vector;
typedef CGAL::Surface_mesh<Point> Mesh;
typedef CGAL::AABB_face_graph_triangle_primitive<Mesh> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> Traits;
typedef CGAL::AABB_tree<Traits> Tree;
typedef std::vector<Point> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
// computes the distance of a point p from the mesh with the use of a AABB_tree
inline Kernel::FT distance_to_mesh(const Tree& tree, const Point& p) {
const Point& x = tree.closest_point(p);
return std::sqrt((p - x).squared_length());
}
int main() {
const std::string input_name = "../../../data/bunny.off";
const int n_voxels = 20;
const FT offset_value = -0.03;
// load the original mesh
Mesh mesh_input;
if (!CGAL::IO::read_OFF(input_name, mesh_input)) {
std::cerr << "Could not read mesh" << std::endl;
exit(-1);
}
// compute the new bounding box
CGAL::Bbox_3 aabb_grid = CGAL::Polygon_mesh_processing::bbox(mesh_input);
Vector aabb_increase_vec = Vector(offset_value + 0.01, offset_value + 0.01, offset_value + 0.01);
aabb_grid += (Point(aabb_grid.xmax(), aabb_grid.ymax(), aabb_grid.zmax()) + aabb_increase_vec).bbox();
aabb_grid += (Point(aabb_grid.xmin(), aabb_grid.ymin(), aabb_grid.zmin()) - aabb_increase_vec).bbox();
// construct AABB tree
Tree tree(mesh_input.faces_begin(), mesh_input.faces_end(), mesh_input);
CGAL::Side_of_triangle_mesh<Mesh, CGAL::GetGeomTraits<Mesh>::type> sotm(mesh_input);
// create the grid
Grid grid(n_voxels, n_voxels, n_voxels, aabb_grid);
for (std::size_t z = 0; z < grid.zdim(); z++) {
for (std::size_t y = 0; y < grid.ydim(); y++) {
for (std::size_t x = 0; x < grid.xdim(); x++) {
const FT pos_x = x * grid.get_spacing()[0] + grid.get_bbox().xmin();
const FT pos_y = y * grid.get_spacing()[1] + grid.get_bbox().ymin();
const FT pos_z = z * grid.get_spacing()[2] + grid.get_bbox().zmin();
const Point p(pos_x, pos_y, pos_z);
// compute the distance
grid.value(x, y, z) = distance_to_mesh(tree, p);
// flip the sign, so the distance is negative inside the mesh
const bool is_inside = (sotm(p) == CGAL::ON_BOUNDED_SIDE);
if (is_inside) {
grid.value(x, y, z) *= -1;
}
}
}
}
// create a domain from the grid
// prepare collections for the result
Point_range points;
Polygon_range polygons;
// execute marching cubes with an isovalue equal to the offset
CGAL::Isosurfacing::make_triangle_mesh_using_marching_cubes(domain, offset_value, points, polygons);
// save the result in the OFF format
CGAL::IO::write_OFF("result.off", points, polygons);
}

Octree Dual Contouring

The following example shows how to extract an isosurface from an octree using Dual Contouring. The domain is an Octree_domain that describes a sphere by the distance to its origin stored in an octree. The octree is highly refined in one octant and only coarse in the others.


File Isosurfacing_3/dual_contouring_octree.cpp

#include <CGAL/Dual_contouring_3.h>
#include <CGAL/Octree_domain.h>
#include <CGAL/Octree_wrapper.h>
#include <CGAL/Simple_cartesian.h>
#include <CGAL/boost/graph/IO/OFF.h>
#include <math.h>
#include <iostream>
typedef typename Kernel::FT FT;
typedef typename Kernel::Vector_3 Vector_3;
typedef typename Kernel::Point_3 Point_3;
typedef std::vector<Point_3> Point_range;
typedef std::vector<std::vector<std::size_t>> Polygon_range;
Kernel::FT sphere_function(const Point_3& point) {
return std::sqrt(point.x() * point.x() + point.y() * point.y() + point.z() * point.z());
}
struct Refine_one_eighth {
std::size_t min_depth_;
std::size_t max_depth_;
std::size_t octree_dim_;
Octree_wrapper_::Uniform_coords uniform_coordinates(const Octree_wrapper_::Octree::Node& node) const {
auto coords = node.global_coordinates();
const std::size_t depth_factor = std::size_t(1) << (max_depth_ - node.depth());
for (int i = 0; i < Octree_wrapper_::Octree::Node::Dimension::value; ++i) {
coords[i] *= (uint32_t)depth_factor;
}
return coords;
}
Refine_one_eighth(std::size_t min_depth, std::size_t max_depth) : min_depth_(min_depth), max_depth_(max_depth) {
octree_dim_ = std::size_t(1) << max_depth_;
}
bool operator()(const Octree_wrapper_::Octree::Node& n) const {
// n.depth()
if (n.depth() < min_depth_) {
return true;
}
if (n.depth() == max_depth_) {
return false;
}
auto leaf_coords = uniform_coordinates(n);
if (leaf_coords[0] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[1] >= octree_dim_ / 2) {
return false;
}
if (leaf_coords[2] >= octree_dim_ / 2) {
// return false;
}
return true;
}
};
int main() {
Octree_wrapper_ octree_wrap({-1, -1, -1, 1, 1, 1});
Refine_one_eighth split_predicate(4, 6);
octree_wrap.refine(split_predicate);
Octree_domain_ octree_domain(octree_wrap);
auto lam = [&](const Octree_domain_::Vertex_handle& v) {
Point_3 p = octree_domain.position(v);
const auto val = sphere_function(p);
Vector_3 gradient = p - CGAL::ORIGIN;
gradient = gradient / std::sqrt(gradient.squared_length());
octree_wrap.value(v) = val;
octree_wrap.gradient(v) = gradient;
};
octree_domain.iterate_vertices(lam, CGAL::Sequential_tag());
Point_range points;
Polygon_range polygons;
CGAL::Isosurfacing::make_quad_mesh_using_dual_contouring(octree_domain, 0.8, points, polygons);
CGAL::IO::write_OFF("result.off", points, polygons);
}