[{"data":1,"prerenderedAt":1269},["ShallowReactive",2],{"algo-mesh-collision-detection":3},{"id":4,"title":5,"author":6,"body":7,"date":1243,"description":1244,"extension":1245,"hub":359,"keywords":1246,"meta":1256,"navigation":359,"ogImage":1257,"path":1258,"published":359,"seo":1259,"stem":1262,"tags":1263,"updated":1267,"__hash__":1268},"algorithms\u002Falgorithms\u002Fmesh-collision-detection.md","Mesh collision detection","Žiga Sajovic",{"type":8,"value":9,"toc":1232},"minimark",[10,21,25,34,39,44,52,56,59,62,68,71,82,85,90,93,97,100,103,144,147,152,155,163,167,171,174,180,187,193,199,205,209,212,225,229,236,246,250,253,304,307,398,402,405,1143,1146,1150,1165,1176,1193,1212,1220,1224,1228],[11,12,13,14,20],"p",{},"Two meshes can be asked three things. Do they touch? How far apart are they? Which faces meet? The questions sound alike, but they cost different amounts, because each one lets the search stop at a different point. Ask the cheapest one that settles your case. This page shows which is which, and what happens to each when the meshes move. Drag two dragons into each other in the ",[15,16,19],"a",{"href":17,"external":18},"\u002Ftrueform\u002Flive\u002Fcollision","","live example",".",[22,23],"hub-language-cards",{":items":24},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Robust Mesh Collision Detection in C++\",\"to\":\"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-cpp\",\"detail\":\"Header-only. Trees and poses tagged onto your own mesh views.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Robust Mesh Collision Detection in Python\",\"to\":\"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python\",\"detail\":\"One pip install. A 4×4 matrix per mesh, answers as NumPy arrays.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Robust Mesh Collision Detection in JavaScript\",\"to\":\"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript\",\"detail\":\"WebAssembly. three.js matrices in, the same answers in the browser.\"}]",[11,26,27],{},[28,29],"img",{"alt":30,"height":31,"src":32,"width":33},"Two Stanford dragons, one turned and moved into the other; the faces near the contact are marked in orange on both, and the curve where the surfaces cross is drawn in pink",800,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcollision-hero.webp",1200,[35,36,38],"h2",{"id":37},"pick-the-question","Pick the question",[40,41],"data-table",{":headers":42,":rows":43},"[\"Question\",\"Call\",\"Returns\",\"When the search stops\"]","[[\"Do they touch?\",\"intersects\",\"true or false\",\"at the first pair of faces that touch\"],[\"Is anything within d?\",\"neighbor search with a radius\",\"the closest pair, or nothing\",\"when no pair of boxes is closer than d\"],[\"How far apart?\",\"distance, distance2, neighbor search\",\"the gap, or the closest pair of points and their faces\",\"when no box can beat the best gap found\"],[\"Which faces touch?\",\"gather intersecting ids\",\"every pair of touching faces\",\"after every overlapping pair is checked\"],[\"Which faces come close?\",\"gather ids within distance\",\"every pair of faces closer than d\",\"after every pair within d is checked\"],[\"Does one mesh cross itself?\",\"has self intersections\",\"true or false\",\"at the first pair of faces that touch\"]]",[11,45,46,47,51],{},"The first question is the cheapest. ",[48,49,50],"code",{},"intersects"," stops at the first pair of faces that touch, so the colliding case is the fast case: in the JavaScript benchmark below, 0.107 ms at 58,298 faces per mesh and 0.043 ms at 1,025,766. Touching counts: surfaces that meet at a point or along a face return true. A motion planner that only needs to reject a pose should ask it and nothing else. A clearance check needs the second or third. An interference report needs the fourth, and pays for it by visiting the whole contact.",[35,53,55],{"id":54},"broad-phase-and-narrow-phase-in-one-call","Broad phase and narrow phase in one call",[11,57,58],{},"Collision engines split the work in two. The broad phase finds the pairs of parts that might touch, usually by bounding boxes. The narrow phase tests the triangles of those pairs.",[11,60,61],{},"In trueform both happen inside one call. Each mesh has its own tree. The query walks the two trees as pairs of nodes and drops every pair whose boxes cannot meet, or cannot beat the best distance found so far. That is the broad phase. When two leaves survive, their triangles are tested in world coordinates. That is the narrow phase. You write neither.",[11,63,64],{},[28,65],{"alt":66,"height":31,"src":67,"width":33},"Two closed curves that overlap in two places, each covered by its own boxes; only a handful of leaf boxes near the two crossings are marked, and the segments that cross are drawn in pink","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcollision-traversal.webp",[11,69,70],{},"The diagram is in 2D, so every box can be seen. Each curve stands in for a mesh, its segments for faces; the boxes are a schematic tree, the crossings are trueform's. Each curve has 32 leaf boxes, so there are 1,024 leaf pairs. The walk starts from the two root boxes. When two boxes overlap, the larger one is split and its children are paired with the other. When they do not overlap, the pair is dropped with every pair below it. Only four leaf pairs reach the test of their segments, all at the two places where the curves cross. A test that only needs a yes stops at the first of them.",[11,72,73,74,77,78,81],{},"The box shape matters for distance queries. The default tree, in every language, uses axis-aligned boxes. In C++ you can also build an ",[48,75,76],{},"obb_tree",", with oriented boxes, or an ",[48,79,80],{},"obbrss_tree",", which adds rectangle-swept spheres. They fit the surface tighter and prune more pairs on closest-pair queries. The query calls stay the same.",[11,83,84],{},"Closest pair between the Stanford dragon and a randomly transformed copy of itself, 1,025,766 polygons each, in C++.",[40,86],{":headers":87,":rows":88,":highlight":89},"[\"Library and tree\",\"Closest pair\"]","[[\"trueform, OBBRSS tree\",\"0.20 ms\"],[\"trueform, OBB tree\",\"0.28 ms\"],[\"trueform, AABB tree\",\"0.79 ms\"],[\"Coal, OBBRSS\",\"0.47 ms\"],[\"FCL, OBBRSS\",\"0.48 ms\"],[\"FCL, AABB\",\"370 ms\"]]","0",[11,91,92],{},"Apple M4 Max, 16 threads. On the same tree type, trueform is about 2.4× faster than FCL and Coal with OBBRSS, and about 470× faster than FCL with AABB.",[35,94,96],{"id":95},"moving-meshes-in-real-time","Moving meshes in real time",[11,98,99],{},"Things that collide move. If moving a mesh meant rewriting its points and rebuilding its tree, every frame would pay the full build.",[11,101,102],{},"In trueform a mesh carries a 4×4 transformation. Every query applies it on the fly, to the boxes as they are visited and to the triangles as they are tested. The points are not rewritten and the tree stays valid. A frame costs a matrix assignment and the query.",[104,105,106,114,120,134],"ul",{},[107,108,109,113],"li",{},[110,111,112],"strong",{},"Many copies."," Shared views of one mesh share its points, faces and tree. Each view carries its own pose. A hundred placements of one part cost one tree.",[107,115,116,119],{},[110,117,118],{},"three.js."," A world matrix goes in transposed, because three.js stores matrices column-major.",[107,121,122,125,126,129,130,133],{},[110,123,124],{},"The browser frame loop."," The async variants, ",[48,127,128],{},"tf.async.intersects"," and ",[48,131,132],{},"tf.async.distance",", run off the main thread.",[107,135,136,139,140,143],{},[110,137,138],{},"Local edits."," When the shape itself changes in a small region, the C++ ",[48,141,142],{},"mod_tree"," updates the changed faces instead of rebuilding the whole tree. Every query works on it unchanged.",[11,145,146],{},"What a frame costs, in Node.js with WebAssembly, on the dragon against a transformed copy. The closest pair is taken across a gap; the test, on a pose where the surfaces cross:",[40,148],{":headers":149,":rows":150,":highlight":151},"[\"Faces each\",\"Closest pair, trueform\",\"three-mesh-bvh\",\"Test, trueform\",\"three-mesh-bvh\"]","[[\"58,298\",\"0.045 ms\",\"7.46 ms\",\"0.107 ms\",\"0.386 ms\"],[\"244,026\",\"0.088 ms\",\"24.1 ms\",\"0.091 ms\",\"0.747 ms\"],[\"1,025,766\",\"0.194 ms\",\"80.2 ms\",\"0.043 ms\",\"2.68 ms\"],[\"1,025,766, trees built inside the timer\",\"22.2 ms\",\"573 ms\",\"23.4 ms\",\"466 ms\"]]","1",[11,153,154],{},"Apple M4 Max, 16 threads, trueform 0.10.8. The last row is the price of the first frame. Every frame after it is one of the rows above.",[11,156,157,158,162],{},"The tree build itself, and how it compares across languages, is on the ",[15,159,161],{"href":160},"\u002Falgorithms\u002Fclosest-point-on-a-mesh#build-the-tree-once","closest point"," page.",[164,165],"hub-benchmark-figures",{"category":166},"spatial-tree-build",[35,168,170],{"id":169},"clearance-and-near-misses","Clearance and near misses",[11,172,173],{},"When the meshes do not touch, the useful number is the gap.",[11,175,176],{},[28,177],{"alt":178,"height":31,"src":179,"width":33},"The Stanford dragon held above a tilted plate; a pink segment joins the closest pair of points between them, and the faces of both within a band of that gap are orange","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcollision-clearance.webp",[11,181,182,183,186],{},"The dragon above is the public Stanford mesh; the plate is a synthetic box. One neighbor search returns the closest pair: a face on each mesh, a point on each face in world coordinates, and the squared distance between them. That is the pink segment. ",[48,184,185],{},"distance"," returns the same gap as one number, and zero when the meshes touch.",[11,188,189,192],{},[110,190,191],{},"Is anything within d?"," Pass a radius. The search prunes every pair of boxes farther apart than the radius and returns nothing when the gap is larger. A minimum-clearance rule needs no more than that.",[11,194,195,198],{},[110,196,197],{},"Where are they close?"," Gather the pairs of faces within a distance. The orange faces above are that set, on both meshes, for a band just past the gap. It marks the region to inspect before anything collides.",[11,200,201,204],{},[110,202,203],{},"Proxies."," A capsule, a segment with a radius, often stands in for a tool or a robot link. Its clearance is the distance from the segment to the mesh, minus the radius: one query and a subtraction. Segments, triangles, polygons, rays, lines, planes and boxes all work against a mesh. In Python a batch of segments is one call, with one answer per segment, computed in parallel.",[35,206,208],{"id":207},"contact","Contact",[11,210,211],{},"When the meshes do touch, the face pairs say where. Each row of the result holds one face of the first mesh and one face of the second. The unique ids in each column are the faces in contact on each side. Colour them, count them, carry their attributes, or restrict a finer check to them.",[11,213,214,215,219,220,224],{},"When the contact is needed as geometry, the ",[15,216,218],{"href":217},"\u002Ftutorials\u002Fmesh-intersection-curves-in-python","intersection curves"," give the exact curves where the surfaces cross. When the overlap is needed as a solid, ",[15,221,223],{"href":222},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans"," give it.",[35,226,228],{"id":227},"one-mesh-against-itself","One mesh against itself",[11,230,231,232,235],{},"A cloth, a hose or a scanned surface can fold through itself. ",[48,233,234],{},"has_self_intersections"," asks that of one mesh. It stops at the first contact, like the test between two meshes, and decides each pair in exact arithmetic. Faces that share a vertex or an edge are neighbours, not a contact, so a fan or a bowtie does not count on its own.",[11,237,238,239,245],{},"When the answer is yes, the ",[15,240,244],{"href":241,"rel":242},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fintersect",[243],"nofollow","self-intersection curves"," say where the surface crosses itself.",[35,247,249],{"id":248},"in-a-larger-pipeline","In a larger pipeline",[11,251,252],{},"Each query answers a question about surfaces at one pose. Some collision questions need one more step, and each step is a trueform call.",[104,254,255,275,292,298],{},[107,256,257,260,261,263,264,266,267,270,271,274],{},[110,258,259],{},"One mesh inside another."," A mesh wholly inside another crosses none of its faces, so ",[48,262,50],{}," returns false and ",[48,265,185],{}," returns the gap between the surfaces. Test one vertex of the inner mesh as well: a negative ",[48,268,269],{},"signed_distance",", or a ",[48,272,273],{},"winding_number"," near 1, means inside.",[107,276,277,280,281,284,285,288,289,291],{},[110,278,279],{},"How much they overlap."," When two meshes intersect, ",[48,282,283],{},"boolean_intersection"," returns the overlap as a closed solid, and ",[48,286,287],{},"volume"," measures it. The ",[15,290,218],{"href":217}," give the rim of the overlap.",[107,293,294,297],{},[110,295,296],{},"Motion between poses."," A thin part that moves fast can pass through a wall between two poses with both clear. Sample the motion so that no step moves the part farther than its thickness. Each sample costs a matrix assignment and a query, against trees built once.",[107,299,300,303],{},[110,301,302],{},"Many bodies."," In a scene of many bodies, ask only the pairs whose bounding boxes overlap. Every body keeps its tree as it moves.",[11,305,306],{},"The first two, in Python:",[308,309,313],"pre",{"className":310,"code":311,"language":312,"meta":18,"style":18},"language-python shiki shiki-themes github-dark","# wholly inside: no faces cross, and one vertex of the part is inside the fixture\ninside = not tf.intersects(fixture, part) and tf.signed_distance(fixture, vertex) \u003C 0\n\n# how much they overlap: the overlap as a closed solid, then its volume\nif tf.intersects(fixture, part):\n    (faces, points), _, _ = tf.boolean_intersection(fixture, part)\n    overlap = tf.volume((faces, points))\n","python",[48,314,315,324,354,361,367,376,387],{"__ignoreMap":18},[316,317,320],"span",{"class":318,"line":319},"line",1,[316,321,323],{"class":322},"sAwPA","# wholly inside: no faces cross, and one vertex of the part is inside the fixture\n",[316,325,327,331,335,338,341,344,347,350],{"class":318,"line":326},2,[316,328,330],{"class":329},"s95oV","inside ",[316,332,334],{"class":333},"snl16","=",[316,336,337],{"class":333}," not",[316,339,340],{"class":329}," tf.intersects(fixture, part) ",[316,342,343],{"class":333},"and",[316,345,346],{"class":329}," tf.signed_distance(fixture, vertex) ",[316,348,349],{"class":333},"\u003C",[316,351,353],{"class":352},"sDLfK"," 0\n",[316,355,357],{"class":318,"line":356},3,[316,358,360],{"emptyLinePlaceholder":359},true,"\n",[316,362,364],{"class":318,"line":363},4,[316,365,366],{"class":322},"# how much they overlap: the overlap as a closed solid, then its volume\n",[316,368,370,373],{"class":318,"line":369},5,[316,371,372],{"class":333},"if",[316,374,375],{"class":329}," tf.intersects(fixture, part):\n",[316,377,379,382,384],{"class":318,"line":378},6,[316,380,381],{"class":329},"    (faces, points), _, _ ",[316,383,334],{"class":333},[316,385,386],{"class":329}," tf.boolean_intersection(fixture, part)\n",[316,388,390,393,395],{"class":318,"line":389},7,[316,391,392],{"class":329},"    overlap ",[316,394,334],{"class":333},[316,396,397],{"class":329}," tf.volume((faces, points))\n",[35,399,401],{"id":400},"collision-in-code","Collision in code",[11,403,404],{},"Each example poses one mesh against another and asks the three questions.",[406,407,408,745,935],"code-group",{},[308,409,414],{"className":410,"code":411,"filename":412,"language":413,"meta":18,"style":18},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\ntf::aabb_tree\u003Cint, float, 3> fixture_tree(fixture, tf::config_tree(4, 4));\ntf::aabb_tree\u003Cint, float, 3> part_tree(part, tf::config_tree(4, 4));\nauto fixture_form = fixture | tf::tag(fixture_tree);\nauto posed = part | tf::tag(part_tree) | tf::tag(\n    tf::make_transformation_from_translation(tf::make_vector(1.3f, 0.f, 0.f)));\n\nbool hit = tf::intersects(fixture_form, posed);\nfloat gap = tf::distance(fixture_form, posed);\n\nstd::vector\u003Cstd::pair\u003Cint, int>> pairs;\ntf::gather_ids(fixture_form, posed, tf::intersects_f, std::back_inserter(pairs));\n","C++","cpp",[48,415,416,425,429,490,538,565,599,642,647,667,685,690,717],{"__ignoreMap":18},[316,417,418,421],{"class":318,"line":319},[316,419,420],{"class":333},"#include",[316,422,424],{"class":423},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[316,426,427],{"class":318,"line":326},[316,428,360],{"emptyLinePlaceholder":359},[316,430,431,435,438,441,443,446,449,452,454,457,460,463,466,469,471,473,475,478,480,483,485,487],{"class":318,"line":356},[316,432,434],{"class":433},"svObZ","tf",[316,436,437],{"class":329},"::",[316,439,440],{"class":433},"aabb_tree",[316,442,349],{"class":329},[316,444,445],{"class":333},"int",[316,447,448],{"class":329},", ",[316,450,451],{"class":333},"float",[316,453,448],{"class":329},[316,455,456],{"class":352},"3",[316,458,459],{"class":329},"> ",[316,461,462],{"class":433},"fixture_tree",[316,464,465],{"class":329},"(",[316,467,468],{"class":433},"fixture",[316,470,448],{"class":329},[316,472,434],{"class":433},[316,474,437],{"class":329},[316,476,477],{"class":433},"config_tree",[316,479,465],{"class":329},[316,481,482],{"class":352},"4",[316,484,448],{"class":329},[316,486,482],{"class":352},[316,488,489],{"class":329},"));\n",[316,491,492,494,496,498,500,502,504,506,508,510,512,515,517,520,522,524,526,528,530,532,534,536],{"class":318,"line":363},[316,493,434],{"class":433},[316,495,437],{"class":329},[316,497,440],{"class":433},[316,499,349],{"class":329},[316,501,445],{"class":333},[316,503,448],{"class":329},[316,505,451],{"class":333},[316,507,448],{"class":329},[316,509,456],{"class":352},[316,511,459],{"class":329},[316,513,514],{"class":433},"part_tree",[316,516,465],{"class":329},[316,518,519],{"class":433},"part",[316,521,448],{"class":329},[316,523,434],{"class":433},[316,525,437],{"class":329},[316,527,477],{"class":433},[316,529,465],{"class":329},[316,531,482],{"class":352},[316,533,448],{"class":329},[316,535,482],{"class":352},[316,537,489],{"class":329},[316,539,540,543,546,548,551,554,557,559,562],{"class":318,"line":369},[316,541,542],{"class":333},"auto",[316,544,545],{"class":329}," fixture_form ",[316,547,334],{"class":333},[316,549,550],{"class":329}," fixture ",[316,552,553],{"class":333},"|",[316,555,556],{"class":433}," tf",[316,558,437],{"class":329},[316,560,561],{"class":433},"tag",[316,563,564],{"class":329},"(fixture_tree);\n",[316,566,567,569,572,574,577,579,581,583,585,588,590,592,594,596],{"class":318,"line":378},[316,568,542],{"class":333},[316,570,571],{"class":329}," posed ",[316,573,334],{"class":333},[316,575,576],{"class":329}," part ",[316,578,553],{"class":333},[316,580,556],{"class":433},[316,582,437],{"class":329},[316,584,561],{"class":433},[316,586,587],{"class":329},"(part_tree) ",[316,589,553],{"class":333},[316,591,556],{"class":433},[316,593,437],{"class":329},[316,595,561],{"class":433},[316,597,598],{"class":329},"(\n",[316,600,601,604,606,609,611,613,615,618,620,623,626,628,631,633,635,637,639],{"class":318,"line":389},[316,602,603],{"class":433},"    tf",[316,605,437],{"class":329},[316,607,608],{"class":433},"make_transformation_from_translation",[316,610,465],{"class":329},[316,612,434],{"class":433},[316,614,437],{"class":329},[316,616,617],{"class":433},"make_vector",[316,619,465],{"class":329},[316,621,622],{"class":352},"1.3",[316,624,625],{"class":333},"f",[316,627,448],{"class":329},[316,629,630],{"class":352},"0.",[316,632,625],{"class":333},[316,634,448],{"class":329},[316,636,630],{"class":352},[316,638,625],{"class":333},[316,640,641],{"class":329},")));\n",[316,643,645],{"class":318,"line":644},8,[316,646,360],{"emptyLinePlaceholder":359},[316,648,650,653,656,658,660,662,664],{"class":318,"line":649},9,[316,651,652],{"class":333},"bool",[316,654,655],{"class":329}," hit ",[316,657,334],{"class":333},[316,659,556],{"class":433},[316,661,437],{"class":329},[316,663,50],{"class":433},[316,665,666],{"class":329},"(fixture_form, posed);\n",[316,668,670,672,675,677,679,681,683],{"class":318,"line":669},10,[316,671,451],{"class":333},[316,673,674],{"class":329}," gap ",[316,676,334],{"class":333},[316,678,556],{"class":433},[316,680,437],{"class":329},[316,682,185],{"class":433},[316,684,666],{"class":329},[316,686,688],{"class":318,"line":687},11,[316,689,360],{"emptyLinePlaceholder":359},[316,691,693,696,699,701,703,706,709,711,714],{"class":318,"line":692},12,[316,694,695],{"class":433},"std",[316,697,698],{"class":329},"::vector",[316,700,349],{"class":333},[316,702,695],{"class":433},[316,704,705],{"class":329},"::pair",[316,707,708],{"class":333},"\u003Cint",[316,710,448],{"class":329},[316,712,713],{"class":333},"int>>",[316,715,716],{"class":329}," pairs;\n",[316,718,720,722,724,727,730,732,735,737,739,742],{"class":318,"line":719},13,[316,721,434],{"class":433},[316,723,437],{"class":329},[316,725,726],{"class":433},"gather_ids",[316,728,729],{"class":329},"(fixture_form, posed, ",[316,731,434],{"class":433},[316,733,734],{"class":329},"::intersects_f, ",[316,736,695],{"class":433},[316,738,437],{"class":329},[316,740,741],{"class":433},"back_inserter",[316,743,744],{"class":329},"(pairs));\n",[308,746,749],{"className":310,"code":747,"filename":748,"language":312,"meta":18,"style":18},"import numpy as np\nimport trueform as tf\n\nfixture = tf.Mesh(*tf.read_stl(\"fixture.stl\"))\npart = tf.Mesh(*tf.read_obj(\"part.obj\"))\n\nT = np.eye(4, dtype=np.float32)\nT[:3, 3] = [1.3, 0.0, 0.0]\npart.transformation = T                       # no rebuild\n\nhit = tf.intersects(fixture, part)            # stops at the first hit\ngap = tf.distance(fixture, part)\npairs = tf.gather_intersecting_ids(fixture, part)   # (M, 2) face pairs\n","Python",[48,750,751,765,777,781,803,822,826,849,882,895,899,912,922],{"__ignoreMap":18},[316,752,753,756,759,762],{"class":318,"line":319},[316,754,755],{"class":333},"import",[316,757,758],{"class":329}," numpy ",[316,760,761],{"class":333},"as",[316,763,764],{"class":329}," np\n",[316,766,767,769,772,774],{"class":318,"line":326},[316,768,755],{"class":333},[316,770,771],{"class":329}," trueform ",[316,773,761],{"class":333},[316,775,776],{"class":329}," tf\n",[316,778,779],{"class":318,"line":356},[316,780,360],{"emptyLinePlaceholder":359},[316,782,783,786,788,791,794,797,800],{"class":318,"line":363},[316,784,785],{"class":329},"fixture ",[316,787,334],{"class":333},[316,789,790],{"class":329}," tf.Mesh(",[316,792,793],{"class":333},"*",[316,795,796],{"class":329},"tf.read_stl(",[316,798,799],{"class":423},"\"fixture.stl\"",[316,801,802],{"class":329},"))\n",[316,804,805,808,810,812,814,817,820],{"class":318,"line":369},[316,806,807],{"class":329},"part ",[316,809,334],{"class":333},[316,811,790],{"class":329},[316,813,793],{"class":333},[316,815,816],{"class":329},"tf.read_obj(",[316,818,819],{"class":423},"\"part.obj\"",[316,821,802],{"class":329},[316,823,824],{"class":318,"line":378},[316,825,360],{"emptyLinePlaceholder":359},[316,827,828,831,833,836,838,840,844,846],{"class":318,"line":389},[316,829,830],{"class":329},"T ",[316,832,334],{"class":333},[316,834,835],{"class":329}," np.eye(",[316,837,482],{"class":352},[316,839,448],{"class":329},[316,841,843],{"class":842},"s9osk","dtype",[316,845,334],{"class":333},[316,847,848],{"class":329},"np.float32)\n",[316,850,851,854,856,858,860,863,865,868,870,872,875,877,879],{"class":318,"line":644},[316,852,853],{"class":329},"T[:",[316,855,456],{"class":352},[316,857,448],{"class":329},[316,859,456],{"class":352},[316,861,862],{"class":329},"] ",[316,864,334],{"class":333},[316,866,867],{"class":329}," [",[316,869,622],{"class":352},[316,871,448],{"class":329},[316,873,874],{"class":352},"0.0",[316,876,448],{"class":329},[316,878,874],{"class":352},[316,880,881],{"class":329},"]\n",[316,883,884,887,889,892],{"class":318,"line":649},[316,885,886],{"class":329},"part.transformation ",[316,888,334],{"class":333},[316,890,891],{"class":329}," T                       ",[316,893,894],{"class":322},"# no rebuild\n",[316,896,897],{"class":318,"line":669},[316,898,360],{"emptyLinePlaceholder":359},[316,900,901,904,906,909],{"class":318,"line":687},[316,902,903],{"class":329},"hit ",[316,905,334],{"class":333},[316,907,908],{"class":329}," tf.intersects(fixture, part)            ",[316,910,911],{"class":322},"# stops at the first hit\n",[316,913,914,917,919],{"class":318,"line":692},[316,915,916],{"class":329},"gap ",[316,918,334],{"class":333},[316,920,921],{"class":329}," tf.distance(fixture, part)\n",[316,923,924,927,929,932],{"class":318,"line":719},[316,925,926],{"class":329},"pairs ",[316,928,334],{"class":333},[316,930,931],{"class":329}," tf.gather_intersecting_ids(fixture, part)   ",[316,933,934],{"class":322},"# (M, 2) face pairs\n",[308,936,941],{"className":937,"code":938,"filename":939,"language":940,"meta":18,"style":18},"language-ts shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst fixed = tf.readStl(readFileSync(\"fixture.stl\"));\nconst moving = tf.readStl(readFileSync(\"part.stl\"));\nmoving.transformation = tf.makeTranslation(1.3, 0, 0);\n\nconst hit = tf.intersects(fixed, moving);\nconst gap = tf.distance(fixed, moving);\nconst { elementId0, elementId1, point0, point1, distance2 } =\n  tf.neighborSearch(fixed, moving);\n","JavaScript","ts",[48,942,943,965,979,983,1011,1035,1062,1066,1082,1097,1133],{"__ignoreMap":18},[316,944,945,947,950,953,956,959,962],{"class":318,"line":319},[316,946,755],{"class":333},[316,948,949],{"class":352}," *",[316,951,952],{"class":333}," as",[316,954,955],{"class":329}," tf ",[316,957,958],{"class":333},"from",[316,960,961],{"class":423}," \"@polydera\u002Ftrueform\"",[316,963,964],{"class":329},";\n",[316,966,967,969,972,974,977],{"class":318,"line":326},[316,968,755],{"class":333},[316,970,971],{"class":329}," { readFileSync } ",[316,973,958],{"class":333},[316,975,976],{"class":423}," \"node:fs\"",[316,978,964],{"class":329},[316,980,981],{"class":318,"line":356},[316,982,360],{"emptyLinePlaceholder":359},[316,984,985,988,991,994,997,1000,1002,1005,1007,1009],{"class":318,"line":363},[316,986,987],{"class":333},"const",[316,989,990],{"class":352}," fixed",[316,992,993],{"class":333}," =",[316,995,996],{"class":329}," tf.",[316,998,999],{"class":433},"readStl",[316,1001,465],{"class":329},[316,1003,1004],{"class":433},"readFileSync",[316,1006,465],{"class":329},[316,1008,799],{"class":423},[316,1010,489],{"class":329},[316,1012,1013,1015,1018,1020,1022,1024,1026,1028,1030,1033],{"class":318,"line":369},[316,1014,987],{"class":333},[316,1016,1017],{"class":352}," moving",[316,1019,993],{"class":333},[316,1021,996],{"class":329},[316,1023,999],{"class":433},[316,1025,465],{"class":329},[316,1027,1004],{"class":433},[316,1029,465],{"class":329},[316,1031,1032],{"class":423},"\"part.stl\"",[316,1034,489],{"class":329},[316,1036,1037,1040,1042,1044,1047,1049,1051,1053,1055,1057,1059],{"class":318,"line":378},[316,1038,1039],{"class":329},"moving.transformation ",[316,1041,334],{"class":333},[316,1043,996],{"class":329},[316,1045,1046],{"class":433},"makeTranslation",[316,1048,465],{"class":329},[316,1050,622],{"class":352},[316,1052,448],{"class":329},[316,1054,89],{"class":352},[316,1056,448],{"class":329},[316,1058,89],{"class":352},[316,1060,1061],{"class":329},");\n",[316,1063,1064],{"class":318,"line":389},[316,1065,360],{"emptyLinePlaceholder":359},[316,1067,1068,1070,1073,1075,1077,1079],{"class":318,"line":644},[316,1069,987],{"class":333},[316,1071,1072],{"class":352}," hit",[316,1074,993],{"class":333},[316,1076,996],{"class":329},[316,1078,50],{"class":433},[316,1080,1081],{"class":329},"(fixed, moving);\n",[316,1083,1084,1086,1089,1091,1093,1095],{"class":318,"line":649},[316,1085,987],{"class":333},[316,1087,1088],{"class":352}," gap",[316,1090,993],{"class":333},[316,1092,996],{"class":329},[316,1094,185],{"class":433},[316,1096,1081],{"class":329},[316,1098,1099,1101,1104,1107,1109,1112,1114,1117,1119,1122,1124,1127,1130],{"class":318,"line":669},[316,1100,987],{"class":333},[316,1102,1103],{"class":329}," { ",[316,1105,1106],{"class":352},"elementId0",[316,1108,448],{"class":329},[316,1110,1111],{"class":352},"elementId1",[316,1113,448],{"class":329},[316,1115,1116],{"class":352},"point0",[316,1118,448],{"class":329},[316,1120,1121],{"class":352},"point1",[316,1123,448],{"class":329},[316,1125,1126],{"class":352},"distance2",[316,1128,1129],{"class":329}," } ",[316,1131,1132],{"class":333},"=\n",[316,1134,1135,1138,1141],{"class":318,"line":687},[316,1136,1137],{"class":329},"  tf.",[316,1139,1140],{"class":433},"neighborSearch",[316,1142,1081],{"class":329},[11,1144,1145],{},"In a motion loop, build the trees once and change only the transformation. The tutorials walk through each question with a box and a sphere you can check by hand.",[35,1147,1149],{"id":1148},"where-to-go-next","Where to go next",[11,1151,1152,1155,1156,448,1159,448,1162,20],{},[110,1153,1154],{},"Tutorials."," ",[15,1157,412],{"href":1158},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-cpp",[15,1160,748],{"href":1161},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python",[15,1163,939],{"href":1164},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript",[11,1166,1167,1170,1171,1175],{},[110,1168,1169],{},"Points instead of a second mesh."," When the query is a batch of points, a scan or a probe, the job is ",[15,1172,1174],{"href":1173},"\u002Falgorithms\u002Fclosest-point-on-a-mesh","closest point on a mesh",": deviation maps, registration and snapping.",[11,1177,1178,1181,1182,448,1185,129,1187,1190,1191,20],{},[110,1179,1180],{},"The contact as geometry."," Intersection curves in ",[15,1183,412],{"href":1184},"\u002Ftutorials\u002Fmesh-intersection-curves-in-cpp",[15,1186,748],{"href":217},[15,1188,939],{"href":1189},"\u002Ftutorials\u002Fmesh-intersection-curves-in-javascript",". The overlap as a solid: ",[15,1192,223],{"href":222},[11,1194,1195,1198,1199,448,1203,129,1207,20],{},[110,1196,1197],{},"Documentation."," Intersection tests, distance, neighbor search and gathering ids, in ",[15,1200,412],{"href":1201,"rel":1202},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fspatial",[243],[15,1204,748],{"href":1205,"rel":1206},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#intersection-testing",[243],[15,1208,1211],{"href":1209,"rel":1210},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial",[243],"TypeScript",[11,1213,1214,1215,1219],{},"Collision queries are one part of ",[15,1216,1218],{"href":1217},"\u002Ftrueform","trueform",". The same engine does booleans, curvature, topology and remeshing, in C++, Python and the browser.",[1221,1222],"article-cta",{":buttons":1223},"[{\"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\u002Fspatial#intersection-testing\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"\u002Ftrueform\u002Flive\u002Fcollision\",\"icon\":\"i-lucide-play\"}]",[1225,1226],"cite-as",{"author":1227,"title":5},"Sajovic, {\\v{Z}}iga",[1229,1230,1231],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":18,"searchDepth":326,"depth":326,"links":1233},[1234,1235,1236,1237,1238,1239,1240,1241,1242],{"id":37,"depth":326,"text":38},{"id":54,"depth":326,"text":55},{"id":95,"depth":326,"text":96},{"id":169,"depth":326,"text":170},{"id":207,"depth":326,"text":208},{"id":227,"depth":326,"text":228},{"id":248,"depth":326,"text":249},{"id":400,"depth":326,"text":401},{"id":1148,"depth":326,"text":1149},"2026-11-08","Do two triangle meshes touch, how far apart are they, and which faces meet? Three questions with three costs. How trueform answers each, stops at the first hit, and keeps moving meshes cheap, in C++, Python and JavaScript.","md",[1247,1248,1249,1250,1251,1252,1253,1254,1255],"mesh collision detection","triangle mesh collision","mesh mesh distance","clearance between meshes","mesh intersection test","contact faces","broad phase narrow phase","FCL alternative","three-mesh-bvh alternative",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fmesh-collision-detection.png","\u002Falgorithms\u002Fmesh-collision-detection",{"title":1260,"description":1261},"Mesh Collision Detection: Hit, Clearance, Contact","Mesh collision detection: a yes\u002Fno test that stops at the first hit, clearance, contact faces. Moving meshes without rebuilds, in C++, Python and JS.","algorithms\u002Fmesh-collision-detection",[1264,1265,1266],"spatial","collision","overview","2026-11-12T12:06:28.000Z","aHdE7Acbk7tvyXpKy00I58o3DtveoVzR5GjX2BaTL80",1794486339528]