[{"data":1,"prerenderedAt":1652},["ShallowReactive",2],{"tutorial-isocontours-on-a-triangle-mesh-in-cpp":3},{"id":4,"title":5,"author":6,"body":7,"date":1628,"description":1629,"extension":1630,"keywords":1631,"meta":1642,"navigation":148,"ogImage":1643,"path":1644,"published":148,"seo":1645,"stem":1646,"tags":1647,"__hash__":1651},"tutorials\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp.md","Fast and Robust Isocontours on a Triangle Mesh in C++","Žiga Sajovic",{"type":8,"value":9,"toc":1617},"minimark",[10,28,36,40,47,52,55,114,124,128,197,223,227,233,236,321,327,333,558,562,601,607,614,711,714,759,768,772,775,882,888,891,967,970,974,981,1067,1073,1084,1091,1172,1175,1223,1227,1230,1264,1269,1396,1399,1502,1524,1528,1557,1561,1564,1568,1580,1585,1591,1597,1609,1613],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript","JavaScript",".",[14,29,30,31,35],{},"An isocontour is the curve where a scalar field on a mesh takes a given value. Height gives contour lines. Distance from a point gives rings. Curvature gives the borders between shapes. ",[32,33,34],"code",{},"trueform"," extracts isocontours of any per-vertex field, at one level or many, and cuts the mesh into isobands along them. It is header-only C++17. This tutorial covers single and multiple levels, the curve format, and isobands.",[37,38],"article-cta",{":buttons":39},"[{\"label\":\"trueform\",\"to\":\"\u002Ftrueform\",\"icon\":\"i-lucide-cpu\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"GitHub\",\"to\":\"https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\",\"icon\":\"i-simple-icons-github\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Documentation\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fiso\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[14,41,42],{},[43,44],"img",{"alt":45,"src":46},"Height isobands on the Stanford dragon, alternating teal and orange, with the band boundaries as curves","\u002Fimg\u002Ftutorials\u002Fisocontours.png",[48,49,51],"h2",{"id":50},"install","Install",[14,53,54],{},"Header-only. It needs C++17 and oneTBB. Add it to your project via CMake FetchContent:",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-cmake shiki shiki-themes github-dark","FetchContent_Declare(\n  trueform\n  GIT_REPOSITORY https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\n)\nFetchContent_MakeAvailable(trueform)\ntarget_link_libraries(my_app PRIVATE tf::trueform)\n","cmake","",[32,63,64,73,79,85,91,97],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"s95oV","FetchContent_Declare(\n",[65,74,76],{"class":67,"line":75},2,[65,77,78],{"class":71},"  trueform\n",[65,80,82],{"class":67,"line":81},3,[65,83,84],{"class":71},"  GIT_REPOSITORY https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\n",[65,86,88],{"class":67,"line":87},4,[65,89,90],{"class":71},")\n",[65,92,94],{"class":67,"line":93},5,[65,95,96],{"class":71},"FetchContent_MakeAvailable(trueform)\n",[65,98,100,104,107,111],{"class":67,"line":99},6,[65,101,103],{"class":102},"snl16","target_link_libraries",[65,105,106],{"class":71},"(my_app ",[65,108,110],{"class":109},"svObZ","PRIVATE",[65,112,113],{"class":71}," tf::trueform)\n",[14,115,116,117,123],{},"Also available via conan, nuget, and pip. See the ",[18,118,122],{"href":119,"rel":120},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fgetting-started\u002Finstallation",[121],"nofollow","installation guide"," for all options.",[48,125,127],{"id":126},"loading-the-mesh","Loading the mesh",[56,129,133],{"className":130,"code":131,"language":132,"meta":61,"style":61},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto mesh_buffer = tf::read_stl(\"part.stl\");\nauto mesh = mesh_buffer.polygons();\n","cpp",[32,134,135,144,150,179],{"__ignoreMap":61},[65,136,137,140],{"class":67,"line":68},[65,138,139],{"class":102},"#include",[65,141,143],{"class":142},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[65,145,146],{"class":67,"line":75},[65,147,149],{"emptyLinePlaceholder":148},true,"\n",[65,151,152,155,158,161,164,167,170,173,176],{"class":67,"line":81},[65,153,154],{"class":102},"auto",[65,156,157],{"class":71}," mesh_buffer ",[65,159,160],{"class":102},"=",[65,162,163],{"class":109}," tf",[65,165,166],{"class":71},"::",[65,168,169],{"class":109},"read_stl",[65,171,172],{"class":71},"(",[65,174,175],{"class":142},"\"part.stl\"",[65,177,178],{"class":71},");\n",[65,180,181,183,186,188,191,194],{"class":67,"line":87},[65,182,154],{"class":102},[65,184,185],{"class":71}," mesh ",[65,187,160],{"class":102},[65,189,190],{"class":71}," mesh_buffer.",[65,192,193],{"class":109},"polygons",[65,195,196],{"class":71},"();\n",[14,198,199,202,203,206,207,210,211,214,215,218,219,222],{},[32,200,201],{},"tf::read_stl"," returns a ",[32,204,205],{},"tf::polygons_buffer"," that owns the data. ",[32,208,209],{},".polygons()"," is a view on it. ",[32,212,213],{},"tf::read_obj"," works the same way. If your faces and points already live in your own arrays, wrap them with ",[32,216,217],{},"tf::make_polygons(tf::make_faces\u003C3>(faces), tf::make_points\u003C3>(points))",". No copy is made. To follow along without a file, ",[32,220,221],{},"tf::make_sphere_mesh(1.f, 60, 80)"," gives a unit sphere.",[48,224,226],{"id":225},"the-scalar-field","The scalar field",[14,228,229,230,27],{},"A scalar field is one value per point, in the mesh's coordinate type. Most meshes are float, so most fields are float. Any range works; pass it through ",[32,231,232],{},"tf::make_range",[14,234,235],{},"Height is the simplest field:",[56,237,239],{"className":130,"code":238,"language":132,"meta":61,"style":61},"tf::buffer\u003Cfloat> field;\nfield.allocate(mesh.points().size());\ntf::parallel_transform(mesh.points(), field,\n                       [](auto pt) { return pt[2]; });\n",[32,240,241,255,278,294],{"__ignoreMap":61},[65,242,243,246,249,252],{"class":67,"line":68},[65,244,245],{"class":109},"tf",[65,247,248],{"class":71},"::buffer",[65,250,251],{"class":102},"\u003Cfloat>",[65,253,254],{"class":71}," field;\n",[65,256,257,260,263,266,269,272,275],{"class":67,"line":75},[65,258,259],{"class":71},"field.",[65,261,262],{"class":109},"allocate",[65,264,265],{"class":71},"(mesh.",[65,267,268],{"class":109},"points",[65,270,271],{"class":71},"().",[65,273,274],{"class":109},"size",[65,276,277],{"class":71},"());\n",[65,279,280,282,284,287,289,291],{"class":67,"line":81},[65,281,245],{"class":109},[65,283,166],{"class":71},[65,285,286],{"class":109},"parallel_transform",[65,288,265],{"class":71},[65,290,268],{"class":109},[65,292,293],{"class":71},"(), field,\n",[65,295,296,299,301,305,308,311,314,318],{"class":67,"line":87},[65,297,298],{"class":71},"                       [](",[65,300,154],{"class":102},[65,302,304],{"class":303},"s9osk"," pt",[65,306,307],{"class":71},") { ",[65,309,310],{"class":102},"return",[65,312,313],{"class":71}," pt[",[65,315,317],{"class":316},"sDLfK","2",[65,319,320],{"class":71},"]; });\n",[14,322,323],{},[43,324],{"alt":325,"src":326},"The scalar field: height on the Nefertiti bust, scaled to the terrain's range, teal low to orange high","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-1-field.png",[14,328,329,330,332],{},"Any field works. A few that come from ",[32,331,34],{}," itself:",[56,334,336],{"className":130,"code":335,"language":132,"meta":61,"style":61},"\u002F\u002F signed distance to a plane: x = 0\nauto plane = tf::make_plane(tf::make_unit_vector(1.f, 0.f, 0.f),\n                            tf::make_point(0.f, 0.f, 0.f));\ntf::buffer\u003Cfloat> across;\nacross.allocate(mesh.points().size());\ntf::parallel_transform(mesh.points(), across, tf::distance_f(plane));\n\n\u002F\u002F geodesic distance from a point\nauto along = tf::make_geodesic_distances(mesh, std::vector\u003Cint>{0});\n\n\u002F\u002F shape index, from curvature\nauto shape = tf::make_shape_index(mesh);\n",[32,337,338,344,394,425,436,453,478,483,489,527,532,538],{"__ignoreMap":61},[65,339,340],{"class":67,"line":68},[65,341,343],{"class":342},"sAwPA","\u002F\u002F signed distance to a plane: x = 0\n",[65,345,346,348,351,353,355,357,360,362,364,366,369,371,374,377,380,383,385,387,389,391],{"class":67,"line":75},[65,347,154],{"class":102},[65,349,350],{"class":71}," plane ",[65,352,160],{"class":102},[65,354,163],{"class":109},[65,356,166],{"class":71},[65,358,359],{"class":109},"make_plane",[65,361,172],{"class":71},[65,363,245],{"class":109},[65,365,166],{"class":71},[65,367,368],{"class":109},"make_unit_vector",[65,370,172],{"class":71},[65,372,373],{"class":316},"1.",[65,375,376],{"class":102},"f",[65,378,379],{"class":71},", ",[65,381,382],{"class":316},"0.",[65,384,376],{"class":102},[65,386,379],{"class":71},[65,388,382],{"class":316},[65,390,376],{"class":102},[65,392,393],{"class":71},"),\n",[65,395,396,399,401,404,406,408,410,412,414,416,418,420,422],{"class":67,"line":81},[65,397,398],{"class":109},"                            tf",[65,400,166],{"class":71},[65,402,403],{"class":109},"make_point",[65,405,172],{"class":71},[65,407,382],{"class":316},[65,409,376],{"class":102},[65,411,379],{"class":71},[65,413,382],{"class":316},[65,415,376],{"class":102},[65,417,379],{"class":71},[65,419,382],{"class":316},[65,421,376],{"class":102},[65,423,424],{"class":71},"));\n",[65,426,427,429,431,433],{"class":67,"line":87},[65,428,245],{"class":109},[65,430,248],{"class":71},[65,432,251],{"class":102},[65,434,435],{"class":71}," across;\n",[65,437,438,441,443,445,447,449,451],{"class":67,"line":93},[65,439,440],{"class":71},"across.",[65,442,262],{"class":109},[65,444,265],{"class":71},[65,446,268],{"class":109},[65,448,271],{"class":71},[65,450,274],{"class":109},[65,452,277],{"class":71},[65,454,455,457,459,461,463,465,468,470,472,475],{"class":67,"line":99},[65,456,245],{"class":109},[65,458,166],{"class":71},[65,460,286],{"class":109},[65,462,265],{"class":71},[65,464,268],{"class":109},[65,466,467],{"class":71},"(), across, ",[65,469,245],{"class":109},[65,471,166],{"class":71},[65,473,474],{"class":109},"distance_f",[65,476,477],{"class":71},"(plane));\n",[65,479,481],{"class":67,"line":480},7,[65,482,149],{"emptyLinePlaceholder":148},[65,484,486],{"class":67,"line":485},8,[65,487,488],{"class":342},"\u002F\u002F geodesic distance from a point\n",[65,490,492,494,497,499,501,503,506,509,512,515,518,521,524],{"class":67,"line":491},9,[65,493,154],{"class":102},[65,495,496],{"class":71}," along ",[65,498,160],{"class":102},[65,500,163],{"class":109},[65,502,166],{"class":71},[65,504,505],{"class":109},"make_geodesic_distances",[65,507,508],{"class":71},"(mesh, ",[65,510,511],{"class":109},"std",[65,513,514],{"class":71},"::vector",[65,516,517],{"class":102},"\u003Cint>",[65,519,520],{"class":71},"{",[65,522,523],{"class":316},"0",[65,525,526],{"class":71},"});\n",[65,528,530],{"class":67,"line":529},10,[65,531,149],{"emptyLinePlaceholder":148},[65,533,535],{"class":67,"line":534},11,[65,536,537],{"class":342},"\u002F\u002F shape index, from curvature\n",[65,539,541,543,546,548,550,552,555],{"class":67,"line":540},12,[65,542,154],{"class":102},[65,544,545],{"class":71}," shape ",[65,547,160],{"class":102},[65,549,163],{"class":109},[65,551,166],{"class":71},[65,553,554],{"class":109},"make_shape_index",[65,556,557],{"class":71},"(mesh);\n",[48,559,561],{"id":560},"one-level","One level",[56,563,565],{"className":130,"code":564,"language":132,"meta":61,"style":61},"auto contour = tf::make_isocontours(mesh, tf::make_range(field), 0.f);\n",[32,566,567],{"__ignoreMap":61},[65,568,569,571,574,576,578,580,583,585,587,589,592,595,597,599],{"class":67,"line":68},[65,570,154],{"class":102},[65,572,573],{"class":71}," contour ",[65,575,160],{"class":102},[65,577,163],{"class":109},[65,579,166],{"class":71},[65,581,582],{"class":109},"make_isocontours",[65,584,508],{"class":71},[65,586,245],{"class":109},[65,588,166],{"class":71},[65,590,591],{"class":109},"make_range",[65,593,594],{"class":71},"(field), ",[65,596,382],{"class":316},[65,598,376],{"class":102},[65,600,178],{"class":71},[14,602,603],{},[43,604],{"alt":605,"src":606},"One level: the height isocontour at 0.0 on the Nefertiti bust, a pink curve across the face","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-2-one-level.png",[14,608,609,610,613],{},"The result is a ",[32,611,612],{},"tf::curves_buffer",": points on the surface, and one index path per curve.",[56,615,617],{"className":130,"code":616,"language":132,"meta":61,"style":61},"for (auto path : contour.paths()) {\n  bool closed = path.front() == path.back();  \u002F\u002F a closed loop repeats its first id\n  for (auto id : path) {\n    auto pt = contour.points()[id];\n  }\n}\n",[32,618,619,638,671,683,701,706],{"__ignoreMap":61},[65,620,621,624,627,629,632,635],{"class":67,"line":68},[65,622,623],{"class":102},"for",[65,625,626],{"class":71}," (",[65,628,154],{"class":102},[65,630,631],{"class":71}," path : contour.",[65,633,634],{"class":109},"paths",[65,636,637],{"class":71},"()) {\n",[65,639,640,643,646,648,651,654,657,660,662,665,668],{"class":67,"line":75},[65,641,642],{"class":102},"  bool",[65,644,645],{"class":71}," closed ",[65,647,160],{"class":102},[65,649,650],{"class":71}," path.",[65,652,653],{"class":109},"front",[65,655,656],{"class":71},"() ",[65,658,659],{"class":102},"==",[65,661,650],{"class":71},[65,663,664],{"class":109},"back",[65,666,667],{"class":71},"();",[65,669,670],{"class":342},"  \u002F\u002F a closed loop repeats its first id\n",[65,672,673,676,678,680],{"class":67,"line":81},[65,674,675],{"class":102},"  for",[65,677,626],{"class":71},[65,679,154],{"class":102},[65,681,682],{"class":71}," id : path) {\n",[65,684,685,688,691,693,696,698],{"class":67,"line":87},[65,686,687],{"class":102},"    auto",[65,689,690],{"class":71}," pt ",[65,692,160],{"class":102},[65,694,695],{"class":71}," contour.",[65,697,268],{"class":109},[65,699,700],{"class":71},"()[id];\n",[65,702,703],{"class":67,"line":93},[65,704,705],{"class":71},"  }\n",[65,707,708],{"class":67,"line":99},[65,709,710],{"class":71},"}\n",[14,712,713],{},"Or walk the points of each curve directly:",[56,715,717],{"className":130,"code":716,"language":132,"meta":61,"style":61},"for (auto curve : contour.curves()) {\n  for (auto pt : curve) {\n    \u002F\u002F points along one curve\n  }\n}\n",[32,718,719,735,746,751,755],{"__ignoreMap":61},[65,720,721,723,725,727,730,733],{"class":67,"line":68},[65,722,623],{"class":102},[65,724,626],{"class":71},[65,726,154],{"class":102},[65,728,729],{"class":71}," curve : contour.",[65,731,732],{"class":109},"curves",[65,734,637],{"class":71},[65,736,737,739,741,743],{"class":67,"line":75},[65,738,675],{"class":102},[65,740,626],{"class":71},[65,742,154],{"class":102},[65,744,745],{"class":71}," pt : curve) {\n",[65,747,748],{"class":67,"line":81},[65,749,750],{"class":342},"    \u002F\u002F points along one curve\n",[65,752,753],{"class":67,"line":87},[65,754,705],{"class":71},[65,756,757],{"class":67,"line":93},[65,758,710],{"class":71},[14,760,761,762,22,765,27],{},"A curve that runs off the edge of an open mesh ends at the boundary. A curve on a closed region closes on itself. The flat arrays are there when you need to hand them on: ",[32,763,764],{},"contour.paths_buffer().data_buffer().data()",[32,766,767],{},"contour.points_buffer().data_buffer().data()",[48,769,771],{"id":770},"many-levels","Many levels",[14,773,774],{},"Pass a range of levels. One call extracts them all.",[56,776,778],{"className":130,"code":777,"language":132,"meta":61,"style":61},"std::vector\u003Cfloat> levels{-0.75f, -0.5f, -0.25f, 0.f, 0.25f, 0.5f, 0.75f};\nauto contours = tf::make_isocontours(mesh, tf::make_range(field),\n                                     tf::make_range(levels));\n",[32,779,780,844,870],{"__ignoreMap":61},[65,781,782,784,786,788,791,794,797,799,801,803,806,808,810,812,815,817,819,821,823,825,827,829,831,833,835,837,839,841],{"class":67,"line":68},[65,783,511],{"class":109},[65,785,514],{"class":71},[65,787,251],{"class":102},[65,789,790],{"class":71}," levels{",[65,792,793],{"class":102},"-",[65,795,796],{"class":316},"0.75",[65,798,376],{"class":102},[65,800,379],{"class":71},[65,802,793],{"class":102},[65,804,805],{"class":316},"0.5",[65,807,376],{"class":102},[65,809,379],{"class":71},[65,811,793],{"class":102},[65,813,814],{"class":316},"0.25",[65,816,376],{"class":102},[65,818,379],{"class":71},[65,820,382],{"class":316},[65,822,376],{"class":102},[65,824,379],{"class":71},[65,826,814],{"class":316},[65,828,376],{"class":102},[65,830,379],{"class":71},[65,832,805],{"class":316},[65,834,376],{"class":102},[65,836,379],{"class":71},[65,838,796],{"class":316},[65,840,376],{"class":102},[65,842,843],{"class":71},"};\n",[65,845,846,848,851,853,855,857,859,861,863,865,867],{"class":67,"line":75},[65,847,154],{"class":102},[65,849,850],{"class":71}," contours ",[65,852,160],{"class":102},[65,854,163],{"class":109},[65,856,166],{"class":71},[65,858,582],{"class":109},[65,860,508],{"class":71},[65,862,245],{"class":109},[65,864,166],{"class":71},[65,866,591],{"class":109},[65,868,869],{"class":71},"(field),\n",[65,871,872,875,877,879],{"class":67,"line":81},[65,873,874],{"class":109},"                                     tf",[65,876,166],{"class":71},[65,878,591],{"class":109},[65,880,881],{"class":71},"(levels));\n",[14,883,884],{},[43,885],{"alt":886,"src":887},"Many levels: seven height isocontours from -0.15 to 0.15 on the Nefertiti bust, extracted in one call","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-3-many-levels.png",[14,889,890],{},"The result does not say which level each curve belongs to, and the curves are not in level order. When you need that, call once per level:",[56,892,894],{"className":130,"code":893,"language":132,"meta":61,"style":61},"std::vector\u003Ctf::curves_buffer\u003Cint, float, 3>> by_level;\nfor (float t : levels)\n  by_level.push_back(tf::make_isocontours(mesh, tf::make_range(field), t));\n",[32,895,896,929,940],{"__ignoreMap":61},[65,897,898,900,902,905,907,910,913,915,918,920,923,926],{"class":67,"line":68},[65,899,511],{"class":109},[65,901,514],{"class":71},[65,903,904],{"class":102},"\u003C",[65,906,245],{"class":109},[65,908,909],{"class":71},"::curves_buffer",[65,911,912],{"class":102},"\u003Cint",[65,914,379],{"class":71},[65,916,917],{"class":102},"float",[65,919,379],{"class":71},[65,921,922],{"class":316},"3",[65,924,925],{"class":102},">>",[65,927,928],{"class":71}," by_level;\n",[65,930,931,933,935,937],{"class":67,"line":75},[65,932,623],{"class":102},[65,934,626],{"class":71},[65,936,917],{"class":102},[65,938,939],{"class":71}," t : levels)\n",[65,941,942,945,948,950,952,954,956,958,960,962,964],{"class":67,"line":81},[65,943,944],{"class":71},"  by_level.",[65,946,947],{"class":109},"push_back",[65,949,172],{"class":71},[65,951,245],{"class":109},[65,953,166],{"class":71},[65,955,582],{"class":109},[65,957,508],{"class":71},[65,959,245],{"class":109},[65,961,166],{"class":71},[65,963,591],{"class":109},[65,965,966],{"class":71},"(field), t));\n",[14,968,969],{},"For height, you can also read the level from the curve itself: every point on a height contour has the level as its z.",[48,971,973],{"id":972},"isobands","Isobands",[14,975,976,977,980],{},"Isobands are the regions between levels. ",[32,978,979],{},"tf::embedded_isocurves"," cuts the mesh along the isocontours, so the curves become edges, and labels each face by band.",[56,982,984],{"className":130,"code":983,"language":132,"meta":61,"style":61},"std::array\u003Cfloat, 3> cuts{-0.5f, 0.f, 0.5f};\nauto [bands, labels, face_labels] = tf::embedded_isocurves(\n    mesh, tf::make_range(field), tf::make_range(cuts));\n",[32,985,986,1026,1045],{"__ignoreMap":61},[65,987,988,990,993,996,998,1000,1003,1006,1008,1010,1012,1014,1016,1018,1020,1022,1024],{"class":67,"line":68},[65,989,511],{"class":109},[65,991,992],{"class":71},"::array",[65,994,995],{"class":102},"\u003Cfloat",[65,997,379],{"class":71},[65,999,922],{"class":316},[65,1001,1002],{"class":102},">",[65,1004,1005],{"class":71}," cuts{",[65,1007,793],{"class":102},[65,1009,805],{"class":316},[65,1011,376],{"class":102},[65,1013,379],{"class":71},[65,1015,382],{"class":316},[65,1017,376],{"class":102},[65,1019,379],{"class":71},[65,1021,805],{"class":316},[65,1023,376],{"class":102},[65,1025,843],{"class":71},[65,1027,1028,1030,1033,1035,1037,1039,1042],{"class":67,"line":75},[65,1029,154],{"class":102},[65,1031,1032],{"class":71}," [bands, labels, face_labels] ",[65,1034,160],{"class":102},[65,1036,163],{"class":109},[65,1038,166],{"class":71},[65,1040,1041],{"class":109},"embedded_isocurves",[65,1043,1044],{"class":71},"(\n",[65,1046,1047,1050,1052,1054,1056,1058,1060,1062,1064],{"class":67,"line":81},[65,1048,1049],{"class":71},"    mesh, ",[65,1051,245],{"class":109},[65,1053,166],{"class":71},[65,1055,591],{"class":109},[65,1057,594],{"class":71},[65,1059,245],{"class":109},[65,1061,166],{"class":71},[65,1063,591],{"class":109},[65,1065,1066],{"class":71},"(cuts));\n",[14,1068,1069],{},[43,1070],{"alt":1071,"src":1072},"Isobands: the Nefertiti bust cut at heights -0.1, 0.0 and 0.1 into four bands, each in its own color","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-4-isobands.png",[14,1074,1075,1076,1079,1080,1083],{},"Three cuts give four bands. Band 0 is below -0.5, band 3 is above 0.5. ",[32,1077,1078],{},"labels"," gives the band of each output face. ",[32,1081,1082],{},"face_labels"," gives the original face it came from.",[14,1085,1086,1087,1090],{},"Keep only some bands with ",[32,1088,1089],{},"tf::make_isobands",":",[56,1092,1094],{"className":130,"code":1093,"language":132,"meta":61,"style":61},"std::array\u003Cint, 2> selected{0, 3};\nauto [kept, kept_labels, kept_face_labels] = tf::make_isobands(\n    mesh, tf::make_range(field), tf::make_range(cuts),\n    tf::make_range(selected));\n",[32,1095,1096,1121,1139,1160],{"__ignoreMap":61},[65,1097,1098,1100,1102,1104,1106,1108,1110,1113,1115,1117,1119],{"class":67,"line":68},[65,1099,511],{"class":109},[65,1101,992],{"class":71},[65,1103,912],{"class":102},[65,1105,379],{"class":71},[65,1107,317],{"class":316},[65,1109,1002],{"class":102},[65,1111,1112],{"class":71}," selected{",[65,1114,523],{"class":316},[65,1116,379],{"class":71},[65,1118,922],{"class":316},[65,1120,843],{"class":71},[65,1122,1123,1125,1128,1130,1132,1134,1137],{"class":67,"line":75},[65,1124,154],{"class":102},[65,1126,1127],{"class":71}," [kept, kept_labels, kept_face_labels] ",[65,1129,160],{"class":102},[65,1131,163],{"class":109},[65,1133,166],{"class":71},[65,1135,1136],{"class":109},"make_isobands",[65,1138,1044],{"class":71},[65,1140,1141,1143,1145,1147,1149,1151,1153,1155,1157],{"class":67,"line":81},[65,1142,1049],{"class":71},[65,1144,245],{"class":109},[65,1146,166],{"class":71},[65,1148,591],{"class":109},[65,1150,594],{"class":71},[65,1152,245],{"class":109},[65,1154,166],{"class":71},[65,1156,591],{"class":109},[65,1158,1159],{"class":71},"(cuts),\n",[65,1161,1162,1165,1167,1169],{"class":67,"line":87},[65,1163,1164],{"class":109},"    tf",[65,1166,166],{"class":71},[65,1168,591],{"class":109},[65,1170,1171],{"class":71},"(selected));\n",[14,1173,1174],{},"Get the boundary curves in the same call:",[56,1176,1178],{"className":130,"code":1177,"language":132,"meta":61,"style":61},"auto [cut_mesh, cut_labels, cut_face_labels, curves] = tf::embedded_isocurves(\n    mesh, tf::make_range(field), tf::make_range(cuts), tf::return_curves);\n",[32,1179,1180,1197],{"__ignoreMap":61},[65,1181,1182,1184,1187,1189,1191,1193,1195],{"class":67,"line":68},[65,1183,154],{"class":102},[65,1185,1186],{"class":71}," [cut_mesh, cut_labels, cut_face_labels, curves] ",[65,1188,160],{"class":102},[65,1190,163],{"class":109},[65,1192,166],{"class":71},[65,1194,1041],{"class":109},[65,1196,1044],{"class":71},[65,1198,1199,1201,1203,1205,1207,1209,1211,1213,1215,1218,1220],{"class":67,"line":75},[65,1200,1049],{"class":71},[65,1202,245],{"class":109},[65,1204,166],{"class":71},[65,1206,591],{"class":109},[65,1208,594],{"class":71},[65,1210,245],{"class":109},[65,1212,166],{"class":71},[65,1214,591],{"class":109},[65,1216,1217],{"class":71},"(cuts), ",[65,1219,245],{"class":109},[65,1221,1222],{"class":71},"::return_curves);\n",[48,1224,1226],{"id":1225},"working-with-results","Working with results",[14,1228,1229],{},"Split the bands into separate meshes:",[56,1231,1233],{"className":130,"code":1232,"language":132,"meta":61,"style":61},"auto [parts, part_labels] = tf::split_into_components(bands.polygons(), labels);\n\u002F\u002F parts[i] is a mesh for band part_labels[i]\n",[32,1234,1235,1259],{"__ignoreMap":61},[65,1236,1237,1239,1242,1244,1246,1248,1251,1254,1256],{"class":67,"line":68},[65,1238,154],{"class":102},[65,1240,1241],{"class":71}," [parts, part_labels] ",[65,1243,160],{"class":102},[65,1245,163],{"class":109},[65,1247,166],{"class":71},[65,1249,1250],{"class":109},"split_into_components",[65,1252,1253],{"class":71},"(bands.",[65,1255,193],{"class":109},[65,1257,1258],{"class":71},"(), labels);\n",[65,1260,1261],{"class":67,"line":75},[65,1262,1263],{"class":342},"\u002F\u002F parts[i] is a mesh for band part_labels[i]\n",[14,1265,1266,1267,1090],{},"Carry per-face data from the input to the output with ",[32,1268,1082],{},[56,1270,1272],{"className":130,"code":1271,"language":132,"meta":61,"style":61},"std::vector\u003Cint> material(mesh.faces().size(), 0);   \u002F\u002F your per-face data\nstd::vector\u003Cint> band_material(face_labels.size());\nfor (std::size_t i = 0; i \u003C face_labels.size(); ++i)\n  band_material[i] = material[face_labels[i]];\n",[32,1273,1274,1319,1346,1386],{"__ignoreMap":61},[65,1275,1276,1278,1280,1283,1285,1288,1291,1294,1296,1299,1301,1304,1306,1308,1311,1313,1316],{"class":67,"line":68},[65,1277,511],{"class":109},[65,1279,166],{"class":71},[65,1281,1282],{"class":109},"vector",[65,1284,904],{"class":71},[65,1286,1287],{"class":102},"int",[65,1289,1290],{"class":71},"> ",[65,1292,1293],{"class":109},"material",[65,1295,172],{"class":71},[65,1297,1298],{"class":109},"mesh",[65,1300,27],{"class":71},[65,1302,1303],{"class":109},"faces",[65,1305,271],{"class":71},[65,1307,274],{"class":109},[65,1309,1310],{"class":71},"(), ",[65,1312,523],{"class":316},[65,1314,1315],{"class":71},");",[65,1317,1318],{"class":342},"   \u002F\u002F your per-face data\n",[65,1320,1321,1323,1325,1327,1329,1331,1333,1336,1338,1340,1342,1344],{"class":67,"line":75},[65,1322,511],{"class":109},[65,1324,166],{"class":71},[65,1326,1282],{"class":109},[65,1328,904],{"class":71},[65,1330,1287],{"class":102},[65,1332,1290],{"class":71},[65,1334,1335],{"class":109},"band_material",[65,1337,172],{"class":71},[65,1339,1082],{"class":109},[65,1341,27],{"class":71},[65,1343,274],{"class":109},[65,1345,277],{"class":71},[65,1347,1348,1350,1352,1354,1356,1359,1362,1364,1367,1370,1372,1375,1377,1380,1383],{"class":67,"line":81},[65,1349,623],{"class":102},[65,1351,626],{"class":71},[65,1353,511],{"class":109},[65,1355,166],{"class":71},[65,1357,1358],{"class":102},"size_t",[65,1360,1361],{"class":71}," i ",[65,1363,160],{"class":102},[65,1365,1366],{"class":316}," 0",[65,1368,1369],{"class":71},"; i ",[65,1371,904],{"class":102},[65,1373,1374],{"class":71}," face_labels.",[65,1376,274],{"class":109},[65,1378,1379],{"class":71},"(); ",[65,1381,1382],{"class":102},"++",[65,1384,1385],{"class":71},"i)\n",[65,1387,1388,1391,1393],{"class":67,"line":87},[65,1389,1390],{"class":71},"  band_material[i] ",[65,1392,160],{"class":102},[65,1394,1395],{"class":71}," material[face_labels[i]];\n",[14,1397,1398],{},"Write to file:",[56,1400,1402],{"className":130,"code":1401,"language":132,"meta":61,"style":61},"tf::write_obj(bands.polygons(), \"bands.obj\");\nfor (std::size_t i = 0; i \u003C parts.size(); ++i)\n  tf::write_stl(parts[i].polygons(),\n                \"band_\" + std::to_string(part_labels[i]) + \".stl\");\n",[32,1403,1404,1424,1457,1475],{"__ignoreMap":61},[65,1405,1406,1408,1410,1413,1415,1417,1419,1422],{"class":67,"line":68},[65,1407,245],{"class":109},[65,1409,166],{"class":71},[65,1411,1412],{"class":109},"write_obj",[65,1414,1253],{"class":71},[65,1416,193],{"class":109},[65,1418,1310],{"class":71},[65,1420,1421],{"class":142},"\"bands.obj\"",[65,1423,178],{"class":71},[65,1425,1426,1428,1430,1432,1434,1436,1438,1440,1442,1444,1446,1449,1451,1453,1455],{"class":67,"line":75},[65,1427,623],{"class":102},[65,1429,626],{"class":71},[65,1431,511],{"class":109},[65,1433,166],{"class":71},[65,1435,1358],{"class":102},[65,1437,1361],{"class":71},[65,1439,160],{"class":102},[65,1441,1366],{"class":316},[65,1443,1369],{"class":71},[65,1445,904],{"class":102},[65,1447,1448],{"class":71}," parts.",[65,1450,274],{"class":109},[65,1452,1379],{"class":71},[65,1454,1382],{"class":102},[65,1456,1385],{"class":71},[65,1458,1459,1462,1464,1467,1470,1472],{"class":67,"line":81},[65,1460,1461],{"class":109},"  tf",[65,1463,166],{"class":71},[65,1465,1466],{"class":109},"write_stl",[65,1468,1469],{"class":71},"(parts[i].",[65,1471,193],{"class":109},[65,1473,1474],{"class":71},"(),\n",[65,1476,1477,1480,1483,1486,1488,1491,1494,1497,1500],{"class":67,"line":87},[65,1478,1479],{"class":142},"                \"band_\"",[65,1481,1482],{"class":102}," +",[65,1484,1485],{"class":109}," std",[65,1487,166],{"class":71},[65,1489,1490],{"class":109},"to_string",[65,1492,1493],{"class":71},"(part_labels[i]) ",[65,1495,1496],{"class":102},"+",[65,1498,1499],{"class":142}," \".stl\"",[65,1501,178],{"class":71},[14,1503,1504,1505,1509,1510,1514,1515,1519,1520,27],{},"Contours of height are slices. See ",[18,1506,1508],{"href":1507},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-cpp","mesh slicing and cross-sections"," for layers and sections. Contours of ",[18,1511,1513],{"href":1512},"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp","geodesic distance"," are rings. Contours of the ",[18,1516,1518],{"href":1517},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp","shape index"," separate cups, saddles, and caps. To cut one mesh by another mesh instead of by a field, use ",[18,1521,1523],{"href":1522},"\u002Ftutorials\u002Ffast-mesh-booleans-in-cpp","booleans",[48,1525,1527],{"id":1526},"related-tutorials","Related tutorials",[1529,1530,1531,1538,1544,1550],"ul",{},[1532,1533,1534,1537],"li",{},[18,1535,1536],{"href":1512},"Fast and Accurate Geodesic Distances in C++"," — distance along the surface from one seed or many.",[1532,1539,1540,1543],{},[18,1541,1542],{"href":1507},"Fast and Robust Mesh Slicing and Cross-Sections in C++"," — plane sections, layers and slabs of a mesh.",[1532,1545,1546,1549],{},[18,1547,1548],{"href":1517},"Fast and Robust Principal Curvatures of a Mesh in C++"," — curvature values and directions at every vertex.",[1532,1551,1552,1556],{},[18,1553,1555],{"href":1554},"\u002Ftutorials\u002Fmesh-intersection-curves-in-cpp","Fast and Exact Mesh Intersection Curves in C++"," — exact curves where two or more meshes cross.",[48,1558,1560],{"id":1559},"performance","Performance",[14,1562,1563],{},"We measured on the Stanford dragon at several sizes, against VTK and libigl.",[1565,1566],"headline-numbers",{":items":1567},"[{\"value\":\"5.9ms\",\"label\":\"Isocontours\",\"detail\":\"1,025,766 polygons (VTK: 236.2 ms)\"},{\"value\":\"11.6ms\",\"label\":\"Many levels\",\"detail\":\"480,868 polygons (VTK: 336.6 ms)\"},{\"value\":\"18.2ms\",\"label\":\"Embedded isobands\",\"detail\":\"1,025,766 polygons (VTK: 740.6 ms)\"}]",[14,1569,1570,1575,1576],{},[65,1571,1574],{"className":1572},[1573],"tag-pill","Apple M4 Max"," ",[65,1577,1579],{"className":1578},[1573],"16 threads",[1581,1582],"data-table",{":headers":1583,":highlight":317,":rows":1584},"[\"Operation\",\"Mesh\",\"trueform\",\"VTK\",\"libigl\"]","[[\"Isocontours\",\"1,025,766 polygons\",\"5.9 ms\",\"236.2 ms (40×)\",\"736.9 ms (124×)\"],[\"Isocontours, many levels\",\"480,868 polygons\",\"11.6 ms\",\"336.6 ms (29×)\",\"963.2 ms (83×)\"],[\"Embedded isobands\",\"1,025,766 polygons\",\"18.2 ms\",\"740.6 ms (41×)\",\"no equivalent\"],[\"Embedded isocurves\",\"1,025,766 polygons\",\"20.6 ms\",\"736.3 ms (36×)\",\"no equivalent\"]]",[14,1586,1587],{},[43,1588],{"alt":1589,"src":1590},"Benchmark chart: trueform against other C++ libraries, isocontours","\u002Fimg\u002Falgorithms\u002Fbenchmark-isocontours-cpp-2026.png",[14,1592,1593],{},[43,1594],{"alt":1595,"src":1596},"Benchmark chart: trueform against other C++ libraries, embedded isocontours","\u002Fimg\u002Falgorithms\u002Fbenchmark-embedded-isocontours-cpp-2026.png",[14,1598,1599,1603,1604],{},[18,1600,1602],{"href":1601},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1605,1608],{"href":1606,"rel":1607},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fiso",[121],"Documentation",[1610,1611],"cite-as",{"author":1612,"title":5},"Sajovic, {\\v{Z}}iga",[1614,1615,1616],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":75,"depth":75,"links":1618},[1619,1620,1621,1622,1623,1624,1625,1626,1627],{"id":50,"depth":75,"text":51},{"id":126,"depth":75,"text":127},{"id":225,"depth":75,"text":226},{"id":560,"depth":75,"text":561},{"id":770,"depth":75,"text":771},{"id":972,"depth":75,"text":973},{"id":1225,"depth":75,"text":1226},{"id":1526,"depth":75,"text":1527},{"id":1559,"depth":75,"text":1560},"2026-10-31","Extract isocontours (isolines) of any scalar field on a triangle mesh in C++. One level or many at once, plus isobands that cut the mesh along the curves. Header-only, C++17. 5.9 ms on 1M polygons, 40× faster than VTK.","md",[1632,1633,1634,1635,1636,1637,1638,1639,1640,1641],"isocontours mesh C++","isolines triangle mesh C++","contour lines on mesh C++","scalar field contours mesh C++","isobands C++","VTK vtkContourFilter alternative","VTK vtkBandedPolyDataContourFilter alternative","libigl isolines alternative","level set curves mesh C++","mesh contouring library C++",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp.png","\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp",{"title":5,"description":1629},"tutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp",[1648,1649,1650],"c++","isocontours","tutorial","OhzczzYKtTbcMotEZHpDHf3OGusFIrvPXMtJCFF1u3Q",1793517811885]