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

Model Optimization Improvements (low poly simplification, edge collapsing, keep volume) #19

Open
hybridherbst opened this issue Oct 11, 2019 · 36 comments
Assignees
Labels
bug Something isn't working

Comments

@hybridherbst
Copy link
Contributor

hybridherbst commented Oct 11, 2019

The original title was "Model where simplification fails badly", but the thread has become a very valuable discussion around optimizations and improvements.


Simplification of this (already pretty simple) model fails pretty badly using the default options:
boat_vertexpaint.zip

Target
image

Actual
LOD 0
image

LOD 1
image

Any ideas?

@Whinarn
Copy link
Owner

Whinarn commented Dec 7, 2019

I don't have any ideas as to why this would happen, but it's clearly a bug.
I don't have much time to look into this now, so I'll see when I have time.

@Whinarn Whinarn self-assigned this Dec 7, 2019
@Whinarn Whinarn added the bug Something isn't working label Dec 7, 2019
@bawar9
Copy link
Contributor

bawar9 commented Mar 15, 2020

This might be partly due to be an inherent limitation of the original algorithm and partly because of some bug. The original mesh simplification algorithm (Surface Simplification Using Quadric Error Metrics by Michael Garland) doesn't take into account the surface curvature to preserve edges with sharper corners(curved edges). I have tweaked the algorithm based of a new approach that takes discrete curvature into account and it generates some very good quality mesh simplification without sacrificing the low poly aspect of the original Garland algorithm. I will contribute my changes to this project when I get time to do so.

@Whinarn
Copy link
Owner

Whinarn commented Mar 23, 2020

@bawar9 I'm pretty sure the bug is related to this calculation. I might have made a mistake when I ported the original algorithm, or when I optimized it. Or it's even a problem in the original algorithm.

@bawar9
Copy link
Contributor

bawar9 commented Mar 23, 2020

@Whinarn There is an obvious problem in the implementation of Garland's algorithm here https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification
The guy modified the original algorithm and skipped the step of keeping a sorted list of Vertices with the least error on top, instead he just used an empirical threshold value to check which vertex pair (edge) to collapse. He did this to gain a big performance boost, but this can result in bad quality simplification. The same can be seen in the UnityMeshSimplifier implementation here https://github.com/Whinarn/UnityMeshSimplifier/blob/master/Runtime/MeshSimplifier.cs#L2467.
@Whinarn can't we keep a sorted heap with the least cost vertex at top instead of just using an empirical threshold value.

@Whinarn
Copy link
Owner

Whinarn commented Mar 23, 2020

@bawar9 I am aware. This was actually the reason why I picked this algorithm. I wanted something that was fast. I would be okay with adding a sorted heap, but only if it's optional. I would still like there to be a fast simplification, but that people can enable a less optimized simplification for a higher quality output.

@bawar9
Copy link
Contributor

bawar9 commented Mar 23, 2020

@bawar9 I am aware. This was actually the reason why I picked this algorithm. I wanted something that was fast. I would be okay with adding a sorted heap, but only if it's optional. I would still like there to be a fast simplification, but that people can enable a less optimized simplification for a higher quality output.

That would be really great, giving people the option to choose for themselves. For quite some time now I've been thinking which algorithm does Cinema4D use for this. It simplifies models super fast and the results are really great. I even created a threaded version of the UnityMeshSimplifier, even that couldn't surpass or even come closer to the Cinema4D mesh simplifier. I don't really mean to question your work in any way, you really have created a very good tool. I am just wondering that the Garland's algorithm has been living for quite some time and there might be newer and better algorithms out there. If I ever find anything better I would love to contribute my changes to the project.

@is3D-1
Copy link
Contributor

is3D-1 commented Apr 29, 2020

@Whinarn Thanks for sharing your code !
I came across your project while searching for ways to manipulate meshes in Unity as a personal journey. I sometimes have to import CAD files data into Unity and the process always involves simplifying the meshes as much as possible. I recently tried a third party plugin called PiXYZ to do it and, I must say, it does a tremendous job at importing from many source formats and at reducing polygons to a high degree with high quality. The only down side is its license cost (more than 1000 Euro/year).
I tried UnityMeshSimplifier (which is free) to "crunch" some imported meshes to a high degree but I've been a bit disappointed with the result. The tool gives good results when the simplification ratio is not to high. After reading the comments here, I decided to dive into the project and try to improve (accuracy and speed) the algorithm as much as possible. Here is what I have experimented so far:

  1. implemented a sorted list of edges by increasing order of error to determine the next edge to collapse.
  2. improve the quadrics error estimation by updating the error matrix Q after every edge contraction. this may ultimately avoid the need to update the mesh every iteration to improve the error calculation.
  3. Unioning the vertices matrix Q instead of adding them when calculating the error associated to an edge contraction.
  4. optimize the execution speed to alleviate the additional calculations required by steps 1 to 3.

The resulting algorithm gives pretty good results but it too fails badly on the boat mesh above ! I'm currently working on the problem. What I can say right now about the boat mesh is that there are two problems:

  1. Some new vertices appears far away from the boat. This comes from the geometry around the edge and the math behind the calculation of the new vertex location when an edge is collapsed. The location corresponds to a point p(x,y,z) that minimize the quadric error and is calculated by solving a set of 3 linear equations for x,y and z. The quadric error surfaces defined by the error matrix Q normally corresponds to ellipsoids (see section 5.1 of [https://dl.acm.org/doi/pdf/10.1145/258734.258849] and point p should corresponds to the center of these ellipsoids but in some situations, the surface can be degenerated. I suspect it is the case here as this outlier vertex actually corresponds to the minimum error location. So I don't think it's a bug in your code. I tried to catch these cases by inspecting the determinant of matrix Q' but with no success. I neither succeed with solving the set of equations with Gauss-Jordan Elimination method. I end up fixing the problem by testing the location of point p. If p is not along the collapsing edge, I reject p and fall back on the code that choose one the edge end point or midpoint.
  2. Some surfaces disappear. This is how the quadric error method works. The authors of the method suggest to add virtual planes with high weigh along the border to avoid collapsing it. I will implement this approach.

If I succeed with all of this I will post the code here and let you decide if it can be incorporated into your project.

Regards

@bawar9
Copy link
Contributor

bawar9 commented Apr 29, 2020

@is3D-1 Thank you for dedicating your time on the project, it looks like you have really dig deep into the issue. Can you please share your current code step (1- 4)?. I would like to test it on my side as well. You can send me your new "MeshSimplifier.cs" file if that's feasible.

Thank you

@is3D-1
Copy link
Contributor

is3D-1 commented Apr 29, 2020

@bawar9 I would be please to share it but the code has intricate parts with my development project. There are modifications in more than just "MeshSimplifier.cs" file and I have to manually operate the algorithm during this development. In fact I have dropped the uvs, normals and colors... until the mesh is ok. Again I will post it when it will be in a usable state, if you can wait until then...
I have made an interesting observation about the fix to Boat Problem 1 (BP1): it actually improve the result on other meshes as well when the simplification ratio is high. It is like the quadric error metric degenerate the further you increase the reduction ratio and BP1 fix seems to alleviate this.
I will fork the project and gradually pull the code there.

@Whinarn
Copy link
Owner

Whinarn commented Apr 29, 2020

@is3D-1 Great work! I'm impressed. I'm looking forward to seeing your results.

@is3D-1
Copy link
Contributor

is3D-1 commented Apr 29, 2020

@Whinarn Thank you. I have to admit I'm far from a working version. In the meantime, it would help if I could get some typical meshes to benchmark the solution. Regards.

@bawar9
Copy link
Contributor

bawar9 commented May 1, 2020

eresting observation about the fix to Boat Proble

Thank you, I'll wait for you changes to get published

@is3D-1
Copy link
Contributor

is3D-1 commented May 2, 2020

@bawar9 I don't know where to address this but I have looked at the curvature error method you have committed and I have some concerns:

The idea of taking curvature data into consideration is a great addition to the algorithm however, the way it is implemented is not correct. Let me explain:
1- In the CurvatureError(ref Vertex vert0, ref Vertex vert1) function, you take the maximum value of the dot product of all triangles touching Vert0_or_Vert1 as outer loop and Vert0_and_Vert1 as inner loop. Any triangle that is part of Vert0_and_Vert1 is also part of Vert0_or_Vert1 and thus the dot product of a triangle normal by himself is always 1. Hence, the CurvatureError() function only return the length of the edge... You could fix it by rejecting the case where a triangle is member of both set like that:

                foreach (var triangleWithViAndVjBoth in trianglesWithViAndVjBoth)
                {
                    if (triangleWithViAndVjBoth == triangleWithViOrVjOrBoth)
                        continue;
                    Vector3d normVecTriangleWithViAndVjBoth = triangleWithViAndVjBoth.n;
                    double dot = Vector3d.Dot(ref normVecTriangleWithViOrVjOrBoth, ref normVecTriangleWithViAndVjBoth);

                    if (dot > maxDotInner)
                        maxDotInner = dot;
                }

2- The CalculateError(...) function incorporate CurvatureError(..) only for the first case. Why it is not applied to all case ?

3- A good way to implement the curvature error would be to specify a value between 0..1 instead of a check box [0 or 1] to specify whether to preserve curvature or not. The resulting error would be:
error = E(vertex) + alpha * E(curvature) where alpha is [0..1]. Alpha =0 corresponds to keep details while alpha=1 corresponds to keep general shape...

Regards

@bawar9
Copy link
Contributor

bawar9 commented May 2, 2020

@bawar9 I don't know where to address this but I have looked at the curvature error method you have committed and I have some concerns:
The idea of taking curvature data into consideration is a great addition to the algorithm however, the way it is implemented is not correct. Let me explain:
1- In the CurvatureError(ref Vertex vert0, ref Vertex vert1) function, you take the maximum value of the dot product of all triangles touching Vert0_or_Vert1 as outer loop and Vert0_and_Vert1 as inner loop. Any triangle that is part of Vert0_and_Vert1 is also part of Vert0_or_Vert1 and thus the dot product of a triangle normal by himself is always 1. Hence, the CurvatureError() function only return the length of the edge... You could fix it by rejecting the case where a triangle is member of both set like that:
foreach (var triangleWithViAndVjBoth in trianglesWithViAndVjBoth)
{
if (triangleWithViAndVjBoth == triangleWithViOrVjOrBoth)
continue;
Vector3d normVecTriangleWithViAndVjBoth = triangleWithViAndVjBoth.n;
double dot = Vector3d.Dot(ref normVecTriangleWithViOrVjOrBoth, ref normVecTriangleWithViAndVjBoth);

                if (dot > maxDotInner)
                    maxDotInner = dot;
            }

2- The CalculateError(...) function incorporate CurvatureError(..) only for the first case. Why it is not applied to all case ?
3- A good way to implement the curvature error would be to specify a value between 0..1 instead of a check box [0 or 1] to specify whether to preserve curvature or not. The resulting error would be:
error = E(vertex) + alpha * E(curvature) where alpha is [0..1]. Alpha =0 corresponds to keep details while alpha=1 corresponds to keep general shape...
Regards

@is3D-1 Thank you for pointing out the issues. To keep it organized I will answer your points in order

► You're correct in regard to saying "Any triangle that is part of Vert0_and_Vert1 is also part of Vert0_or_Vert1 and thus the dot product of a triangle normal by himself is always 1", however you're incorrect in saying that the "CurvatureError" function will only return the length of the edge (Vi, Vj). This will be true only where the afore mentioned condition is true, which won't be the case always. This is not a mistake done by me but what the authors of the original paper https://www.hindawi.com/journals/mpe/2015/428917/ intended. Below you see 2 images of a model (Before incorporating you condition and After incorporating you condition). The model is simplified to 80% (0.8) and the resulting triangles count is 3608 in both the cases, however your condition seems to disregard discrete surface curvature in some areas of the model.

The curvature of the arm and circle on the belt is preserved nicely
Cap2(Bawar)

The curvature of the highlighted regions is slightly disrupted (Which is not the case with the original code. See the image above ^^^)
InkedCap1(continue)_LI

► Thank you for pointing it out this is a mistake on my side, I actually overlooked the second case. I'll make the changes and do a pull request after @Whinarn approves my last pull request.

► We can incorporate curvature error using
error = E(vertex) + alpha * E(curvature) where alpha is [0..1]. However the effect would be the same, I usually stress on simplicity and code readability so I used simple boolean conditions so that anyone can easily understand the code

@is3D-1
Copy link
Contributor

is3D-1 commented May 2, 2020

@bawar9 Thanks for your reply. I don't want to accuse you or anyone else who is contributing to the project. I view it simply as a discussion to improve the final result and again I appreciate your answer with pictures that clearly illustrate your point !
Regards

@bawar9
Copy link
Contributor

bawar9 commented May 3, 2020

@bawar9 Thanks for your reply. I don't want to accuse you or anyone else who is contributing to the project. I view it simply as a discussion to improve the final result and again I appreciate your answer with pictures that clearly illustrate your point !
Regards

@is3D-1 I am sorry if I sounded a little offensive, I can assure you that I was not. I am very happy to see some one like you contributing to the project, someone who knows what he's doing. This is a great chance for me to learn and enhance my knowledge from good people around. By no means am I an expert on this subject, in fact I am still a student and by far from being a pro. Please do point out any problems that you see, I would appreciate positive discussion and/or criticism.

Kind Regards

@is3D-1
Copy link
Contributor

is3D-1 commented May 7, 2020

I finally got some results I would like to share with you. I've put a lot of effort to come to a solution for the boat problem but I'm not sure if it's a robust solution that would solve all the cases out there. I basically implemented the solution described above for a heap of edges keyed by increasing quadrics error order. I added weighted penalty planes to try to preserve 2D borders on open meshes. This version utilizes the available "smart link" and "preserve surface" features but implements its own "preserve border edge". The method collapses all edges in a single pass until the quality ratio is reached. Parameters "max iteration count" and "agressiveness" are not used. Also note that vertices attributes like uv, colors, etc are not fixed yet.
Test case 1. The boat (posted version has 2196Tris, 3851Verts).
From top left to bottom right: (Quality=0.5, 1098Tris, 604Verts), (Quality=0.2, 439Tris, 267Verts), (Quality=0.1, 220Tris, 149Verts)
image
Test case 2. Men washroom device (original version has 8866Tris, 3851Verts). From left to right: (Quality=1.0, 8866Tris, 3851Verts), (Quality=0.3, 2500Tris, 1255Verts), (Quality=0.1, 726Tris, 368Verts), (Quality=0.05, 284Tris, 148Verts).
image
handle.zip
...And the execution time is quite fast too.
Some observations from my experiments:

  1. I would check on "smart link" all the time with a link distance of 0.0005. In metric units, this corresponds to 0.5 mm and is not noticeable. This feature improves greatly the quality of the reduced mesh as it closes the mesh and favours uniformity of the resulting mesh.
  2. Option "preserve surface" is also beneficial almost all the times as it tends to eliminates small details and preserves the general shape at high quality ratio. I have also noticed that it improves the performance (speed) of the keyed heap and favours uniformity of the resulting mesh.
  3. "Preserve border edges" helps only for open meshes and is detrimental for execution speed.

Regards

@is3D-1
Copy link
Contributor

is3D-1 commented May 8, 2020

I improved the approach to preserve 2D border edges: instead of adding a heavily weighted plane along the border, I add two very close planes, one on each side of the border so that the quadrics error metric is always much greater than zero anywhere near the border. This has solved the problem:
Test case 3. Two basic perpendicular planes. From left to right:
a) Quality=1.0, 400Tris, 242Verts
b) Quality=0.1, 40Tris, 32Verts, preserve border edge = ON, preserve surface = ON
c) Quality=0.1, 40Tris, 32Verts, preserve border edge = ON, preserve surface = OFF
d) Quality=0.1, 40Tris, 32Verts, preserve border edge = OFF, preserve surface = OFF
image

I know almost nothing about UV (same for vertex attributes) but I'm asking anyway : do you think the approach to preserve border edge could work to preserve UV seam edges ?

I so, I could easily extend the code to manage these options before I publish.

regards

@bawar9
Copy link
Contributor

bawar9 commented May 8, 2020

@is3D-1 Great work !. Indeed your approach to preserve 2D border edges using weighted virtual planes seems to work. I can't wait to get my hands on the code. About preserving UV Seam Edges, I haven't looked at how it is implemented and without seeing your current changes, it would be hard to say that whether it would work or not. I think @Whinarn would know better. One question I wanted to ask, how much does this all affect the performance of the Whinarn's original implementation?. I suspect the sorted heap would have made a huge difference and caused the original algorithm to become slower. Did you try to actually benchmark the 2 variants using an actual time metric ?.

Thanks

@is3D-1
Copy link
Contributor

is3D-1 commented May 8, 2020

@bawar9 You are right, the performance has taken its toll ! The algorithm is somewhere between 5 to 10 times slower than @Whinarn version. This is partly due to the sorted heap management and partly to quadrics error planes and error evaluation on every edge collapsed. I have not changed the data structure of @Whinarn model beside to add an Edge class. Although I may have changed many things, I have tried to fit into @Whinarn model as much as possible. The major aspects are :

I- Managing the edges heap with two data structures:

  1. A sorted list of edges by increasing order of error.
  2. A dictionary of all edges to allow referencing an edge from any other object in the model

II- Convert all struct data type to class data type:
This was necessary for the edge based approach to create pointer to other objects in the model without having to instantiate data for every variable. This change alone speed up @Whinarn algorithm by a factor of 2 to 3.

III- Managing the option to preserve features usually add extra calculation that increased time. however, I have found that "preserving surface" option often reduced the calculation time. I think the error change calculated by this option improve the hashing of the sorted list algorithm and ultimately speed up the Add/Remove operations.

So overall the algorithm is an order of magnitude slower than @Whinarn version.

Regards

@bawar9
Copy link
Contributor

bawar9 commented May 8, 2020

@is3D-1 Thanks for the detailed breakdown. In the custom version of Whinarn's mesh simplifier that I have tailored to fit my needs, I had also made the following changes:

1> As you did, I had also converted all struct/value types to class types, this greatly increases the performance simply because structures are immutable and any change to a structure requires copy pasting all the data from one to another.

2> Heavily threaded Whinarn's implementation so that it uses best of what the CPU can offer.

3> Did some other minor tweaks to gain some performance.

Although this made a huge difference but it still was no where near to the mesh simplification offered in 3D Modeling tools. I use Cinema4D and don't know what kind of magic it does but it is super fast at simplifying even complex meshes with a lot of preservation options checked (Preserve border edges, UV Seams etc) and the simplified mesh quality is great. @is3D-1 This just leaves me with one clue, I think they are somehow using the parallel power of GPU to do all the heavy lifting. They might have their own proprietary algorithm which would be tailored to run best on parallel cores. What do you think of this?. Can we use a compute shader in Unity and somehow parallelize /distribute the algorithm for the GPU?

Oh, and btw in point 1 I suggest you use HashSet instead of a Dictionary if that's possible. Insertions in Dictionaries are a little slower than HashSets. Also if you somehow get duplicate values in the Dictionary the lookup can get very slow somewhere around O(n) due to Hash collisions.

Thanks

@Whinarn
Copy link
Owner

Whinarn commented May 8, 2020

@is3D-1 Impressive work.

do you think the approach to preserve border edge could work to preserve UV seam edges ?

I'd need to see the code in order to answer that. But I'd assume that preserving UV seams would require a different approach, but don't take my word for it.

II- Convert all struct data type to class data type:
This was necessary for the edge based approach to create pointer to other objects in the model without having to instantiate data for every variable. This change alone speed up @Whinarn algorithm by a factor of 2 to 3.

I'm actually very surprised about this, I might have to evaluate it myself. I might have done some pre-mature optimizations here then, because the idea of using struct was to make it faster and avoid extra allocations. But I should have profiled the code before making assumptions.

So overall the algorithm is an order of magnitude slower than @Whinarn version.

As long as it's opt-in, I'd be fine with it.

If you keep this up, and your code looks promising, I might ask you to take over maintaining this project. With me being absent and uninterested, and people still using it, I feel like it deserves a more active and interested maintainer (with a lot more knowledge about this as well). If you'd be up for it that is. I have wanted to archive this project, because I don't feel like I have the time for it, but I would feel bad about the fact that people still use it in their projects.

@is3D-1
Copy link
Contributor

is3D-1 commented May 8, 2020

Please be very cautious with respect to this algorithm. I have made a lot of promises based on the test cases above after tweaking the code to obtain the best result possible at high reduction ratio. But it has not been tested seriously yet...

I have finally not implemented the fix for the boat problem because it has disappeared by itself when I implemented the quadrics unioning for error calculation. Hence it may reappear and probably will...

My approach from the beginning is to avoid restricting the quadrics natural behavior as much as possible to achieve high reduction ratio while still recognizing the shape. So I tolerate triangles that are visually degenerated for the benefit of a high ratio. I have also observed that the more triangles that get merged at a vertex, the more stiff this vertex becomes and prevents the algorithm from doing a better job around it (the "preserve surface" feature help a lot to overcome this problem). So there is still a lot of work to do if one wants... and much more than I expected at the beginning of the project...

Hope to post the code soon

@is3D-1
Copy link
Contributor

is3D-1 commented May 9, 2020

@bawar9

This just leaves me with one clue, I think they are somehow using the parallel power of GPU to do all the heavy lifting. They might have their own proprietary algorithm which would be tailored to run best on parallel cores. What do you think of this?

All these editing tools have detailed and efficient data structures to store the mesh information and to be able to quickly locate a face, edge or vertex when you click somewhere in the screen, and this is already setup before you click the optimize/reduce mesh button. On the other end, @Whinarn version receive the data from Unity and it needs to create internal arrays of triangles, vertices and references plus edges analysis to find borders... just to have the information ready for processing by the SimplifyMesh() function. To be fair with @Whinarn version, I would only measure the time it takes to actually run the SimplifyMesh() function and compare this result with the time Cinema4D takes on his side when you click its optimize button. Timing Cinema4D is feasible if you run the optimization algorithm from a python script (I think I may have such a script if you want). I would be curious to see the result.
For the GPU, I think you could do a test: if you have a built-in video card, then you could disable the GPU card in Windows (Device Manager>Graphic cards>Deactivate) and test Cinema4D with/without the GPU.

They might have their own proprietary algorithm which would be tailored to run best on parallel cores. What do you think of this?. Can we use a compute shader in Unity and somehow parallelize /distribute the algorithm for the GPU?

I can't help you on this one.

Regards

@is3D-1
Copy link
Contributor

is3D-1 commented May 9, 2020

@bawar9

Oh, and btw in point 1 I suggest you use HashSet instead of a Dictionary if that's possible. Insertions in Dictionaries are a little slower than HashSets. Also if you somehow get duplicate values in the Dictionary the lookup can get very slow somewhere around O(n) due to Hash collisions.

I would like to explore that possibility and some others but only after you have seen the code. I think I could post the code this week-end.
Thanks

@is3D-1
Copy link
Contributor

is3D-1 commented May 9, 2020

@Whinarn

If you keep this up, and your code looks promising, I might ask ...

Please give me some time. I will get back to you for sure.
Regards

@bawar9
Copy link
Contributor

bawar9 commented May 11, 2020

@bawar9

This just leaves me with one clue, I think they are somehow using the parallel power of GPU to do all the heavy lifting. They might have their own proprietary algorithm which would be tailored to run best on parallel cores. What do you think of this?

All these editing tools have detailed and efficient data structures to store the mesh information and to be able to quickly locate a face, edge or vertex when you click somewhere in the screen, and this is already setup before you click the optimize/reduce mesh button.

Hi, Sorry for the late response. I have been quite busy lately. You're right I should do performance benchmarking disregarding the initialization on Whinarn's mesh simplifier. If you have the python script for C4D (Cinema4D) please do send it over. I'll perform some performance tests on both and get back to you later this week. Disabling the dedicated GPU and then testing on C4D would still allow it to access the integrated GPU in the processor, but I'll give this a try.

I have also explored the topic of GPU based mesh decimation and it looks like there are a lot of papers on this topic that utilize the parallel processing capabilities of the GPU. I read one of them that used OpenCL to practically implement the algorithm.

@bawar9
Copy link
Contributor

bawar9 commented May 11, 2020

I would like to explore that possibility and some others but only after you have seen the code. I think I could post the code this week-end.
Thanks

Sure, Thank you

@is3D-1
Copy link
Contributor

is3D-1 commented May 15, 2020

@bawar9

I have also explored the topic of GPU based mesh decimation and it looks like there are a lot of papers on this topic that utilize the parallel processing capabilities of the GPU. I read one of them that used OpenCL to practically implement the algorithm.

Very interesting. We may not be at the right place to discuss it without polluting the OP Boat Problem thread ! However, it seems you are seeking a solution to process thousands of gameobjects in the fastest time. I have just reduced a scene containing 311500 objects from 15.5M triangles down to 10.0M with a commercial application that did it in 65 seconds with quadrics. The app takes advantage of duplicated objects by just reducing one of the siblings and copying it over. It uses the CPU at 100% so all cores are working and no GPU at all ! But it takes a lot of memory, around 8 Gb. This suggests the data structure is extremely important...

@bawar9
Copy link
Contributor

bawar9 commented May 16, 2020

This suggests the data structure is extremely important...

You're right we shouldn't spam this thread. Great conclusions anyways, indeed choosing the right data structures are crucial.

@hybridherbst hybridherbst changed the title Model where simplification fails badly Model Optimization Improvements (low poly simplification, edge collapsing, keep volume) May 16, 2020
@hybridherbst
Copy link
Contributor Author

hybridherbst commented May 16, 2020

Feel free to continue the discussion here! I changed the title to reflect that there's more in here then just a bug about the model. I'm following along with great interest :)

@is3D-1
Copy link
Contributor

is3D-1 commented Jun 14, 2020

More than a month ago, I tested the algorithm on something bigger. I have red a very interesting article about Optimization for Unity that manipulated this publicly available Space Exploration Vehicle mesh model. The model is shown below:

  • Test case 4a (left image) is the Space Exploration Vehicle containing 413K tris and 347K verts in 157 gameobjects (157 submeshes and 157 materials)
  • Test case 4b (right image) is the vehicle shell containing 89.5K tris and 75.7K verts in 1 gameobject (1 mesh and 1 material)
    image

The algorithm took an eternity to complete a mesh reduction to quality = 0.1 on test case 4a. That is why I isolated the vehicle shell and created test case 4b. The algorithm took more than 75 seconds to achieve a reduction to quality = 0.1. I have been extremely disappointed with this performance and I took a month to redo all my work to try to improve the result. I want to summarize the work in the following points:

1. Speed. With test case 4b, I profiled the code with the Unity Profiler and it shows that using a dictionary of edges as a way to get any edge from anywhere was the most consuming thing. I decided to eliminate the dictionary and replaced it by a resizable array that gives the edges connected to any vertex, identical to the refs array in @Whinarn version that gives the triangles connected to any vertex. The next most consuming part was the sorted list of edges by increasing order of quadrics error! This is the main structure of the algorithm. This list was implemented as a <SortedList<TKey,TValue> Class>. I decided to replace it by simple <List <<>T> Class> that I keep sorted myself. Then I found that the next most consuming thing was to remove edges from this list ! Every loop iteration I removed the topmost edge (the one with least error) that was being collapsed and this was the most time consuming operation ! I decided not to remove any edges from the list. So the list can only grow and I use a pointer to keep track of the head of the list. All of these measures have greatly improve the speed of the algorithm such that test case 4b now takes less than 5 seconds to complete a reduction to quality = 0.1. Unfortunately this has made the code more obscure.

2. Boat Problem 1. Boat problem 1 reappear in test case 4. Test case 4b has near 7500 border edges out of around 47000 edges. Borders really seem to create degenerated quadrics matrices that I think is the source of the boat problem. To eliminate these artifacts, I have sketched an approach that compute the eigenvalues of the characteristic equation of the quadrics matrix. My approach uses the Newton method with deflation to find the roots (there are three) of the characteristic equation. Application usually uses QR matrix decomposition to find eigenvalues and eigenvectors but I am more familiar with Newton... I reject all cases where an eigenvalue is 0 or when the function does not converge to an eigenvalue in a reasonable number of iterations. This approach has eliminated much of these artifacts but some remains. I have made many observations with test cases 1 to 4 above and the function I have sketched rarely obtained the results mentioned by the authors of the original method (Garland & Heckbert) that the eigenvalues of the matrix are positive most of the time. In fact, I observed the opposite: the eigenvalues are negative or zero most of the time! My approach may not be mathematically sound or may contain errors but it is a work in progress and actually gives good results.

3. Boat Problem 2. Boat problem 2 concerns border edges that were destroyed by the simplification algorithm. I have mentioned that I solved this problem by using two very close virtual planes. This is not the case anymore as I propagate the penalty quadrics matrix to the vertices at each edge end and this suffices to protect the border edges. So only one single virtual plan is required per edge to preserve it.

4. Difference between single and multiple pass. I am still wondering why the result is better (visually) when I run a simplification of Quality = 0,5 twice on a mesh instead of running it once with Quality = 0.25. A big part of the answer is that the rejected edges of the first pass are reevaluated in the second pass. This is not possible if the algorithm run only once. So I have created a mechanism that recycle the rejected edges in a way that they can be reevaluated and deleted within a single pass algorithm. The recycle rate must be carefully chosen to avoid stalling the algorithm: the algorithm could reject-recycle and edge forever...

5. Simplification Options. I have added an option to specify the sorted edge method (described here) when reducing a mesh. The option is called "Use Sorted Edge Method". The method uses virtual penalty planes to preserve borders and uv seams/foldover and the algorithm deletes all edges in a single pass. The user interface is not completed yet: when you opt to use the sorted edge method, other options like "Max Iteration Count" and "Aggressiveness" are not taken into consideration but this is not reflected from the user interface. The same is true for preserving edges and uv seams: they will not be effective until "Enable Smart Link" is enabled. So some work is still needed on the UI.

6. Some results. Previous results for test cases 1 to 3 have not changed much since the focus was on execution speed. Results for test case 4 are shown below. From left to right:
a) Quality = 1.0 : 413K tris, 347K verts, 157 gameobjects
b) Quality = 0.1 : 41.3K tris, 53K verts, 157 gameobjects
c) Quality = 0.01 : 4K tris, 7.5K verts, 157 gameobjects
d) Quality = 0.001 : 382 tris, 898 verts, 157 gameobjects
They all completed the reduction pass in less than 15 seconds.

image

7. Give it a try !. The code is all there

@bawar9
Copy link
Contributor

bawar9 commented Jun 30, 2020

6. Some results. Previous results for test cases 1 to 3 have not changed much since the focus was on execution speed. Results for test case 4 are shown below. From left to right:
a) Quality = 1.0 : 413K tris, 347K verts, 157 gameobjects
b) Quality = 0.1 : 41.3K tris, 53K verts, 157 gameobjects
c) Quality = 0.01 : 4K tris, 7.5K verts, 157 gameobjects
d) Quality = 0.001 : 382 tris, 898 verts, 157 gameobjects
They all completed the reduction pass in less than 15 seconds.

image

7. Give it a try !. The code is all there

Good job man !, keep up the good work.

I was wondering if there is a way that I can skip deleting some edges in the "RemoveEdgePass" method ?. I tried to increase the error value of an edge but that resulted in the algorithm iterating forever.

@is3D-1
Copy link
Contributor

is3D-1 commented Jul 1, 2020

The algorithm iterates forever most probably because it tries to recycle previously rejected edges...forever. You can deactivate edge recycling by commenting these lines in MeshSimplifier.cs :
image

@bawar9
Copy link
Contributor

bawar9 commented Jul 1, 2020

Thanks, I just figured out a way to serve my use case. Btw I can recommend the following models to base your tests on:

https://assetstore.unity.com/packages/3d/characters/humanoids/barbarian-warrior-75519
https://assetstore.unity.com/packages/3d/characters/creatures/creature-cave-troll-115707

I have noticed that the original whinarn implementation performs better with both these models. See below the results on the Barbarian model. Please note that I have only enabled smartLinking and no other simplification options in both the cases

Original Model 18062 Tris

OriginalModel(18062 Tris)

Reduced by Whinarn Original Method (3607 Tris) at Quality 0.2f or 80% reduction intensity

WhinarnOriginalMethod(3607 Tris)

Reduced by the Edge Sort Method (3613 Tris) at Quality 0.2f or 80% reduction intensity

EdgeSortMethod(3613 Tris)

@John-Nagle
Copy link

Here's a possible enhancement.
Unreal Engine 4's mesh simplifier has "silhouette protection". The outline of the object is kept from shrinking. This is a big help for thin objects. Quadric mesh reduction likes to pull in the edges of thin objects, because that doesn't change the volume much. When this happens, the reduced mesh looks awful at distance - it's missing whole areas. UE4's reducer doesn't do that. They're onto something here.

So, how to do this? A few options:

  1. Take convex hull of mesh. Pin points on the hull. Reduce mesh. Not too good for things with big convexity, but worth a try.
  2. Compute silhouettes from multiple directions (maybe 6 (cube) or 12 (dodecahedron). Pin points that are on silhouettes. Reduce mesh.
  3. Look at UE4 code and find out how they do it.

If you have silhouette protection, you can push mesh reduction for very low LODs much further. Eventually you get down to something close to an impostor.

Here's a Harvard paper on silhouette extraction.. No code, unfortunately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants