How to compute accurate vertex normals on irregular triangle meshes

There are various ways to compute normals of a triangle mesh. This can greatly impact the accuracy of rendering. In this note, we will see simple alternatives to the standard averaging of triangle normals.



Wires

Uniform weights

Area weights

Angle weights


Wires

Uniform weights

Area weights

Angle weights


Let's define \(\vec n_t\) as the normal of a single triangle:

\[ \vec n_t = \frac{(\vec p_j - \vec p_i) \times (\vec p_k - \vec p_i)}{ \|(\vec p_j - \vec p_i) \times (\vec p_k - \vec p_i)\|} \]

To compute the normal of a vertex \(\vec n_v\) on a triangle mesh, one would usually average the normal of the neighboring triangles. For one cell of the mesh we could mathematically describe it as:

\[ \vec n_v = \frac{ \sum\limits_{t=0}^{ \text{nb_triangles}} w_t \ \vec n_t }{ \left \| \sum\limits_{t=0}^{ \text{nb_triangles}} w_t \ \vec n_t \right \| } \quad \text{ with } w_t \in \mathbb R \]

The standard way is to not weight the normal, or rather use uniform weights (\(w_t = 1\)). This may produce inaccurate normals (and ultimately inaccurate lighting) for meshes with an uneven distribution of vertices. Another solution is to pick \(w_t\) as the area of the current triangle \(t\) (essentially the norm of the cross product divided by 2) or better the angle of the incident edges to the central vertex \(p_i\)

void off_create_adjacency( vector< int > *adj_list, OFFTrilist *off_tris)
{
    for( int i = 0 ; i < (*off_tris).size() ; i++ ) {

        adj_list[ (*off_tris)[i].v[0] ].push_back(i);
        adj_list[ (*off_tris)[i].v[1] ].push_back(i);
        adj_list[ (*off_tris)[i].v[2] ].push_back(i);
    }

}

The standard way:

void off_compute_uniform_normals(Vector *off_normals, vector< int > *adj_list, OFFVertlist *off_verts, OFFTrilist *off_tris) {

    std::cerr << "[INFO]: Using uniform weights";

    Vector *face_normal = new Vector [ (*off_tris).size() ];
    Vector p0, p1;

    for( int i = 0 ; i < (*off_tris).size() ; i++ ) {

       p0 = (*off_verts)[(*off_tris)[i].v[1]] - (*off_verts)[(*off_tris)[i].v[0]] ;
       p1 = (*off_verts)[(*off_tris)[i].v[2]] - (*off_verts)[(*off_tris)[i].v[0]] ;

       face_normal[i] = Normalize(Cross(p0, p1));
    }

    for( int k = 0 ; k < (*off_verts).size() ; k++) {
        Vector n(0.f);
        for( int j = 0 ; j < adj_list[k].size() ; j++ ) {
            n += face_normal[ (adj_list[k])[j] ];
        }
        off_normals[k] = Normalize( n );

    }
}

Area based

Since we compute the normal with a cross product \( \vec v = \vec a \times \vec b\) and that the norm \(\| \vec v \|\) represent the area of the parallelogram spread by \(\vec a, \vec b\) if we skip normalizing \(\vec v\) at the triangle level

void off_compute_area_normals(Vector *off_normals, vector< int > *adj_list, OFFVertlist *off_verts, OFFTrilist *off_tris) {

    std::cerr << "[INFO]: Using area weights";
    Vector *face_normal = new Vector [ (*off_tris).size() ];
    Vector p0, p1, p2;

    for( int i = 0 ; i < (*off_tris).size() ; i++ ) {

       p0 = (*off_verts)[(*off_tris)[i].v[1]] - (*off_verts)[(*off_tris)[i].v[0]] ;
       p1 = (*off_verts)[(*off_tris)[i].v[2]] - (*off_verts)[(*off_tris)[i].v[0]] ;
       face_normal[i] = Cross(p0, p1);

    }

    for( int k = 0 ; k < (*off_verts).size() ; k++) {
        Vector n(0.f);
        for( int j = 0 ; j < adj_list[k].size() ; j++ ) {
            n += face_normal[ (adj_list[k])[j] ];
        }
        off_normals[k] = Normalize( n );

    }

}

Angle based

void off_compute_angle_normals(Vector *off_normals, vector< int > *adj_list, OFFVertlist *off_verts, OFFTrilist *off_tris) {

    std::cerr << "[INFO]: Using geodesic angle weights";

    Vector *face_normal = new Vector [ (*off_tris).size() ];
    float *face_angles = new float [ 3*(*off_tris).size() ];
    Vector p0, p1, p2;
    float n0, n1, n2;

    for( int i = 0 ; i < (*off_tris).size() ; i++ ) {

        p0 = (*off_verts)[(*off_tris)[i].v[1]] - (*off_verts)[(*off_tris)[i].v[0]] ;
        p1 = (*off_verts)[(*off_tris)[i].v[2]] - (*off_verts)[(*off_tris)[i].v[0]] ;
        p2 = (*off_verts)[(*off_tris)[i].v[2]] - (*off_verts)[(*off_tris)[i].v[1]] ;
        n0 = p0.Length();
        n1 = p1.Length();
        n2 = p2.Length();

        face_angles[3*i]   = acos ( Dot( p0, p1)/(n0*n1) );
        face_angles[3*i+1] = acos ( Dot(-p0, p2)/(n0*n2) );
        face_angles[3*i+2] = M_PI - face_angles[3*i] - face_angles[3*i+1];

        face_normal[i] = Normalize(Cross(p0, p1));
    }

    for( int k = 0 ; k < (*off_verts).size() ; k++) {
        Vector n(0.f);
        for( int j = 0 ; j < adj_list[k].size() ; j++ ) {
            int s = (adj_list[k])[j];
            for (int l=0 ; l < 3 ; l++ ) {

                if (  (*off_tris)[s].v[l] == k) {
                    n += face_angles[3*s+l] * face_normal[ s ];
                }
            }
            off_normals[k] = Normalize( n );

        }
    }
}

Going further

Papers

- Nelson Max. “Weights for Computing Vertex Normals from Facet Normals.” Journal of Graphics, GPU, and Game Tools 4:2 (1999), 1–6.

- Shuangshuang Jin, Robert R. Lewis, and David West. “A Comparison of Algorithms for Vertex-Normal Computation.” The Visual Computer 21:1–2 (2005), 71–82.

Skinning

If your mesh is deformed with skinning, you can also more accurately compute how the normals deform. If you're interested see the paper Accurate and Efficient Lighting for Skinned Models by Marco Tarini & Al (Eurographics 2014)

References

- Book: Polygon Mesh processing (Chapter 3 "Differential Geometry" Section 3.3.2 "Normal Vectors"
- Code snippets and images comes from http://w3.impa.br/~zang/pg2012/exe2.html

No comments

(optional field, I won't disclose or spam but it's necessary to notify you if I respond to your comment)
All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.
Anti-spam question: