Hi guys!

In this article I'll show how to calculate per-vertex Normals and the Tangent Space. Here you'll see the most accurate technique, generating real Normals and breaking the vertex when necessary. This article is an intermediate part of the "All about Shaders" series. Would be nice if you've read the first part All about Shaders – (part 1/3)


At a glance

First off, the calculations and routines that we'll create here is not an easy task, there are complexes concepts and calculations involved here. So, be sure you have this macro view:

  • Usually the 3D softwares will export an Optimized Per-Vertex Normal, for those cases we can save our time, avoiding re-create the Normals. So we'll create the Normal Vector ONLY when the Normals from 3D file was not optimized or don't exist.
  • Non optimized Normals means that the same Normal vector was written many times by the 3D software. It happens in some 3D file formats, like COLLADA, that the same Normal vector can appear hundred times, making the parse processing very expensive.
  • It's a good idea to calculate the Tangent Space always as possible (all that we need is Vertex Position and Vertex Texcoord).
  • The best way to deal with meshes using OpenGL is to use what we call "Array of Structures", however I'll show you a generice algorithym that can be used with "Structure of Arrays" as well.

If these four bullets sounds like "Greek*" for you, I highly recommend you read some other articles before proceed:

(* well, if you are a greek guy, sorry for that and please, take this word as equivalent to "chinese" or something like that).

As I explained before, to calculate the Normals we just need the vertex position, but to calculate the Tangent Space we'll need the texcoord already calculated. If the mesh we are working on doesn't have texcoord we'll skip the Tangent Space phase, because is not possible to create an arbitrary UV Map in the code, UV Maps are design dependents and change the way as the texture is made.

Hands at work!


Calculating Normals - Step 1

In theory, the technique to calculate the face normals is simple: We'll find the perpendicular vector for each face (triangle). However as you saw in the first tutorial of "All About Shaders", the face normals are not so good. So, we need to calculate the vertex normals.

The things become a little bit more complex when we try to calculate the vertex normals. Every Face Normal will affect the vertex that compose the face. One single vertex can be shared by multiple faces, so, the final Vertex Normal will be the averaged vector of each Face that share this Vertex. As each face has its own size, the averaged Vertex Normal vector should consider those differences.

Vertices shared by multiple faces will have the resulting Normal as an average of all adjacent faces' Normals.

Just with this concept, you can create the Normals, but they will seem strange in some meshes. Like this teapot at the left side. When I created my first Normals, I spent weeks trying to find what was wrong with my calculus or with this concept... "Everything is OK, but only that fucking vertex is not. Why?", I thought. Many others said that was a problem with my code, a problem with my memory allocation, and all other kinds of shits. But only after look closely to a great 3D software I found the problem. I'll show you the same image that I spent hours looking to until I find the solution.

Vertex Normals

Did you notice something strange? This is a basic teapot mesh, many 3D softwares give you this to make tests with materials and lights. But this mesh has ONE single strange vertex: a vertex with 2 Normal Vectors. Is it possible? Actually, NO. The mesh structure must follow a pattern, so you can't have all vertices with 1 Normal and only one vertex with 2 Normals. This is a very important point that no one talk about it, actually, I never seen anyone talking about this.

It's time to understand what happens. Your mesh structure is not complete until you calculate the Normals. Why? Because some vertices will be "break/split" into two or more vertices by the Normals. This is what happens in that image. The 3D softwares will not show you this, but there are two vertices, with the same position and texcoords, but with different Normals. Obviously the 3D softwares prefer to omit this for performance reasons, but we can't omit this fact to OpenGL. We must inform to the Shaders that there are two vertices instead of a single one.

And how will we know where to break a vertex? By the angle between faces. WOW, this thing of Normals is becoming very complex! Yeah, I told you that this is a very important and complex part. Let's think by steps. Usually the light looks continuous on a surface until an angle of ~80º (like a sphere), however two faces with an angle > ~80º will form a hard edge, like the table's corners. OK, now translating to English, this is what we'll do:

  1. Calculate each face's Normal.
  2. Calculate the angle between face's Normals.
  3. At each group of ~80º we'll create a new set of Normals.
  4. Finally we'll find the averaged Normal for each group, respecting the face's size.

Well, now it looks more simple, with just 4 simple steps. Nice! OK, let's put our hands on code. First off, let's create our Vector/Math functions.

Vector Math
  
// Early in definitions...
typedef struct  
{
    float x;
    float y;
} vec2;

// Vector's distance.
static inline vec2 vec2Subtract(vec2 vecA, vec2 vecB)  
{
    return (vec2){vecA.x - vecB.x, vecA.y - vecB.y};
}

typedef struct  
{
    float x;
    float y;
    float z;
} vec3;

static const vec3 kvec3Zero = {0.0f, 0.0f, 0.0f};

// Vector's length.
static inline float vec3Length(vec3 vec)  
{
    // Square root.
    return sqrtf(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
}

// Vector's normalization.
static inline vec3 vec3Normalize(vec3 vec)  
{
    // Find the magnitude/length. This variable is called inverse magnitude (iMag)
    // because instead divide each element by this magnitude, let's do multiplication, it's faster.
    float iMag = vec3Length(vec);

    // Avoid divisions by 0.
    if (iMag != 0.0f)
    {
        iMag = 1.0f / iMag;

        vec.x *= iMag;
        vec.y *= iMag;
        vec.z *= iMag;
    }

    return vec;
}

// Vector's sum.
static inline vec3 vec3Add(vec3 vecA, vec3 vecB)  
{
    return (vec3){vecA.x + vecB.x, vecA.y + vecB.y, vecA.z + vecB.z};
}

// Vector's distance.
static inline vec3 vec3Subtract(vec3 vecA, vec3 vecB)  
{
    return (vec3){vecA.x - vecB.x, vecA.y - vecB.y, vecA.z - vecB.z};
}

// Checks for zero values.
vec3IsZero(vec3 vec)  
{
    return (vec.x == 0.0f && vec.y == 0.0f && vec.z == 0.0f);
}

// The dot product returns the cosine of the angle formed by two vectors.
static float vec3Dot(vec3 vecA, vec3 vecB)  
{
    return vecA.x * vecB.x + vecA.y * vecB.y + vecA.z * vecB.z;
}

// The cross product returns an orthogonal vector with the other two,
// that means, the new vector is mutually perpendicular to the other two.
static vec3 vec3Cross(vec3 vecA, vec3 vecB)  
{
    vec3 vec;

    vec.x = vecA.y * vecB.z - vecA.z * vecB.y;
    vec.y = vecA.z * vecB.x - vecA.x * vecB.z;
    vec.z = vecA.x * vecB.y - vecA.y * vecB.x;

    return vec;
}

// Checks if there is a NaN value inside the informed vector.
// If a NaN value is found, it's changed to 0.0f (zero).
static vec3 vec3Cleared(vec3 vec)  
{
    vec3 cleared;
    cleared.x = (vec.x != vec.x) ? 0.0f : vec.x;
    cleared.y = (vec.y != vec.y) ? 0.0f : vec.y;
    cleared.z = (vec.z != vec.z) ? 0.0f : vec.z;

    return cleared;
}

Now we're ready to go further:

Calculating Face Normals
  
// Private variables...
// Assumes that all the variables that start with "_" is a private one
// and you must implement it by your self and some of those values must
// be present before you start:

// vec3 *_vertices    "array of vertices positions"   (required)
// vec3 *_texcoords   "array of texture coordinates"  (optional)
// vec3 *_normals     "array of normals"              (optional/to calculate)
// vec3 *_tangents    "array of tangents"             (to calculate)
// vec3 *_bitangents  "array of bitangents"           (to calculate)

// int  _vCount       "vertices count"                (required)
// int  _tCount       "texture coordinates count"     (optional)
// int  _nCount       "normals count"                 (optional/to calculate)
// int  _taCount      "tangents count"                (to calculate)
// int  _biCount      "bitangents count"              (to calculate)

// int *_faces        "array of face indices"         (required)
// int  _facesCount   "faces indices count"           (required)
// int  _facesStride  "stride of faces indices"       (required)

// Checks the crease angle for the normal calculations.
// This function creates and divides the normals for a vertex, recursively.
static unsigned int creaseAngle(unsigned int index, vec3 vector, vec3 **buffer, unsigned int *count, NSMutableDictionary *list)  
{
    // Let's talk about this function later on.
}

// Calculating the Tangent Space.
void calculateTangentSpace()  
{
    unsigned int i, length;
    unsigned int j, lengthJ;

    unsigned int *newFaces, *outFaces;
    unsigned int oldFaceStride = _facesStride;

    int i1, i2, i3;
    int vi1, vi2, vi3;
    int ti1, ti2, ti3;

    vec3 vA, vB, vC;
    vec2 tA, tB, tC;
    vec3 distBA, distCA;
    vec2 tdistBA, tdistCA;

    vec3 normal;
    vec3 tangent;
    vec3 bitangent;

    vec3 *normalBuffer;
    vec3 *tangentBuffer;
    vec3 *bitangentBuffer;

    NSMutableDictionary *multiples;

    Element *element;
    int vLength, vOffset;
    int nLength, nOffset;
    int tLength, tOffset;

    float area, delta;
    float *outValue;

    // Checks if the parsed mesh has Normals and Texture Coordinates.
    BOOL hasNormals = NO;// CUSTOM: Check if your mesh structure already has normals.
    BOOL hasTextures = YES;// CUSTOM: Check if your mesh structure already has texcoords.

    // Gets the vertex element.
    vLength = 4;// CUSTOM: Take the length of your vertex position component.
    vOffset = 0;// CUSTOM: Take the offset  of your vertex position component in the Array of Structures.

    // If the normal element doesn't exist yet, creates a new one.
    if (!hasNormals)
    {
        // CUSTOM: Create the Normal elements if it doesn't exist yet.
        // The element should be just as the vertex one, having a length and an offset.
        // IMPORTANT: Increate the stride of your Array of Structures (_facesStride).
    }

    // Gets the normal element.
    nLength = 3;// CUSTOM: Take the normal length.
    nOffset = 7;// CUSTOM: Take the normal offset. In this case (7) we consider it's after the texcoord.

    // If the texture coordinate element exist, gets it and create tangent and bitangent element.
    if (hasTextures)
    {
        tLength = 3;// CUSTOM: Take the texcoord length.
        tOffset = 4;// CUSTOM: Take the texcoord offset. In this case it's after the vertices positions.

        // CUSTOM: Here you should create the Tangent and Bitangent elements. Just as any
        // other element until here, they must have length and offset.
        // IMPORTANT: Increate the stride of your Array of Structures (_facesStride).
    }

    // Allocates memory to the new faces.
    newFaces = malloc(sizeof(int) * (_facesCount * _facesStride));

    // A priori, assumes that for each vertex exists only one normal.
    _nCount = _taCount = _biCount = _vCount;

    // Initializes the buffers for the tangent space elements.
    // This memory allocation must use calloc because they must be 0 (zero) value,
    // otherwise NaN values can be generated.
    normalBuffer = calloc(_nCount, sizeof(vec3));
    tangentBuffer = calloc(_taCount, sizeof(vec3));
    bitangentBuffer = calloc(_biCount, sizeof(vec3));

    // Initializes the dictionaries to deal with vertices with multiple normals in it.
    multiples = [[NSMutableDictionary alloc] init];

    // Loop through each triangle.
    length = _facesCount;
    for (i = 0; i < length; i += 3)
    {
        // Triangle Vertices. At this moment _faces is an ordered list of elements' indices:
        // iv1, it1, in1, iv2, in2, it2, iv3, it3, in3...
        //  |              |              |
        //  V              V              V
        // iv1,           iv2,           iv3
        // So the following lines will extract the indices of vertices that form a triangle.
        i1 = _faces[i * oldFaceStride + vOffset];
        i2 = _faces[(i + 1) * oldFaceStride + vOffset];
        i3 = _faces[(i + 2) * oldFaceStride + vOffset];

        // Calculates the vertex indices in the array of vertices.
        vi1 = i1 * vLength;
        vi2 = i2 * vLength;
        vi3 = i3 * vLength;

        // Retrieves 3 vertices from the array of vertices.
        vA = (vec3){_vertices[vi1], _vertices[vi1 + 1], _vertices[vi1 + 2]};
        vB = (vec3){_vertices[vi2], _vertices[vi2 + 1], _vertices[vi2 + 2]};
        vC = (vec3){_vertices[vi3], _vertices[vi3 + 1], _vertices[vi3 + 2]};

        // Calculates the vector of the edges, the distance between the vertices.
        distBA = vec3Subtract(vB, vA);
        distCA = vec3Subtract(vC, vA);

        //*************************
        //  Normals
        //*************************
        if (!hasNormals)
        {
            // Calculates the face normal to the current triangle.
            normal = vec3Cross(distBA, distCA);

            // Searches for crease angles considering the adjacent triangles.
            // This function also initialize new blocks of memory to the buffer, setting them to 0 (zero).
            i1 = creaseAngle(i1, normal, &normalBuffer, &_nCount, multiples);
            i2 = creaseAngle(i2, normal, &normalBuffer, &_nCount, multiples);
            i3 = creaseAngle(i3, normal, &normalBuffer, &_nCount, multiples);

            // Averages the new normal vector with the oldest buffered.
            normalBuffer[i1] = vec3Add(normal, normalBuffer[i1]);
            normalBuffer[i2] = vec3Add(normal, normalBuffer[i2]);
            normalBuffer[i3] = vec3Add(normal, normalBuffer[i3]);
        }
        else
        {
            // If the parsed file has normals in it, retrieves their indices in the array of normals.
            vi1 = _faces[i * oldFaceStride + nOffset] * nLength;
            vi2 = _faces[(i + 1) * oldFaceStride + nOffset] * nLength;
            vi3 = _faces[(i + 2) * oldFaceStride + nOffset] * nLength;

            // Retrieves the normals.
            vA = (vec3){_normals[vi1], _normals[vi1 + 1], _normals[vi1 + 2]};
            vB = (vec3){_normals[vi2], _normals[vi2 + 1], _normals[vi2 + 2]};
            vC = (vec3){_normals[vi3], _normals[vi3 + 1], _normals[vi3 + 2]};

            // Calculates the face normal to the current triangle.
            normal = vec3Add(vec3Add(vA, vB), vC);
        }

// CONTINUE...

Let's understand all those lines:

  • Lines 1-16: Definition of the output variables.
  • Lines 20-23: A very important function to calculate the crease angle between faces. We'll discuss more about it later on.
  • Lines 28-59: Defining the variables we'll use to calculate the Normals and the Tangent Space.
  • Lines 62-63: CUSTOM CODE. Checks if there are Normals and Texture Coordinates already calculated in your mesh structure.
  • Lines 66-67: CUSTOM CODE. Gets the length of each Vertex set (usually 3 or 4) and its offset in the Array of Faces.
  • Lines 70-75: CUSTOM CODE. If there is no previous calculated Normals, create a new element and define its length (usually 3) and its offset in the Array of Faces.
  • Lines 78-79: CUSTOM CODE. Gets the length of each Normal set (usually 3) and its offset in the Array of Faces.
  • Lines 82-90: CUSTOM CODE. If there is a calculated Texture Coordinates, then create the Tangent and Bitangent elements. As both Tangent and Bitangent will follow the same elements rule, so you just need to define the length and offset in face for one of them.
  • Lines 93-106: Allocating the necessary memory for the calculations.
  • Lines 109-110: Starting the loop through all triangles of your mesh.
  • Lines 118-120: Getting the indices form the current working triangle.
  • Lines 123-130: Getting the vertices related to the current triangle.
  • Lines 133-134: Calculating the distance between triangle's vertices. Depending on the order which you calculate this distance, the resulting normal vector direction can change. So, be careful about changing this order.
  • Lines 142-153: If there are no previous normals. The cross product between the calculated distances will generate a perpendicular vector, it's the face normal to the current triangle (Cross Product info). Then the crease angles are calculated, so we add the current normal vector to the normalBuffer. When we sum two vectors we are automatically calculating the middle vector. (Add Vector info). IMPORTANT: We're not normalizing the vector in this step, by doing so we make sure each triangle area (size) will be considered, because they have different lengths.
  • Lines 158-168: If there are previous calculated normals. We calculate the current face normal, because the final format for normals are always vertex normal, not face normal. For the next steps we'll need the face normal.


The Crease Angle - Step 2

As I said before, the crease angle is a crucial part to create perfect normals, otherwise your normals will look correct for some meshes but very strange in other ones. My crease angle is recursive, that means, it will automatically break the vertex index and count as many times as necessary. So if one single vertex has 3 different normals, this function will break it 3 times.

This function must be called once per vertex. In the above code it's called at:
i1 = creaseAngle(i1, normal, &normalBuffer, &nCount, multiples);
i2 = creaseAngle(i2, normal, &normalBuffer, &
nCount, multiples);
i3 = creaseAngle(i3, normal, &normalBuffer, &_nCount, multiples);

So the original vertex index can become a new index and this new one will be used in new normal vector and in tangent space (tangent and bitanget).

Calculating Crease Angle
  
// Defines the maximum crease angle ~80.
#define kCreaseAngle        0.2

// Checks the crease angle for the normal calculations.
// This function creates and divides the normals to a vertex.
static unsigned int creaseAngle(unsigned int index,  
                                vec3 vector,
                                vec3 **buffer,
                                unsigned int *count,
                                NSMutableDictionary *list)
{
    NSNumber *newIndex, *oldIndex;

    // Eliminates the NaN points.
    (*buffer)[index] = vec3Cleared((*buffer)[index]);

    // Checks if the informed normal vector is not zero.
    if (!vec3IsZero((*buffer)[index]))
    {
        // Calculates the cosine of the angle between the current normal vector and the
        // averaged normal in the buffer.
        float cos = vec3Dot(vec3Normalize(vector), vec3Normalize((*buffer)[index]));

        // If the cosine is greater than the crease angle, that means the current normal vector
        // forms an acceptable angle witht the averaged normal in the buffer. Otherwise, proceeds and
        // creates a new normal vector to the current triangle face.
        if (cos <= kCreaseAngle)
        {
            // Tries to retrieve an already buffered normal with the same bend.
            oldIndex = [NSNumber numberWithInt:index];
            newIndex = [list objectForKey:oldIndex];

            // If no buffer was found, create a new register to the current normal vector.
            if (newIndex == nil)
            {
                // Retrieves the new index and stores its value as a linked list to the old one.
                newIndex = [NSNumber numberWithInt:*count];
                [list setObject:newIndex forKey:oldIndex];
                index = [newIndex intValue];

                // Reallocates the buffer and set the new buffer value to zero, avoiding NaN.
                *buffer = realloc(*buffer, NGL_SIZE_VEC3 * ++*count);
                (*buffer)[index] = kvec3Zero;
            }
            // Otherwise, repeat the process with the buffered value to check for new crease angles.
            else
            {
                index = creaseAngle([newIndex intValue], vector, buffer, count, list);
            }
        }
    }

    return index;
}

Understanding line by line:

  • Line 12: Clearing any possible NaN value. If a NaN value is found, it'll be set to 0.0f (zero).
  • Line 15: Making sure that an empty buffer index will be skipped, because there is no reason to calculate the crease angle between a face and no other.
  • Line 19: Calculating the "dot" product (Dot Product info) we get the cosine of the angle formed between two faces. These faces are: The new normal vector and the value that is already in the normal buffer. Both of them must be normalized at this point, otherwise their lengths will affect the resulting dot product.
  • Line 24: Check if the calculated cosine is bellow the crease angle. So, if a cosine value is lower than the crease angle it means the calculated angle is higher than the maximum angle. Then the process of breaking the buffer index will start.
  • Lines 27-31: Creating NSNumbers. The old value is, actually, the current index parameter. The line 28 tries to retrieve if there is an already calculated broken index referenced by the current index. If there is a previous calculated reference, that means, if the current index was broken before, then the crease angle function will be called again. Otherwise, the process of breaking the buffer index will continue.
  • Lines 34-36: The new buffer index will be created in the end of the current buffer array. This new index will be stored and linked with the current index. So, if the current index appear again we already know where is it broken index.
  • Lines 39-40: Reallocating the memory of the buffer array and setting the new index to zero, avoiding NaN values.
  • Line 50: The returned index will always be one of these two situations:
    1. An empty buffer index (zero vector).
    2. A buffer index in which its stored normal forms an acceptable angle with the new normal.

Nice! Now we have the correct calculated Normals, respecting the crease angle. Besides if there is a previous calculated normals, this routine respects that and calculates only the face normal again.

Now it's time to enter in the Tanget Space.


Calculating the Tanget Space - Step 3

As you know the Tangent Space is formed by the Tangent Vector and the Bitangent Vector. Just to make this point clear, the word "Binormal" is wrong in the 3D context. Because a 2D circle can have a Binormal, however there is only one Normal Vector in the 3D space. So the perpendicular angle between the Normal and the Tangent is the Bitangent. Some guys still calling as Binormal, we can understand what they want to say, but we know this term is a misspelling, there is no Binormal in the 3D space.

OK, now let's understand the concept of the Tangent Space.

The Tangent Space is a local stuff related at how the light interacts on each face of the surface. But the tangent space doesn't exist in the reality, right? Right, it doesn't. The tangent space is something created as a workaround to use the "Bump Technique". Now a day, there are many bump techniques, like bump map, parallax map , displacement map and many others. Independent of the technique the tangent space vector must be calculated.

The tangent and bitangent are orthogonal vector with the Normal and instruct us about the direction of the face's texture coordinate (U and V map directions). This direction will be used to calculate the light based on the RGB colors of the bump image file.

So in short, the Tangent Space is just a convention that we create as a workaround the bump technique. Here is how we'll calculate the Tangent Space.

Calculating Tangent Space

// CONTINUING...

        //*************************
        //  Tangent Space
        //*************************
        if (hasTextures)
        {
            // The crease angle process produces splits on the per-vertex normals, as the tangent space
            // must be orthogonalized, the tangent and bitanget follow those splits.
            if (_nCount > _taCount)
            {
                // Normals, Tangents and Bitangents buffers will always have the same number of elements.
                tangentBuffer = realloc(tangentBuffer, sizeof(vec3) * _nCount);
                bitangentBuffer = realloc(bitangentBuffer, sizeof(vec3) * _nCount);

                // Setting the brand new buffers to 0 (zero).
                lengthJ = _nCount;
                for (j = _taCount - 1; j < lengthJ; ++j)
                {
                    tangentBuffer[j] = kvec3Zero;
                    bitangentBuffer[j] = kvec3Zero;
                }

                _taCount = _biCount = _nCount;
            }

            // Retrieves texture coordinate indices.
            ti1 = _faces[i * oldFaceStride + tOffset] * tLength;
            ti2 = _faces[(i + 1) * oldFaceStride + tOffset] * tLength;
            ti3 = _faces[(i + 2) * oldFaceStride + tOffset] * tLength;

            // Retrieves the texture coordinates.
            tA = (vec2){_texcoords[ti1], _texcoords[ti1 + 1]};
            tB = (vec2){_texcoords[ti2], _texcoords[ti2 + 1]};
            tC = (vec2){_texcoords[ti3], _texcoords[ti3 + 1]};

            // Calculates the vector of the texture coordinates edges, the distance between them.
            tdistBA = vec2Subtract(tB, tA);
            tdistCA = vec2Subtract(tC, tA);

            // Calculates the triangle's area.
            area = tdistBA.x * tdistCA.y - tdistBA.y * tdistCA.x;

            //*************************
            //  Tangent
            //*************************
            if (area == 0.0f)
            {
                tangent = kvec3Zero;
            }
            else
            {
                delta = 1.0f / area;

                // Calculates the face tangent to the current triangle.
                tangent.x = delta * ((distBA.x * tdistCA.y) + (distCA.x * -tdistBA.y));
                tangent.y = delta * ((distBA.y * tdistCA.y) + (distCA.y * -tdistBA.y));
                tangent.z = delta * ((distBA.z * tdistCA.y) + (distCA.z * -tdistBA.y));
            }

            // Averages the new tagent vector with the oldest buffered.
            tangentBuffer[i1] = vec3Add(tangent, tangentBuffer[i1]);
            tangentBuffer[i2] = vec3Add(tangent, tangentBuffer[i2]);
            tangentBuffer[i3] = vec3Add(tangent, tangentBuffer[i3]);

            //*************************
            //  Bitangent
            //*************************
            // Calculates the face bitangent to the current triangle,
            // completing the orthogonalized tangent space.
            bitangent = vec3Cross(normal, tangent);

            // Averages the new bitangent vector with the oldest buffered.
            bitangentBuffer[i1] = vec3Add(bitangent, bitangentBuffer[i1]);
            bitangentBuffer[i2] = vec3Add(bitangent, bitangentBuffer[i2]);
            bitangentBuffer[i3] = vec3Add(bitangent, bitangentBuffer[i3]);
        }

// CONTINUE...

Understanding line by line:

  • Line 6: Just create tangent space if there is texture coordinates on the mesh.
  • Lines 10 - 25: Checking if the "Crease Angle" routine changed the normals. If so, adjust the tangents and bitangents buffers to have the same size as the normals buffer.
  • Lines 27 - 42: Calculates the direction of the UV coordinates and the area of the current face (triangle).
  • Lines 47 - 64: Calculates the tangent vector for the current triangle and add this value into the tangents buffer.
  • Lines 71 - 76: As the Tangent Space is formed by three orthogonal vectors, we can calculate the last one (bitangent) very easy just by calculating the cross product between the Normal and Tangent.

OK, that's all, it's done!
Now we have all that we need: Normals, Tangents and Bitangents. They all are pretty perfect now and we just need to bring their values back into the arrays format. You can make this last step as you wish, I'll show you here a way that I'm used to.


Updating the Original Arrays - Step 4

OK, as I told you at the start, we'll use the "Array of Structures" in our final format. So we need to create/update that array and its indices based on the new elements, which may include Normals, Tangents and Bitangents.

I'll rewrite the "calculateTangentSpace()" function just to you take a look at it without breaks/continues.

Puting All Together

// CONTINUING...        

        // Copies the oldest face indices and inserts the new tangent space indices.
        outFaces = newFaces + (i * _faceStride);
        lengthJ = _faceStride;
        for (j = 0; j < lengthJ; ++j)
        {
            *outFaces++ = (j < oldFaceStride) ? _faces[i * oldFaceStride + j] : i1;
        }

        outFaces = newFaces + ((i + 1) * _faceStride);
        for (j = 0; j < lengthJ; ++j)
        {
            *outFaces++ = (j < oldFaceStride) ? _faces[(i + 1) * oldFaceStride + j] : i2;
        }

        outFaces = newFaces + ((i + 2) * _faceStride);
        for (j = 0; j < lengthJ; ++j)
        {
            *outFaces++ = (j < oldFaceStride) ? _faces[(i + 2) * oldFaceStride + j] : i3;
        }
    }

    // Commits the changes for the original array of faces. At this time, it could looks like:
    // iv1, it1, in1, ita1, ibt1, iv2, it2, in2, ita2, ibt2,...
    _faces = realloc(_faces, sizeof(int) * (_faceNumber * _faceStride));
    memcpy(_faces, newFaces, sizeof(int) * (_faceNumber * _faceStride));

    // Reallocates the memory for the array of normals, if needed. 
    if (!hasNormals)
    {
        _normals = realloc(_normals, sizeof(vec3) * _nCount);
    }

    // Reallocates the memory for the array of tangents and array of bitangents, if needed.
    if (hasTextures)
    {
        _tangents = realloc(_tangents, sizeof(vec3) * _taCount);
        _bitangents = realloc(_bitangents, sizeof(vec3) * _biCount);
    }

    // Loops through all new values of the tangent space, normalizing all the averaged vectors.
    length = _nCount;
    for (i = 0; i < length; i++)
    {
        // Puts the new normals, if needed.
        if (!hasNormals)
        {
            normal = vec3Normalize(normalBuffer[i]);
            outValue = _normals + (i * 3);
            *outValue++ = normal.x;
            *outValue++ = normal.y;
            *outValue = normal.z;
        }

        // Puts the new tangent and bitangent, if needed.
        // Isn't necessary here the Gram–Schmidt Orthogonalization process, because all the vectors
        // of the tangent space are already orthogonalized in reason of the crease angle approach.
        if (hasTextures)
        {
            tangent = vec3Normalize(tangentBuffer[i]);
            bitangent = vec3Normalize(bitangentBuffer[i]);

            outValue = _tangents + (i * 3);
            *outValue++ = tangent.x;
            *outValue++ = tangent.y;
            *outValue = tangent.z;

            outValue = _bitangents + (i * 3);
            *outValue++ = bitangent.x;
            *outValue++ = bitangent.y;
            *outValue = bitangent.z;
        }
    }

    // Frees the memories.
    free(newFaces);
    free(normalBuffer);
    free(tangentBuffer);
    free(bitangentBuffer);

    [multiples release];
}

Let's understand more about those last lines:

  • Lines 4 - 21: Using a pointer to insert the new face indices in the "newFaces" array, pointers is much faster than accessing the array by indices. These lines also respect the old face indices to place the new ones in the last positions.
  • Lines 26 - 27: Updating the original "array of faces". As you can see in the comments, at this point the new "array of faces" will be as "index of vertex 1, index of texcoord 1, index of normal 1, index of tangent 1, index of bitangent 1, index of vertex 2, ...". Remember that in this article I'll not include the job of converting those array into one single "array of structures", however with this "array of faces" you can do it very easy.
  • Lines 30 - 40: Preparing the Normals, Tangents and Bitangents to receive the calculated values.
  • Lines 43 - 74: Updating the Normals, Tangents and Bitangents values using pointers.
  • Lines 77 - 82: Clearing the allocated memory.


Conclusion

WOW, a lot of code in this article, don't you think? I'll try to simplify the steps, as you may notice was only 4 steps until here. So, here is all that you need:

  1. Calculate the Normals or use the Face Normals you already has.
  2. When you need to calculate the Normals you must calculate the crease angle and split the Normals according to this calculation.
  3. Calculate the Tangent Space, which includes Tangent and Bitangent.
  4. Update the original arrays, including the Normals, Tangents and Bitangents.

OK, my friends, now you have all that you need to construct your preferable array for OpenGL ("array of structures" or "structure of arrays").

In my next article will continue the series about shaders. Let's talk about shader some techniques advanced skills in OpenGL ES Shaders.

If you have any doubts, just Tweet me:

See you soon!

© db-in 2017. All Rights Reserved.