15
$\begingroup$

I have a class that generates a 3D shape based on inputs from the calling code. The inputs are things like length, depth, arc, etc. My code generates the geometry perfectly, however I am running into trouble when calculating the surface normals. When lit, my shape has very bizarre coloring/texture from the incorrect surface normals that are being calculated. From all my research I believe my math is correct, it seems that something is wrong with my technique or method.

At a high level how does one go about programmatically calculating the surface normals for a generated shape? I am using Swift/SceneKit on iOS for my code but a generic answer is fine.

I have two arrays that represent my shape. One is an array of 3D points that represents the vertices that make up the shape. The other array is a list of indexes of the first array that map the vertices into triangles. I need to take that data and generate a 3rd array that is a set of surface normals that aid in the lighting of the shape. (see SCNGeometrySourceSemanticNormal in SceneKit`)

The list of vertices and indexes is always different depending on the inputs to the class so I cannot pre-calculate or hard code the surface normals.

$\endgroup$
7
  • $\begingroup$ Need more context. Are you trying to calculate analytic normals for a parametric surface? An implicit surface? Or do you want to calculate the normals from a generic triangle mesh? Or something else? $\endgroup$ Oct 2, 2015 at 1:23
  • $\begingroup$ Thanks, I added more detail. To answer your question I need to calculate normals from a generic triangle mesh. Though to be clear that mesh is different depending on the inputs. My shape is a 3D arrow, as an example here is a screenshot of it 2 different forms (i.e. radial, and linear). The class changes the width, depth, length, arc, and radius of the mesh as requested. cl.ly/image/3O0P3X3N3d1d You can see the odd lighting I am getting with my poor attempts at solving this. $\endgroup$
    – joshwbrick
    Oct 2, 2015 at 1:42
  • 3
    $\begingroup$ The short version is: calculate each vertex normal as the normalized sum of the normals of all the triangles that touch it. However, this will make everything smooth, which may not be what you want for this shape. I'll try to expand into a full answer later. $\endgroup$ Oct 2, 2015 at 1:58
  • $\begingroup$ Smooth is what I am going for! $\endgroup$
    – joshwbrick
    Oct 2, 2015 at 2:03
  • 4
    $\begingroup$ In most cases, if you calculate the vertex positions analytically, you can also calculate the normals analytically. For a parametric surface, the normals are the cross product of the two gradient vectors. Calculating the average of triangle normals is just an approximation, and often results in visually much poorer quality. I would post an answer, but I already posted a detailed example on SO (stackoverflow.com/questions/27233820/…), and I'm not sure if we want replicated content here. $\endgroup$ Oct 2, 2015 at 7:03

2 Answers 2

11
$\begingroup$

You simply dont want fully smooth results. While the commented method by Nathan Reed: "Calculate each vertex to face normal, sum them, normalize sum", generally works it sometimes fails spectacularly. But that is of no importance here, we can use that method by adding a rejection clause to it.

In this case you simply want certain parts not to be smoothed against certain other parts. You want selective hard edges. So for example the flat top and bottom is separate form the triangle strip on the side, as is each flat area.

Image we are after

Image 1: The result you want.

In effect you only want to average the vertices of the curved area all others can use the normal they get form their triangle alone. So you are better of thinking of the mesh as 9 separate regions that are handled without the others.

Showing mesh and normals]

Image 2: Image showing the mesh structure and the normals.

You can certainly automatically deduce this by not including normals that are outside certain angle from the primary vertexes normal. Pseudocode:

For vertex in faceVertex:
    normal = vertex.normal
    For adjVertex in adjacentVertices:
        if anglebetween(vertex.normal, adjVertex.normal )  < treshold:
            normal += adjVertex.normal
    normal = normalize(normal)

That works, but you can simply avoid all of this at creation time because you understand that separate planes are working differently. So only the curved sides need normal direction merging. And in fact you can just directly calulate them from the underlying mathematical shape.

$\endgroup$
12
$\begingroup$

I see mainly three ways of computing normals for a generated shape.

Analytic normals

In some cases you have enough information about the surface to generate the normals. For example, the normal of any point on a sphere is trivial to compute. Put simply, when you know the derivative of the function, you also know the normal.

If your case is narrow enough to allow you to use analytic normals, they will probably give the best result in terms of precision. The technique doesn't scale too well though: if you also need to handle cases where you cannot use analytic normals, it may be easier to keep the technique that handles the general case and drop the analytic one altogether.

Vertex normals

The cross product of two vectors gives a vector perpendicular to the plane they belong to. So getting the normal of a triangle is straightforward:

vec3 computeNormal(vec3 a, vec3 b, vec3 c)
{
    return normalize(crossProduct(b - a, c - a));
}

Moreover, in the above example, the length of the cross product is proportional to the area inside abc. So the smoothed normal at a vertex shared by several triangles can be computed by summing up the cross products and normalizing as a last step, thus weighting each triangle by its area.

vec3 computeNormal(vertex a)
{
    vec3 sum = vec3(0, 0, 0);
    list<vertex> adjacentVertices = getAdjacentVertices(a);
    for (int i = 1; i < adjacentVertices; ++i)
    {
        vec3 b = adjacentVertices[i - 1];
        vec3 c = adjacentVertices[i];
        sum += crossProduct(b - a, c - a);
    }
    if (norm(sum) == 0)
    {
        // Degenerate case
        return sum;
    }
    return normalize(sum);
}

If you are working with quads, there is a nice trick you can use: for a quad abcd, use crossProduct(c - a, d - b) and it will handle nicely cases where the quad is in fact a triangle.

Iñigo quilez wrote a few short articles on the topic: clever normalization of a mesh, and normal and area of n sided polygons.

Normals from partial derivatives

Normals can be computed in the fragment shader from the partial derivatives. The math behind is the same, except this time it is done in screen space. This article by Angelo Pesce describes the technique: Normals without normals.

$\endgroup$
4
  • 1
    $\begingroup$ There is a fourth way, artist supplied normals ;) $\endgroup$
    – joojaa
    Oct 6, 2015 at 4:19
  • $\begingroup$ @joojaa: I assume you are referring to normal maps? I've never heard of manually authored normals otherwise. $\endgroup$ Oct 6, 2015 at 5:09
  • 1
    $\begingroup$ No, manually authored normals. It sometimes happens that your artist knows more about how the normals should behave than the programmers models do. It is sometimes a bit problematic to the calculation engines if they assume normals come from underlying calculations. But certainly it happens and you save lot of time in mathematical modeling. $\endgroup$
    – joojaa
    Oct 6, 2015 at 7:30
  • 1
    $\begingroup$ These are sometimes referred to as "explicit normals" (3ds max and maya terminology). $\endgroup$ Aug 29, 2016 at 14:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.