Skip to content

Commit 3bb898d

Browse files
MeshAlgo::distributePoints : Avoiding duplicate points
1 parent b5314b0 commit 3bb898d

File tree

2 files changed

+226
-16
lines changed

2 files changed

+226
-16
lines changed

src/IECoreScene/MeshAlgoDistributePoints.cpp

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,101 @@ T triangleInterpolatedPrimVarValue(
136136
}
137137
}
138138

139+
class TriangleTester
140+
{
141+
public:
142+
TriangleTester( const V2f (&points)[3] )
143+
{
144+
for( int i = 0; i < 3; i++ )
145+
{
146+
V2f a = points[ ( i + 1 ) % 3];
147+
V2f b = points[ ( i + 2 ) % 3 ];
148+
149+
// Swap the vertices so that the edge is always handled in the same order, regardless of
150+
// which triangle it belongs to. This ensures consistency that if two triangles share an
151+
// edge, any point will always be one side of the edge or the other
152+
bool swap;
153+
if( a.y != b.y )
154+
{
155+
swap = a.y > b.y;
156+
}
157+
else
158+
{
159+
swap = a.x < b.x;
160+
}
161+
162+
if( swap )
163+
{
164+
V2f temp = a;
165+
a = b;
166+
b = temp;
167+
}
168+
169+
// Store values that will make it easier to compute the signed area of a triangle formed
170+
// by connected a point to this edge
171+
m_edgeOrigins[i] = a;
172+
m_edgeNormals[i] = V2f( b.y - a.y, a.x - b.x );
173+
174+
// Compute the signed edge of the triangle relative to this edge.
175+
// Note that all signed areas in this class are actually stored as 2 times the area
176+
// of the triangle, since the area of the extended parallelogram is what naturally
177+
// falls out of the cross product, and all the areas are relative so it doesn't matter
178+
V2f c = points[ i ];
179+
float doubleSignedArea = ( c - m_edgeOrigins[i] ).dot( m_edgeNormals[i] );
180+
181+
// Store which side of the edge is inside the triangle
182+
m_insideDir[i] = doubleSignedArea >= 0;
183+
184+
if( i == 0 )
185+
{
186+
// Having computed the signed area of the whole triangle to get the sign, we can store it
187+
// so we have our divisor for when we divide the areas to get barycentric coordinates.
188+
m_areaNormalize = 1.0f / fabsf( doubleSignedArea );
189+
}
190+
}
191+
}
192+
193+
inline bool contains( const V2f &p, V3f &bary )
194+
{
195+
// Compute the signed areas formed by connecting this point to the 3 edges
196+
float doubleSignedAreas[3];
197+
for( int i = 0; i < 3; i++ )
198+
{
199+
doubleSignedAreas[i] = ( p - m_edgeOrigins[i] ).dot( m_edgeNormals[i] );
200+
}
201+
202+
// To be inside the triangle, all the comparisons must match. Note that
203+
// m_insideDir stores which side we need to be on, but the comparison is
204+
// always a >= comparison that is the same on either side of the edge.
205+
// This ensures that a point near the edge will appear in exactly one
206+
// of two triangles sharing the edge.
207+
if(
208+
( ( doubleSignedAreas[0] >= 0 ) != m_insideDir[0] ) ||
209+
( ( doubleSignedAreas[1] >= 0 ) != m_insideDir[1] ) ||
210+
( ( doubleSignedAreas[2] >= 0 ) != m_insideDir[2] )
211+
)
212+
{
213+
return false;
214+
}
215+
216+
// Compute 2 barycentric coordinates by using the ratios of the areas to the total area.
217+
bary[0] = fabsf( doubleSignedAreas[0] ) * m_areaNormalize;
218+
bary[1] = fabsf( doubleSignedAreas[1] ) * m_areaNormalize;
219+
220+
// The 3rd barycentric coordinate is determined by the first two.
221+
bary[2] = 1.0f - bary[0] - bary[1];
222+
223+
return true;
224+
}
225+
226+
private:
227+
228+
float m_areaNormalize;
229+
V2f m_edgeOrigins[3];
230+
V2f m_edgeNormals[3];
231+
bool m_insideDir[3];
232+
};
233+
139234
void processInputs(
140235
const MeshPrimitive *mesh,
141236
const std::string &refPosition, const std::string &uvSet, const std::string &densityMask,
@@ -255,16 +350,16 @@ void distributePointsInTriangle(
255350
std::vector< BaryAndFaceIdx >& results, const Canceller *canceller
256351
)
257352
{
258-
Imath::V2f uv0, uv1, uv2;
259-
triangleCornerPrimVarValues< Imath::V2f >( uvInterpolation, uvView, vertexIds, faceIdx, uv0, uv1, uv2 );
260-
uv0 += offset;
261-
uv1 += offset;
262-
uv2 += offset;
353+
Imath::V2f uvs[3];
354+
triangleCornerPrimVarValues< Imath::V2f >( uvInterpolation, uvView, vertexIds, faceIdx, uvs[0], uvs[1], uvs[2] );
355+
uvs[0] += offset;
356+
uvs[1] += offset;
357+
uvs[2] += offset;
263358

264359
Imath::Box2f uvBounds;
265-
uvBounds.extendBy( uv0 );
266-
uvBounds.extendBy( uv1 );
267-
uvBounds.extendBy( uv2 );
360+
uvBounds.extendBy( uvs[0] );
361+
uvBounds.extendBy( uvs[1] );
362+
uvBounds.extendBy( uvs[2] );
268363

269364
const float maxCandidatePoints = 1e9;
270365
const float approxCandidatePoints = uvBounds.size().x * uvBounds.size().y * textureDensity;
@@ -307,6 +402,7 @@ void distributePointsInTriangle(
307402
cornerDensities[2] *= invMaxDensity;
308403
}
309404

405+
TriangleTester triTester( uvs );
310406
PointDistribution::defaultInstance()(
311407
uvBounds, finalDensity,
312408
[&]( const Imath::V2f pos, float densityThreshold )
@@ -318,7 +414,7 @@ void distributePointsInTriangle(
318414
}
319415

320416
Imath::V3f bary;
321-
if( triangleContainsPoint( uv0, uv1, uv2, pos, bary ) )
417+
if( triTester.contains( pos, bary ) )
322418
{
323419
if( hasDensityVar )
324420
{

test/IECoreScene/MeshAlgoDistributePointsTest.py

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import re
3737
import sys
3838
import unittest
39+
import random
3940
import imath
4041
import time
4142
import threading
@@ -45,7 +46,46 @@
4546

4647
class MeshAlgoDistributePointsTest( unittest.TestCase ) :
4748

48-
def pointTest( self, mesh, points, density, error=0.05 ) :
49+
def checkForDuplicates( self, positions ) :
50+
duplicatePositions = []
51+
uniquePositions = set()
52+
for p in positions:
53+
prevNumUnique = len( uniquePositions )
54+
uniquePositions.add( tuple( p ) )
55+
if not len( uniquePositions ) > prevNumUnique:
56+
duplicatePositions.append( p )
57+
58+
self.assertEqual( duplicatePositions, [] )
59+
60+
def checkVectorListsAlmostEqualUnordered( self, aIn, bIn, tolerance ) :
61+
l = len( aIn )
62+
self.assertEqual( len( bIn ), l )
63+
a = IECore.V3fVectorData( sorted( aIn, key = lambda v : v[0] ) )
64+
b = IECore.V3fVectorData( sorted( bIn, key = lambda v : v[0] ) )
65+
aMatched = IECore.BoolVectorData( l )
66+
67+
aStartIndex = 0
68+
69+
for i in b:
70+
while aStartIndex < l and a[aStartIndex][0] < i[0] - tolerance:
71+
aStartIndex += 1
72+
73+
matched = False
74+
aIndex = aStartIndex
75+
while aIndex < l and a[aIndex][0] < i[0] + tolerance:
76+
if not aMatched[aIndex]:
77+
if i.equalWithAbsError( a[aIndex], tolerance ):
78+
aMatched[aIndex] = True
79+
matched = True
80+
continue
81+
aIndex += 1
82+
83+
if not matched:
84+
raise AssertionError( "No match for point ", i )
85+
86+
self.assertTrue( all( aMatched ) )
87+
88+
def pointTest( self, mesh, points, density, error=0.05, checkDuplicates = True ) :
4989

5090
self.assertTrue( "P" in points )
5191
self.assertEqual( points.numPoints, points['P'].data.size() )
@@ -58,6 +98,9 @@ def pointTest( self, mesh, points, density, error=0.05 ) :
5898
pointsPerFace = [ 0 ] * mesh.verticesPerFace.size()
5999
positions = points["P"].data
60100

101+
if checkDuplicates:
102+
self.checkForDuplicates( positions )
103+
61104
## test that the points are on the mesh
62105
for p in positions :
63106
self.assertAlmostEqual( meshEvaluator.signedDistance( p, result ), 0.0, 6 )
@@ -82,6 +125,30 @@ def testSimple( self ) :
82125
p = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 100 )
83126
self.pointTest( m, p, 100 )
84127

128+
def testActualDistribution( self ) :
129+
# Most of these tests target the properties of the generated points, rather than the specific points
130+
# chosen. This is good for the generality of the tests, and will come in handy if we ever switch
131+
# the implementation. But it's probably worth having one test that actually asserts the exact
132+
# distribution we are currently using.
133+
134+
# This also checks the handling of points that lie on edges - we accept <0,0> as lying within the
135+
# rect from 0 -> 1, but <1,0>, <0,1> and <1,1> are considered outside ( this ensures that if adjacent
136+
# polygons are added, the point will be included exactly once )
137+
138+
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ), imath.V2i( 1 ) )
139+
points = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 5 )["P"].data
140+
refPoints = [
141+
imath.V3f( 0 ),
142+
imath.V3f( 0.612342954, 0.365361691, 0 ),
143+
imath.V3f( 0.309612036, 0.182079479, 0 ),
144+
imath.V3f( 0.0924436674, 0.497548342, 0 ),
145+
imath.V3f( 0.512147605, 0.871189654, 0 )
146+
]
147+
self.assertEqual( len( points ), len( refPoints ) )
148+
for a, b in zip( points, refPoints ):
149+
with self.subTest( a=a, b=b ):
150+
self.assertTrue( a.equalWithRelError( b, 0.0000001 ) )
151+
85152
def testRaisesExceptionIfInvalidUVs( self ) :
86153

87154
m = IECore.Reader.create( os.path.join( "test", "IECore", "data", "cobFiles", "pCubeShape1.cob" ) ).read()
@@ -129,7 +196,10 @@ def testDensityMaskPrimVar( self ) :
129196

130197
m['density'] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 2 ) )
131198
p = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 1000 )
132-
self.pointTest( m, p, 1000, error=0.1 )
199+
# We happen to hit an edge here which has two different UV space locations, so we don't avoid creating
200+
# duplicates - that's probably OK, we just want to make sure that we don't produce the same UV point
201+
# multiple times
202+
self.pointTest( m, p, 1000, error=0.1, checkDuplicates = False )
133203

134204
m['density'] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( -0.001 ) )
135205
p = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 1000 )
@@ -167,8 +237,8 @@ def testRefPosition( self ):
167237
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 1 ) )
168238
m["altP"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ i * 2 + imath.V3f( 1, 7, 10 ) for i in m["P"].data ] ) )
169239

170-
p1 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 40 )
171-
p2 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 10, refPosition = "altP" )
240+
p1 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 400 )
241+
p2 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 100, refPosition = "altP" )
172242

173243
# Using a reference position with a scale affects the density ( which is compensated by changing the
174244
# density argument, but otherwise does not affect anything )
@@ -179,7 +249,7 @@ def testRefPosition( self ):
179249
# across a much larger area, but the point counts are kept constant
180250
m["P"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ i * 100 for i in m["P"].data ] ) )
181251

182-
p3 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 10, refPosition = "altP" )
252+
p3 = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = 100, refPosition = "altP" )
183253
self.assertLess( p3.bound().min()[0], -100 )
184254
self.assertGreater( p3.bound().max()[0], 100 )
185255
self.assertEqual( p3.numPoints, p2.numPoints )
@@ -227,6 +297,50 @@ def testDensityRange( self ) :
227297
m = IECore.Reader.create( os.path.join( "test", "IECore", "data", "cobFiles", "pCubeShape1.cob" ) ).read()
228298
self.assertRaises( RuntimeError, IECoreScene.MeshAlgo.distributePoints, m, -1.0 )
229299

300+
def testEdgeOverlaps( self ):
301+
# As long as we don't change the edges or stretch the UVs, we should be able to add more subdivisions,
302+
# or more vertices around within the mesh, and still get identical points out ( down to floating point
303+
# error ). This test is run with enough points and polygons to have a good chance of triggering
304+
# special case interactions of points lying on or near edges and vertices. We want to make sure that
305+
# every point gets output exactly once, not being doubled up when it's on a shared edge between two
306+
# polygons, or being missed.
307+
308+
dens = 10000
309+
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ), imath.V2i( 1 ) )
310+
refPoints = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = dens )["P"].data
311+
312+
div = 256
313+
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ), imath.V2i( div ) )
314+
pointsFromHighDensity = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = dens )["P"].data
315+
316+
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ), imath.V2i( div ) )
317+
jitterUVs = m["uv"].data.copy()
318+
jitterPs = m["P"].data.copy()
319+
320+
jitter = 0.25 / div
321+
random.seed( 42 )
322+
for y in range( 1, div ):
323+
for x in range( 1, div ):
324+
i = y * ( div + 1 ) + x
325+
jitterUVs[ i ] = jitterUVs[ i ] + imath.V2f(
326+
random.uniform( -jitter, jitter ), random.uniform( -jitter, jitter )
327+
)
328+
jitterPs[ i ] = imath.V3f( jitterUVs[ i ][0], jitterUVs[ i ][1], 0 )
329+
m["uv"] = IECoreScene.PrimitiveVariable(
330+
IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, jitterUVs, m["uv"].indices
331+
)
332+
m["P"] = IECoreScene.PrimitiveVariable(
333+
IECoreScene.PrimitiveVariable.Interpolation.Vertex, jitterPs
334+
)
335+
pointsFromJittered = IECoreScene.MeshAlgo.distributePoints( mesh = m, density = dens )["P"].data
336+
337+
self.checkForDuplicates( refPoints )
338+
self.checkForDuplicates( pointsFromHighDensity )
339+
self.checkForDuplicates( pointsFromJittered )
340+
341+
self.checkVectorListsAlmostEqualUnordered( pointsFromHighDensity, refPoints, 0.0000002 )
342+
self.checkVectorListsAlmostEqualUnordered( pointsFromJittered, refPoints, 0.0000002 )
343+
230344
def testVertexUVs( self ) :
231345

232346
m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 4 ) )
@@ -259,7 +373,7 @@ def testPrimitiveVariables( self ):
259373
self.assertEqual( p.keys(), ['N', 'P', 'cA', 'cB', 'fvA', 'fvB', 'uA', 'uB', 'uv', 'vA', 'vB', 'vC'] )
260374
self.assertEqual( p['cA'], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 42 ) ) )
261375
self.assertEqual( p['cB'], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.M44fData( imath.M44f( 7 ) ) ) )
262-
self.assertEqual( p.numPoints, 15 )
376+
self.assertEqual( p.numPoints, 10 )
263377
for i in range( p.numPoints ):
264378
v = { k : p[k].data[i] for k in p.keys() if k[0] != 'c' }
265379
self.assertAlmostEqual( v['uv'][0], v['P'][0] + 0.5, places = 6 )
@@ -288,7 +402,7 @@ def testPrimitiveVariables( self ):
288402
self.assertEqual( p.keys(), ['N', 'P', 'cA', 'cB', 'fvA', 'fvB', 'uA', 'uB', 'uv', 'vA', 'vB', 'vC'] )
289403

290404
# Check that the overrides applied correctly
291-
self.assertEqual( p.numPoints, 410 )
405+
self.assertEqual( p.numPoints, 406 )
292406

293407
# Test variable types that can't be interpolated
294408
m['invalid1'] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.StringVectorData( [ "foo" ] * 16 ) )

0 commit comments

Comments
 (0)