Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow 3d triangulation #1691

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

drewish
Copy link
Contributor

@drewish drewish commented Dec 23, 2016

I needed to do some basic 3d triangulation and noticed that libtess2 supports it but the Triangulator wrapper did not. It wasn't too hard to hook it up but the mesh output is wonky. I've added some really basic unit tests to demonstrate what it's doing.

@@ -200,7 +200,7 @@ class TriMesh : public geom::Source {
size_t getNumIndices() const override { return mIndices.size(); }
//! Returns the total number of triangles contained by the TriMesh.
size_t getNumTriangles() const { return mIndices.size() / 3; }
//! Returns the total number of indices contained by the TriMesh.
//! Returns the total number of verticies contained by the TriMesh.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to these docs and this seemed like a bit of copy paste from a couple lines above.

//! Performs the tesselation, returning a TriMesh2d
TriMeshRef createMesh( Winding winding = WINDING_ODD );
TriMeshRef createMesh( Winding winding = WINDING_ODD, u_int8_t positionDims = 2 );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this interface but couldn't think of anything better.

if( positionDims == 2 ) {
result.appendPositions( (vec2*)tessGetVertices( mTess.get() ), tessGetVertexCount( mTess.get() ) );
} else {
result.appendPositions( (vec3*)tessGetVertices( mTess.get() ), tessGetVertexCount( mTess.get() ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love suggestions on better ways to make this type conversion conditional.

@paulhoux
Copy link
Collaborator

paulhoux commented Dec 23, 2016

Will this PR allow people to add both 2D and 3D vertices to the same Triangulate instance? Because I think that would be problematic, conceptually.

In that case it would be better to have a Triangulate2d and Triangulate3d, similar to other Cinder classes.

@drewish
Copy link
Contributor Author

drewish commented Dec 23, 2016

It seems like libtess2 uses 3 dimensions internally and leaves the z at 0 if you don't provide a value but I think it could be clearer to have specialized classes for each. I'll take a swing at that interface and see how it looks.

@drewish
Copy link
Contributor Author

drewish commented Dec 23, 2016

I took a swing at putting the 3d version in it's own class. I'm not sure if there's a good way to share code between the two classes. Since this is really just a proof of concept I set it up as a Triangulator3d class that used Triangulator::Winding and Exception. Ended up having to duplicate a fair bit of code but open to see what people think.

@vinjn
Copy link
Contributor

vinjn commented Dec 24, 2016

How about converting into a templated class?

template<typename T>
class Triangulator {
};

typedef Triangulator<vec2>		Triangulator2;
typedef Triangulator<vec3>		Triangulator3;

Copy link
Contributor

@MikeGitb MikeGitb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the code is copied from the existing class, but the allocate function seems buggy to me. However, the fact that people apparently used it successfully in the past might mean that I'm wrong.

ma.memalloc = stdAlloc;
ma.memfree = stdFree;
ma.userData = (void*)&mAllocated;
ma.extraVertices = 2560; // realloc not provided, allow 256 extra vertices.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment doesn't fit the code (256 <-> 2560 ) - is the 0 there deliberate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah been noticing some other weird behavior since that structure has an outOfMemory member that isn't explicitly initialized to 0. So sporadically it seems to fail once it reuses memory the OS hasn't zeroed out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or rather, the TESStesselator's outOfMemory value wasn't being initialized. It seems like we could switch the malloc to a calloc in our stdAlloc.

But to your original point I'm not sure why that number changed or if it was accidental.

ma.userData = (void*)&mAllocated;
ma.extraVertices = 2560; // realloc not provided, allow 256 extra vertices.

mTess = shared_ptr<TESStesselator>( tessNewTess( &ma ), tessDeleteTess );
Copy link
Contributor

@MikeGitb MikeGitb Dec 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit fishy to me. I don't know how exactly the tess library works, but you are passing the address of a local variable (ma) to a function, that will take that address and store it in a heap allocated TESStesselator object which will then be used long after the local object is gone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah agreed it seem suspect. it looks like tessNewTess stores a reference to it in
the mTess we get back. i'm not sure why it doesn't cause more obvious problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MikeGitb what seems like a sane way to handle the TESSalloc? it seems like it should be an instance variable but then you'd need to include tesselator.h in Triangulate.h. so pointer or shared_ptr?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drewish: If backwards compatibility was not an issue, I'd wrap the mTess pointer, the allocator and the used tess... functions and in particular the allocate function into a - forward declared - class and store a single std::unique_ptr. As things stand just creating a member variable for the allocator as you suggested seems to be the best solution.
However, as it is a private/protected variable that isn't shared with anything, I'd not use a shared_ptr but a unique_ptr.

{
TriMesh result( TriMesh::Format().positions( 3 ) );

tessTesselate( mTess.get(), (int)winding, TESS_POLYGONS, 3, 3, 0 );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a 1 for success but we're not checking it leading to very strange results when you hit the outOfMemory bug I mentioned in the other comment.

@drewish
Copy link
Contributor Author

drewish commented Dec 25, 2016

@vinjn yeah that was what i'd looked at initially but i wasn't clear how you'd get a scalar 2 or 3 to pass as a parameter out of the template type.

@vinjn
Copy link
Contributor

vinjn commented Dec 26, 2016

int getVecDim(vec3) { return 3;}
int getVecDim(vec2) { return 2;}

or using type traits like ChanTraits.h
@drewish

@drewish
Copy link
Contributor Author

drewish commented Dec 26, 2016

I'm thinking this might be hard to generalize for random 3d input. I've been getting really strange behavior with multiple contours. Adding a second contour causes it to return no output. I think this is due to the way libtess builds the arrangement by projecting everything onto a single sweep plane. Duplicated points on the plan seem to clear the output. It looks like you can influence the choice of plane by passing in a normal. So I think the only way it would reliably work in 3d would be to either require a normal be provided when calculating the mesh or just limit it to a single input contour.

Copy link
Contributor

@MikeGitb MikeGitb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm beginning to think that Triangulate needs a major overhaul. found two more bugs.


tessTesselate( mTess.get(), (int)winding, TESS_POLYGONS, 3, 3, 0 );
result->appendPositions( (vec3*)tessGetVertices( mTess.get() ), tessGetVertexCount( mTess.get() ) );
result->appendIndices( (uint32_t*)( tessGetElements( mTess.get() ) ), tessGetElementCount( mTess.get() ) * 3 );
Copy link
Contributor

@MikeGitb MikeGitb Dec 29, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that cast from const int* (returned by tessGetElements) to uint32_t* is actually undefined bahavior (illegal)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there's a few of these conversion that seemed sketchy but short of changing the libtess I wasn't sure what to do about that. the lib seems to use signed ints for a lot of things that seemed best as unsigned.

void Triangulator3d::addPolyLine( const vec3 *points, size_t numPoints )
{
if( numPoints > 0 )
tessAddContour( mTess.get(), 3, &points, sizeof(vec3), (int)numPoints );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the & in front of points correct here? I don't think we want a pointer to a pointer here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope it's a bug. Added a test case to drive this code path and it failed until I removed the &.


mTess = shared_ptr<TESStesselator>( tessNewTess( &ma ), tessDeleteTess );

mAlloc = shared_ptr<TESSalloc>( new TESSalloc() );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MikeGitb I tried doing a shared_ptr on this but got a rather cryptic error:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:2523:27: Invalid application of 'sizeof' to an incomplete type 'TESSalloc'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the problem is that Triangulate has an implicitly defined destructor. That means that the compiler tries to generate the destructor in every translation unit that includes the header file. However, in most translation units, it doesn't have access to the Definition of TESSalloc which it apparently needs for the destructor of mAlloc.
Try defining an empty destructor in Triangulate.cpp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really want to use a shared_ptr instead of a unique_ptr may I suggest using std::make_shared?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry that was a typo, i meant to write unique_ptr. i agree that makes more sense so i'll try adding a destructor and see if that helps.

Copy link
Contributor Author

@drewish drewish Dec 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least after adding the destructor I'm getting an error that's understandable:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:2523:27: Invalid application of 'sizeof' to an incomplete type 'TESSalloc'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that the same message as you got before?

void Triangulator::addPolyLine( const PolyLine3f &polyLine )
{
if( polyLine.size() > 0 )
tessAddContour( mTess.get(), 3, &polyLine.getPoints()[0], sizeof(vec3), (int)polyLine.size() );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to just do polyLine.getPoints().data() here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that also seems more logical to me (probably pre c++11 code when vector didn't have the data() member function). I'd usually also not write sizeof(vec3), but sizeof(*polyLine.getPoints().data()) as this makes the code more generic and less prone to refactoring erors (but admittedly also more ugly)

@@ -482,6 +482,7 @@ TESStesselator* tessNewTess( TESSalloc* alloc )
if ( tess == NULL ) {
return 0; /* out of memory */
}
tess->outOfMemory = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there was already one other edit to this lib this seemed like the cleanest way to make sure this value was initialized.

@drewish
Copy link
Contributor Author

drewish commented Dec 29, 2016

I went ahead and folded this back into one class. The only difference is flow is really in the mesh outputting so I just added a parallel set of calc/createMesh3d functions. The 3d version takes a normal and I think in the docs we just need to make it clear that you only want to tesselate things on the same plane unless you know what you're doing.

@MikeGitb
Copy link
Contributor

I was wrong btw.: The cast from int* to uin32_t* seems to be legal (at least on systems, where uint32_t is the same as unsigned int

@drewish
Copy link
Contributor Author

drewish commented Jan 8, 2017

Since over on #1698 it sounded like PolyLine3 might not be around long I switched this interface to just use std::vector<vec3> instead.

@drewish drewish force-pushed the triangulate-3d branch 2 times, most recently from eb1430b to 2ffc84e Compare January 16, 2017 03:44
@drewish drewish force-pushed the triangulate-3d branch 2 times, most recently from 9365852 to e53ef4a Compare January 23, 2017 03:39

tessTesselate( mTess.get(), (int)winding, TESS_POLYGONS, 3, 2, 0 );

float normal[3] = { 0, 0, 1 };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'd started passing this in to avoid computing it, but I need to test it out. I think the old behavior would flip the normal to use a (0,0,-1) if the winding order was backwards. I need to add test cases for that.

- Provide a normal for 2d tessleation to avoid computing one
- Fix bug when adding array of vec2
- Adding some unit tests
@MikeGitb
Copy link
Contributor

Any reason this didn't get merged?

@drewish
Copy link
Contributor Author

drewish commented Feb 20, 2020

Not that I'm aware of. Happy to rebase it or make any changes that are necessary.

@andrewfb
Copy link
Collaborator

Sorry for the unreasonable delay on this. Two quick questions as my head is no longer in this code unfortunately. First, should we consider moving to the latest here? https://github.com/memononen/libtess2 And second, does the tesselator do the right thing if I supply a mixture of 2D and 3D contours as input?

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.

5 participants