Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Closed
wongshek opened this issue Sep 14, 2021 · 29 comments
Closed

Faces normal reverse after triangulation ? #319

wongshek opened this issue Sep 14, 2021 · 29 comments
Labels

Comments

@wongshek
Copy link

Describe the issue
Import a polygon .obj file with triangulation (earcut option on), then export to .off, but some face normal reverse.

To Reproduce

/*********************Read Mesh*******************/
std::string inputfile = meshName;
tinyobj::ObjReaderConfig reader_config;
reader_config.triangulate = true;
reader_config.triangulation_method = "earcut";
reader_config.vertex_color = false;
reader_config.mtl_search_path = "./";

tinyobj::ObjReader reader;

if (!reader.ParseFromFile(inputfile, reader_config)) {
	if (!reader.Error().empty()) {
		std::cerr << "TinyObjReader: " << reader.Error();
	}
	exit(1);
}

auto& attrib = reader.GetAttrib();
auto& shapes = reader.GetShapes();
auto& materials = reader.GetMaterials();

// Loop over vertices
for (size_t i = 0; i < attrib.vertices.size(); i += 3)
{
	Point3d XYZ;
	XYZ.x = attrib.vertices[i + 0];
	XYZ.y = attrib.vertices[i + 1];
	XYZ.z = attrib.vertices[i + 2];
	vertexdPosition.emplace_back(XYZ);
}

// Loop over shapes
for (size_t s = 0; s < shapes.size(); s++)
{
	// Loop over faces(triangle)
	for (size_t f = 0; f < shapes[s].mesh.num_face_vertices.size(); f++)
	{
		// Access to vertex
		int id_x = shapes[s].mesh.indices[3 * f + 0].vertex_index;
		int id_y = shapes[s].mesh.indices[3 * f + 1].vertex_index;
		int id_z = shapes[s].mesh.indices[3 * f + 2].vertex_index;

		Point3i index;
		index.x = id_x;
		index.y = id_y;
		index.z = id_z;

		triangleIndex.emplace_back(index);
	}
}

/*********************Write Mesh*******************/
std::ofstream outFile("mesh.off");
outFile << "OFF" << std::endl;
outFile << vertexdPosition.size() << " "
		<< triangleIndex.size() << " " << 0 << std::endl;
for (const auto& pos : vertexdPosition)
{
	outFile << pos.x << " " << pos.y << " " << pos.z << std::endl;
}
outFile << std::endl;
for (const auto& idx : triangleIndex)
{
	outFile << 3 << " " << idx.x << " " << idx.y << " " << idx.z << std::endl;
}
outFile.close();
  • Result:
    image

Expected behavior
Face without reverse.

Environment

  • TinyObjLoader version 2.0.0
  • OS: [windows 10]
@syoyo syoyo added the bug label Sep 14, 2021
@syoyo
Copy link
Collaborator

syoyo commented Sep 14, 2021

@wsh2021 Thanks!

According to earcut's document, mapbox earcut produces triangles in clockwise

Three subsequent indices form a triangle. Output triangles are 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)

#320

@wongshek
Copy link
Author

@syoyo

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):
ZH2_big.obj
ZH2_big.mtl
ZH2_result.off

image

@syoyo
Copy link
Collaborator

syoyo commented Sep 14, 2021

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)

@wongshek
Copy link
Author

Can we cross multiply two adjacent convex edges(CCW order) to determine the normal vector?

@wongshek
Copy link
Author

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?

@syoyo
Copy link
Collaborator

syoyo commented Sep 15, 2021

@wsh2021 Axis selection for 3D -> 2D conversion is done in here:

// find the two axes to work in

You can print axes to debug whether it is working well or not.

@wongshek
Copy link
Author

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:

image

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:

CounterClockWise: 5  0  1  2  3  4  5  1  2  2  4  5
ClockWise: 1  0  5  4  3  2  1  5  4  4  2  1
TinyObjLoader: 5  4  3  2  1  0  5  3  2  2  0  5

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.

@syoyo
Copy link
Collaborator

syoyo commented Sep 16, 2021

@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

@wongshek
Copy link
Author

Waiting for your good news 👍

@syoyo
Copy link
Collaborator

syoyo commented Sep 16, 2021

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:

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

@syoyo
Copy link
Collaborator

syoyo commented Sep 18, 2021

@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.

zh2-big-triangulated

@wongshek
Copy link
Author

@syoyo

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.

@syoyo
Copy link
Collaborator

syoyo commented Sep 29, 2021

@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 ZH2_three_face.obj

zh2-three-tri

@wongshek
Copy link
Author

wongshek commented Oct 8, 2021

@syoyo

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.

@syoyo
Copy link
Collaborator

syoyo commented Oct 9, 2021

Hmm... at least I found an issue when determining 2D projection axis.
If we can find best 2D projection axis(averaged normal direction of a given polygon?), ZH3_one_face.obj can be tessellated correctly.

@wongshek
Copy link
Author

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.

@syoyo
Copy link
Collaborator

syoyo commented Oct 13, 2021

@wsh2021 Oh you can contribute your 3D to 2D projection code! PR is much appreciated.

@syoyo syoyo mentioned this issue Dec 17, 2021
@wongshek
Copy link
Author

@syoyo
I'm sorry I haven't contact you for quite a while, too busy recently...

In order to convert 3D points into 2D, we can left multiply 3D point vector by a rotation matrix.
The function to get this 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
center is one vertex
axis is another vertex

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);
	}
}

@syoyo
Copy link
Collaborator

syoyo commented Dec 23, 2021

@wsh2021 Thanks!

Vec3d normal = mesh.normal(f_h);

How you calculate the normal for (concave and convex) polygon?

@wongshek
Copy link
Author

@syoyo

Use the Newell's Method, here are some related links:
opengl-wiki
stackoverflow

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;
}

@syoyo
Copy link
Collaborator

syoyo commented Dec 24, 2021

@wsh2021 Great! Thanks! I'll implement it into tinyobjloader and test if it goes well.

@tylermorganwall
Copy link
Contributor

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.

@syoyo
Copy link
Collaborator

syoyo commented Oct 2, 2022

@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.

Screenshot from 2022-10-02 19-38-42

@syoyo
Copy link
Collaborator

syoyo commented Oct 9, 2022

PR #340 has been merged. So the issue should be solved in the recent release version of tinyobjloader.

@wongshek If you still encounter the issue, please reopen the issue.

@syoyo syoyo closed this as completed Oct 9, 2022
@tylermorganwall
Copy link
Contributor

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.

@syoyo
Copy link
Collaborator

syoyo commented Oct 21, 2022

@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
Copy link

cedarz commented Jan 15, 2023

@syoyo I meet the same question when using the latest release branch. When setting the CullMode to back, some submeshes can't be rendered, while CullMode as none gets nothing wrong. So I think it must be the reversing of face orientation after triangulation.
test obj file is from here

@syoyo
Copy link
Collaborator

syoyo commented Jan 15, 2023

@cedarz You should post minimal reproducible .obj with screenshots to illustrate the issue.

@cedarz
Copy link

cedarz commented Jan 16, 2023

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants