[{"data":1,"prerenderedAt":1781},["ShallowReactive",2],{"tutorial-principal-curvatures-of-a-mesh-in-cpp":3},{"id":4,"title":5,"author":6,"body":7,"date":1756,"description":1757,"extension":1758,"keywords":1759,"meta":1771,"navigation":156,"ogImage":1772,"path":1773,"published":156,"seo":1774,"stem":1775,"tags":1776,"__hash__":1780},"tutorials\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp.md","Fast and Robust Principal Curvatures of a Mesh in C++","Žiga Sajovic",{"type":8,"value":9,"toc":1745},"minimark",[10,28,44,48,55,60,63,122,132,136,205,227,230,288,292,322,328,331,359,362,486,489,496,500,503,531,537,552,753,756,760,767,819,825,887,890,1055,1058,1062,1070,1132,1138,1145,1148,1381,1385,1392,1400,1443,1446,1536,1539,1646,1654,1658,1688,1692,1698,1702,1714,1719,1725,1737,1741],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript","JavaScript",".",[14,29,30,31,35,36,39,40,43],{},"Principal curvatures tell you how a surface bends at each point. ",[32,33,34],"code",{},"k0"," is the largest bend, ",[32,37,38],{},"k1"," the smallest. From them you get Gaussian curvature, mean curvature, and the shape index. ",[32,41,42],{},"trueform"," computes all of it per vertex. It is header-only C++17. This tutorial covers principal curvatures, principal directions, the shape index, and how to read the signs.",[45,46],"article-cta",{":buttons":47},"[{\"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\u002Fgeometry#curvature-analysis\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fshape-histogram\",\"icon\":\"i-lucide-play\"}]",[14,49,50],{},[51,52],"img",{"alt":53,"src":54},"Mean curvature on the Stanford dragon: creases orange, flat areas teal","\u002Fimg\u002Ftutorials\u002Fprincipal-curvatures.png",[56,57,59],"h2",{"id":58},"install","Install",[14,61,62],{},"Header-only. It needs C++17 and oneTBB. Add it to your project via CMake FetchContent:",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"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,71,72,81,87,93,99,105],{"__ignoreMap":69},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"s95oV","FetchContent_Declare(\n",[73,82,84],{"class":75,"line":83},2,[73,85,86],{"class":79},"  trueform\n",[73,88,90],{"class":75,"line":89},3,[73,91,92],{"class":79},"  GIT_REPOSITORY https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\n",[73,94,96],{"class":75,"line":95},4,[73,97,98],{"class":79},")\n",[73,100,102],{"class":75,"line":101},5,[73,103,104],{"class":79},"FetchContent_MakeAvailable(trueform)\n",[73,106,108,112,115,119],{"class":75,"line":107},6,[73,109,111],{"class":110},"snl16","target_link_libraries",[73,113,114],{"class":79},"(my_app ",[73,116,118],{"class":117},"svObZ","PRIVATE",[73,120,121],{"class":79}," tf::trueform)\n",[14,123,124,125,131],{},"Also available via conan, nuget, and pip. See the ",[18,126,130],{"href":127,"rel":128},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fgetting-started\u002Finstallation",[129],"nofollow","installation guide"," for all options.",[56,133,135],{"id":134},"loading-the-mesh","Loading the mesh",[64,137,141],{"className":138,"code":139,"language":140,"meta":69,"style":69},"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,142,143,152,158,187],{"__ignoreMap":69},[73,144,145,148],{"class":75,"line":76},[73,146,147],{"class":110},"#include",[73,149,151],{"class":150},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[73,153,154],{"class":75,"line":83},[73,155,157],{"emptyLinePlaceholder":156},true,"\n",[73,159,160,163,166,169,172,175,178,181,184],{"class":75,"line":89},[73,161,162],{"class":110},"auto",[73,164,165],{"class":79}," mesh_buffer ",[73,167,168],{"class":110},"=",[73,170,171],{"class":117}," tf",[73,173,174],{"class":79},"::",[73,176,177],{"class":117},"read_stl",[73,179,180],{"class":79},"(",[73,182,183],{"class":150},"\"part.stl\"",[73,185,186],{"class":79},");\n",[73,188,189,191,194,196,199,202],{"class":75,"line":95},[73,190,162],{"class":110},[73,192,193],{"class":79}," mesh ",[73,195,168],{"class":110},[73,197,198],{"class":79}," mesh_buffer.",[73,200,201],{"class":117},"polygons",[73,203,204],{"class":79},"();\n",[14,206,207,210,211,214,215,218,219,222,223,226],{},[32,208,209],{},"tf::read_stl"," returns a ",[32,212,213],{},"tf::polygons_buffer"," that owns the data. ",[32,216,217],{},".polygons()"," is a view on it. ",[32,220,221],{},"tf::read_obj"," works the same way. If your faces and points already live in your own arrays, wrap them with ",[32,224,225],{},"tf::make_polygons(tf::make_faces\u003C3>(faces), tf::make_points\u003C3>(points))",". No copy is made.",[14,228,229],{},"To follow along without a file, make a sphere of radius 2:",[64,231,233],{"className":138,"code":232,"language":140,"meta":69,"style":69},"auto sphere_buffer = tf::make_sphere_mesh(2.f, 100, 200);\nauto mesh = sphere_buffer.polygons();\n",[32,234,235,273],{"__ignoreMap":69},[73,236,237,239,242,244,246,248,251,253,257,260,263,266,268,271],{"class":75,"line":76},[73,238,162],{"class":110},[73,240,241],{"class":79}," sphere_buffer ",[73,243,168],{"class":110},[73,245,171],{"class":117},[73,247,174],{"class":79},[73,249,250],{"class":117},"make_sphere_mesh",[73,252,180],{"class":79},[73,254,256],{"class":255},"sDLfK","2.",[73,258,259],{"class":110},"f",[73,261,262],{"class":79},", ",[73,264,265],{"class":255},"100",[73,267,262],{"class":79},[73,269,270],{"class":255},"200",[73,272,186],{"class":79},[73,274,275,277,279,281,284,286],{"class":75,"line":83},[73,276,162],{"class":110},[73,278,193],{"class":79},[73,280,168],{"class":110},[73,282,283],{"class":79}," sphere_buffer.",[73,285,201],{"class":117},[73,287,204],{"class":79},[56,289,291],{"id":290},"principal-curvatures","Principal curvatures",[64,293,295],{"className":138,"code":294,"language":140,"meta":69,"style":69},"auto [k0, k1] = tf::make_principal_curvatures(mesh);\n\u002F\u002F k0, k1: tf::buffer\u003Cfloat>, one value per point; k0 >= k1\n",[32,296,297,316],{"__ignoreMap":69},[73,298,299,301,304,306,308,310,313],{"class":75,"line":76},[73,300,162],{"class":110},[73,302,303],{"class":79}," [k0, k1] ",[73,305,168],{"class":110},[73,307,171],{"class":117},[73,309,174],{"class":79},[73,311,312],{"class":117},"make_principal_curvatures",[73,314,315],{"class":79},"(mesh);\n",[73,317,318],{"class":75,"line":83},[73,319,321],{"class":320},"sAwPA","\u002F\u002F k0, k1: tf::buffer\u003Cfloat>, one value per point; k0 >= k1\n",[14,323,324],{},[51,325],{"alt":326,"src":327},"Principal curvatures on a carved pot from Thingi10K: Gaussian curvature k0 times k1, saddles in orange, domes and dents in teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-1-gaussian.png",[14,329,330],{},"On the sphere both are close to 0.5, one over the radius. Each vertex fits a quadric to its neighborhood. The neighborhood is the k-ring, two rings by default. A larger ring smooths out noise:",[64,332,334],{"className":138,"code":333,"language":140,"meta":69,"style":69},"auto [s0, s1] = tf::make_principal_curvatures(mesh, 3);\n",[32,335,336],{"__ignoreMap":69},[73,337,338,340,343,345,347,349,351,354,357],{"class":75,"line":76},[73,339,162],{"class":110},[73,341,342],{"class":79}," [s0, s1] ",[73,344,168],{"class":110},[73,346,171],{"class":117},[73,348,174],{"class":79},[73,350,312],{"class":117},[73,352,353],{"class":79},"(mesh, ",[73,355,356],{"class":255},"3",[73,358,186],{"class":79},[14,360,361],{},"Gaussian and mean curvature follow directly:",[64,363,365],{"className":138,"code":364,"language":140,"meta":69,"style":69},"tf::buffer\u003Cfloat> gaussian, mean;\ngaussian.allocate(k0.size());\nmean.allocate(k0.size());\n\nfor (auto [a, b, g, m] : tf::zip(k0, k1, gaussian, mean)) {\n  g = a * b;\n  m = (a + b) \u002F 2;\n}\n",[32,366,367,381,398,411,415,438,454,480],{"__ignoreMap":69},[73,368,369,372,375,378],{"class":75,"line":76},[73,370,371],{"class":117},"tf",[73,373,374],{"class":79},"::buffer",[73,376,377],{"class":110},"\u003Cfloat>",[73,379,380],{"class":79}," gaussian, mean;\n",[73,382,383,386,389,392,395],{"class":75,"line":83},[73,384,385],{"class":79},"gaussian.",[73,387,388],{"class":117},"allocate",[73,390,391],{"class":79},"(k0.",[73,393,394],{"class":117},"size",[73,396,397],{"class":79},"());\n",[73,399,400,403,405,407,409],{"class":75,"line":89},[73,401,402],{"class":79},"mean.",[73,404,388],{"class":117},[73,406,391],{"class":79},[73,408,394],{"class":117},[73,410,397],{"class":79},[73,412,413],{"class":75,"line":95},[73,414,157],{"emptyLinePlaceholder":156},[73,416,417,420,423,425,428,430,432,435],{"class":75,"line":101},[73,418,419],{"class":110},"for",[73,421,422],{"class":79}," (",[73,424,162],{"class":110},[73,426,427],{"class":79}," [a, b, g, m] : ",[73,429,371],{"class":117},[73,431,174],{"class":79},[73,433,434],{"class":117},"zip",[73,436,437],{"class":79},"(k0, k1, gaussian, mean)) {\n",[73,439,440,443,445,448,451],{"class":75,"line":107},[73,441,442],{"class":79},"  g ",[73,444,168],{"class":110},[73,446,447],{"class":79}," a ",[73,449,450],{"class":110},"*",[73,452,453],{"class":79}," b;\n",[73,455,457,460,462,465,468,471,474,477],{"class":75,"line":456},7,[73,458,459],{"class":79},"  m ",[73,461,168],{"class":110},[73,463,464],{"class":79}," (a ",[73,466,467],{"class":110},"+",[73,469,470],{"class":79}," b) ",[73,472,473],{"class":110},"\u002F",[73,475,476],{"class":255}," 2",[73,478,479],{"class":79},";\n",[73,481,483],{"class":75,"line":482},8,[73,484,485],{"class":79},"}\n",[14,487,488],{},"On the sphere, Gaussian curvature is close to 0.25 and mean curvature close to 0.5.",[14,490,491,492,495],{},"For repeated runs into memory you already own, ",[32,493,494],{},"tf::compute_principal_curvatures(mesh, k0, k1)"," writes into pre-allocated buffers.",[56,497,499],{"id":498},"principal-directions","Principal directions",[14,501,502],{},"Ask for the directions too:",[64,504,506],{"className":138,"code":505,"language":140,"meta":69,"style":69},"auto [c0, c1, d0, d1] = tf::make_principal_directions(mesh);\n\u002F\u002F d0, d1: tf::unit_vectors_buffer\u003Cfloat, 3>, one direction per point\n",[32,507,508,526],{"__ignoreMap":69},[73,509,510,512,515,517,519,521,524],{"class":75,"line":76},[73,511,162],{"class":110},[73,513,514],{"class":79}," [c0, c1, d0, d1] ",[73,516,168],{"class":110},[73,518,171],{"class":117},[73,520,174],{"class":79},[73,522,523],{"class":117},"make_principal_directions",[73,525,315],{"class":79},[73,527,528],{"class":75,"line":83},[73,529,530],{"class":320},"\u002F\u002F d0, d1: tf::unit_vectors_buffer\u003Cfloat, 3>, one direction per point\n",[14,532,533],{},[51,534],{"alt":535,"src":536},"Principal directions on the carved pot's relief: the direction of k0 in pink and of k1 in orange at each vertex, following the ridges and grooves","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-2-directions.png",[14,538,539,542,543,262,545,548,549,551],{},[32,540,541],{},"d0"," is the direction of ",[32,544,34],{},[32,546,547],{},"d1"," the direction of ",[32,550,38],{},". Both are unit vectors, perpendicular to each other and to the vertex normal. You can check:",[64,553,555],{"className":138,"code":554,"language":140,"meta":69,"style":69},"auto normals = tf::compute_normals(mesh);\nauto fm = tf::make_face_membership(mesh);\nauto point_normals = tf::compute_point_normals(\n    mesh | tf::tag(fm) | tf::tag_normals(normals.unit_vectors()));\n\nfloat worst = 0.f;   \u002F\u002F close to 0\nfor (auto [u, v, n] : tf::zip(d0, d1, point_normals.unit_vectors()))\n  worst = std::max({worst, std::abs(tf::dot(u, v)), std::abs(tf::dot(u, n))});\n",[32,556,557,575,593,612,648,652,673,698],{"__ignoreMap":69},[73,558,559,561,564,566,568,570,573],{"class":75,"line":76},[73,560,162],{"class":110},[73,562,563],{"class":79}," normals ",[73,565,168],{"class":110},[73,567,171],{"class":117},[73,569,174],{"class":79},[73,571,572],{"class":117},"compute_normals",[73,574,315],{"class":79},[73,576,577,579,582,584,586,588,591],{"class":75,"line":83},[73,578,162],{"class":110},[73,580,581],{"class":79}," fm ",[73,583,168],{"class":110},[73,585,171],{"class":117},[73,587,174],{"class":79},[73,589,590],{"class":117},"make_face_membership",[73,592,315],{"class":79},[73,594,595,597,600,602,604,606,609],{"class":75,"line":89},[73,596,162],{"class":110},[73,598,599],{"class":79}," point_normals ",[73,601,168],{"class":110},[73,603,171],{"class":117},[73,605,174],{"class":79},[73,607,608],{"class":117},"compute_point_normals",[73,610,611],{"class":79},"(\n",[73,613,614,617,620,622,624,627,630,632,634,636,639,642,645],{"class":75,"line":95},[73,615,616],{"class":79},"    mesh ",[73,618,619],{"class":110},"|",[73,621,171],{"class":117},[73,623,174],{"class":79},[73,625,626],{"class":117},"tag",[73,628,629],{"class":79},"(fm) ",[73,631,619],{"class":110},[73,633,171],{"class":117},[73,635,174],{"class":79},[73,637,638],{"class":117},"tag_normals",[73,640,641],{"class":79},"(normals.",[73,643,644],{"class":117},"unit_vectors",[73,646,647],{"class":79},"()));\n",[73,649,650],{"class":75,"line":101},[73,651,157],{"emptyLinePlaceholder":156},[73,653,654,657,660,662,665,667,670],{"class":75,"line":107},[73,655,656],{"class":110},"float",[73,658,659],{"class":79}," worst ",[73,661,168],{"class":110},[73,663,664],{"class":255}," 0.",[73,666,259],{"class":110},[73,668,669],{"class":79},";",[73,671,672],{"class":320},"   \u002F\u002F close to 0\n",[73,674,675,677,679,681,684,686,688,690,693,695],{"class":75,"line":456},[73,676,419],{"class":110},[73,678,422],{"class":79},[73,680,162],{"class":110},[73,682,683],{"class":79}," [u, v, n] : ",[73,685,371],{"class":117},[73,687,174],{"class":79},[73,689,434],{"class":117},[73,691,692],{"class":79},"(d0, d1, point_normals.",[73,694,644],{"class":117},[73,696,697],{"class":79},"()))\n",[73,699,700,703,705,708,710,713,716,719,721,724,726,728,730,733,736,738,740,742,744,746,748,750],{"class":75,"line":482},[73,701,702],{"class":79},"  worst ",[73,704,168],{"class":110},[73,706,707],{"class":117}," std",[73,709,174],{"class":79},[73,711,712],{"class":117},"max",[73,714,715],{"class":79},"({worst, ",[73,717,718],{"class":117},"std",[73,720,174],{"class":79},[73,722,723],{"class":117},"abs",[73,725,180],{"class":79},[73,727,371],{"class":117},[73,729,174],{"class":79},[73,731,732],{"class":117},"dot",[73,734,735],{"class":79},"(u, v)), ",[73,737,718],{"class":117},[73,739,174],{"class":79},[73,741,723],{"class":117},[73,743,180],{"class":79},[73,745,371],{"class":117},[73,747,174],{"class":79},[73,749,732],{"class":117},[73,751,752],{"class":79},"(u, n))});\n",[14,754,755],{},"Directions are useful for drawing hatching along a surface, for aligning tool paths, and for finding ridges and valleys.",[56,757,759],{"id":758},"shape-index","Shape index",[14,761,762,763,766],{},"The shape index folds the two curvatures into one number in ",[73,764,765],{},"-1, 1",". It describes the local shape, not its size. A small ball and a large ball have the same shape index.",[64,768,770],{"className":138,"code":769,"language":140,"meta":69,"style":69},"auto si = tf::make_shape_index(mesh);      \u002F\u002F one value per point\nauto si3 = tf::make_shape_index(mesh, 3);  \u002F\u002F larger neighborhood\n",[32,771,772,794],{"__ignoreMap":69},[73,773,774,776,779,781,783,785,788,791],{"class":75,"line":76},[73,775,162],{"class":110},[73,777,778],{"class":79}," si ",[73,780,168],{"class":110},[73,782,171],{"class":117},[73,784,174],{"class":79},[73,786,787],{"class":117},"make_shape_index",[73,789,790],{"class":79},"(mesh);",[73,792,793],{"class":320},"      \u002F\u002F one value per point\n",[73,795,796,798,801,803,805,807,809,811,813,816],{"class":75,"line":83},[73,797,162],{"class":110},[73,799,800],{"class":79}," si3 ",[73,802,168],{"class":110},[73,804,171],{"class":117},[73,806,174],{"class":79},[73,808,787],{"class":117},[73,810,353],{"class":79},[73,812,356],{"class":255},[73,814,815],{"class":79},");",[73,817,818],{"class":320},"  \u002F\u002F larger neighborhood\n",[14,820,821],{},[51,822],{"alt":823,"src":824},"Shape index on the carved pot: each vertex classed as cup, trough, saddle, ridge or cap, from dark orange through sand to pale teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-3-shape-index.png",[826,827,828,841],"table",{},[829,830,831],"thead",{},[832,833,834,838],"tr",{},[835,836,837],"th",{},"Range",[835,839,840],{},"Shape",[842,843,844,853,861,869,877],"tbody",{},[832,845,846,850],{},[847,848,849],"td",{},"[-1, -5\u002F8)",[847,851,852],{},"Cup",[832,854,855,858],{},[847,856,857],{},"[-5\u002F8, -3\u002F8)",[847,859,860],{},"Trough",[832,862,863,866],{},[847,864,865],{},"[-3\u002F8, 3\u002F8)",[847,867,868],{},"Saddle",[832,870,871,874],{},[847,872,873],{},"[3\u002F8, 5\u002F8)",[847,875,876],{},"Ridge",[832,878,879,884],{},[847,880,881],{},[73,882,883],{},"5\u002F8, 1",[847,885,886],{},"Cap",[14,888,889],{},"Classify every vertex:",[64,891,893],{"className":138,"code":892,"language":140,"meta":69,"style":69},"std::array\u003Cint, 5> counts{};   \u002F\u002F cup, trough, saddle, ridge, cap\nfor (float s : si) {\n  int c = s \u003C -5.f \u002F 8 ? 0\n        : s \u003C -3.f \u002F 8 ? 1\n        : s \u003C  3.f \u002F 8 ? 2\n        : s \u003C  5.f \u002F 8 ? 3 : 4;\n  ++counts[c];\n}\n",[32,894,895,919,930,966,991,1013,1043,1051],{"__ignoreMap":69},[73,896,897,899,902,905,907,910,913,916],{"class":75,"line":76},[73,898,718],{"class":117},[73,900,901],{"class":79},"::array",[73,903,904],{"class":110},"\u003Cint",[73,906,262],{"class":79},[73,908,909],{"class":255},"5",[73,911,912],{"class":110},">",[73,914,915],{"class":79}," counts{};",[73,917,918],{"class":320},"   \u002F\u002F cup, trough, saddle, ridge, cap\n",[73,920,921,923,925,927],{"class":75,"line":83},[73,922,419],{"class":110},[73,924,422],{"class":79},[73,926,656],{"class":110},[73,928,929],{"class":79}," s : si) {\n",[73,931,932,935,938,940,943,946,949,952,954,957,960,963],{"class":75,"line":89},[73,933,934],{"class":110},"  int",[73,936,937],{"class":79}," c ",[73,939,168],{"class":110},[73,941,942],{"class":79}," s ",[73,944,945],{"class":110},"\u003C",[73,947,948],{"class":110}," -",[73,950,951],{"class":255},"5.",[73,953,259],{"class":110},[73,955,956],{"class":110}," \u002F",[73,958,959],{"class":255}," 8",[73,961,962],{"class":110}," ?",[73,964,965],{"class":255}," 0\n",[73,967,968,971,973,975,977,980,982,984,986,988],{"class":75,"line":95},[73,969,970],{"class":110},"        :",[73,972,942],{"class":79},[73,974,945],{"class":110},[73,976,948],{"class":110},[73,978,979],{"class":255},"3.",[73,981,259],{"class":110},[73,983,956],{"class":110},[73,985,959],{"class":255},[73,987,962],{"class":110},[73,989,990],{"class":255}," 1\n",[73,992,993,995,997,999,1002,1004,1006,1008,1010],{"class":75,"line":101},[73,994,970],{"class":110},[73,996,942],{"class":79},[73,998,945],{"class":110},[73,1000,1001],{"class":255},"  3.",[73,1003,259],{"class":110},[73,1005,956],{"class":110},[73,1007,959],{"class":255},[73,1009,962],{"class":110},[73,1011,1012],{"class":255}," 2\n",[73,1014,1015,1017,1019,1021,1024,1026,1028,1030,1032,1035,1038,1041],{"class":75,"line":107},[73,1016,970],{"class":110},[73,1018,942],{"class":79},[73,1020,945],{"class":110},[73,1022,1023],{"class":255},"  5.",[73,1025,259],{"class":110},[73,1027,956],{"class":110},[73,1029,959],{"class":255},[73,1031,962],{"class":110},[73,1033,1034],{"class":255}," 3",[73,1036,1037],{"class":110}," :",[73,1039,1040],{"class":255}," 4",[73,1042,479],{"class":79},[73,1044,1045,1048],{"class":75,"line":456},[73,1046,1047],{"class":110},"  ++",[73,1049,1050],{"class":79},"counts[c];\n",[73,1052,1053],{"class":75,"line":482},[73,1054,485],{"class":79},[14,1056,1057],{},"On the sphere every vertex is a cap.",[56,1059,1061],{"id":1060},"reading-the-signs","Reading the signs",[14,1063,1064,1065,22,1067,1069],{},"Signs follow the face orientation. With outward-facing normals, a convex surface has positive curvature. On the sphere ",[32,1066,34],{},[32,1068,38],{}," are positive and the shape index is near 1. Flip the faces and the signs flip with them:",[64,1071,1073],{"className":138,"code":1072,"language":140,"meta":69,"style":69},"auto flipped = sphere_buffer;   \u002F\u002F a copy\ntf::reverse_winding(flipped.faces());\nauto si_flipped = tf::make_shape_index(flipped.polygons());   \u002F\u002F near -1\n",[32,1074,1075,1090,1107],{"__ignoreMap":69},[73,1076,1077,1079,1082,1084,1087],{"class":75,"line":76},[73,1078,162],{"class":110},[73,1080,1081],{"class":79}," flipped ",[73,1083,168],{"class":110},[73,1085,1086],{"class":79}," sphere_buffer;",[73,1088,1089],{"class":320},"   \u002F\u002F a copy\n",[73,1091,1092,1094,1096,1099,1102,1105],{"class":75,"line":83},[73,1093,371],{"class":117},[73,1095,174],{"class":79},[73,1097,1098],{"class":117},"reverse_winding",[73,1100,1101],{"class":79},"(flipped.",[73,1103,1104],{"class":117},"faces",[73,1106,397],{"class":79},[73,1108,1109,1111,1114,1116,1118,1120,1122,1124,1126,1129],{"class":75,"line":89},[73,1110,162],{"class":110},[73,1112,1113],{"class":79}," si_flipped ",[73,1115,168],{"class":110},[73,1117,171],{"class":117},[73,1119,174],{"class":79},[73,1121,787],{"class":117},[73,1123,1101],{"class":79},[73,1125,201],{"class":117},[73,1127,1128],{"class":79},"());",[73,1130,1131],{"class":320},"   \u002F\u002F near -1\n",[14,1133,1134],{},[51,1135],{"alt":1136,"src":1137},"Reading the signs on a cat figurine from Thingi10K: shape-index classes with outward normals on the left and with the faces flipped on the right, where caps turn into cups and ridges into troughs","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-4-signs.png",[14,1139,1140,1141,1144],{},"If your results look inverted, check the winding first. ",[32,1142,1143],{},"tf::ensure_positive_orientation"," fixes it on closed meshes.",[14,1146,1147],{},"A height field shows all the shapes. Take a flat grid with +z normals and bend it:",[64,1149,1151],{"className":138,"code":1150,"language":140,"meta":69,"style":69},"auto plane_buffer = tf::make_plane_mesh(2.f, 2.f, 100, 100);\n\nauto saddle = plane_buffer;   \u002F\u002F shape index 0 at the center\nfor (auto pt : saddle.points()) pt[2] = pt[0] * pt[0] - pt[1] * pt[1];\n\nauto ridge = plane_buffer;    \u002F\u002F bends away from the normal: +0.5\nfor (auto pt : ridge.points()) pt[2] = -pt[0] * pt[0];\n\nauto trough = plane_buffer;   \u002F\u002F bends toward the normal: -0.5\nfor (auto pt : trough.points()) pt[2] = pt[0] * pt[0];\n",[32,1152,1153,1191,1195,1210,1270,1274,1288,1326,1330,1345],{"__ignoreMap":69},[73,1154,1155,1157,1160,1162,1164,1166,1169,1171,1173,1175,1177,1179,1181,1183,1185,1187,1189],{"class":75,"line":76},[73,1156,162],{"class":110},[73,1158,1159],{"class":79}," plane_buffer ",[73,1161,168],{"class":110},[73,1163,171],{"class":117},[73,1165,174],{"class":79},[73,1167,1168],{"class":117},"make_plane_mesh",[73,1170,180],{"class":79},[73,1172,256],{"class":255},[73,1174,259],{"class":110},[73,1176,262],{"class":79},[73,1178,256],{"class":255},[73,1180,259],{"class":110},[73,1182,262],{"class":79},[73,1184,265],{"class":255},[73,1186,262],{"class":79},[73,1188,265],{"class":255},[73,1190,186],{"class":79},[73,1192,1193],{"class":75,"line":83},[73,1194,157],{"emptyLinePlaceholder":156},[73,1196,1197,1199,1202,1204,1207],{"class":75,"line":89},[73,1198,162],{"class":110},[73,1200,1201],{"class":79}," saddle ",[73,1203,168],{"class":110},[73,1205,1206],{"class":79}," plane_buffer;",[73,1208,1209],{"class":320},"   \u002F\u002F shape index 0 at the center\n",[73,1211,1212,1214,1216,1218,1221,1224,1227,1230,1233,1235,1238,1241,1243,1245,1247,1249,1251,1254,1256,1259,1261,1263,1265,1267],{"class":75,"line":95},[73,1213,419],{"class":110},[73,1215,422],{"class":79},[73,1217,162],{"class":110},[73,1219,1220],{"class":79}," pt : saddle.",[73,1222,1223],{"class":117},"points",[73,1225,1226],{"class":79},"()) pt[",[73,1228,1229],{"class":255},"2",[73,1231,1232],{"class":79},"] ",[73,1234,168],{"class":110},[73,1236,1237],{"class":79}," pt[",[73,1239,1240],{"class":255},"0",[73,1242,1232],{"class":79},[73,1244,450],{"class":110},[73,1246,1237],{"class":79},[73,1248,1240],{"class":255},[73,1250,1232],{"class":79},[73,1252,1253],{"class":110},"-",[73,1255,1237],{"class":79},[73,1257,1258],{"class":255},"1",[73,1260,1232],{"class":79},[73,1262,450],{"class":110},[73,1264,1237],{"class":79},[73,1266,1258],{"class":255},[73,1268,1269],{"class":79},"];\n",[73,1271,1272],{"class":75,"line":101},[73,1273,157],{"emptyLinePlaceholder":156},[73,1275,1276,1278,1281,1283,1285],{"class":75,"line":107},[73,1277,162],{"class":110},[73,1279,1280],{"class":79}," ridge ",[73,1282,168],{"class":110},[73,1284,1206],{"class":79},[73,1286,1287],{"class":320},"    \u002F\u002F bends away from the normal: +0.5\n",[73,1289,1290,1292,1294,1296,1299,1301,1303,1305,1307,1309,1311,1314,1316,1318,1320,1322,1324],{"class":75,"line":456},[73,1291,419],{"class":110},[73,1293,422],{"class":79},[73,1295,162],{"class":110},[73,1297,1298],{"class":79}," pt : ridge.",[73,1300,1223],{"class":117},[73,1302,1226],{"class":79},[73,1304,1229],{"class":255},[73,1306,1232],{"class":79},[73,1308,168],{"class":110},[73,1310,948],{"class":110},[73,1312,1313],{"class":79},"pt[",[73,1315,1240],{"class":255},[73,1317,1232],{"class":79},[73,1319,450],{"class":110},[73,1321,1237],{"class":79},[73,1323,1240],{"class":255},[73,1325,1269],{"class":79},[73,1327,1328],{"class":75,"line":482},[73,1329,157],{"emptyLinePlaceholder":156},[73,1331,1333,1335,1338,1340,1342],{"class":75,"line":1332},9,[73,1334,162],{"class":110},[73,1336,1337],{"class":79}," trough ",[73,1339,168],{"class":110},[73,1341,1206],{"class":79},[73,1343,1344],{"class":320},"   \u002F\u002F bends toward the normal: -0.5\n",[73,1346,1348,1350,1352,1354,1357,1359,1361,1363,1365,1367,1369,1371,1373,1375,1377,1379],{"class":75,"line":1347},10,[73,1349,419],{"class":110},[73,1351,422],{"class":79},[73,1353,162],{"class":110},[73,1355,1356],{"class":79}," pt : trough.",[73,1358,1223],{"class":117},[73,1360,1226],{"class":79},[73,1362,1229],{"class":255},[73,1364,1232],{"class":79},[73,1366,168],{"class":110},[73,1368,1237],{"class":79},[73,1370,1240],{"class":255},[73,1372,1232],{"class":79},[73,1374,450],{"class":110},[73,1376,1237],{"class":79},[73,1378,1240],{"class":255},[73,1380,1269],{"class":79},[56,1382,1384],{"id":1383},"working-with-results","Working with results",[14,1386,1387,1388,1391],{},"All results are ",[32,1389,1390],{},"tf::buffer","s with one value per point, in the mesh's coordinate type. Use them as colors, as weights, or as fields for other tools.",[14,1393,1394,1395,1399],{},"Draw the lines where the surface changes from saddle to ridge with ",[18,1396,1398],{"href":1397},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp","isocontours",":",[64,1401,1403],{"className":138,"code":1402,"language":140,"meta":69,"style":69},"auto lines = tf::make_isocontours(mesh, tf::make_range(si), 3.f \u002F 8);\n",[32,1404,1405],{"__ignoreMap":69},[73,1406,1407,1409,1412,1414,1416,1418,1421,1423,1425,1427,1430,1433,1435,1437,1439,1441],{"class":75,"line":76},[73,1408,162],{"class":110},[73,1410,1411],{"class":79}," lines ",[73,1413,168],{"class":110},[73,1415,171],{"class":117},[73,1417,174],{"class":79},[73,1419,1420],{"class":117},"make_isocontours",[73,1422,353],{"class":79},[73,1424,371],{"class":117},[73,1426,174],{"class":79},[73,1428,1429],{"class":117},"make_range",[73,1431,1432],{"class":79},"(si), ",[73,1434,979],{"class":255},[73,1436,259],{"class":110},[73,1438,956],{"class":110},[73,1440,959],{"class":255},[73,1442,186],{"class":79},[14,1444,1445],{},"Cut the mesh into regions by shape. The curves become edges and each face gets a band label:",[64,1447,1449],{"className":138,"code":1448,"language":140,"meta":69,"style":69},"std::array\u003Cfloat, 2> cuts{-3.f \u002F 8, 3.f \u002F 8};\nauto [regions, labels, face_labels] =\n    tf::embedded_isocurves(mesh, tf::make_range(si), tf::make_range(cuts));\n\u002F\u002F labels: 0 concave, 1 saddle, 2 convex\n",[32,1450,1451,1492,1502,1531],{"__ignoreMap":69},[73,1452,1453,1455,1457,1460,1462,1464,1466,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489],{"class":75,"line":76},[73,1454,718],{"class":117},[73,1456,901],{"class":79},[73,1458,1459],{"class":110},"\u003Cfloat",[73,1461,262],{"class":79},[73,1463,1229],{"class":255},[73,1465,912],{"class":110},[73,1467,1468],{"class":79}," cuts{",[73,1470,1253],{"class":110},[73,1472,979],{"class":255},[73,1474,259],{"class":110},[73,1476,956],{"class":110},[73,1478,959],{"class":255},[73,1480,262],{"class":79},[73,1482,979],{"class":255},[73,1484,259],{"class":110},[73,1486,956],{"class":110},[73,1488,959],{"class":255},[73,1490,1491],{"class":79},"};\n",[73,1493,1494,1496,1499],{"class":75,"line":83},[73,1495,162],{"class":110},[73,1497,1498],{"class":79}," [regions, labels, face_labels] ",[73,1500,1501],{"class":110},"=\n",[73,1503,1504,1507,1509,1512,1514,1516,1518,1520,1522,1524,1526,1528],{"class":75,"line":89},[73,1505,1506],{"class":117},"    tf",[73,1508,174],{"class":79},[73,1510,1511],{"class":117},"embedded_isocurves",[73,1513,353],{"class":79},[73,1515,371],{"class":117},[73,1517,174],{"class":79},[73,1519,1429],{"class":117},[73,1521,1432],{"class":79},[73,1523,371],{"class":117},[73,1525,174],{"class":79},[73,1527,1429],{"class":117},[73,1529,1530],{"class":79},"(cuts));\n",[73,1532,1533],{"class":75,"line":95},[73,1534,1535],{"class":320},"\u002F\u002F labels: 0 concave, 1 saddle, 2 convex\n",[14,1537,1538],{},"Flag sharp spots for inspection:",[64,1540,1542],{"className":138,"code":1541,"language":140,"meta":69,"style":69},"std::vector\u003Cint> sharp;\nfor (int i = 0; i \u003C (int)k0.size(); ++i)\n  if (std::max(std::abs(k0[i]), std::abs(k1[i])) > 10.f) sharp.push_back(i);\n",[32,1543,1544,1557,1597],{"__ignoreMap":69},[73,1545,1546,1548,1551,1554],{"class":75,"line":76},[73,1547,718],{"class":117},[73,1549,1550],{"class":79},"::vector",[73,1552,1553],{"class":110},"\u003Cint>",[73,1555,1556],{"class":79}," sharp;\n",[73,1558,1559,1561,1563,1566,1569,1571,1574,1577,1579,1581,1583,1586,1588,1591,1594],{"class":75,"line":83},[73,1560,419],{"class":110},[73,1562,422],{"class":79},[73,1564,1565],{"class":110},"int",[73,1567,1568],{"class":79}," i ",[73,1570,168],{"class":110},[73,1572,1573],{"class":255}," 0",[73,1575,1576],{"class":79},"; i ",[73,1578,945],{"class":110},[73,1580,422],{"class":79},[73,1582,1565],{"class":110},[73,1584,1585],{"class":79},")k0.",[73,1587,394],{"class":117},[73,1589,1590],{"class":79},"(); ",[73,1592,1593],{"class":110},"++",[73,1595,1596],{"class":79},"i)\n",[73,1598,1599,1602,1604,1606,1608,1610,1612,1614,1616,1618,1621,1623,1625,1627,1630,1632,1635,1637,1640,1643],{"class":75,"line":89},[73,1600,1601],{"class":110},"  if",[73,1603,422],{"class":79},[73,1605,718],{"class":117},[73,1607,174],{"class":79},[73,1609,712],{"class":117},[73,1611,180],{"class":79},[73,1613,718],{"class":117},[73,1615,174],{"class":79},[73,1617,723],{"class":117},[73,1619,1620],{"class":79},"(k0[i]), ",[73,1622,718],{"class":117},[73,1624,174],{"class":79},[73,1626,723],{"class":117},[73,1628,1629],{"class":79},"(k1[i])) ",[73,1631,912],{"class":110},[73,1633,1634],{"class":255}," 10.",[73,1636,259],{"class":110},[73,1638,1639],{"class":79},") sharp.",[73,1641,1642],{"class":117},"push_back",[73,1644,1645],{"class":79},"(i);\n",[14,1647,1648,1649,1653],{},"Combine curvature with ",[18,1650,1652],{"href":1651},"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp","geodesic distances"," to look only at the neighborhood of a point.",[56,1655,1657],{"id":1656},"related-tutorials","Related tutorials",[1659,1660,1661,1668,1674,1681],"ul",{},[1662,1663,1664,1667],"li",{},[18,1665,1666],{"href":1651},"Fast and Accurate Geodesic Distances in C++"," — distance along the surface from one seed or many.",[1662,1669,1670,1673],{},[18,1671,1672],{"href":1397},"Fast and Robust Isocontours on a Triangle Mesh in C++"," — curves and bands of equal value on a scalar field.",[1662,1675,1676,1680],{},[18,1677,1679],{"href":1678},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-cpp","Fast and Robust Mesh Slicing and Cross-Sections in C++"," — plane sections, layers and slabs of a mesh.",[1662,1682,1683,1687],{},[18,1684,1686],{"href":1685},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-cpp","Fast STL and OBJ Reading in C++"," — load and write meshes, the first step of every pipeline.",[56,1689,1691],{"id":1690},"performance","Performance",[14,1693,1694,1695,27],{},"We measured principal curvatures on the Stanford dragon at 1M faces, against libigl's ",[32,1696,1697],{},"principal_curvature",[1699,1700],"headline-numbers",{":items":1701},"[{\"value\":\"30.1ms\",\"label\":\"Principal curvatures\",\"detail\":\"1M faces, C++\"},{\"value\":\"51×\",\"label\":\"Faster than libigl\",\"detail\":\"1M faces, C++ (1,520 ms)\"},{\"value\":\"27.7ms\",\"label\":\"From Python\",\"detail\":\"1M faces (libigl: 1,941 ms)\"}]",[14,1703,1704,1709,1710],{},[73,1705,1708],{"className":1706},[1707],"tag-pill","Apple M4 Max"," ",[73,1711,1713],{"className":1712},[1707],"16 threads",[1715,1716],"data-table",{":headers":1717,":highlight":1258,":rows":1718},"[\"1M faces\",\"trueform\",\"libigl\",\"Speedup\"]","[[\"C++\",\"30.1 ms\",\"1,520 ms\",\"51×\"],[\"Python\",\"27.7 ms\",\"1,941 ms\",\"70×\"]]",[14,1720,1721],{},[51,1722],{"alt":1723,"src":1724},"Benchmark chart: trueform against other C++ libraries, principal curvatures","\u002Fimg\u002Falgorithms\u002Fbenchmark-principal-curvatures-cpp-2026.png",[14,1726,1727,1731,1732],{},[18,1728,1730],{"href":1729},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1733,1736],{"href":1734,"rel":1735},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fgeometry#curvature-analysis",[129],"Documentation",[1738,1739],"cite-as",{"author":1740,"title":5},"Sajovic, {\\v{Z}}iga",[1742,1743,1744],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":69,"searchDepth":83,"depth":83,"links":1746},[1747,1748,1749,1750,1751,1752,1753,1754,1755],{"id":58,"depth":83,"text":59},{"id":134,"depth":83,"text":135},{"id":290,"depth":83,"text":291},{"id":498,"depth":83,"text":499},{"id":758,"depth":83,"text":759},{"id":1060,"depth":83,"text":1061},{"id":1383,"depth":83,"text":1384},{"id":1656,"depth":83,"text":1657},{"id":1690,"depth":83,"text":1691},"2026-10-31","Compute principal curvatures, principal directions, Gaussian and mean curvature, and the shape index of a triangle mesh in C++. Header-only, C++17. 30.1 ms on 1M faces, 51× faster than libigl.","md",[1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770],"principal curvature C++","mesh curvature C++","Gaussian curvature mesh C++","mean curvature mesh C++","principal directions mesh C++","shape index mesh","libigl principal_curvature alternative","CGAL curvature estimation alternative","VTK curvatures alternative","MeshLib curvature alternative","mesh curvature library C++",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp.png","\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp",{"title":5,"description":1757},"tutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp",[1777,1778,1779],"c++","curvature","tutorial","DUvp4HugEQ9hCRglJTTDcINLfg37d0vGnchFqj0w_vM",1793517812008]