Calculating Vertex Normals

Started by
7 comments, last by DerekB 5 years, 8 months ago

So I'm programming a game engine with LWJGL and I'm making a method that generates vertex normals using an indices array and a vertices array. My idea was to have a list of normals, add the face normals to them with a loop, and then average the variables. However when I tried that I get really odd effects with the lighting. This is my code:


public static Vector3f calculateNormal(Vector3f a, Vector3f b, Vector3f c)
    {
        Vector3f out = new Vector3f();
        GeometryUtils.normal(a, b, c, out);
        return out;
    }

    public static float[] buildNormals(int[] indices, float[] vertices)
    {
        List<List<Vector3f>> rawNormals = new ArrayList<>();
        for(int i = 0; i < vertices.length / 3; i++)
            rawNormals.add(new ArrayList<>());

        for(int i = 0; i < indices.length; i += 3)
        {
            Vector3f a = new Vector3f(vertices[indices[i] * 3], vertices[indices[i + 1] * 3], vertices[indices[i + 2] * 3]);
            Vector3f b = new Vector3f(vertices[indices[i] * 3 + 1], vertices[indices[i + 1] * 3 + 1], vertices[indices[i + 2] * 3 + 1]);
            Vector3f c = new Vector3f(vertices[indices[i] * 3 + 2], vertices[indices[i + 1] * 3 + 2], vertices[indices[i + 2] * 3 + 2]);

            Vector3f normal = calculateNormal(a, b, c);

            rawNormals.get(indices[i]).add(normal);
            rawNormals.get(indices[i + 1]).add(normal);
            rawNormals.get(indices[i + 2]).add(normal);
        }

        float[] normals = new float[rawNormals.size() * 3];
        int n = 0;
        for(int i = 0; i < rawNormals.size(); i++)
        {
            Vector3f value = new Vector3f();
            for(Vector3f v : rawNormals.get(i))
                value = addVectors(value, v);

            int size = rawNormals.get(i).size();
            normals[n] = value.x / size;
            normals[n + 1] = value.y / size;
            normals[n + 2] = value.z / size;

            n += 3;
        }

        return normals;
    }

Is there anything that looks wrong with it? Thanks.

Advertisement
10 minutes ago, TheRealSmolt said:

int size = rawNormals.get(i).size(); normals[n] = value.x / size; normals[n + 1] = value.y / size; normals[n + 2] = value.z / size; 

Instead dividing by size, you need to divide the resulting normal by its length to normalize. Otherwise you end up with too short normals on curved geometry.

Also, you should consider using angle weighted normals, which is much more correct than simple average. For this you multiply each face normal by the angle of the face at the vertex before accumulating.

4 minutes ago, JoeJ said:

Instead dividing by size, you need to divide the resulting normal by its length to normalize. Otherwise you end up with too short normals on curved geometry.

Also, you should consider using angle weighted normals, which is much more correct than simple average. For this you multiply each face normal by the angle of the face at the vertex before accumulating.

This is what it's doing. It's dividing by the length of its array of vectors, right?

Just now, TheRealSmolt said:

This is what it's doing. It's dividing by the length of its array of vectors, right?

No, i do not mean the length / size of the array, but the length of the summed normals in 3D space.

Example with summing 2 2D vectors:

(1,0)

(0,1)

____

(1,1) / 2 = (0.5, 0.5) which has no unit length.

Using Pythagoras instead: length = sqrt(1*1 + 1*1) = sqrt(2)

(1,1) / length = (0.71, 0.71) which has expected unit length

22 minutes ago, JoeJ said:

No, i do not mean the length / size of the array, but the length of the summed normals in 3D space.

Example with summing 2 2D vectors:

(1,0)

(0,1)

____

(1,1) / 2 = (0.5, 0.5) which has no unit length.

Using Pythagoras instead: length = sqrt(1*1 + 1*1) = sqrt(2)

(1,1) / length = (0.71, 0.71) which has expected unit length

Sorry, I don't understand.

A normal is a unit vector, you can imagine a unit vector starting at the origin of a sphere with a radius of one.

No matter at which direction the vector points, it always ends at the surface of the sphere, so it has a length of one.

Same in 2D where the sphere becomes a circle, the radius is one and the length (or 'magnitude') of the vector must be one too.

 

By summing up various face normals, you get a vector of arbitary length, and you want to have it length of exactly one but keep its direction as is.

Lets say we end up with a vector (3,4,0). We now want to calculate its length, and then divide by that length so it has final length of one.

Some code how to do this usually looks like so:

n.Normalize();

n /= sqrt (n.dot(n));

n *= 1 / sqrt (n.dot(n));

... all the same. But lets do it ourself. Notice the right angled triangle you get if you think of the 2D vector (3,4) in the coordinate system: a = 3 (x axis), b = 4 (y axis), c is the unknown diagonal length of the vector to its origin at (0,0).

So we can use Pythagoras right angled triangle rule:

a^2 + b^2 = c^2

c = sqrt (a^2 + b^2)

c = sqrt (3*3 + 4*4)

c = 5

So the length of the vector is five, and we divide by that to make it unit length: (3/5, 4/5)

This works for any number of dimensions.

3D: (3,4,0) normalized = (3,4,0) / sqrt (3*3 +4*4 + 0*0) = (3,4,0) / 5

even for 1D: (3) normalized = (3) / sqrt (3*3) = 3/3 = 1

 

 

29 minutes ago, JoeJ said:

A normal is a unit vector, you can imagine a unit vector starting at the origin of a sphere with a radius of one.

No matter at which direction the vector points, it always ends at the surface of the sphere, so it has a length of one.

Same in 2D where the sphere becomes a circle, the radius is one and the length (or 'magnitude') of the vector must be one too.

 

By summing up various face normals, you get a vector of arbitary length, and you want to have it length of exactly one but keep its direction as is.

Lets say we end up with a vector (3,4,0). We now want to calculate its length, and then divide by that length so it has final length of one.

Some code how to do this usually looks like so:

n.Normalize();

n /= sqrt (n.dot(n));

n *= 1 / sqrt (n.dot(n));

... all the same. But lets do it ourself. Notice the right angled triangle you get if you think of the 2D vector (3,4) in the coordinate system: a = 3 (x axis), b = 4 (y axis), c is the unknown diagonal length of the vector to its origin at (0,0).

So we can use Pythagoras right angled triangle rule:

a^2 + b^2 = c^2

c = sqrt (a^2 + b^2)

c = sqrt (3*3 + 4*4)

c = 5

So the length of the vector is five, and we divide by that to make it unit length: (3/5, 4/5)

This works for any number of dimensions.

3D: (3,4,0) normalized = (3,4,0) / sqrt (3*3 +4*4 + 0*0) = (3,4,0) / 5

even for 1D: (3) normalized = (3) / sqrt (3*3) = 3/3 = 1

 

 

Ohhhhhhhh. Okay thanks. I found out the my issue was that the normals where flipped on certain faces for some reason, but thanks, and for the tip about weighted normals.

On 8/15/2018 at 7:59 AM, TheRealSmolt said:

Ohhhhhhhh. Okay thanks. I found out the my issue was that the normals where flipped on certain faces for some reason, but thanks, and for the tip about weighted normals.

You may have had some flipped face normals in your input, and fixing that may have made the result of your lighting more plausible, but if you continue to average your summed normals rather than normalising them as JoeJ is suggesting then you are still not getting the correct results. 

Honestly it's really worth your while to get your head around the concepts JoeJ talked about, unit normals and normalisation are kind of a big deal. Everything you do in 3D graphics will be easier and make more sense if you can understand fundamental linear algebra.

This topic is closed to new replies.

Advertisement