[{"data":1,"prerenderedAt":1773},["ShallowReactive",2],{"tutorial-fast-closest-point-on-a-mesh-in-cpp":3},{"id":4,"title":5,"author":6,"body":7,"date":1749,"description":1750,"extension":1751,"keywords":1752,"meta":1762,"navigation":148,"ogImage":1763,"path":1764,"published":148,"seo":1765,"stem":1766,"tags":1767,"__hash__":1772},"tutorials\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp.md","Fast and Robust Closest Point on a Mesh in C++","Žiga Sajovic",{"type":8,"value":9,"toc":1735},"minimark",[10,28,36,40,47,52,55,114,124,128,197,223,227,238,327,337,341,351,462,468,491,494,535,539,554,883,889,892,896,907,962,968,978,982,993,1100,1106,1114,1118,1121,1251,1255,1258,1501,1517,1521,1524,1566,1572,1578,1598,1602,1632,1636,1639,1643,1655,1660,1663,1666,1678,1682,1685,1689,1704,1707,1710,1716,1727,1731],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript","JavaScript",".",[14,29,30,31,35],{},"A closest-point query takes a point in space and returns the nearest point on a mesh surface, the face it lies on, and the distance. It is the base of projection, snapping, distance checks, and scan-to-CAD comparison. ",[32,33,34],"code",{},"trueform"," is header-only C++17. You build a spatial tree once, tag it onto the mesh, and query. This page covers single queries, batch queries over many points, radius-limited search, k-nearest faces, posed meshes, and the closest pair between two meshes.",[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\u002Fspatial#neighbor-search\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fclosest-points\",\"icon\":\"i-lucide-play\"}]",[14,41,42],{},[43,44],"img",{"alt":45,"src":46},"Query points around the Stanford dragon joined to their closest points on the surface","\u002Fimg\u002Ftutorials\u002Fclosest-point.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,27],{},[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. See ",[18,220,222],{"href":221},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-cpp","fast STL and OBJ reading",[48,224,226],{"id":225},"building-the-spatial-tree","Building the spatial tree",[14,228,229,230,237],{},"Queries run against a ",[18,231,234],{"href":232,"rel":233},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fspatial#tftree",[121],[32,235,236],{},"tf::aabb_tree",". Build it once and tag it onto the mesh:",[56,239,241],{"className":130,"code":240,"language":132,"meta":61,"style":61},"tf::aabb_tree\u003Cint, float, 3> tree(mesh, tf::config_tree(4, 4));\nauto form = mesh | tf::tag(tree);\n",[32,242,243,303],{"__ignoreMap":61},[65,244,245,248,250,253,256,259,262,265,267,271,274,277,279,282,284,286,288,291,293,296,298,300],{"class":67,"line":68},[65,246,247],{"class":109},"tf",[65,249,166],{"class":71},[65,251,252],{"class":109},"aabb_tree",[65,254,255],{"class":71},"\u003C",[65,257,258],{"class":102},"int",[65,260,261],{"class":71},", ",[65,263,264],{"class":102},"float",[65,266,261],{"class":71},[65,268,270],{"class":269},"sDLfK","3",[65,272,273],{"class":71},"> ",[65,275,276],{"class":109},"tree",[65,278,172],{"class":71},[65,280,281],{"class":109},"mesh",[65,283,261],{"class":71},[65,285,247],{"class":109},[65,287,166],{"class":71},[65,289,290],{"class":109},"config_tree",[65,292,172],{"class":71},[65,294,295],{"class":269},"4",[65,297,261],{"class":71},[65,299,295],{"class":269},[65,301,302],{"class":71},"));\n",[65,304,305,307,310,312,314,317,319,321,324],{"class":67,"line":75},[65,306,154],{"class":102},[65,308,309],{"class":71}," form ",[65,311,160],{"class":102},[65,313,185],{"class":71},[65,315,316],{"class":102},"|",[65,318,163],{"class":109},[65,320,166],{"class":71},[65,322,323],{"class":109},"tag",[65,325,326],{"class":71},"(tree);\n",[14,328,329,332,333,336],{},[32,330,331],{},"(4, 4)"," is four children per node and up to four faces per leaf. The docs recommend it for all workloads. ",[32,334,335],{},"form"," is the mesh with its tree attached. Every query below takes it.",[48,338,340],{"id":339},"one-point","One point",[14,342,343,350],{},[18,344,347],{"href":345,"rel":346},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fspatial#neighbor-search",[121],[32,348,349],{},"tf::neighbor_search"," finds the nearest face to a query point:",[56,352,354],{"className":130,"code":353,"language":132,"meta":61,"style":61},"auto query = tf::make_point(0.f, 0.f, 2.f);\n\nauto nearest = tf::neighbor_search(form, query);\nauto [face_id, metric_point] = nearest;\nauto [dist2, closest] = metric_point;\nfloat distance = std::sqrt(dist2);\n",[32,355,356,395,399,418,430,442],{"__ignoreMap":61},[65,357,358,360,363,365,367,369,372,374,377,380,382,384,386,388,391,393],{"class":67,"line":68},[65,359,154],{"class":102},[65,361,362],{"class":71}," query ",[65,364,160],{"class":102},[65,366,163],{"class":109},[65,368,166],{"class":71},[65,370,371],{"class":109},"make_point",[65,373,172],{"class":71},[65,375,376],{"class":269},"0.",[65,378,379],{"class":102},"f",[65,381,261],{"class":71},[65,383,376],{"class":269},[65,385,379],{"class":102},[65,387,261],{"class":71},[65,389,390],{"class":269},"2.",[65,392,379],{"class":102},[65,394,178],{"class":71},[65,396,397],{"class":67,"line":75},[65,398,149],{"emptyLinePlaceholder":148},[65,400,401,403,406,408,410,412,415],{"class":67,"line":81},[65,402,154],{"class":102},[65,404,405],{"class":71}," nearest ",[65,407,160],{"class":102},[65,409,163],{"class":109},[65,411,166],{"class":71},[65,413,414],{"class":109},"neighbor_search",[65,416,417],{"class":71},"(form, query);\n",[65,419,420,422,425,427],{"class":67,"line":87},[65,421,154],{"class":102},[65,423,424],{"class":71}," [face_id, metric_point] ",[65,426,160],{"class":102},[65,428,429],{"class":71}," nearest;\n",[65,431,432,434,437,439],{"class":67,"line":93},[65,433,154],{"class":102},[65,435,436],{"class":71}," [dist2, closest] ",[65,438,160],{"class":102},[65,440,441],{"class":71}," metric_point;\n",[65,443,444,446,449,451,454,456,459],{"class":67,"line":99},[65,445,264],{"class":102},[65,447,448],{"class":71}," distance ",[65,450,160],{"class":102},[65,452,453],{"class":109}," std",[65,455,166],{"class":71},[65,457,458],{"class":109},"sqrt",[65,460,461],{"class":71},"(dist2);\n",[14,463,464],{},[43,465],{"alt":466,"src":467},"Step: one point. A query point (orange) off the Stanford dragon, joined to its closest point on the surface (pink)","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-1-one-point.png",[14,469,470,473,474,477,478,481,482,261,485,487,488,490],{},[32,471,472],{},"face_id"," is the index of the nearest face. ",[32,475,476],{},"dist2"," is the squared distance. ",[32,479,480],{},"closest"," is the point on that face. For a unit sphere and a query at ",[32,483,484],{},"(0, 0, 2)",[32,486,476],{}," is 1 and ",[32,489,480],{}," is the north pole.",[14,492,493],{},"If you only need the distance, ask for it directly:",[56,495,497],{"className":130,"code":496,"language":132,"meta":61,"style":61},"float d = tf::distance(form, query);\nfloat d2 = tf::distance2(form, query);\n",[32,498,499,517],{"__ignoreMap":61},[65,500,501,503,506,508,510,512,515],{"class":67,"line":68},[65,502,264],{"class":102},[65,504,505],{"class":71}," d ",[65,507,160],{"class":102},[65,509,163],{"class":109},[65,511,166],{"class":71},[65,513,514],{"class":109},"distance",[65,516,417],{"class":71},[65,518,519,521,524,526,528,530,533],{"class":67,"line":75},[65,520,264],{"class":102},[65,522,523],{"class":71}," d2 ",[65,525,160],{"class":102},[65,527,163],{"class":109},[65,529,166],{"class":71},[65,531,532],{"class":109},"distance2",[65,534,417],{"class":71},[48,536,538],{"id":537},"many-points-at-once","Many points at once",[14,540,541,542,545,546,549,550,553],{},"A query is a ",[32,543,544],{},"const"," read of the tree, so queries run in parallel. ",[32,547,548],{},"tf::parallel_for_each"," over a ",[32,551,552],{},"tf::zip"," of inputs and outputs is the usual form:",[56,555,557],{"className":130,"code":556,"language":132,"meta":61,"style":61},"std::vector\u003Ctf::point\u003Cfloat, 3>> queries(100000);\nfor (auto &q : queries)\n  q = tf::make_point(tf::random(-1.5f, 1.5f), tf::random(-1.5f, 1.5f),\n                     tf::random(-1.5f, 1.5f));\n\nstd::vector\u003Cint> face_ids(queries.size());\nstd::vector\u003Ctf::point\u003Cfloat, 3>> closest_pts(queries.size());\n\ntf::parallel_for_each(tf::zip(queries, face_ids, closest_pts), [&](auto row) {\n  auto &&[q, id, pt] = row;\n  auto [fid, mp] = tf::neighbor_search(form, q);\n  id = fid;\n  pt = mp.point;\n});\n",[32,558,559,599,615,679,704,708,737,777,782,819,836,855,866,877],{"__ignoreMap":61},[65,560,561,564,566,569,571,573,575,578,580,582,584,586,589,592,594,597],{"class":67,"line":68},[65,562,563],{"class":109},"std",[65,565,166],{"class":71},[65,567,568],{"class":109},"vector",[65,570,255],{"class":71},[65,572,247],{"class":109},[65,574,166],{"class":71},[65,576,577],{"class":109},"point",[65,579,255],{"class":71},[65,581,264],{"class":102},[65,583,261],{"class":71},[65,585,270],{"class":269},[65,587,588],{"class":71},">> ",[65,590,591],{"class":109},"queries",[65,593,172],{"class":71},[65,595,596],{"class":269},"100000",[65,598,178],{"class":71},[65,600,601,604,607,609,612],{"class":67,"line":75},[65,602,603],{"class":102},"for",[65,605,606],{"class":71}," (",[65,608,154],{"class":102},[65,610,611],{"class":102}," &",[65,613,614],{"class":71},"q : queries)\n",[65,616,617,620,622,624,626,628,630,632,634,637,639,642,645,647,649,651,653,656,658,660,662,664,666,668,670,672,674,676],{"class":67,"line":81},[65,618,619],{"class":71},"  q ",[65,621,160],{"class":102},[65,623,163],{"class":109},[65,625,166],{"class":71},[65,627,371],{"class":109},[65,629,172],{"class":71},[65,631,247],{"class":109},[65,633,166],{"class":71},[65,635,636],{"class":109},"random",[65,638,172],{"class":71},[65,640,641],{"class":102},"-",[65,643,644],{"class":269},"1.5",[65,646,379],{"class":102},[65,648,261],{"class":71},[65,650,644],{"class":269},[65,652,379],{"class":102},[65,654,655],{"class":71},"), ",[65,657,247],{"class":109},[65,659,166],{"class":71},[65,661,636],{"class":109},[65,663,172],{"class":71},[65,665,641],{"class":102},[65,667,644],{"class":269},[65,669,379],{"class":102},[65,671,261],{"class":71},[65,673,644],{"class":269},[65,675,379],{"class":102},[65,677,678],{"class":71},"),\n",[65,680,681,684,686,688,690,692,694,696,698,700,702],{"class":67,"line":87},[65,682,683],{"class":109},"                     tf",[65,685,166],{"class":71},[65,687,636],{"class":109},[65,689,172],{"class":71},[65,691,641],{"class":102},[65,693,644],{"class":269},[65,695,379],{"class":102},[65,697,261],{"class":71},[65,699,644],{"class":269},[65,701,379],{"class":102},[65,703,302],{"class":71},[65,705,706],{"class":67,"line":93},[65,707,149],{"emptyLinePlaceholder":148},[65,709,710,712,714,716,718,720,722,725,727,729,731,734],{"class":67,"line":99},[65,711,563],{"class":109},[65,713,166],{"class":71},[65,715,568],{"class":109},[65,717,255],{"class":71},[65,719,258],{"class":102},[65,721,273],{"class":71},[65,723,724],{"class":109},"face_ids",[65,726,172],{"class":71},[65,728,591],{"class":109},[65,730,27],{"class":71},[65,732,733],{"class":109},"size",[65,735,736],{"class":71},"());\n",[65,738,740,742,744,746,748,750,752,754,756,758,760,762,764,767,769,771,773,775],{"class":67,"line":739},7,[65,741,563],{"class":109},[65,743,166],{"class":71},[65,745,568],{"class":109},[65,747,255],{"class":71},[65,749,247],{"class":109},[65,751,166],{"class":71},[65,753,577],{"class":109},[65,755,255],{"class":71},[65,757,264],{"class":102},[65,759,261],{"class":71},[65,761,270],{"class":269},[65,763,588],{"class":71},[65,765,766],{"class":109},"closest_pts",[65,768,172],{"class":71},[65,770,591],{"class":109},[65,772,27],{"class":71},[65,774,733],{"class":109},[65,776,736],{"class":71},[65,778,780],{"class":67,"line":779},8,[65,781,149],{"emptyLinePlaceholder":148},[65,783,785,787,789,792,794,796,798,801,804,807,810,812,816],{"class":67,"line":784},9,[65,786,247],{"class":109},[65,788,166],{"class":71},[65,790,791],{"class":109},"parallel_for_each",[65,793,172],{"class":71},[65,795,247],{"class":109},[65,797,166],{"class":71},[65,799,800],{"class":109},"zip",[65,802,803],{"class":71},"(queries, face_ids, closest_pts), [",[65,805,806],{"class":102},"&",[65,808,809],{"class":71},"](",[65,811,154],{"class":102},[65,813,815],{"class":814},"s9osk"," row",[65,817,818],{"class":71},") {\n",[65,820,822,825,828,831,833],{"class":67,"line":821},10,[65,823,824],{"class":102},"  auto",[65,826,827],{"class":102}," &&",[65,829,830],{"class":71},"[q, id, pt] ",[65,832,160],{"class":102},[65,834,835],{"class":71}," row;\n",[65,837,839,841,844,846,848,850,852],{"class":67,"line":838},11,[65,840,824],{"class":102},[65,842,843],{"class":71}," [fid, mp] ",[65,845,160],{"class":102},[65,847,163],{"class":109},[65,849,166],{"class":71},[65,851,414],{"class":109},[65,853,854],{"class":71},"(form, q);\n",[65,856,858,861,863],{"class":67,"line":857},12,[65,859,860],{"class":71},"  id ",[65,862,160],{"class":102},[65,864,865],{"class":71}," fid;\n",[65,867,869,872,874],{"class":67,"line":868},13,[65,870,871],{"class":71},"  pt ",[65,873,160],{"class":102},[65,875,876],{"class":71}," mp.point;\n",[65,878,880],{"class":67,"line":879},14,[65,881,882],{"class":71},"});\n",[14,884,885],{},[43,886],{"alt":887,"src":888},"Step: many points at once. 100,000 queries around the dragon; their closest points mark the surface in pink, with a sample of queries (orange) joined to them","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-2-many-points.png",[14,890,891],{},"The tree is built once. The loop only pays for the queries.",[48,893,895],{"id":894},"search-within-a-radius","Search within a radius",[14,897,898,899,902,903,906],{},"Pass a radius to ignore anything farther away. The result converts to ",[32,900,901],{},"bool"," and is ",[32,904,905],{},"false"," when nothing is in range:",[56,908,910],{"className":130,"code":909,"language":132,"meta":61,"style":61},"auto within = tf::neighbor_search(form, query, 0.5f);\nif (within) {\n  auto [id, mp] = within;\n}\n",[32,911,912,937,945,957],{"__ignoreMap":61},[65,913,914,916,919,921,923,925,927,930,933,935],{"class":67,"line":68},[65,915,154],{"class":102},[65,917,918],{"class":71}," within ",[65,920,160],{"class":102},[65,922,163],{"class":109},[65,924,166],{"class":71},[65,926,414],{"class":109},[65,928,929],{"class":71},"(form, query, ",[65,931,932],{"class":269},"0.5",[65,934,379],{"class":102},[65,936,178],{"class":71},[65,938,939,942],{"class":67,"line":75},[65,940,941],{"class":102},"if",[65,943,944],{"class":71}," (within) {\n",[65,946,947,949,952,954],{"class":67,"line":81},[65,948,824],{"class":102},[65,950,951],{"class":71}," [id, mp] ",[65,953,160],{"class":102},[65,955,956],{"class":71}," within;\n",[65,958,959],{"class":67,"line":87},[65,960,961],{"class":71},"}\n",[14,963,964],{},[43,965],{"alt":966,"src":967},"Step: search within a radius. Queries inside the radius (orange) joined to their closest points; the rest (grey) come back as misses","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-3-radius.png",[14,969,970,971,974,975,977],{},"The radius is a distance, not a squared distance. The query above is 1 away from the sphere, so ",[32,972,973],{},"within"," is ",[32,976,905],{},". A tight radius also prunes the search. Use it when you know the scale you care about, as in snapping or contact checks.",[48,979,981],{"id":980},"k-nearest-faces","k nearest faces",[14,983,984,985,988,989,992],{},"For the ",[32,986,987],{},"k"," nearest faces, give ",[32,990,991],{},"tf::make_nearest_neighbors"," a buffer to fill. The results come back sorted by distance:",[56,994,996],{"className":130,"code":995,"language":132,"meta":61,"style":61},"std::array\u003Ctf::nearest_neighbor\u003Cint, float, 3>, 4> buffer;\nauto knn = tf::make_nearest_neighbors(buffer.begin(), 4);\ntf::neighbor_search(form, query, knn);\n\nfor (auto [id, mp] : knn) {\n  \u002F\u002F mp.metric is the squared distance, mp.point the closest point\n}\n",[32,997,998,1035,1064,1075,1079,1090,1096],{"__ignoreMap":61},[65,999,1000,1002,1005,1007,1009,1012,1015,1017,1019,1021,1023,1026,1028,1030,1032],{"class":67,"line":68},[65,1001,563],{"class":109},[65,1003,1004],{"class":71},"::array",[65,1006,255],{"class":102},[65,1008,247],{"class":109},[65,1010,1011],{"class":71},"::nearest_neighbor",[65,1013,1014],{"class":102},"\u003Cint",[65,1016,261],{"class":71},[65,1018,264],{"class":102},[65,1020,261],{"class":71},[65,1022,270],{"class":269},[65,1024,1025],{"class":102},">",[65,1027,261],{"class":71},[65,1029,295],{"class":269},[65,1031,1025],{"class":102},[65,1033,1034],{"class":71}," buffer;\n",[65,1036,1037,1039,1042,1044,1046,1048,1051,1054,1057,1060,1062],{"class":67,"line":75},[65,1038,154],{"class":102},[65,1040,1041],{"class":71}," knn ",[65,1043,160],{"class":102},[65,1045,163],{"class":109},[65,1047,166],{"class":71},[65,1049,1050],{"class":109},"make_nearest_neighbors",[65,1052,1053],{"class":71},"(buffer.",[65,1055,1056],{"class":109},"begin",[65,1058,1059],{"class":71},"(), ",[65,1061,295],{"class":269},[65,1063,178],{"class":71},[65,1065,1066,1068,1070,1072],{"class":67,"line":81},[65,1067,247],{"class":109},[65,1069,166],{"class":71},[65,1071,414],{"class":109},[65,1073,1074],{"class":71},"(form, query, knn);\n",[65,1076,1077],{"class":67,"line":87},[65,1078,149],{"emptyLinePlaceholder":148},[65,1080,1081,1083,1085,1087],{"class":67,"line":93},[65,1082,603],{"class":102},[65,1084,606],{"class":71},[65,1086,154],{"class":102},[65,1088,1089],{"class":71}," [id, mp] : knn) {\n",[65,1091,1092],{"class":67,"line":99},[65,1093,1095],{"class":1094},"sAwPA","  \u002F\u002F mp.metric is the squared distance, mp.point the closest point\n",[65,1097,1098],{"class":67,"line":739},[65,1099,961],{"class":71},[14,1101,1102],{},[43,1103],{"alt":1104,"src":1105},"Step: k nearest faces. Close-up of three queries next to the dragon, each with its four nearest faces in pink and joined to them","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-4-k-nearest.png",[14,1107,1108,1111,1112,27],{},[32,1109,1110],{},"tf::make_nearest_neighbors(buffer.begin(), 4, radius)"," adds a radius. It may then find fewer than ",[32,1113,987],{},[48,1115,1117],{"id":1116},"posed-meshes","Posed meshes",[14,1119,1120],{},"Tag a transformation onto the form and queries run in world space. The tree is not rebuilt and no vertex is copied:",[56,1122,1124],{"className":130,"code":1123,"language":132,"meta":61,"style":61},"auto T = tf::make_transformation_from_translation(\n    tf::make_vector(5.f, 0.f, 0.f));\nauto moved = form | tf::tag(T);\n\nauto [mid, mmp] = tf::neighbor_search(moved, tf::make_point(5.f, 0.f, 2.f));\n\u002F\u002F mmp.point is in world space: (5, 0, 1)\n",[32,1125,1126,1145,1176,1198,1202,1246],{"__ignoreMap":61},[65,1127,1128,1130,1133,1135,1137,1139,1142],{"class":67,"line":68},[65,1129,154],{"class":102},[65,1131,1132],{"class":71}," T ",[65,1134,160],{"class":102},[65,1136,163],{"class":109},[65,1138,166],{"class":71},[65,1140,1141],{"class":109},"make_transformation_from_translation",[65,1143,1144],{"class":71},"(\n",[65,1146,1147,1150,1152,1155,1157,1160,1162,1164,1166,1168,1170,1172,1174],{"class":67,"line":75},[65,1148,1149],{"class":109},"    tf",[65,1151,166],{"class":71},[65,1153,1154],{"class":109},"make_vector",[65,1156,172],{"class":71},[65,1158,1159],{"class":269},"5.",[65,1161,379],{"class":102},[65,1163,261],{"class":71},[65,1165,376],{"class":269},[65,1167,379],{"class":102},[65,1169,261],{"class":71},[65,1171,376],{"class":269},[65,1173,379],{"class":102},[65,1175,302],{"class":71},[65,1177,1178,1180,1183,1185,1187,1189,1191,1193,1195],{"class":67,"line":81},[65,1179,154],{"class":102},[65,1181,1182],{"class":71}," moved ",[65,1184,160],{"class":102},[65,1186,309],{"class":71},[65,1188,316],{"class":102},[65,1190,163],{"class":109},[65,1192,166],{"class":71},[65,1194,323],{"class":109},[65,1196,1197],{"class":71},"(T);\n",[65,1199,1200],{"class":67,"line":87},[65,1201,149],{"emptyLinePlaceholder":148},[65,1203,1204,1206,1209,1211,1213,1215,1217,1220,1222,1224,1226,1228,1230,1232,1234,1236,1238,1240,1242,1244],{"class":67,"line":93},[65,1205,154],{"class":102},[65,1207,1208],{"class":71}," [mid, mmp] ",[65,1210,160],{"class":102},[65,1212,163],{"class":109},[65,1214,166],{"class":71},[65,1216,414],{"class":109},[65,1218,1219],{"class":71},"(moved, ",[65,1221,247],{"class":109},[65,1223,166],{"class":71},[65,1225,371],{"class":109},[65,1227,172],{"class":71},[65,1229,1159],{"class":269},[65,1231,379],{"class":102},[65,1233,261],{"class":71},[65,1235,376],{"class":269},[65,1237,379],{"class":102},[65,1239,261],{"class":71},[65,1241,390],{"class":269},[65,1243,379],{"class":102},[65,1245,302],{"class":71},[65,1247,1248],{"class":67,"line":99},[65,1249,1250],{"class":1094},"\u002F\u002F mmp.point is in world space: (5, 0, 1)\n",[48,1252,1254],{"id":1253},"closest-pair-between-two-meshes","Closest pair between two meshes",[14,1256,1257],{},"Pass a second form instead of a point. The result holds a face id on each mesh and a point on each:",[56,1259,1261],{"className":130,"code":1260,"language":132,"meta":61,"style":61},"auto box_buffer = tf::make_box_mesh(1.f, 1.f, 1.f);\nauto box = box_buffer.polygons();\ntf::aabb_tree\u003Cint, float, 3> box_tree(box, tf::config_tree(4, 4));\nauto box_form = box | tf::tag(box_tree)\n    | tf::tag(tf::make_transformation_from_translation(\n          tf::make_vector(3.f, 0.f, 0.f)));\n\nauto pair = tf::neighbor_search(form, box_form);\nauto [ids, mpp] = pair;\nauto [id_a, id_b] = ids;\nauto [pd2, pt_a, pt_b] = mpp;\n\u002F\u002F pd2 == 2.25: sphere at x = 1, box face at x = 2.5\n",[32,1262,1263,1300,1316,1364,1386,1407,1438,1442,1460,1472,1484,1496],{"__ignoreMap":61},[65,1264,1265,1267,1270,1272,1274,1276,1279,1281,1284,1286,1288,1290,1292,1294,1296,1298],{"class":67,"line":68},[65,1266,154],{"class":102},[65,1268,1269],{"class":71}," box_buffer ",[65,1271,160],{"class":102},[65,1273,163],{"class":109},[65,1275,166],{"class":71},[65,1277,1278],{"class":109},"make_box_mesh",[65,1280,172],{"class":71},[65,1282,1283],{"class":269},"1.",[65,1285,379],{"class":102},[65,1287,261],{"class":71},[65,1289,1283],{"class":269},[65,1291,379],{"class":102},[65,1293,261],{"class":71},[65,1295,1283],{"class":269},[65,1297,379],{"class":102},[65,1299,178],{"class":71},[65,1301,1302,1304,1307,1309,1312,1314],{"class":67,"line":75},[65,1303,154],{"class":102},[65,1305,1306],{"class":71}," box ",[65,1308,160],{"class":102},[65,1310,1311],{"class":71}," box_buffer.",[65,1313,193],{"class":109},[65,1315,196],{"class":71},[65,1317,1318,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338,1341,1343,1346,1348,1350,1352,1354,1356,1358,1360,1362],{"class":67,"line":81},[65,1319,247],{"class":109},[65,1321,166],{"class":71},[65,1323,252],{"class":109},[65,1325,255],{"class":71},[65,1327,258],{"class":102},[65,1329,261],{"class":71},[65,1331,264],{"class":102},[65,1333,261],{"class":71},[65,1335,270],{"class":269},[65,1337,273],{"class":71},[65,1339,1340],{"class":109},"box_tree",[65,1342,172],{"class":71},[65,1344,1345],{"class":109},"box",[65,1347,261],{"class":71},[65,1349,247],{"class":109},[65,1351,166],{"class":71},[65,1353,290],{"class":109},[65,1355,172],{"class":71},[65,1357,295],{"class":269},[65,1359,261],{"class":71},[65,1361,295],{"class":269},[65,1363,302],{"class":71},[65,1365,1366,1368,1371,1373,1375,1377,1379,1381,1383],{"class":67,"line":87},[65,1367,154],{"class":102},[65,1369,1370],{"class":71}," box_form ",[65,1372,160],{"class":102},[65,1374,1306],{"class":71},[65,1376,316],{"class":102},[65,1378,163],{"class":109},[65,1380,166],{"class":71},[65,1382,323],{"class":109},[65,1384,1385],{"class":71},"(box_tree)\n",[65,1387,1388,1391,1393,1395,1397,1399,1401,1403,1405],{"class":67,"line":93},[65,1389,1390],{"class":102},"    |",[65,1392,163],{"class":109},[65,1394,166],{"class":71},[65,1396,323],{"class":109},[65,1398,172],{"class":71},[65,1400,247],{"class":109},[65,1402,166],{"class":71},[65,1404,1141],{"class":109},[65,1406,1144],{"class":71},[65,1408,1409,1412,1414,1416,1418,1421,1423,1425,1427,1429,1431,1433,1435],{"class":67,"line":99},[65,1410,1411],{"class":109},"          tf",[65,1413,166],{"class":71},[65,1415,1154],{"class":109},[65,1417,172],{"class":71},[65,1419,1420],{"class":269},"3.",[65,1422,379],{"class":102},[65,1424,261],{"class":71},[65,1426,376],{"class":269},[65,1428,379],{"class":102},[65,1430,261],{"class":71},[65,1432,376],{"class":269},[65,1434,379],{"class":102},[65,1436,1437],{"class":71},")));\n",[65,1439,1440],{"class":67,"line":739},[65,1441,149],{"emptyLinePlaceholder":148},[65,1443,1444,1446,1449,1451,1453,1455,1457],{"class":67,"line":779},[65,1445,154],{"class":102},[65,1447,1448],{"class":71}," pair ",[65,1450,160],{"class":102},[65,1452,163],{"class":109},[65,1454,166],{"class":71},[65,1456,414],{"class":109},[65,1458,1459],{"class":71},"(form, box_form);\n",[65,1461,1462,1464,1467,1469],{"class":67,"line":784},[65,1463,154],{"class":102},[65,1465,1466],{"class":71}," [ids, mpp] ",[65,1468,160],{"class":102},[65,1470,1471],{"class":71}," pair;\n",[65,1473,1474,1476,1479,1481],{"class":67,"line":821},[65,1475,154],{"class":102},[65,1477,1478],{"class":71}," [id_a, id_b] ",[65,1480,160],{"class":102},[65,1482,1483],{"class":71}," ids;\n",[65,1485,1486,1488,1491,1493],{"class":67,"line":838},[65,1487,154],{"class":102},[65,1489,1490],{"class":71}," [pd2, pt_a, pt_b] ",[65,1492,160],{"class":102},[65,1494,1495],{"class":71}," mpp;\n",[65,1497,1498],{"class":67,"line":857},[65,1499,1500],{"class":1094},"\u002F\u002F pd2 == 2.25: sphere at x = 1, box face at x = 2.5\n",[14,1502,1503,1504,1508,1509,1512,1513,1516],{},"The ",[18,1505,1507],{"href":1506},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-cpp","collision detection"," page covers this case in depth. For proximity between two meshes, the docs suggest an ",[32,1510,1511],{},"obb_tree"," or ",[32,1514,1515],{},"obbrss_tree"," for tighter culling. The call is the same.",[48,1518,1520],{"id":1519},"working-with-results","Working with results",[14,1522,1523],{},"The face ids index straight into per-face data. Face normals are one lookup:",[56,1525,1527],{"className":130,"code":1526,"language":132,"meta":61,"style":61},"auto normals = tf::compute_normals(mesh);\nauto n = normals[face_ids[0]];\n",[32,1528,1529,1548],{"__ignoreMap":61},[65,1530,1531,1533,1536,1538,1540,1542,1545],{"class":67,"line":68},[65,1532,154],{"class":102},[65,1534,1535],{"class":71}," normals ",[65,1537,160],{"class":102},[65,1539,163],{"class":109},[65,1541,166],{"class":71},[65,1543,1544],{"class":109},"compute_normals",[65,1546,1547],{"class":71},"(mesh);\n",[65,1549,1550,1552,1555,1557,1560,1563],{"class":67,"line":75},[65,1551,154],{"class":102},[65,1553,1554],{"class":71}," n ",[65,1556,160],{"class":102},[65,1558,1559],{"class":71}," normals[face_ids[",[65,1561,1562],{"class":269},"0",[65,1564,1565],{"class":71},"]];\n",[14,1567,1568],{},[43,1569],{"alt":1570,"src":1571},"Step: working with results. Face normals looked up at the closest points, drawn as short pink lines on the dragon","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-5-normals.png",[14,1573,1574,1575,1577],{},"Project points onto the surface by replacing them with ",[32,1576,766],{},". Report scan deviation as the square root of the metric.",[14,1579,1580,1581,1584,1585,1588,1589,1592,1593,27],{},"The distance here is unsigned. For a sign, negative inside, ",[32,1582,1583],{},"tf::signed_distance"," takes the same form with a ",[32,1586,1587],{},"face_membership"," and a ",[32,1590,1591],{},"manifold_edge_link"," tagged on. See the ",[18,1594,1597],{"href":1595,"rel":1596},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fspatial#distance-and-intersection",[121],"spatial docs",[48,1599,1601],{"id":1600},"related-tutorials","Related tutorials",[1603,1604,1605,1612,1619,1626],"ul",{},[1606,1607,1608,1611],"li",{},[18,1609,1610],{"href":1506},"Fast and Robust Mesh Collision Detection in C++"," — do two meshes touch, how far apart, and where.",[1606,1613,1614,1618],{},[18,1615,1617],{"href":1616},"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp","Fast and Accurate Geodesic Distances in C++"," — distance along the surface from one seed or many.",[1606,1620,1621,1625],{},[18,1622,1624],{"href":1623},"\u002Ftutorials\u002Ffast-connected-components-in-cpp","Fast Connected Components in C++"," — split a mesh into parts and find its boundary loops.",[1606,1627,1628,1631],{},[18,1629,1630],{"href":221},"Fast STL and OBJ Reading in C++"," — load and write meshes, the first step of every pipeline.",[48,1633,1635],{"id":1634},"performance","Performance",[14,1637,1638],{},"Every query on this page runs on the spatial tree. Building it is the up-front cost. The benchmark builds the tree on a 1,025,766-polygon mesh in C++.",[1640,1641],"headline-numbers",{":items":1642},"[{\"value\":\"24.9ms\",\"label\":\"Spatial tree build\",\"detail\":\"1,025,766 polygons\"},{\"value\":\"3×\",\"label\":\"Faster than MeshLib\",\"detail\":\"76.8 ms\"},{\"value\":\"10×\",\"label\":\"Faster than CGAL\",\"detail\":\"248.5 ms\"}]",[14,1644,1645,1650,1651],{},[65,1646,1649],{"className":1647},[1648],"tag-pill","Apple M4 Max"," ",[65,1652,1654],{"className":1653},[1648],"16 threads",[1656,1657],"data-table",{":headers":1658,":highlight":1562,":rows":1659},"[\"Library\",\"Tree build, 1,025,766 polygons\",\"vs trueform\"]","[[\"trueform\",\"24.9 ms\",\"1.0×\"],[\"MeshLib\",\"76.8 ms\",\"3.1×\"],[\"CGAL\",\"248.5 ms\",\"10.0×\"]]",[14,1661,1662],{},"For the queries, we ran 100,000 closest-point queries against each of 100 Thingi10K meshes (100,368 to 938,940 faces) and the Stanford dragon at five sizes. The query points fall in each mesh's bounding box, grown by 20 %. Each query writes its face id, closest point and squared distance. The tree is built before the query clock starts; its build time is in its own table below.",[1640,1664],{":items":1665},"[{\"value\":\"68ms\",\"label\":\"100,000 closest points\",\"detail\":\"Geometric mean over 100 meshes\"},{\"value\":\"2.1×\",\"label\":\"Faster than libigl\",\"detail\":\"144 ms, same queries\"},{\"value\":\"3.1×\",\"label\":\"Faster than CGAL\",\"detail\":\"212 ms, same queries\"}]",[14,1667,1668,1650,1671,1650,1674],{},[65,1669,1649],{"className":1670},[1648],[65,1672,1654],{"className":1673},[1648],[65,1675,1677],{"className":1676},[1648],"Apple clang 17 · C++17 -O3",[1656,1679],{":headers":1680,":highlight":1562,":rows":1681},"[\"100,000 queries\",\"trueform\",\"libigl\",\"CGAL\",\"MeshLib\"]","[[\"100K–250K faces (66 meshes, geometric mean)\",\"64.0 ms\",\"132 ms\",\"202 ms\",\"137 ms\"],[\"250K–500K faces (16 meshes, geometric mean)\",\"71.8 ms\",\"149 ms\",\"193 ms\",\"146 ms\"],[\"500K–939K faces (18 meshes, geometric mean)\",\"80.9 ms\",\"193 ms\",\"278 ms\",\"165 ms\"],[\"All 100 meshes (geometric mean)\",\"68.0 ms\",\"144 ms\",\"212 ms\",\"143 ms\"],[\"Dragon, 58,298 faces\",\"23.4 ms\",\"43.6 ms\",\"56.6 ms\",\"36.4 ms\"],[\"Dragon, 141,966 faces\",\"29.3 ms\",\"63.4 ms\",\"75.0 ms\",\"48.8 ms\"],[\"Dragon, 244,026 faces\",\"38.0 ms\",\"76.0 ms\",\"96.5 ms\",\"63.4 ms\"],[\"Dragon, 480,868 faces\",\"51.8 ms\",\"104 ms\",\"132 ms\",\"92.8 ms\"],[\"Dragon, 1,025,766 faces\",\"69.3 ms\",\"164 ms\",\"177 ms\",\"129 ms\"]]",[14,1683,1684],{},"Building the tree, from your arrays to a tree ready for queries:",[1656,1686],{":headers":1687,":highlight":1562,":rows":1688},"[\"Tree build\",\"trueform\",\"libigl\",\"CGAL\",\"MeshLib\"]","[[\"100K–250K faces (66 meshes, geometric mean)\",\"2.6 ms\",\"65.0 ms\",\"34.8 ms\",\"9.7 ms\"],[\"250K–500K faces (16 meshes, geometric mean)\",\"4.3 ms\",\"158 ms\",\"79.5 ms\",\"17.7 ms\"],[\"500K–939K faces (18 meshes, geometric mean)\",\"5.6 ms\",\"325 ms\",\"160 ms\",\"34.5 ms\"],[\"All 100 meshes (geometric mean)\",\"3.2 ms\",\"100 ms\",\"52.2 ms\",\"13.5 ms\"],[\"Dragon, 58,298 faces\",\"1.1 ms\",\"26.4 ms\",\"13.7 ms\",\"6.5 ms\"],[\"Dragon, 141,966 faces\",\"2.1 ms\",\"66.3 ms\",\"34.9 ms\",\"9.9 ms\"],[\"Dragon, 244,026 faces\",\"3.8 ms\",\"126 ms\",\"61.6 ms\",\"14.7 ms\"],[\"Dragon, 480,868 faces\",\"5.3 ms\",\"253 ms\",\"127 ms\",\"29.0 ms\"],[\"Dragon, 1,025,766 faces\",\"9.6 ms\",\"539 ms\",\"291 ms\",\"73.6 ms\"]]",[14,1690,1691,1692,1695,1696,1699,1700,1703],{},"These builds use this page's ",[32,1693,1694],{},"tf::config_tree(4, 4)"," on a view of your arrays. The build benchmark above first copies the arrays into a trueform buffer and uses ",[32,1697,1698],{},"(4, 12)",". libigl's batch query runs on all threads by itself. CGAL and MeshLib answer one query per call, so their batches ran under ",[32,1701,1702],{},"tbb::parallel_for",". In a plain loop, CGAL took 2.2 s per batch (geometric mean, 63 meshes from 100K to 250K faces). All four libraries agree on every distance to within 0.2 % of the largest one.",[14,1705,1706],{},"Measured 2026-10-31 with trueform 0.10.8; harness and raw results: benchmarks\u002Ftutorials\u002Fcpp-closest-point.",[14,1708,1709],{},"For the closest pair between two meshes, at 1,025,766 polygons, the docs benchmark shows under 1 ms against FCL's 370 ms.",[14,1711,1712],{},[43,1713],{"alt":1714,"src":1715},"Benchmark chart: trueform against other C++ libraries, closest point","\u002Fimg\u002Falgorithms\u002Fbenchmark-closest-point-cpp-2026.png",[14,1717,1718,1722,1723],{},[18,1719,1721],{"href":1720},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1724,1726],{"href":345,"rel":1725},[121],"Documentation",[1728,1729],"cite-as",{"author":1730,"title":5},"Sajovic, {\\v{Z}}iga",[1732,1733,1734],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":75,"depth":75,"links":1736},[1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748],{"id":50,"depth":75,"text":51},{"id":126,"depth":75,"text":127},{"id":225,"depth":75,"text":226},{"id":339,"depth":75,"text":340},{"id":537,"depth":75,"text":538},{"id":894,"depth":75,"text":895},{"id":980,"depth":75,"text":981},{"id":1116,"depth":75,"text":1117},{"id":1253,"depth":75,"text":1254},{"id":1519,"depth":75,"text":1520},{"id":1600,"depth":75,"text":1601},{"id":1634,"depth":75,"text":1635},"2026-10-31","Find the closest point on a triangle mesh, the face it lies on, and the distance, for one point or a hundred thousand, in C++. Header-only, C++17, spatial tree built in 24.9 ms on 1M polygons.","md",[1753,1754,1755,1756,1757,1758,1759,1760,1761],"closest point on mesh C++","point to mesh distance C++","nearest point on triangle mesh C++","AABB tree closest point C++","batch closest point query C++","libigl point_mesh_squared_distance alternative","CGAL AABB_tree closest_point alternative","MeshLib findProjection alternative","mesh distance library C++",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp.png","\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp",{"title":5,"description":1750},"tutorials\u002Ffast-closest-point-on-a-mesh-in-cpp",[1768,1769,1770,1771],"c++","spatial","closest-point","tutorial","3QT9Te9g32FIASgZ4F5iopBKgT6dlHvYw93MOgNrQU0",1793517812008]