-
-
Notifications
You must be signed in to change notification settings - Fork 618
Faces normal reverse after triangulation ? #319
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
Comments
@wsh2021 Thanks! According to earcut's document, mapbox earcut produces triangles in clockwise
https://github.com/mapbox/earcut.hpp/blob/master/README.md Whereas tinyobjloader uses counter-clockwise order, so there are face inversions. I made a fix. Please try this(at least I've confirmed face flipping has been fixed in my local test) |
Thanks for the quick response! I have tried the modified code, however there are still some bugs, I suspect there are some deeper reasons. See these files(I didn't delete unnecessary vertices): |
Hmm... we may need to consider vertex order(plane normal) of input .obj polygon. Although robust normal detection may not be possible or difficult for non-planar surface, at least it should work well for ZH2_big data(faces are mostly planar) |
Can we cross multiply two adjacent convex edges(CCW order) to determine the normal vector? |
I suppose robust normal detection method like Newell's method is adequate to almost-planar and non-convex surface, So is the problem caused by messing up the order when converting points from 3D to 2D? |
@wsh2021 Axis selection for 3D -> 2D conversion is done in here: tinyobjloader/tiny_obj_loader.h Line 1541 in 3e401b5
You can print axes to debug whether it is working well or not. |
Hi @syoyo I did some tests and found some interesting results. Test files: First, I build a 3D to 2D rotate matrix(make sure the normal of result 2D shape is points out of the paper) for comparison with tinyobjloader 3D to 2D method, then plot these 2D points in matlab: As you can see, the left figure is CounterClockWise while the right one is ClockWise, so I suppose tinyobjloader's 2D conversion method is indeed wrong towards some shapes. In addition, I also found that the output order of earcut(without holes) is always CounterClockWise, see below code: #include <array>
#include <vector>
#include <iostream>
#include <earcut.hpp>
int main()
{
// The number type to use for tessellation
using Coord = double;
// The index type. Defaults to uint32_t, but you can also pass uint16_t if you know that your
// data won't have more than 65536 vertices.
using N = uint32_t;
// Create array
using Point = std::array<Coord, 2>;
std::vector<std::vector<Point>> polygon_ccw;
std::vector<std::vector<Point>> polygon_cw;
std::vector<std::vector<Point>> polygon_tol;
// Fill polygon structure with actual data. Any winding order works.
// The first polyline defines the main polygon.
polygon_ccw.push_back({ {100, 0}, {100, 50}, {50, 50}, {50, 100}, {0, 100}, {0, 0} });
polygon_cw.push_back({ {100, 0}, {0, 0}, {0, 100}, {50, 100}, {50, 50}, {100, 50} });
polygon_tol.push_back({ {0, 0}, {0, 100}, {50, 50}, {50, 100}, {100, 100}, {100, 0} });
// Run tessellation
// Three subsequent indices form a triangle. Output triangles are clockwise???????
std::vector<N> indices_ccw = mapbox::earcut<N>(polygon_ccw);
std::vector<N> indices_cw = mapbox::earcut<N>(polygon_cw);
std::vector<N> indices_tol = mapbox::earcut<N>(polygon_tol);
// 4----3
// | |
// | 2----1
// | |
// | |
// 5---------0
std::cout << "CounterClockWise: ";
for (const auto& idx : indices_ccw) { std::cout << idx << " "; }
// 2----3
// | |
// | 4----5
// | |
// | |
// 1---------0
std::cout << "\nClockWise: ";
for (const auto& idx : indices_cw) { std::cout << idx << " "; }
// 3----4
// | |
// 1----2 |
// | |
// | |
// 0---------5
std::cout << "\nTinyObjLoader: ";
for (const auto& idx : indices_tol) { std::cout << idx << " "; }
} The output:
Now, If we use triangle vertex indices of "TinyObjLoader" result, draw triangle on the "CounterClockWise" shape, the triangle winding order is ClockWise, this should be the reason for face inversion. |
@wsh2021 Awesome! So we'll need to reorder indices by checking up face orientation of input triangle when 3D -> 2D conversion. To do that, summing the cross product of each triangle of the polygon and seeing its sign will be suffice, as done in Earcut's implementation: https://github.com/mapbox/earcut.hpp/blob/master/include/mapbox/earcut.hpp#L190 |
Waiting for your good news 👍 |
Oh, this wasn't suffice. We also need to consider input triangle is convex or concave: https://en.wikipedia.org/wiki/Curve_orientation BTW: For .obj, vertex coordinate is right-handed, face orientation uses counter-clockwise order > https://en.wikipedia.org/wiki/Wavefront_.obj_file |
@wsh2021 Ok, so after some investigation, the issue was Mapbox Earcut always reorder points in clockwise order(it ignores the face orientation of input polygon). So we need to flip the order of triangle indices of triangulated mesh by checking the orientation of input polygon and triangulated one(convex or concave wasn't a source of the issue) Possible fix is now this PR: https://github.com/tinyobjloader/tinyobjloader/tree/mapboxearcut-correct-facing @wsh2021 Could you please test more dataset you have? At least I've confirmed triangulation of ZH2-big works well. |
Sorry to reply so late. I just saw the message :) I have tested my data and most of the faces are correct, but I still found three wrong faces, see that ZH2_three_face.obj. Among these three faces, one may be because of the existence of common points, one may be because of the existence of collinearity, and the other I don't know why. |
@wsh2021 Oh, there was a mistake to compute signed area. Please use recent PR branch https://github.com/tinyobjloader/tinyobjloader/tree/mapboxearcut-correct-facing it fixes signed area computation and I've got correct result for |
Sorry to reply so late :) Anyway, I have tested my data and the normal of the faces are all correct, but I found some face missing in the result , see that ZH3_one_face.obj. |
Hmm... at least I found an issue when determining 2D projection axis. |
In fact, although the code doesn't look so elegant, I can get the correct results by projecting the 3D plane to 2D through a 3D to 2D matrix, and then performing earcut tessellate. |
@wsh2021 Oh you can contribute your 3D to 2D projection code! PR is much appreciated. |
@syoyo In order to convert 3D points into 2D, we can left multiply 3D point vector by a rotation matrix. inline Eigen::Matrix3d Get3DTo2DRotMat(
const Eigen::Vector3d& pnormal,
const Eigen::Vector3d& center,
const Eigen::Vector3d& axis)
{
Eigen::Vector3d row0 = (axis - center).normalized();
Eigen::Vector3d row1 = (pnormal).cross(row0).normalized();
Eigen::Vector3d row2 = pnormal.normalized();
Eigen::Matrix3d R;
R.row(0) = row0;
R.row(1) = row1;
R.row(2) = row2;
return R;
} pnormal is the face/plane normal Use this function: // Assume that points of each face are coplanar
for (const auto& f_h : mesh.faces())
{
CFVI fv_it = mesh.fv_range(f_h).begin();
Vec3d cent = mesh.point(*fv_it);
Vec3d axis = mesh.point(*(++fv_it));
// Ensure cent and axis not the same point
while (GetDistance(cent, axis) < 1e-4)
{
axis = mesh.point(*(++fv_it));
}
Vec3d normal = mesh.normal(f_h);
Mat3d R = Get3DTo2DRotMat(normal, cent, axis);
for (const auto& v_h : mesh.fv_range(f_h))
{
Vec3d pt_3d = mesh.point(v_h);
Vec2d pt_2d = (R * pt_3d).topRows(2);
}
} |
@wsh2021 Thanks!
How you calculate the normal for (concave and convex) polygon? |
Use the Newell's Method, here are some related links: And this is the simple implementation refer to openmesh // Get face normal
template <typename Point>
Vec3d GetFaceNormal(const std::vector<Point>& points_3d)
{
// use Newell's Method to compute the surface normal
Vec3d n(0, 0, 0);
for (size_t i = 0; i < points_3d.size(); ++i)
{
size_t j = (i + 1) % points_3d.size();
// http://www.opengl.org/wiki/Calculating_a_Surface_Normal
const auto& a = points_3d[i] - points_3d[j];
const auto& b = points_3d[i] + points_3d[j];
n[0] += (a[1] * b[2]);
n[1] += (a[2] * b[0]);
n[2] += (a[0] * b[1]);
}
n.normalize();
return n;
} |
@wsh2021 Great! Thanks! I'll implement it into tinyobjloader and test if it goes well. |
I have created a pull request that I believe fixes this issue. It uses Newell's method to project the points to the coordinate system defined by the normal and triangulate there. |
@tylermorganwall Thanks! the PR is here: #340 @wongshek Please try this branch https://github.com/tinyobjloader/tinyobjloader/tree/tylermorganwall-triangulation-fix (I've added some compilation fix over #340 ) solves triangulating your models. Apparently ZH3_one_face.obj now tessellated correctly. |
My original pull request had a bug introduced when switching to a struct instead of an array (x-axis selected instead of y while calculating Newell's method), so I submitted another pull request to fix that bug. |
@tylermorganwall Thanks! #346 With that fix, I've confirmed regression tests(e.g. https://github.com/tinyobjloader/tinyobjloader/blob/release/models/issue-319-002.obj ) still process triangulation correctly. |
@cedarz You should post minimal reproducible .obj with screenshots to illustrate the issue. |
@syoyo Feel sorry. I used fbxsdk to load the FBX version with trigulation, but get the same result. There may be something wrong with my coding. I should make it clear first, then post my results and test case here. |
Describe the issue
Import a polygon .obj file with triangulation (earcut option on), then export to .off, but some face normal reverse.
To Reproduce
Minimal files
ZH2.obj
ZH2.mtl
Code:
Expected behavior
Face without reverse.
Environment
The text was updated successfully, but these errors were encountered: