Search code examples
c++openglglm-math

Align a matrix to a vector in OpenGL


I'm trying to visualize normals of triangles. I have created a triangle to use as the visual representation of the normal but I'm having trouble aligning it to the normal.

I have tried using glm::lookAt but the triangle ends up in some weird position and rotation after that. I am able to move the triangle in the right place with glm::translate though.

Here is my code to create the triangle which is used for the visualization:

// xyz rgb
float vertex_data[] =
{
    0.0f, 0.0f, 0.0f,  0.0f, 1.0f, 1.0f,
    0.25f, 0.0f, 0.025f,  0.0f, 1.0f, 1.0f,
    0.25f, 0.0f, -0.025f,  0.0f, 1.0f, 1.0f,
};

unsigned int index_data[] = {0, 1, 2};

glGenVertexArrays(1, &nrmGizmoVAO);
glGenBuffers(1, &nrmGizmoVBO);
glGenBuffers(1, &nrmGizmoEBO);

glBindVertexArray(nrmGizmoVAO);

glBindBuffer(GL_ARRAY_BUFFER, nmrGizmoVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_data), vertex_data, GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, nrmGizmoEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index_data), index_data, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glBindVertexArray(0);

and here is the code to draw the visualizations:

for(unsigned int i = 0; i < worldTriangles->size(); i++)
{
    Triangle *tri = &worldTriangles->at(i);
    glm::vec3 wp = tri->worldPosition;
    glm::vec3 nrm = tri->normal;

    nrmGizmoMatrix = glm::mat4(1.0f);
    //nrmGizmoMatrix = glm::translate(nrmGizmoMatrix, wp);
    nrmGizmoMatrix = glm::lookAt(wp, wp + nrm, glm::vec3(0.0f, 1.0f, 0.0f));

    gizmoShader.setMatrix(projectionMatrix, viewMatrix, nrmGizmoMatrix);
    glBindVertexArray(nrmGizmoVAO);
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}

When using only glm::translate, the triangles appear in right positions but all point in the same direction. How can I rotate them so that they point in the direction of the normal vector?


Solution

  • Your code doesn't work because lookAt is intended to be used as the view matrix, thus it returns the transform from world space to local (camera) space. In your case you want the reverse -- from local (triangle) to world space. Taking an inverse of lookAt should solve that.


    However, I'd take a step back and look at (haha) the bigger picture. What I notice about your approach:

    • It's very inefficient -- you issue a separate call with a different model matrix for every single normal.
    • You don't even need the entire model matrix. A triangle is a 2-d shape, so all you need is two basis vectors.

    I'd instead generate all the vertices for the normals in a single array, and then use glDrawArrays to draw that. For the actual calculation, observe that we have one degree of freedom when it comes to aligning the triangle along the normal. Your lookAt code resolves that DoF rather arbitrary. A better way to resolve that is to constrain it by requiring that it faces towards the camera, thus maximizing the visible area. The calculation is straightforward:

    // inputs: vertices output array, normal position, normal direction, camera position
    void emit_normal(std::vector<vec3> &v, const vec3 &p, const vec3 &n, const vec3 &c) {
        static const float length = 0.25f, width = 0.025f;
        vec3 t = normalize(cross(n, c - p)); // tangent
        v.push_back(p);
        v.push_back(p + length*n + width*t);
        v.push_back(p + length*n - width*t);
    }
    
    // ... in your code, generate normals through:
    std::vector<vec3> normals;
    for(unsigned int i = 0; i < worldTriangles->size(); i++) {
        Triangle *tri = &worldTriangles->at(i);
        emit_normal(normals, tri->worldPosition, tri->normal, camera_position);
    }
    
    // ... create VAO for normals ...
    glDrawArrays(GL_TRIANGLES, 0, normals.size());
    

    Note, however, that this would make the normal mesh camera-dependent -- which is desirable when rendering normals with triangles. Most CAD software draws normals with lines instead, which is much simpler and avoids many problems:

    void emit_normal(std::vector<vec3> &v, const vec3 &p, const vec3 &n) {
        static const float length = 0.25f;
        v.push_back(p);
        v.push_back(p + length*n);
    }
    
    // ... in your code, generate normals through:
    std::vector<vec3> normals;
    for(unsigned int i = 0; i < worldTriangles->size(); i++) {
        Triangle *tri = &worldTriangles->at(i);
        emit_normal(normals, tri->worldPosition, tri->normal);
    }
    
    // ... create VAO for normals ...
    glDrawArrays(GL_LINES, 0, normals.size());