[{"data":1,"prerenderedAt":1504},["ShallowReactive",2],{"algo-principal-curvatures":3},{"id":4,"title":5,"author":6,"body":7,"date":1477,"description":1478,"extension":1479,"hub":274,"keywords":1480,"meta":1491,"navigation":274,"ogImage":1492,"path":1493,"published":274,"seo":1494,"stem":1497,"tags":1498,"updated":1502,"__hash__":1503},"algorithms\u002Falgorithms\u002Fprincipal-curvatures.md","Principal curvatures","Žiga Sajovic",{"type":8,"value":9,"toc":1466},"minimark",[10,20,24,33,38,58,61,91,94,100,108,111,114,118,121,127,133,139,145,148,171,174,178,181,184,188,194,198,209,215,221,227,230,233,354,365,371,385,396,400,403,409,415,892,895,905,915,919,934,938,946,1364,1367,1371,1374,1379,1383,1391,1395,1410,1423,1443,1454,1458,1462],[11,12,13,14,19],"p",{},"A curvature map turns a mesh into answers. Where are the sharp edges? Where is the dent? Which regions are convex, which are saddles? Each of those starts with two numbers per vertex, the principal curvatures, and with a choice of how to estimate them from flat triangles. This page covers that choice, how ",[15,16,18],"a",{"href":17},"\u002Ftrueform","trueform","'s estimator behaves on noisy scans, and what the numbers are good for.",[21,22],"hub-language-cards",{":items":23},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Robust Principal Curvatures of a Mesh in C++\",\"to\":\"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp\",\"detail\":\"Header-only. Values, directions and the shape index as buffers.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Robust Principal Curvatures of a Mesh in Python\",\"to\":\"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python\",\"detail\":\"One pip install. NumPy arrays in, NumPy arrays out.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Robust Principal Curvatures of a Mesh in JavaScript\",\"to\":\"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript\",\"detail\":\"WebAssembly. The same call in Node.js and the browser.\"}]",[11,25,26],{},[27,28],"img",{"alt":29,"height":30,"src":31,"width":32},"Shape index on the Stanford dragon, each vertex classed as cup, trough, saddle, ridge or cap, from dark orange through sand to pale teal",800,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-hero.webp",1200,[34,35,37],"h2",{"id":36},"the-numbers-in-one-minute","The numbers, in one minute",[11,39,40,41,45,46,49,50,53,54,57],{},"At a point on a surface, cut with every plane that holds the normal. Each cut is a curve with its own bend. The largest bend is the first principal curvature, ",[42,43,44],"code",{},"k0",". The smallest is the second, ",[42,47,48],{},"k1",". The directions they occur in, ",[42,51,52],{},"d0"," and ",[42,55,56],{},"d1",", are perpendicular to each other and to the normal.",[11,59,60],{},"Everything else people search for is built from these two:",[62,63,64,76,85],"ul",{},[65,66,67,71,72,75],"li",{},[68,69,70],"strong",{},"Gaussian curvature",", ",[42,73,74],{},"k0 · k1",". Positive on a dome or a dent, negative on a saddle, zero on a cylinder or a plane.",[65,77,78,71,81,84],{},[68,79,80],{},"Mean curvature",[42,82,83],{},"(k0 + k1) \u002F 2",". The average bend.",[65,86,87,90],{},[68,88,89],{},"Shape index",", a number from −1 to 1 that names the kind of shape and ignores its size. A marble and a globe have the same shape index.",[11,92,93],{},"The signs of the two curvatures name the shape. Gaussian curvature tells only whether they agree. The shape index places the shape on one scale, and the sign patterns below hold at the centre of each class.",[95,96],"data-table",{":headers":97,":highlight":98,":rows":99},"[\"Shape\",\"k0, k1 (outward normals)\",\"Gaussian\",\"Shape index\"]","3","[[\"Cup\",\"both negative\",\"positive\",\"[−1, −5\u002F8)\"],[\"Trough\",\"one zero, one negative\",\"zero\",\"[−5\u002F8, −3\u002F8)\"],[\"Saddle\",\"one positive, one negative\",\"negative\",\"[−3\u002F8, 3\u002F8)\"],[\"Ridge\",\"one positive, one zero\",\"zero\",\"[3\u002F8, 5\u002F8)\"],[\"Cap\",\"both positive\",\"positive\",\"[5\u002F8, 1]\"]]",[11,101,102],{},[27,103],{"alt":104,"height":105,"src":106,"width":32,"loading":107},"Five synthetic quadric patches in a row, coloured by trueform's shape index class: a cup in dark orange, a trough in orange, a saddle in sand, a ridge in teal and a cap in pale teal",480,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-shapes.webp","lazy",[11,109,110],{},"The five patches above are synthetic quadrics, one per class, from cup on the left to cap on the right. At the centre of each, trueform reports the shape index the formula predicts: −1, −0.5, 0, 0.5 and 1.",[11,112,113],{},"Signs follow the normals. With outward normals, a convex surface is positive. Flip the faces and every sign flips, so a map that looks inside out usually means the winding is.",[34,115,117],{"id":116},"which-estimator","Which estimator",[11,119,120],{},"A triangle is flat. All of a mesh's bending sits at its edges and vertices, so curvature has to be estimated from a neighbourhood. Four families of estimator are common, and they answer different questions.",[11,122,123,126],{},[68,124,125],{},"Angle defect."," Sum the angles around a vertex and subtract from 2π. It gives Gaussian curvature only, from the one-ring, with no directions. Cheap, but as local as it gets, so noise goes straight through.",[11,128,129,132],{},[68,130,131],{},"Cotangent Laplacian."," The discrete Laplacian of the positions points along the normal, with a length set by mean curvature. Mean curvature only, again from the one-ring.",[11,134,135,138],{},[68,136,137],{},"Per-face tensors."," Estimate a curvature tensor on each face from its normals, then average onto vertices. Values and directions, still from a small neighbourhood.",[11,140,141,144],{},[68,142,143],{},"Surface fitting."," Fit a small analytic surface to a neighbourhood around each vertex and read its curvature. Both values and both directions, and the size of the neighbourhood is a parameter, so it can reach past one ring.",[11,146,147],{},"trueform fits a surface. For each vertex:",[149,150,151,154,161,168],"ol",{},[65,152,153],{},"Take the vertex normal, the average of the normals of its faces, and build a tangent frame.",[65,155,156,157,160],{},"Gather the k-ring: every vertex within ",[42,158,159],{},"k"," edges, two by default.",[65,162,163,164,167],{},"Express each neighbour in the frame and fit ",[42,165,166],{},"z = ax² + bxy + cy² + dx + ey"," by least squares. The linear terms absorb a normal that is slightly off, so the quadratic part reads the bend, not the tilt.",[65,169,170],{},"The principal curvatures are the eigenvalues of the fitted shape operator, and the directions are its eigenvectors, mapped back to 3D.",[11,172,173],{},"Where the two curvatures are equal, as on a sphere or a plane, every direction is principal; the estimate stays defined and returns a tangent direction instead of noise.",[34,175,177],{"id":176},"noise-and-the-k-ring","Noise and the k-ring",[11,179,180],{},"Scans carry noise. A local estimate reports the noise, not the shape. The k-ring is the one knob against it.",[11,182,183],{},"The three pictures below are the head of the Stanford dragon, coloured by shape index. The first is the clean mesh with the default two rings. The second is the same mesh with synthetic scanner noise: every point moved along its normal by Gaussian noise with a standard deviation of a quarter of the mean edge length. At two rings, the classes break into speckle. The third is the noisy mesh at four rings. The scales on the neck, the ridges of the brow and the grooves of the jaw come back.",[185,186],"image-grid",{":images":187},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-clean-k2.webp\",\"width\":1200,\"height\":800,\"alt\":\"Shape index on the clean head of the Stanford dragon at k = 2: neck scales, brow ridges and jaw grooves read as clear bands\",\"label\":\"Clean, k = 2\",\"detail\":\"the reference\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-noisy-k2.webp\",\"width\":1200,\"height\":800,\"alt\":\"Shape index on the dragon head with synthetic noise at k = 2: the classes break into speckle\",\"label\":\"Synthetic noise, k = 2\",\"detail\":\"the fit follows the noise\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-noisy-k4.webp\",\"width\":1200,\"height\":800,\"alt\":\"Shape index on the noisy dragon head at k = 4: neck scales and brow ridges return\",\"label\":\"Synthetic noise, k = 4\",\"detail\":\"a wider fit sees the shape\"}]",[11,189,190,191,193],{},"The ring counts edges, not millimetres. The right ",[42,192,159],{}," depends on how dense the mesh is against the size of the features you care about. A wider ring averages out noise and also rounds off features smaller than the ring. Try two values on a region you know and compare.",[34,195,197],{"id":196},"what-you-will-use-it-for","What you will use it for",[11,199,200,203,204,208],{},[68,201,202],{},"Feature lines."," Ridges and valleys sit where the shape index crosses a class boundary, such as saddle to ridge at 3\u002F8. One ",[15,205,207],{"href":206},"\u002Falgorithms\u002Fisocontours","isocontour"," call on the shape index draws them as curves.",[11,210,211,214],{},[68,212,213],{},"Segmentation by shape."," Isobands on the shape index cut the mesh into concave, saddle and convex regions. Each region is its own mesh, with every face mapped back to its source face.",[11,216,217,220],{},[68,218,219],{},"Inspection."," A dent or a burr bends the surface far more than the design does. Flag every vertex whose larger absolute curvature passes a threshold, and outline the flagged region with an isocontour.",[11,222,223],{},[27,224],{"alt":225,"height":30,"src":226,"width":32,"loading":107},"A synthetic curved panel, a quarter cylinder of radius 1, coloured by the larger absolute principal curvature: the panel reads teal, a round dent and a raised burr read orange, each outlined in pink where the curvature passes three times the panel's own","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-inspection.webp",[11,228,229],{},"The panel above is synthetic: a quarter cylinder of radius 1 with a dent and a burr pressed in. Its own curvature is 1. The pink outline is where the larger absolute curvature passes 3.",[11,231,232],{},"Feature lines and the inspection above, in Python:",[234,235,240],"pre",{"className":236,"code":237,"language":238,"meta":239,"style":239},"language-python shiki shiki-themes github-dark","k0, k1 = tf.principal_curvatures(mesh)\nsi = tf.shape_index(mesh)\n\nridges, ridge_points = tf.isocontours(mesh, si, 0.375)     # saddle to ridge\n\nbend = np.maximum(np.abs(k0), np.abs(k1))\nflagged = bend > 3.0                                       # dents and burrs\noutline, outline_points = tf.isocontours(mesh, bend, 3.0)  # the pink line\n","python","",[42,241,242,258,269,276,298,303,314,334],{"__ignoreMap":239},[243,244,247,251,255],"span",{"class":245,"line":246},"line",1,[243,248,250],{"class":249},"s95oV","k0, k1 ",[243,252,254],{"class":253},"snl16","=",[243,256,257],{"class":249}," tf.principal_curvatures(mesh)\n",[243,259,261,264,266],{"class":245,"line":260},2,[243,262,263],{"class":249},"si ",[243,265,254],{"class":253},[243,267,268],{"class":249}," tf.shape_index(mesh)\n",[243,270,272],{"class":245,"line":271},3,[243,273,275],{"emptyLinePlaceholder":274},true,"\n",[243,277,279,282,284,287,291,294],{"class":245,"line":278},4,[243,280,281],{"class":249},"ridges, ridge_points ",[243,283,254],{"class":253},[243,285,286],{"class":249}," tf.isocontours(mesh, si, ",[243,288,290],{"class":289},"sDLfK","0.375",[243,292,293],{"class":249},")     ",[243,295,297],{"class":296},"sAwPA","# saddle to ridge\n",[243,299,301],{"class":245,"line":300},5,[243,302,275],{"emptyLinePlaceholder":274},[243,304,306,309,311],{"class":245,"line":305},6,[243,307,308],{"class":249},"bend ",[243,310,254],{"class":253},[243,312,313],{"class":249}," np.maximum(np.abs(k0), np.abs(k1))\n",[243,315,317,320,322,325,328,331],{"class":245,"line":316},7,[243,318,319],{"class":249},"flagged ",[243,321,254],{"class":253},[243,323,324],{"class":249}," bend ",[243,326,327],{"class":253},">",[243,329,330],{"class":289}," 3.0",[243,332,333],{"class":296},"                                       # dents and burrs\n",[243,335,337,340,342,345,348,351],{"class":245,"line":336},8,[243,338,339],{"class":249},"outline, outline_points ",[243,341,254],{"class":253},[243,343,344],{"class":249}," tf.isocontours(mesh, bend, ",[243,346,347],{"class":289},"3.0",[243,349,350],{"class":249},")  ",[243,352,353],{"class":296},"# the pink line\n",[11,355,356,359,360,53,362,364],{},[68,357,358],{},"Directions."," ",[42,361,52],{},[42,363,56],{}," follow ridges and grooves. Use them to align hatching, tool paths or fibres with the surface.",[11,366,367],{},[27,368],{"alt":369,"height":30,"src":370,"width":32,"loading":107},"Principal directions on the Stanford dragon: short sand strokes along d0, the direction of the larger curvature, wrap around the body, the horns and the tail","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcurvatures-directions.webp",[11,372,373,374,376,377,379,380,53,382,384],{},"The strokes are ",[42,375,52],{},", the direction of the larger curvature, on a spaced subset of vertices at three rings. On the body they wrap around it, because the body bends most around its girth. ",[42,378,56],{}," runs along the body. Strokes are drawn only where ",[42,381,44],{},[42,383,48],{}," clearly differ. Where they are nearly equal, every direction is principal and a stroke would mean nothing.",[11,386,387,390,391,395],{},[68,388,389],{},"Local questions."," Combine curvature with ",[15,392,394],{"href":393},"\u002Falgorithms\u002Fgeodesic-distances","geodesic distances"," to look only at the surface within a distance of a point.",[34,397,399],{"id":398},"boundaries-creases-and-flat-regions","Boundaries, creases and flat regions",[11,401,402],{},"A quadric fit models a smooth, full patch. Three kinds of vertex are not one, and each has a call that finds it. Run them next to the curvature and combine the results.",[11,404,405,408],{},[68,406,407],{},"Boundary vertices."," At an open edge the ring is one-sided, and the fit extrapolates the missing half. Ignore the estimate there. The boundary edges name the vertices.",[11,410,411,414],{},[68,412,413],{},"Sharp creases."," A machined edge has no finite curvature. Find the creases by their dihedral angle and set the curvature there to infinity. The faces between the creases keep their fitted values.",[416,417,418,627,746],"code-group",{},[234,419,424],{"className":420,"code":421,"filename":422,"language":423,"meta":239,"style":239},"language-cpp shiki shiki-themes github-dark","auto [k0, k1] = tf::make_principal_curvatures(mesh);\nconstexpr float inf = std::numeric_limits\u003Cfloat>::infinity();\nconstexpr float nan = std::numeric_limits\u003Cfloat>::quiet_NaN();\n\nfor (auto edge : tf::make_sharp_edges(mesh, tf::deg(30.f)))\n  for (auto v : edge) k0[v] = k1[v] = inf;    \u002F\u002F creases: infinite\nfor (auto edge : tf::make_boundary_edges(mesh))\n  for (auto v : edge) k0[v] = k1[v] = nan;    \u002F\u002F boundary: no estimate\n","C++","cpp",[42,425,426,449,485,513,517,560,585,605],{"__ignoreMap":239},[243,427,428,431,434,436,440,443,446],{"class":245,"line":246},[243,429,430],{"class":253},"auto",[243,432,433],{"class":249}," [k0, k1] ",[243,435,254],{"class":253},[243,437,439],{"class":438},"svObZ"," tf",[243,441,442],{"class":249},"::",[243,444,445],{"class":438},"make_principal_curvatures",[243,447,448],{"class":249},"(mesh);\n",[243,450,451,454,457,460,462,465,467,470,473,476,479,482],{"class":245,"line":260},[243,452,453],{"class":253},"constexpr",[243,455,456],{"class":253}," float",[243,458,459],{"class":249}," inf ",[243,461,254],{"class":253},[243,463,464],{"class":438}," std",[243,466,442],{"class":249},[243,468,469],{"class":438},"numeric_limits",[243,471,472],{"class":249},"\u003C",[243,474,475],{"class":253},"float",[243,477,478],{"class":249},">::",[243,480,481],{"class":438},"infinity",[243,483,484],{"class":249},"();\n",[243,486,487,489,491,494,496,498,500,502,504,506,508,511],{"class":245,"line":271},[243,488,453],{"class":253},[243,490,456],{"class":253},[243,492,493],{"class":249}," nan ",[243,495,254],{"class":253},[243,497,464],{"class":438},[243,499,442],{"class":249},[243,501,469],{"class":438},[243,503,472],{"class":249},[243,505,475],{"class":253},[243,507,478],{"class":249},[243,509,510],{"class":438},"quiet_NaN",[243,512,484],{"class":249},[243,514,515],{"class":245,"line":278},[243,516,275],{"emptyLinePlaceholder":274},[243,518,519,522,525,527,530,533,535,538,541,543,545,548,551,554,557],{"class":245,"line":300},[243,520,521],{"class":253},"for",[243,523,524],{"class":249}," (",[243,526,430],{"class":253},[243,528,529],{"class":249}," edge : ",[243,531,532],{"class":438},"tf",[243,534,442],{"class":249},[243,536,537],{"class":438},"make_sharp_edges",[243,539,540],{"class":249},"(mesh, ",[243,542,532],{"class":438},[243,544,442],{"class":249},[243,546,547],{"class":438},"deg",[243,549,550],{"class":249},"(",[243,552,553],{"class":289},"30.",[243,555,556],{"class":253},"f",[243,558,559],{"class":249},")))\n",[243,561,562,565,567,569,572,574,577,579,582],{"class":245,"line":305},[243,563,564],{"class":253},"  for",[243,566,524],{"class":249},[243,568,430],{"class":253},[243,570,571],{"class":249}," v : edge) k0[v] ",[243,573,254],{"class":253},[243,575,576],{"class":249}," k1[v] ",[243,578,254],{"class":253},[243,580,581],{"class":249}," inf;",[243,583,584],{"class":296},"    \u002F\u002F creases: infinite\n",[243,586,587,589,591,593,595,597,599,602],{"class":245,"line":316},[243,588,521],{"class":253},[243,590,524],{"class":249},[243,592,430],{"class":253},[243,594,529],{"class":249},[243,596,532],{"class":438},[243,598,442],{"class":249},[243,600,601],{"class":438},"make_boundary_edges",[243,603,604],{"class":249},"(mesh))\n",[243,606,607,609,611,613,615,617,619,621,624],{"class":245,"line":336},[243,608,564],{"class":253},[243,610,524],{"class":249},[243,612,430],{"class":253},[243,614,571],{"class":249},[243,616,254],{"class":253},[243,618,576],{"class":249},[243,620,254],{"class":253},[243,622,623],{"class":249}," nan;",[243,625,626],{"class":296},"    \u002F\u002F boundary: no estimate\n",[234,628,631],{"className":236,"code":629,"filename":630,"language":238,"meta":239,"style":239},"import numpy as np\n\nk0, k1 = tf.principal_curvatures(mesh)\n\nedges, angles = tf.dihedral_angles(mesh)\ncreases = np.unique(edges[angles > np.radians(30)])\nk0[creases] = k1[creases] = np.inf            # creases: infinite\n\nboundary = np.unique(tf.boundary_edges(mesh))\nk0[boundary] = k1[boundary] = np.nan          # boundary: no estimate\n","Python",[42,632,633,647,651,659,663,673,694,712,716,727],{"__ignoreMap":239},[243,634,635,638,641,644],{"class":245,"line":246},[243,636,637],{"class":253},"import",[243,639,640],{"class":249}," numpy ",[243,642,643],{"class":253},"as",[243,645,646],{"class":249}," np\n",[243,648,649],{"class":245,"line":260},[243,650,275],{"emptyLinePlaceholder":274},[243,652,653,655,657],{"class":245,"line":271},[243,654,250],{"class":249},[243,656,254],{"class":253},[243,658,257],{"class":249},[243,660,661],{"class":245,"line":278},[243,662,275],{"emptyLinePlaceholder":274},[243,664,665,668,670],{"class":245,"line":300},[243,666,667],{"class":249},"edges, angles ",[243,669,254],{"class":253},[243,671,672],{"class":249}," tf.dihedral_angles(mesh)\n",[243,674,675,678,680,683,685,688,691],{"class":245,"line":305},[243,676,677],{"class":249},"creases ",[243,679,254],{"class":253},[243,681,682],{"class":249}," np.unique(edges[angles ",[243,684,327],{"class":253},[243,686,687],{"class":249}," np.radians(",[243,689,690],{"class":289},"30",[243,692,693],{"class":249},")])\n",[243,695,696,699,701,704,706,709],{"class":245,"line":316},[243,697,698],{"class":249},"k0[creases] ",[243,700,254],{"class":253},[243,702,703],{"class":249}," k1[creases] ",[243,705,254],{"class":253},[243,707,708],{"class":249}," np.inf            ",[243,710,711],{"class":296},"# creases: infinite\n",[243,713,714],{"class":245,"line":336},[243,715,275],{"emptyLinePlaceholder":274},[243,717,719,722,724],{"class":245,"line":718},9,[243,720,721],{"class":249},"boundary ",[243,723,254],{"class":253},[243,725,726],{"class":249}," np.unique(tf.boundary_edges(mesh))\n",[243,728,730,733,735,738,740,743],{"class":245,"line":729},10,[243,731,732],{"class":249},"k0[boundary] ",[243,734,254],{"class":253},[243,736,737],{"class":249}," k1[boundary] ",[243,739,254],{"class":253},[243,741,742],{"class":249}," np.nan          ",[243,744,745],{"class":296},"# boundary: no estimate\n",[234,747,752],{"className":748,"code":749,"filename":750,"language":751,"meta":239,"style":239},"language-ts shiki shiki-themes github-dark","const { k0, k1 } = tf.principalCurvatures(mesh);\nconst c0 = k0.data, c1 = k1.data;\n\nfor (const v of tf.sharpEdges(mesh, 30).data)\n  c0[v] = c1[v] = Infinity;                    \u002F\u002F creases: infinite\nfor (const v of tf.boundaryEdges(mesh).data)\n  c0[v] = c1[v] = NaN;                         \u002F\u002F boundary: no estimate\n","JavaScript","ts",[42,753,754,781,802,806,832,853,873],{"__ignoreMap":239},[243,755,756,759,762,764,766,768,771,773,776,779],{"class":245,"line":246},[243,757,758],{"class":253},"const",[243,760,761],{"class":249}," { ",[243,763,44],{"class":289},[243,765,71],{"class":249},[243,767,48],{"class":289},[243,769,770],{"class":249}," } ",[243,772,254],{"class":253},[243,774,775],{"class":249}," tf.",[243,777,778],{"class":438},"principalCurvatures",[243,780,448],{"class":249},[243,782,783,785,788,791,794,797,799],{"class":245,"line":260},[243,784,758],{"class":253},[243,786,787],{"class":289}," c0",[243,789,790],{"class":253}," =",[243,792,793],{"class":249}," k0.data, ",[243,795,796],{"class":289},"c1",[243,798,790],{"class":253},[243,800,801],{"class":249}," k1.data;\n",[243,803,804],{"class":245,"line":271},[243,805,275],{"emptyLinePlaceholder":274},[243,807,808,810,812,814,817,820,822,825,827,829],{"class":245,"line":278},[243,809,521],{"class":253},[243,811,524],{"class":249},[243,813,758],{"class":253},[243,815,816],{"class":289}," v",[243,818,819],{"class":253}," of",[243,821,775],{"class":249},[243,823,824],{"class":438},"sharpEdges",[243,826,540],{"class":249},[243,828,690],{"class":289},[243,830,831],{"class":249},").data)\n",[243,833,834,837,839,842,844,847,850],{"class":245,"line":300},[243,835,836],{"class":249},"  c0[v] ",[243,838,254],{"class":253},[243,840,841],{"class":249}," c1[v] ",[243,843,254],{"class":253},[243,845,846],{"class":289}," Infinity",[243,848,849],{"class":249},";                    ",[243,851,852],{"class":296},"\u002F\u002F creases: infinite\n",[243,854,855,857,859,861,863,865,867,870],{"class":245,"line":305},[243,856,521],{"class":253},[243,858,524],{"class":249},[243,860,758],{"class":253},[243,862,816],{"class":289},[243,864,819],{"class":253},[243,866,775],{"class":249},[243,868,869],{"class":438},"boundaryEdges",[243,871,872],{"class":249},"(mesh).data)\n",[243,874,875,877,879,881,883,886,889],{"class":245,"line":316},[243,876,836],{"class":249},[243,878,254],{"class":253},[243,880,841],{"class":249},[243,882,254],{"class":253},[243,884,885],{"class":289}," NaN",[243,887,888],{"class":249},";                         ",[243,890,891],{"class":296},"\u002F\u002F boundary: no estimate\n",[11,893,894],{},"The boundary runs last, so a crease that reaches an open edge reads as no estimate there.",[11,896,897,900,901,904],{},[68,898,899],{},"Nearly flat regions."," The shape index ignores size. On a plane both curvatures are zero and the shape index is 0, the same as a saddle, so on a nearly flat scan the class comes from the noise. Mask by curvedness, ",[42,902,903],{},"sqrt((k0² + k1²) \u002F 2)",", and read a class only where it passes a threshold.",[11,906,907,910,911,914],{},[68,908,909],{},"Uneven meshes."," The ring counts edges, so its size follows the local edge length. Where the density varies a lot, remesh to even edges first with ",[42,912,913],{},"isotropic_remeshed",", then fit.",[34,916,918],{"id":917},"checking-the-result","Checking the result",[11,920,921,922,925,926,929,930,933],{},"Curvature fails silently, so test on shapes with a known answer. On a sphere of radius r, both principal curvatures are 1\u002Fr and every vertex is a cap. On a flat grid bent into ",[42,923,924],{},"z = x² − y²",", the centre is a saddle with shape index 0. ",[42,927,928],{},"z = −x²"," bends away from a +z normal and gives a ridge at +0.5; ",[42,931,932],{},"z = x²"," gives a trough at −0.5. The directions should be perpendicular to each other and to the vertex normal, to within rounding. The tutorials run each check in their language.",[34,935,937],{"id":936},"principal-curvatures-in-code","Principal curvatures in code",[11,939,940,941,53,943,945],{},"Each example computes ",[42,942,44],{},[42,944,48],{}," at every vertex, then the directions with a wider ring.",[416,947,948,1059,1206],{},[234,949,951],{"className":420,"code":950,"filename":422,"language":423,"meta":239,"style":239},"#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto mesh_buffer = tf::read_stl(\"part.stl\");\nauto mesh = mesh_buffer.polygons();\n\nauto [k0, k1] = tf::make_principal_curvatures(mesh);\n\u002F\u002F k0, k1: tf::buffer\u003Cfloat>, one value per point; k0 >= k1\n\nauto [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",[42,952,953,962,966,990,1007,1011,1027,1032,1036,1054],{"__ignoreMap":239},[243,954,955,958],{"class":245,"line":246},[243,956,957],{"class":253},"#include",[243,959,961],{"class":960},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[243,963,964],{"class":245,"line":260},[243,965,275],{"emptyLinePlaceholder":274},[243,967,968,970,973,975,977,979,982,984,987],{"class":245,"line":271},[243,969,430],{"class":253},[243,971,972],{"class":249}," mesh_buffer ",[243,974,254],{"class":253},[243,976,439],{"class":438},[243,978,442],{"class":249},[243,980,981],{"class":438},"read_stl",[243,983,550],{"class":249},[243,985,986],{"class":960},"\"part.stl\"",[243,988,989],{"class":249},");\n",[243,991,992,994,997,999,1002,1005],{"class":245,"line":278},[243,993,430],{"class":253},[243,995,996],{"class":249}," mesh ",[243,998,254],{"class":253},[243,1000,1001],{"class":249}," mesh_buffer.",[243,1003,1004],{"class":438},"polygons",[243,1006,484],{"class":249},[243,1008,1009],{"class":245,"line":300},[243,1010,275],{"emptyLinePlaceholder":274},[243,1012,1013,1015,1017,1019,1021,1023,1025],{"class":245,"line":305},[243,1014,430],{"class":253},[243,1016,433],{"class":249},[243,1018,254],{"class":253},[243,1020,439],{"class":438},[243,1022,442],{"class":249},[243,1024,445],{"class":438},[243,1026,448],{"class":249},[243,1028,1029],{"class":245,"line":316},[243,1030,1031],{"class":296},"\u002F\u002F k0, k1: tf::buffer\u003Cfloat>, one value per point; k0 >= k1\n",[243,1033,1034],{"class":245,"line":336},[243,1035,275],{"emptyLinePlaceholder":274},[243,1037,1038,1040,1043,1045,1047,1049,1052],{"class":245,"line":718},[243,1039,430],{"class":253},[243,1041,1042],{"class":249}," [c0, c1, d0, d1] ",[243,1044,254],{"class":253},[243,1046,439],{"class":438},[243,1048,442],{"class":249},[243,1050,1051],{"class":438},"make_principal_directions",[243,1053,448],{"class":249},[243,1055,1056],{"class":245,"line":729},[243,1057,1058],{"class":296},"\u002F\u002F d0, d1: tf::unit_vectors_buffer\u003Cfloat, 3>, one direction per point\n",[234,1060,1062],{"className":236,"code":1061,"filename":630,"language":238,"meta":239,"style":239},"import trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"part.stl\"))\n\nk0, k1 = tf.principal_curvatures(mesh)          # k = 2 rings by default\nk0, k1, d0, d1 = tf.principal_curvatures(mesh, k=4, directions=True)\n\ngaussian = k0 * k1\nmean = 0.5 * (k0 + k1)\nsi = tf.shape_index(mesh, k=4)\n",[42,1063,1064,1076,1080,1101,1105,1117,1148,1152,1167,1189],{"__ignoreMap":239},[243,1065,1066,1068,1071,1073],{"class":245,"line":246},[243,1067,637],{"class":253},[243,1069,1070],{"class":249}," trueform ",[243,1072,643],{"class":253},[243,1074,1075],{"class":249}," tf\n",[243,1077,1078],{"class":245,"line":260},[243,1079,275],{"emptyLinePlaceholder":274},[243,1081,1082,1085,1087,1090,1093,1096,1098],{"class":245,"line":271},[243,1083,1084],{"class":249},"mesh ",[243,1086,254],{"class":253},[243,1088,1089],{"class":249}," tf.Mesh(",[243,1091,1092],{"class":253},"*",[243,1094,1095],{"class":249},"tf.read_stl(",[243,1097,986],{"class":960},[243,1099,1100],{"class":249},"))\n",[243,1102,1103],{"class":245,"line":278},[243,1104,275],{"emptyLinePlaceholder":274},[243,1106,1107,1109,1111,1114],{"class":245,"line":300},[243,1108,250],{"class":249},[243,1110,254],{"class":253},[243,1112,1113],{"class":249}," tf.principal_curvatures(mesh)          ",[243,1115,1116],{"class":296},"# k = 2 rings by default\n",[243,1118,1119,1122,1124,1127,1130,1132,1135,1137,1140,1142,1145],{"class":245,"line":305},[243,1120,1121],{"class":249},"k0, k1, d0, d1 ",[243,1123,254],{"class":253},[243,1125,1126],{"class":249}," tf.principal_curvatures(mesh, ",[243,1128,159],{"class":1129},"s9osk",[243,1131,254],{"class":253},[243,1133,1134],{"class":289},"4",[243,1136,71],{"class":249},[243,1138,1139],{"class":1129},"directions",[243,1141,254],{"class":253},[243,1143,1144],{"class":289},"True",[243,1146,1147],{"class":249},")\n",[243,1149,1150],{"class":245,"line":316},[243,1151,275],{"emptyLinePlaceholder":274},[243,1153,1154,1157,1159,1162,1164],{"class":245,"line":336},[243,1155,1156],{"class":249},"gaussian ",[243,1158,254],{"class":253},[243,1160,1161],{"class":249}," k0 ",[243,1163,1092],{"class":253},[243,1165,1166],{"class":249}," k1\n",[243,1168,1169,1172,1174,1177,1180,1183,1186],{"class":245,"line":718},[243,1170,1171],{"class":249},"mean ",[243,1173,254],{"class":253},[243,1175,1176],{"class":289}," 0.5",[243,1178,1179],{"class":253}," *",[243,1181,1182],{"class":249}," (k0 ",[243,1184,1185],{"class":253},"+",[243,1187,1188],{"class":249}," k1)\n",[243,1190,1191,1193,1195,1198,1200,1202,1204],{"class":245,"line":729},[243,1192,263],{"class":249},[243,1194,254],{"class":253},[243,1196,1197],{"class":249}," tf.shape_index(mesh, ",[243,1199,159],{"class":1129},[243,1201,254],{"class":253},[243,1203,1134],{"class":289},[243,1205,1147],{"class":249},[234,1207,1209],{"className":748,"code":1208,"filename":750,"language":751,"meta":239,"style":239},"import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"part.stl\"));\n\nconst { k0, k1 } = tf.principalCurvatures(mesh);      \u002F\u002F k = 2 by default\nconst gaussian = k0.mul(k1);\n\nconst { d0, d1 } = tf.principalDirections(mesh, 4);   \u002F\u002F a wider ring\n\u002F\u002F d0, d1: NDArray, shape [numberOfPoints, 3]\n",[42,1210,1211,1232,1246,1250,1276,1280,1306,1324,1328,1359],{"__ignoreMap":239},[243,1212,1213,1215,1217,1220,1223,1226,1229],{"class":245,"line":246},[243,1214,637],{"class":253},[243,1216,1179],{"class":289},[243,1218,1219],{"class":253}," as",[243,1221,1222],{"class":249}," tf ",[243,1224,1225],{"class":253},"from",[243,1227,1228],{"class":960}," \"@polydera\u002Ftrueform\"",[243,1230,1231],{"class":249},";\n",[243,1233,1234,1236,1239,1241,1244],{"class":245,"line":260},[243,1235,637],{"class":253},[243,1237,1238],{"class":249}," { readFileSync } ",[243,1240,1225],{"class":253},[243,1242,1243],{"class":960}," \"node:fs\"",[243,1245,1231],{"class":249},[243,1247,1248],{"class":245,"line":271},[243,1249,275],{"emptyLinePlaceholder":274},[243,1251,1252,1254,1257,1259,1261,1264,1266,1269,1271,1273],{"class":245,"line":278},[243,1253,758],{"class":253},[243,1255,1256],{"class":289}," mesh",[243,1258,790],{"class":253},[243,1260,775],{"class":249},[243,1262,1263],{"class":438},"readStl",[243,1265,550],{"class":249},[243,1267,1268],{"class":438},"readFileSync",[243,1270,550],{"class":249},[243,1272,986],{"class":960},[243,1274,1275],{"class":249},"));\n",[243,1277,1278],{"class":245,"line":300},[243,1279,275],{"emptyLinePlaceholder":274},[243,1281,1282,1284,1286,1288,1290,1292,1294,1296,1298,1300,1303],{"class":245,"line":305},[243,1283,758],{"class":253},[243,1285,761],{"class":249},[243,1287,44],{"class":289},[243,1289,71],{"class":249},[243,1291,48],{"class":289},[243,1293,770],{"class":249},[243,1295,254],{"class":253},[243,1297,775],{"class":249},[243,1299,778],{"class":438},[243,1301,1302],{"class":249},"(mesh);      ",[243,1304,1305],{"class":296},"\u002F\u002F k = 2 by default\n",[243,1307,1308,1310,1313,1315,1318,1321],{"class":245,"line":316},[243,1309,758],{"class":253},[243,1311,1312],{"class":289}," gaussian",[243,1314,790],{"class":253},[243,1316,1317],{"class":249}," k0.",[243,1319,1320],{"class":438},"mul",[243,1322,1323],{"class":249},"(k1);\n",[243,1325,1326],{"class":245,"line":336},[243,1327,275],{"emptyLinePlaceholder":274},[243,1329,1330,1332,1334,1336,1338,1340,1342,1344,1346,1349,1351,1353,1356],{"class":245,"line":718},[243,1331,758],{"class":253},[243,1333,761],{"class":249},[243,1335,52],{"class":289},[243,1337,71],{"class":249},[243,1339,56],{"class":289},[243,1341,770],{"class":249},[243,1343,254],{"class":253},[243,1345,775],{"class":249},[243,1347,1348],{"class":438},"principalDirections",[243,1350,540],{"class":249},[243,1352,1134],{"class":289},[243,1354,1355],{"class":249},");   ",[243,1357,1358],{"class":296},"\u002F\u002F a wider ring\n",[243,1360,1361],{"class":245,"line":729},[243,1362,1363],{"class":296},"\u002F\u002F d0, d1: NDArray, shape [numberOfPoints, 3]\n",[11,1365,1366],{},"Results come back with one value per vertex, in the mesh's own type, float or double. In JavaScript every call also has an async form that runs off the main thread.",[34,1368,1370],{"id":1369},"speed","Speed",[11,1372,1373],{},"Principal curvature values and directions on sphere meshes from 57K to 1.02M faces. Apple M4 Max, 16 threads, trueform 0.10.10.",[95,1375],{":headers":1376,":highlight":1377,":rows":1378},"[\"Language\",\"trueform, ~1M faces\",\"Compared against\"]","1","[[\"C++\",\"20.0 ms\",\"libigl 1,697 ms (84.8×)\"],[\"Python\",\"19.5 ms\",\"libigl 2,119 ms (108.7×)\"],[\"JavaScript (WebAssembly)\",\"24.5 ms\",\"no direct npm peer\"]]",[1380,1381],"hub-benchmark-figures",{"category":1382},"principal-curvatures",[11,1384,1385,1386,1390],{},"Every trueform benchmark, by module and language, is on the ",[15,1387,1389],{"href":1388},"\u002Ftrueform\u002Fbenchmarks","trueform benchmarks"," page.",[34,1392,1394],{"id":1393},"where-to-go-next","Where to go next",[11,1396,1397,359,1400,71,1403,53,1406,1409],{},[68,1398,1399],{},"Tutorials.",[15,1401,422],{"href":1402},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp",[15,1404,630],{"href":1405},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python",[15,1407,750],{"href":1408},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript",". Each covers values, directions, the shape index, signs, and working with the results.",[11,1411,1412,1415,1416,1419,1420,1422],{},[68,1413,1414],{},"Related operations."," Curves and bands of equal value with ",[15,1417,1418],{"href":206},"isocontours",". Distance along the surface with ",[15,1421,394],{"href":393},".",[11,1424,1425,1428,1429,71,1434,53,1438,1422],{},[68,1426,1427],{},"Documentation."," Curvature analysis in the geometry module, in ",[15,1430,422],{"href":1431,"rel":1432},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fgeometry",[1433],"nofollow",[15,1435,630],{"href":1436,"rel":1437},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fgeometry#curvature-analysis",[1433],[15,1439,1442],{"href":1440,"rel":1441},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fgeometry",[1433],"TypeScript",[11,1444,1445,1446,1448,1449,1453],{},"Curvature is one part of ",[15,1447,18],{"href":17},". The same engine does ",[15,1450,1452],{"href":1451},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans",", spatial queries, topology and remeshing, in C++, Python and the browser.",[1455,1456],"article-cta",{":buttons":1457},"[{\"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\u002Fpy\u002Fmodules\u002Fgeometry#curvature-analysis\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[1459,1460],"cite-as",{"author":1461,"title":5},"Sajovic, {\\v{Z}}iga",[1463,1464,1465],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":239,"searchDepth":260,"depth":260,"links":1467},[1468,1469,1470,1471,1472,1473,1474,1475,1476],{"id":36,"depth":260,"text":37},{"id":116,"depth":260,"text":117},{"id":176,"depth":260,"text":177},{"id":196,"depth":260,"text":197},{"id":398,"depth":260,"text":399},{"id":917,"depth":260,"text":918},{"id":936,"depth":260,"text":937},{"id":1369,"depth":260,"text":1370},{"id":1393,"depth":260,"text":1394},"2026-11-08","How a mesh bends at every vertex: principal curvatures and directions, Gaussian and mean curvature, the shape index. Which estimator trueform uses, how it holds up on noisy scans, and what to do with the result.","md",[1481,1482,1483,1484,1485,1486,1487,1488,1489,1490],"principal curvature","principal curvatures mesh","mesh curvature","gaussian curvature mesh","mean curvature mesh","shape index","principal directions","curvature noisy scan","curvature python","curvature c++",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fprincipal-curvatures.png","\u002Falgorithms\u002Fprincipal-curvatures",{"title":1495,"description":1496},"Principal Curvatures of a Mesh: Estimator and Noise","Principal curvature on a triangle mesh: a quadric fit over the k-ring, how it holds up on noisy scans, and its use for feature lines and inspection.","algorithms\u002Fprincipal-curvatures",[1499,1500,1501],"curvature","geometry","overview","2026-11-12T12:06:28.000Z","keOC3SOt3j5bhmO12J0T5HCfnoAm0cGdZR8VSVPLB6A",1794486339528]