Skip to content

meshToDensePointCloud: cover a thin triangle by the samples of its longest edge - #6726

Closed
Fedr wants to merge 5 commits into
masterfrom
thin-triangle-strip
Closed

meshToDensePointCloud: cover a thin triangle by the samples of its longest edge#6726
Fedr wants to merge 5 commits into
masterfrom
thin-triangle-strip

Conversation

@Fedr

@Fedr Fedr commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #6725, which it is based on — the diff against master will shrink to this one once #6725 merges.

The waste

The in-triangle grid is isotropic: it divides all three sides by the same n and fills the inside with (n-1)(n-2)/2 samples. 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 e be the longest edge of a triangle, L its length and h = 2*area/L the height over it. The angles at the ends of e are acute, so every point of the triangle projects onto e inside the segment. A point is then at most h away from the edge and at most half a division step along it, so with e divided in k parts it is within

sqrt( (L/2k)^2 + h^2 )

of the nearest sample of that edge. Hence, when h < radius and

k >= L / ( 2*sqrt( radius^2 - h^2 ) )

the 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)/2 against k-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:

radius master #6725 this PR
0.2 646 727 points, 3.77 ms 591 789, 3.65 ms 293 831, 3.13 ms
0.5 193 581 points, 2.41 ms 185 353, 2.35 ms 130 525, 2.42 ms

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.MeshToDensePointCloudThin is 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.MeshToDensePointCloudDegenerate drops from 15 points to 7 for the same reason. Every existing test of the guarantee passes unchanged.

Fedr added 3 commits August 28, 2026 23:52
…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.
Base automatically changed from covering-radius-sampling to master August 31, 2026 10:30
Fedr added 2 commits August 31, 2026 13:32
#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.
@Fedr

Fedr commented Aug 31, 2026

Copy link
Copy Markdown
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.

@Fedr Fedr closed this Aug 31, 2026
@Fedr
Fedr deleted the thin-triangle-strip branch August 31, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant