-
Notifications
You must be signed in to change notification settings - Fork 111
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
Add JS bindings for triangulation #393
Conversation
I would definitely love to see this too :) . I also have a tendency to produce some shapes without booleans procedurally. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good idea. I've basically been waiting for someone to ask for this, because I wasn't quite sure what the right API shape would be.
bindings/wasm/bindings.cpp
Outdated
for (auto& poly : polygons) { | ||
SimplePolygonIdx simpleIndexed; | ||
for (const glm::vec2& polyVert : poly) { | ||
simpleIndexed.push_back({polyVert, idx++}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implies the output indices refer to the polygon points in order, which I suppose is simple enough. I guess you have to do a mapping back to your polyhedron verts then? It would be helpful to include an example showing how to use this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I do backmapping of the verts. What is the right place for such an example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an example how I use the triangulation API:
// Inputs:
// vert_pos: set of 3D points
// polygons: set of polygons, wound CCW and representing multiple polygons
// and/or holes, polygon point is expressed as an index to vert_pos
// normal: normal of the polygons plane
const projection = axis_aligned_projection(normal);
let idx = 0;
const vert_i_map: number[] = [];
const polygons_2d = polygons.map((polygon) =>
polygon.map((vert_i) => {
vert_i_map[idx++] = vert_i;
return projection(vert_pos[vert_i]);
}),
);
const result = manifold.triangulate(polygons_2d);
const tris = result.map((tri) => tri.map((vert_i) => vert_i_map[vert_i]));
function axis_aligned_projection(normal) {
const abs_normal = new Vector3(Math.abs(normal.x), Math.abs(normal.y), Math.abs(normal.z));
let x, y, z;
if (abs_normal.z > abs_normal.x && abs_normal.z > abs_normal.y) {
// x-y plane
z = normal.z;
x = 0;
y = 1;
} else if (abs_normal.y > abs_normal.x) {
// z-x plane
z = normal.y;
x = 2;
y = 0;
} else {
// y-z plane
z = normal.x;
x = 1;
y = 2;
}
if (z < 0) {
return (v) => [v[y], v[x]];
} else {
return (v) => [v[x], v[y]];
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about creating a new sample next to knot.cpp
- Write in some reasonably simple polyhedron, then run your code to turn it into a Manifold
. Then you can add it to samples_test
and check its volume, which should give us decent test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble coming up with a good example. If I make something simple, e.g. a modified cube with the top face a little bit smaller than the bottom face and with a square hole through both top and bottom face, it feels too simple to be included in the samples. Right now, samples are pretty complex awesome artistic objects.
On the other hand, if I make something more sophisticated like a basic chair (without the need for booleans), it would be more practical to have some kind of general polyhedron constructor function than just a triangulation function. And for this polyhedron constructor, TriangulateIdx
is more practical than Triangulate
.
So after thinking about this more deeply I suggest two things:
- Add a polyhedron constructor. Possibly in
constructors.cpp
? This could be also exposed in the JS bindings. There is, however, one limitation of the following implementation. The face triangles can't be backmapped to the input face polygons as we don't know how many triangles make up which face. We could easily returntri2face
mapping from this function alongside the Manifold object (which would make it an outlier compared to the other constructors always returning just a manifold), or leave the users to use the triangulation directly if they need the mapping. I personally prefer the direct use of triangulation in this case as I'm able to prepare all the triangles together with normals and UVs in one go.
Manifold Manifold::Polyhedron(
const std::vector<glm::vec3> vertPos,
const std::vector<std::vector<std::vector<int>>>& polygonsVec,
const std::vector<glm::vec3> normals) {
Mesh mesh;
mesh.vertPos = vertPos;
for (int i = 0; i < polygonsVec.size(); i++) {
auto& polygons = polygonsVec[i];
std::vector<glm::ivec3> tris;
if (polygons.size() == 1 && polygons[0].size() == 3) {
auto& p = polygons[0];
tris.push_back({p[0], p[1], p[2]});
} else if (polygons.size() == 1 && polygons[0].size() == 4) {
auto& p = polygons[0];
tris.push_back({p[0], p[1], p[3]});
tris.push_back({p[1], p[2], p[3]});
} else {
auto& normal = normals[i];
const glm::mat3x2 projection = GetAxisAlignedProjection(normal);
auto polygons2d = To2DPolygon(polygons, projection, vertPos);
tris = Triangulate(polygons2d);
}
for (auto& tri : tris) {
mesh.triVerts.push_back(tri);
}
}
return Manifold(mesh);
}
- Add
TriangulateIdx
to the JS bindings, either as a replacement ofTriangulate
, or as an extra option. This would make it easier to implement custom polyhedron constructors in JS as it would not need remapping. The typescript signature could follow the C++ signature.
export type PolyVert = [Vec2, number]
export type SimplePolygonIdx = PolyVert[];
export type PolygonsIdx = SimplePolygonIdx[];
export function triangulateIdx(polygons: PolygonsIdx, precision?: number): Vec3[];
I look forward to hearing what you think about this. I'm sorry that I can't react faster, but the timezone shift makes it hard 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it looks like adding polygon triangualtion utility into manifold would make sense if a big performance boost can be achieved. How about only for 2d that is extruded ? ... I am following discussions, but have not found time to go further than git pull and running existing examples, so my comments may be out of place from lack of insight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have Manifold::Extrude()
already, so that should be covered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit, I got lost in the discussion 😄 I'll try to add something reasonable.
From my perspective, polyhedron constructor is just a convenient way to make triangle meshes. I don't expect any special validity checks to happen on the input - it is my problem if I have it wrong.
In my project, I do a lot of derived geometry. Let's say I have a polyhedron as input and I need to push one of its faces. Or I want to "extrude" one of its faces. Or both at the same time. See the pictures below.
The first picture shows the input polyhedron. The front face is selected, so you can see what triangles it consists of.
The second picture shows the output after "extruding" the blue object from one of the other faces. The geometry of the blue object is calculated by moving the face points and using custom JS implementation of polyhedron constructor which uses manifold's triangulation. The input polyhedron is also modified. The affected points are shifted and the faces are re-triangulated. In this case, the re-triangulation is really needed, the input triangles would not work for the changed front face.
You may wonder why I need manifold at all as this could be done with triangulation only. But I also need fast booleans, mainly for cutting the objects into pieces.
So this is my use case for working with polyhedrons (as an abstraction over triangle mesh) and I accept the risk of rounding errors. After all, who can find a perfectly planar polygon in the real world?
But now back to the original topic:
I think it is enough for manifold to give access to the triangulation 📐 So it is ok to merge this PR without further additions 😄
I just can't think of any example showcasing the triangulation that wouldn't be trivial or too tedious to make. Please, do you have any ideas? And regarding tests, I think the triangulation is well tested right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, I agree this PR can merge as is. That's great that you're working on face push/pull - honestly I was thinking this could be a good feature for Manifold. Internally we already have a concept of polygon faces: groups of neighboring triangles with the same triRef.tri
. Would you have any interest in contributing this feature into Manifold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for merging! Regarding the push/pull, I don't feel so confident in C++ to implement it. I feel more at home in JS/TypeScript world.
And I'm aware of the triRef.tri
and rely on that heavily when extracting face polygons. It is extremely valuable for me. It allows me to reference faces in a stable way across multiple operations with the manifold.
rename original Triangulate to TriangulateIdx, move TriangulateJS from bindings.cpp to polygon.cpp, rename TrinagulateJS to Triangulate for consistent C++ and JS API, add optional precision parameter
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## master #393 +/- ##
==========================================
- Coverage 86.04% 85.90% -0.14%
==========================================
Files 36 36
Lines 4485 4492 +7
==========================================
Hits 3859 3859
- Misses 626 633 +7
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report in Codecov by Sentry. |
This looks ready to merge to me; let's just get the sample in there so people know how to use it. |
And maybe add a few simple tests as well? So we can check if it breaks later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
* js binding for triangulation * don't use CrossSection class as it alters the order of the vertices * remove unused code, rename original Triangulate to TriangulateIdx, move TriangulateJS from bindings.cpp to polygon.cpp, rename TrinagulateJS to Triangulate for consistent C++ and JS API, add optional precision parameter
The triangulation algorithm available from JS allows you to create specific shapes efficiently, often without the need for boolean operations.
I was missing this very much as I frequently create custom polyhedrons, either from scratch, or based on modified geometry. This allows me to construct a manifold from a set of planar 3D polygons. I just project them to 2D (axis-aligned projection), triangulate and feed all the triangles to the manifold mesh constructor. Similar functionality is in OpenSCAD too (polyhedron).
I believe the triangulation could be useful for many other users of the JS bindings and hope you'll be willing to merge this in some form.