meshToDensePointCloud: cover a thin triangle by the samples of its longest edge - #6726
Closed
Fedr wants to merge 5 commits into
Closed
meshToDensePointCloud: cover a thin triangle by the samples of its longest edge#6726Fedr wants to merge 5 commits into
Fedr wants to merge 5 commits into
Conversation
…ot by its enclosing circle coveringRadiusSq( a, b, c ) is the squared largest distance from a point of the triangle to the nearest of its vertices, i.e. the radius at which the balls around the vertices stop covering it. For non-obtuse triangles it is the circumradius as before, but for obtuse ones it accounts for the third vertex being close to the longest edge, where the minimal enclosing circle sees only that edge. The maximum is attained either in the circumcenter, when the triangle contains it, or on the boundary where the two nearest vertices are equidistant, which gives a closed form. As a result a triangle covered by its own vertices is not divided however long its edges are, and the division of an edge by its length is not needed at all any more: an edge belongs to its incident triangles, so it is covered once they are. The edges are still divided as much as the incident divided faces require, to keep the grids conforming.
On a building mesh of 169k faces the exact covering radius cost 0.7 ms more per call than mincircleDiameterSq, the same at every radius, i.e. a fixed price per face. Two changes bring it back to parity: edgeCoveringRadiusSq works in the relative position along the segment and needs no square root, and the minimal enclosing circle, which bounds the covering radius from above, is tested first, so the exact value is only computed for the faces that may be divided.
…ngest edge Every point of a triangle projects on its longest edge inside it, because the angles at the ends of that edge are acute. Such a point is h away from the edge and no farther than half a division step along it, so if h < radius the samples of that edge alone cover the whole triangle, and it needs no grid however long its edges are. The face asks that edge for the division it needs, the edge takes the maximum of the requests as before, and the choice between the grid and the edge is made by the number of samples each of them costs. On a building mesh of 169k faces this gives 304278 points instead of 591789 at radius 0.2, and 132187 instead of 185353 at radius 0.5, with the same cover and slightly less time.
#6725 is merged as one squashed commit, so its own commits here conflicted with it. The files it touched are taken from master, and the thin triangle rule is re-applied on top of them, now with no square roots of its own: h < radius and the division of the edge are both tested on squares, as the covering radius is since ceilPow2Sq.
The grid of a face needs its nodes on an edge to be among the division points of that edge, which is why those numbers are powers of two: the maximum of two of them is a multiple of both. A strip needs no nodes, only a step along the edge no longer than it asks for, so any number of parts will do and its own number is left exact. The division of an edge is now the smallest multiple of what the grids ask that is not less than what the strips ask. On the building mesh: 293831 points instead of 304278 at radius 0.2 and 130525 instead of 132187 at 0.5, with the largest surface-to-cloud distance unchanged at 93.15% and 93.77% of the radius.
Fedr
added a commit
that referenced
this pull request
Aug 31, 2026
… alone A face flatter than the radius is covered by its longest edge alone once that edge is divided finely enough: ceil( len / 2*sqrt( radius^2 - h^2 ) ) parts. The face asks for that division when the samples it adds to the edge are fewer than the rows it saves, and asks nothing otherwise; the edge takes the maximum of what its faces ask, and no divisibility is involved anywhere. A face that got what it asked needs no rows, and its other two edges need no samples of their own. This is the thin-triangle rule of #6726 expressed in the new scheme. On the isolated slivers where that PR was measured this now matches it exactly - 9, 9 and 15 points - and on the building mesh it brings 227846 points down to 217531 at radius 0.2 and 125389 to 123935 at 0.5.
Contributor
Author
|
Superseded by #6732, which replaces the grid of similar triangles altogether and keeps the thin-triangle rule of this PR as one of the three patterns a face can choose. On the same building mesh it gives 207214 points instead of the 293831 here at radius 0.2 and 121822 instead of 130525 at 0.5, and matches this PR exactly on the isolated shapes (9, 9, 15, 25, 25, 45 and 561 samples), at the same speed or slightly faster. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #6725, which it is based on — the diff against
masterwill shrink to this one once #6725 merges.The waste
The in-triangle grid is isotropic: it divides all three sides by the same
nand fills the inside with(n-1)(n-2)/2samples. For a thin triangle that is quadratic work in a direction that has no extent — a long sliver is fully described by samples along its length.The rule
Let
ebe the longest edge of a triangle,Lits length andh = 2*area/Lthe height over it. The angles at the ends ofeare acute, so every point of the triangle projects ontoeinside the segment. A point is then at mosthaway from the edge and at most half a division step along it, so withedivided inkparts it is withinof the nearest sample of that edge. Hence, when
h < radiusandthe samples of that one edge cover the whole triangle: no interior samples, nothing on the other two edges, however long they are.
A face now asks its longest edge for the division it needs instead of dividing itself, the edge still takes the maximum over the faces that ask (so a neighbour's grid or another strip only refines it), and the choice between the grid and the edge is made by counting what each costs:
3*(n-1) + (n-1)(n-2)/2againstk-1.Measured on the same building mesh
169k faces, 106k vertices, bounding box diagonal 27.7; best of 4 runs, each the minimum of 3 calls:
Half the points at radius 0.2 and a third fewer at 0.5, and slightly faster, because the samples that are no longer produced are not written either. The cover is again unchanged: the largest surface-to-cloud distance stays 0.1863 and 0.4688, 93% of the radius.
An architectural model like this is full of long thin wall triangles, which is exactly the shape the rule targets; a mesh of well-shaped triangles will see little difference.
The strip division is exact
The numbers of parts are powers of two because a face's grid needs its nodes on an edge to be among that edge's division points, and the maximum of two powers of two is a multiple of both. A strip has no nodes to place: it only needs a step along the edge no longer than it asks for, so any number of parts will do, and its number is left exact. The division of an edge is therefore the smallest multiple of what the grids ask that is not less than what the strips ask.
That is worth another 3.4% at radius 0.2 and 1.3% at 0.5 on the mesh above, and it is what turns 11 points into 9 in the test below. Rounding the grid numbers exactly the same way is not possible: it breaks the guarantee, and the torus test catches it at 0.05019 against a radius of 0.05, because
max(3,5)is not a multiple of 3 and the boundary sub-triangles of the coarser face lose their corners.Tests
MRMesh.MeshToDensePointCloudThinis a 10 x 0.05 quad of two triangles that share its long diagonal: the cloud is 4 vertices plus the 5 samples dividing that diagonal, where a grid would need 42 samples per triangle.MRMesh.MeshToDensePointCloudDegeneratedrops from 15 points to 7 for the same reason. Every existing test of the guarantee passes unchanged.