Mesh smoothing function for CMeshManipulator

Post those lines of code you feel like sharing or find what you require for your project here; or simply use them as tutorials.
Post Reply
robmar
Posts: 1125
Joined: Sun Aug 14, 2011 11:30 pm

Mesh smoothing function for CMeshManipulator

Post by robmar »

This procedure can be dropped into CMeshManipulator.cpp and works nicely with Irrlicht's 3DS models and others.
It scans all normals and blends them together so that surface normals are not perpendicular to each triangle, resulting in smoothing.
It also maintains correct lighting on sharp edges, but its not as fast as the smoother in Assimp, and I don't know why... yet :roll:
As it scans through the normal vectors, it eliminates found normals, resulting in an every decreasing search range, but somehow Assimp's smoother works twice as fast... I will figure it when I have time.

Code: Select all

 
CMeshManipulator::SmoothMesh.h
    //! Smooths the normals of the mesh if < 30º divergence (adjustable**)
    virtual void SmoothMesh(scene::IMesh* mesh, int uBuffer = -1, f32 fAngleLimit = 0);
 
 
CMeshManipulator::SmoothMesh.cpp
void CMeshManipulator::SmoothMesh(scene::IMesh* mesh, int iBuffer, f32 fAngleLimit) // If iBuffer is -1, smooths all buffers
{
    u32 uMeshCount, l;
    uMeshCount = mesh->getMeshBufferCount();
 
    if (iBuffer < 0)
        l = 0;
    else
        l = iBuffer;
    if (fAngleLimit == 0.f)
        fAngleLimit = 80.f / 180.f;
 
    for (; l < uMeshCount; ++l) // Processing normals from each sub-mesh leaves normals at sub-mesh boundaries unsmoothed, but usually its not noticeable
    {
        IMeshBuffer *buf = mesh->getMeshBuffer(l);
        if (!buf)
            return;
 
        video::S3DVertex *pV = (video::S3DVertex*)buf->getVertices();   // Casting a void* to a pointer to array
        u32 uVertices = buf->getVertexCount();
        u8 *pDone = (u8*)malloc(uVertices);
        memset(pDone, 0, uVertices);
        core::vector3df pos, n1, n2, n3, n4, n5, n6, n7, n8, Normal;
        f32 nDot;
        u32 ixFound[8];
        u32 ixF, uix0, uMatched = 0, uOutsideAngle = 0;
        // 1. Each vertex pos is joined to 4 other triangles in a simple surface, so take 1st pos, seek out the other matching positions
        // 2. Compare the angles between their normals, and if below say 30º, set the normals to the angle between them all.
        for (u32 ix = 0; ix < uVertices; ix++)
        {
            if (!pDone[ix])     // If vertex not already smoothed
            {
                uix0 = ix;              // Index of ref vertex for pos compare
                pos = pV[ix].Pos;   // Load next ref vertex pos, and find all those sharing same position
                ixFound[0] = ix;
                ixFound[1] = 0;
                ixFound[2] = 0;
                ixFound[3] = 0;
                ixFound[4] = 0;
                ixFound[5] = 0;
                ixFound[6] = 0;
                ixFound[7] = 0;
                ixF = 1;
                for (u32 iv = ix + 1; iv < uVertices; iv++) // Read all vertices to find matching positions, ignoring those processed
                {
                    if (pDone[iv])  // Ignore if already smoothed
                        continue;
 
                    //if (pos == pV[iv + 1].Pos)
                    if (core::equals(pos.X, pV[iv].Pos.X, core::ROUNDING_ERROR_f32) &&
                        core::equals(pos.Y, pV[iv].Pos.Y, core::ROUNDING_ERROR_f32) &&
                        core::equals(pos.Z, pV[iv].Pos.Z, core::ROUNDING_ERROR_f32))
                    {
                        // Check angle between normals is < 30º, as we don't want to smooth major edges
                        n1 = pV[uix0].Normal;
                        n2 = pV[iv].Normal;
                        nDot = n1.dotProduct(n2);
                        if (nDot > fAngleLimit) // 1.f is exactly the same dir
                        {
                            ixFound[ixF++] = iv;
                            uMatched++;
                            pDone[iv] = 1;  // Mark done
#define SMOOTH_CHECK_DEPTH 7    // Contoured surfaces can easily have 7 triangles meeting at one point
                            if (ixF > SMOOTH_CHECK_DEPTH)
                                break;
                        }
                        //else
                        //  uOutsideAngle++;    // Testing only
                    }
                }
                if (ixF == 2)   // Vertice matches found, so lets average their normals
                {
                    n1 = pV[ixFound[0]].Normal;
                    n2 = pV[ixFound[1]].Normal;
                    Normal = (n1 + n2).normalize();
                    pV[ixFound[0]].Normal = Normal;
                    pV[ixFound[1]].Normal = Normal;
                }
                else
                    if (ixF == 3)
                    {
                        n1 = pV[ixFound[0]].Normal;
                        n2 = pV[ixFound[1]].Normal;
                        n3 = pV[ixFound[2]].Normal;
                        Normal = (n1 + n2 + n3).normalize();
                        pV[ixFound[0]].Normal = Normal;
                        pV[ixFound[1]].Normal = Normal;
                        pV[ixFound[2]].Normal = Normal;
                    }
                    else
                        if (ixF == 4)
                        {
                            n1 = pV[ixFound[0]].Normal;
                            n2 = pV[ixFound[1]].Normal;
                            n3 = pV[ixFound[2]].Normal;
                            n4 = pV[ixFound[3]].Normal;
                            Normal = (n1 + n2 + n3 + n4).normalize();
                            pV[ixFound[0]].Normal = Normal;
                            pV[ixFound[1]].Normal = Normal;
                            pV[ixFound[2]].Normal = Normal;
                            pV[ixFound[3]].Normal = Normal;
                        }
                        else
                            if (ixF == 5)
                            {
                                n1 = pV[ixFound[0]].Normal;
                                n2 = pV[ixFound[1]].Normal;
                                n3 = pV[ixFound[2]].Normal;
                                n4 = pV[ixFound[3]].Normal;
                                n5 = pV[ixFound[4]].Normal;
                                Normal = (n1 + n2 + n3 + n4 + n5).normalize();
                                pV[ixFound[0]].Normal = Normal;
                                pV[ixFound[1]].Normal = Normal;
                                pV[ixFound[2]].Normal = Normal;
                                pV[ixFound[3]].Normal = Normal;
                                pV[ixFound[4]].Normal = Normal;
                            }
                            else
                                if (ixF == 6)
                                {
                                    n1 = pV[ixFound[0]].Normal;
                                    n2 = pV[ixFound[1]].Normal;
                                    n3 = pV[ixFound[2]].Normal;
                                    n4 = pV[ixFound[3]].Normal;
                                    n5 = pV[ixFound[4]].Normal;
                                    n6 = pV[ixFound[5]].Normal;
                                    Normal = (n1 + n2 + n3 + n4 + n5 + n6).normalize();
                                    pV[ixFound[0]].Normal = Normal;
                                    pV[ixFound[1]].Normal = Normal;
                                    pV[ixFound[2]].Normal = Normal;
                                    pV[ixFound[3]].Normal = Normal;
                                    pV[ixFound[4]].Normal = Normal;
                                    pV[ixFound[5]].Normal = Normal;
                                }
                                else
                                    if (ixF == 7)
                                    {
                                        n1 = pV[ixFound[0]].Normal;
                                        n2 = pV[ixFound[1]].Normal;
                                        n3 = pV[ixFound[2]].Normal;
                                        n4 = pV[ixFound[3]].Normal;
                                        n5 = pV[ixFound[4]].Normal;
                                        n6 = pV[ixFound[5]].Normal;
                                        n7 = pV[ixFound[6]].Normal;
                                        Normal = (n1 + n2 + n3 + n4 + n5 + n6 + n7).normalize();
                                        pV[ixFound[0]].Normal = Normal;
                                        pV[ixFound[1]].Normal = Normal;
                                        pV[ixFound[2]].Normal = Normal;
                                        pV[ixFound[3]].Normal = Normal;
                                        pV[ixFound[4]].Normal = Normal;
                                        pV[ixFound[5]].Normal = Normal;
                                        pV[ixFound[6]].Normal = Normal;
                                    }
                                    else
                                        if (ixF == 8)
                                        {
                                            n1 = pV[ixFound[0]].Normal;
                                            n2 = pV[ixFound[1]].Normal;
                                            n3 = pV[ixFound[2]].Normal;
                                            n4 = pV[ixFound[3]].Normal;
                                            n5 = pV[ixFound[4]].Normal;
                                            n6 = pV[ixFound[5]].Normal;
                                            n7 = pV[ixFound[6]].Normal;
                                            n8 = pV[ixFound[7]].Normal;
                                            Normal = (n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8).normalize();
                                            pV[ixFound[0]].Normal = Normal;
                                            pV[ixFound[1]].Normal = Normal;
                                            pV[ixFound[2]].Normal = Normal;
                                            pV[ixFound[3]].Normal = Normal;
                                            pV[ixFound[4]].Normal = Normal;
                                            pV[ixFound[5]].Normal = Normal;
                                            pV[ixFound[6]].Normal = Normal;
                                            pV[ixFound[7]].Normal = Normal;
                                        }
            }
        }
        free(pDone);
 
        if (iBuffer >= 0)   // If sub-mesh specified, break, only one to do
            break;
    }
}
 
 
CuteAlien
Admin
Posts: 9643
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Mesh smoothing function for CMeshManipulator

Post by CuteAlien »

First, thanks for posting. And yeah - a few notes.

One slight problem (depending on what you want) is that your algorithm depends on the order of vertices in a mesh. The first one decides which other ones are close by. But all points you find are close to the first one - others can be twice as far away from each other. After thinking about it somewhat - it's likely rarely a real problem as the main point is to find identical vertices and the limit to find them is somewhat arbitrary anyway.

fAngleLimit is a variable, but several times it's mentioned to be 30° in comments. Which is probably close the value you set it to when it's 0 (thought that should be around 26.5° I think, but maybe I calculate something wrong). Setting it to another value when it's 0 is likely not a good idea anyway - as it probably means the user wants it to be 0.

fAngleLimit is not an angle. It's the difference in dot-products. If your normals are normalized you can probably get the angle with acos(dotproductresult)*180/PI (because the dot-product is cos(angle) and the 180/PI to get from radians to angles). If normals are not normalized you have to regard lengths of the normals as well (but it would fail then right now as well... but I guess assuming normals are normalized is fine in this case).

As you only support S3DVertex you should check the other vertex-types and exclude them or your results will be bad for those.
Or you add support for other types.

Instead of using variables n1-n8 just use an array. And you already count how far it's filled, so all the if's can be replaced by a single loop then.

You could probably add a second function to use irr::scene::IMeshBuffer*. Then you can avoid passing the iBuffer as users then can decide themself which meshbuffers to use. And if they pass mesh it should use all of them.

Using malloc and free is not exactly wrong, but highly unusual in c++ code. Why avoid new/delete used by the rest of the engine?

To speed up - use any kind of spatial sorting on the vertices first. Putting them in a grid/quadtree/octree for example. Probably you can already optimize it by sorting by their length (or square-length for more speed) as you can then already limit the search to neighbors (if their distance to the origin point is too far apart from each other then it also means the points can be close together to each other, this will speed up things unless all points are arranged in a sphere around the origin - that would make it even worse then).
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
robmar
Posts: 1125
Joined: Sun Aug 14, 2011 11:30 pm

Re: Mesh smoothing function for CMeshManipulator

Post by robmar »

Thanks for the feedback!

I don't really think the vertices order makes any difference, the routine scans all of the vertices` normals for matches, not just those close together, or have I misunderstood what you mean?

The fAngle is an angle just not in radians or degrees :) Its calculated using dot product with range is 0.0 - 1.0, for 180 to 0 degrees.

I guess I should have used new/delete, but was thinking about memory alignment for speed at one point.

It works well, just could be faster. Assimp's smoother is twice as fast, or it seemed to be, I really want to know how though!?
CuteAlien
Admin
Posts: 9643
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Mesh smoothing function for CMeshManipulator

Post by CuteAlien »

Vertex order makes a difference if you have several vertices close together around your limit. Let's say your first vertex finds one to the left and one to the right inside the limit. But the left and right can still be outside the limit of each other. So if you had started with the left or right then the group would be different.

OK, fAngle is an angle. But a non-linear angle is rather unusual to use as parameter. And you did mention degrees in comments several times - which this one is not.

Btw, this is a very different filter from other smoothing algorithms in Irrlicht. You smooth normals for vertices which are on the same point but come with different normals? When you talked originally about smoothing I still expected this to be about smoothing in regards to the polygon-planes (doesn't matter it also has it's uses I guess).
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
robmar
Posts: 1125
Joined: Sun Aug 14, 2011 11:30 pm

Re: Mesh smoothing function for CMeshManipulator

Post by robmar »

My need was to smooth the 3DS models which are abundant, but not smoothed by the Irrlicht loader. With those 3DS it works perfectly, of course its trying to do it fast, so it will miss some in highly complex peaked meshes, but as those are rare, it does it for my needs.
Its easy to modify to handle more normals though.
Normals that abut each other from different primites, when perpendicular to their own surface, cause the flat shadded lighting.
To make then appear smoothed, abutted normals, in the same position, must be blended into one average normal vector, the surfaces then appear rounded, smoothed. Any sharp "creases" clearly don't want that, hence the 30 or so degree limit.
I imagine you know this anyway, but just to be clear... or not :)
CuteAlien
Admin
Posts: 9643
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Mesh smoothing function for CMeshManipulator

Post by CuteAlien »

Yeah, I get what it does. Just was surprised because it's something different than what I expected you to do in other threads. I thought you wanted something like in Irrlicht to generate smoooth normals based on polygons, but this is about smoothing existing normals independent of polygon planes. Both have their uses I suppose.

And yeah, it seems 3ds loader doesn't get normals from the format but simply generates them per triangle. Which isn't correct - it should use the smoothing-groups it seems. Which Irrlicht even reads, but doesn't use so far. So basically to get correct smoothing in that format the 3ds loader should be changed. Then you don't have to go over the complete meshbuffer but only over the vertices inside a smoothing-group. I suspect if those are used correctly you might also not need the angle-check.

edit: Btw, the slight dependence on order is not really a problem. I thought that at first, but with basically just looking for identical vertices and not vertices within a larger radius it really hardly matters and it's obviously a lot faster than not doing that.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
robmar
Posts: 1125
Joined: Sun Aug 14, 2011 11:30 pm

Re: Mesh smoothing function for CMeshManipulator

Post by robmar »

Yes, fixing the loader would be the best way. Also looking at the code for Assimp, they smooth the normals too, I would have thought that the normals should be output by 3DS etc correctly, so why Assimp smooths them is strange and slows loading by seconds for larger models.
All a bit of a mess really.
CuteAlien
Admin
Posts: 9643
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Mesh smoothing function for CMeshManipulator

Post by CuteAlien »

Yeah, seems 3ds format has no normals itself. It just defines smoothing groups. I also didn't know before.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
devsh
Competition winner
Posts: 2057
Joined: Tue Dec 09, 2008 6:00 pm
Location: UK
Contact:

Re: Mesh smoothing function for CMeshManipulator

Post by devsh »

We've recently had a contributor willing to take this issue on, so he sorted out the algorithm for us:
https://github.com/buildaworldnet/Irrli ... /297/files

Image
Image

He's going to make a few changes, such as allow a `std::function` object to define the rules for connecting a triangle to a vertex (right now only crease angle, but it would be great to respect smooth groups and other user-defined heuristics that separate faces).
Post Reply