C-3: draw a colored pyramid and add a lighting effect

Keywords: C++ Computer Graphics OpenGL


Operation requirements:
a. The side length of the pyramid is 2;
b. The color of each vertex of a pyramid is different;
c. The center of the pyramid is (1,2,3);
d. It is required to use a variety of lights, including ambient light, specular light and scattered light, which can be reflected by controlling the object
Material factors and different light factors achieve different lighting effects.

See the README.md file for operation instructions. See video recording for effect display.

Initialize OpenGL

Create a window, register the callback function, load glad, and set the basic properties of OpenGL.

	glfwInit();

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, true); // comment this line in a release build!

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Materials", NULL, NULL);
    if (window == NULL){
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);// After registering the callback function with GLFW, move the mouse_ The callback function is called
    glfwSetScrollCallback(window, scroll_callback);//Register the callback function of the mouse wheel

    if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global OpenGL state
    glEnable(GL_DEPTH_TEST);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

Prepare data

Create a vertex array object. At the same time, the position attribute, color attribute and normal vector attribute of the vertex are registered respectively. The serial number value of the attribute indicates the storage location of the data, so as to find the position and color data through the corresponding location in the shader.
The position attribute of the vertex meets the design requirements. Set the Color property yourself. The Normal attribute needs to be calculated by myself. Note that I use the form of sharp edge here. When rendering each triangular patch, the three vertices use the same Normal vector (instead of using the Normal vectors of all faces around a vertex to average the Normal of the vertices, so that the lighting effect will transition smoothly). Therefore, in the later rendering, glDrawArrays(GL_TRIANGLES, 0, 36) is also used to draw each triangle, that is, vertex data cannot be reused between different triangular patches.

	float sqr2 = sqrt(2.0f);
    float pyramid_vertices[] = {
        // Positions        Color              Normal
         0.0f, sqr2, 0.0f,  1.0f, 0.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //0
         0.0f, 0.0f, sqr2,  0.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //2
         sqr2, 0.0f, 0.0f,  0.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //1
        
         0.0f, sqr2, 0.0f,  1.0f, 0.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //0
        -sqr2, 0.0f, 0.0f,  1.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //3
         0.0f, 0.0f, sqr2,  0.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //2
         
         0.0f, sqr2, 0.0f,  1.0f, 0.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //0
         0.0f, 0.0f,-sqr2,  1.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //4
        -sqr2, 0.0f, 0.0f,  1.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //3

         0.0f, sqr2, 0.0f,  1.0f, 0.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //0
         sqr2, 0.0f, 0.0f,  0.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //1
         0.0f, 0.0f,-sqr2,  1.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //4

         sqr2, 0.0f, 0.0f,  0.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //1
         0.0f, 0.0f, sqr2,  0.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //2
        -sqr2, 0.0f, 0.0f,  1.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //3

         sqr2, 0.0f, 0.0f,  0.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //1
        -sqr2, 0.0f, 0.0f,  1.0f, 1.0f, 0.0f,  0.0f, 0.0f, 0.0f,    //3
         0.0f, 0.0f,-sqr2,  1.0f, 0.0f, 1.0f,  0.0f, 0.0f, 0.0f,    //4
    };
    for (int iFace = 0; iFace < 6; iFace++) {
        int vid0 = 3*iFace, vid1 = vid0+1, vid2 = vid1+1;
        glm::vec3 a = glm::vec3(pyramid_vertices[vid1*9+0]-pyramid_vertices[vid0*9+0], \
                                 pyramid_vertices[vid1*9+1]-pyramid_vertices[vid0*9+1], \
                                 pyramid_vertices[vid1*9+2]-pyramid_vertices[vid0*9+2]);
        glm::vec3 b = glm::vec3(pyramid_vertices[vid2*9+0]-pyramid_vertices[vid0*9+0], \
                                 pyramid_vertices[vid2*9+1]-pyramid_vertices[vid0*9+1], \
                                 pyramid_vertices[vid2*9+2]-pyramid_vertices[vid0*9+2]);
        glm::vec3 nm= glm::normalize(glm::cross(a, b));
        for (int i = vid0; i <= vid2; i++) {
            pyramid_vertices[i*9+6] = nm[0];
            pyramid_vertices[i*9+7] = nm[1];
            pyramid_vertices[i*9+8] = nm[2];
        }
    }
    // first, configure the pyramid's VAO, VBO, EBO
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(pyramid_vertices), pyramid_vertices, GL_STATIC_DRAW);
    
    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    // Normal attribute
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6*sizeof(float)));
    glEnableVertexAttribArray(2);

    glBindVertexArray(0);//unbind

At the same time, it should be noted that when defining the piramid_ When using the vertices array, it is necessary to reasonably arrange the vertex order to ensure that the face normal vector of each triangular face according to the vertex definition order (in the right-hand coordinate system) faces outward.

In addition to the pyramid, there is another object that acts as a light source (it's not necessary, just to look good). It is processed similarly.

	float cube_vertices[] = {
        // Positions
        -0.5f, -0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        0.5f,  0.5f, -0.5f,
        0.5f,  0.5f, -0.5f,
        -0.5f,  0.5f, -0.5f,
        -0.5f, -0.5f, -0.5f,

        -0.5f, -0.5f,  0.5f,
        0.5f, -0.5f,  0.5f,
        0.5f,  0.5f,  0.5f,
        0.5f,  0.5f,  0.5f,
        -0.5f,  0.5f,  0.5f,
        -0.5f, -0.5f,  0.5f,

        -0.5f,  0.5f,  0.5f,
        -0.5f,  0.5f, -0.5f,
        -0.5f, -0.5f, -0.5f,
        -0.5f, -0.5f, -0.5f,
        -0.5f, -0.5f,  0.5f,
        -0.5f,  0.5f,  0.5f,

        0.5f,  0.5f,  0.5f,
        0.5f,  0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        0.5f, -0.5f,  0.5f,
        0.5f,  0.5f,  0.5f,

        -0.5f, -0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        0.5f, -0.5f,  0.5f,
        0.5f, -0.5f,  0.5f,
        -0.5f, -0.5f,  0.5f,
        -0.5f, -0.5f, -0.5f,

        -0.5f,  0.5f, -0.5f,
        0.5f,  0.5f, -0.5f,
        0.5f,  0.5f,  0.5f,
        0.5f,  0.5f,  0.5f,
        -0.5f,  0.5f,  0.5f,
        -0.5f,  0.5f, -0.5f
    };

    // second, configure the light's VAO (VBO stays the same; the vertices are the same for the light object which is also a 3D cube)
    unsigned int lightCubeVAO, lightCubeVBO;
    glGenVertexArrays(1, &lightCubeVAO);
    glGenBuffers(1, &lightCubeVBO);

    glBindVertexArray(lightCubeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, lightCubeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cube_vertices), cube_vertices, GL_STATIC_DRAW);
    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glBindVertexArray(0);//unbind

Build shader

For fragment shaders, you need to pay attention to converting the normal vector from local space to world space. Since all lighting is calculated in world space, we need a vertex position worldCoord in world space.

#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec3 aNormal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 objectColor;
out vec3 Normal;
out vec3 worldCoord;

void main()
{
    worldCoord = vec3(model * vec4(aPos, 1.0));
    
    //Normal = aNormal;
    //Normal matrix: the transpose matrix of the inverse matrix in the upper left corner of the model matrix, which is used to remove the influence on the wrong scaling of the normal vector
    //Normal = mat3(transpose(inverse(model))) * aNormal;
    Normal = transpose(inverse(mat3(model))) * aNormal;

    gl_Position = projection * view * model * vec4(aPos, 1.0);
    objectColor = aColor;
}

Fragment shaders involving ambient lighting, diffuse lighting and specular lighting are more complex than before. In general, it is necessary to define the material structure material of the object, in which the ambient, diffuse and specific components are vec3 type, so as to characterize anisotropic materials. The same is true for the light source struct Light.

#version 460 core
out vec4 FragColor;
  
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;    
    float shininess;
}; 

struct Light {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform vec3 viewPos;//Observer / camera position
uniform Material material;
uniform Light light;

in vec3 objectColor;
in vec3 Normal;
in vec3 worldCoord;

void main()
{
    //Ambient light
    vec3 ambient = material.ambient * light.ambient;

    //diffuse lighting 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(light.position - worldCoord);//Point light from clip
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * (diff * material.diffuse);

    //Specular illumination
    vec3 specular;
    if (diff > 0.0) {//Only when the included angle between the illumination direction and the normal direction is less than 90 ° can there be mirror illumination effect, otherwise it is useless
        vec3 viewDir = normalize(viewPos - worldCoord);//Point from clip to camera
        vec3 reflectDir = reflect(-lightDir, norm);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);//32 is the shininess of the highlight. The higher the shininess of an object, the stronger the ability to reflect light, the less scattering, and the smaller the highlight point.
        specular = spec * light.specular * material.specular; 
    } else {
        specular = vec3(0.0f, 0.0f, 0.0f);
    }

    vec3 result = (ambient + diffuse + specular) * objectColor;
    // vec3 result = Normal;
    FragColor = vec4(result, 1.0);
}

Object material

For the selection of object materials, I refer to the learnopungl website Material section Included in the links provided in Material list . Add all optional materials to the program as materials_lists. But it's worth noting that, like this one Material list Indicated when providing:

Note that the ambient values in the material table may be different from the diffuse values, and they do not take into account the intensity of the light. To correct this problem, you need to set all light intensities to vec3(1.0) in order to get the correct output

So if you use this Material list In the rendering cycle, you need to set the light source parameter to

	glm::vec3 curr_lightPos = glm::vec3(radius*cos(currentFrame), lightPos.y, radius*sin(currentFrame));
	pyramidShader.setVec3("light.position", curr_lightPos);
	pyramidShader.setVec3("light.ambient", glm::vec3(1.0f, 1.0f, 1.0f));
	pyramidShader.setVec3("light.diffuse", glm::vec3(1.0f, 1.0f, 1.0f));
	pyramidShader.setVec3("light.specular", glm::vec3(1.0f, 1.0f, 1.0f)); 

To add interest, I set the position of the light source to rotate around the central axis of the pyramid at a certain height (fixed y coordinate).

Rendering pyramid and light objects

As usual, set the world space matrix model, view matrix, and projection matrix in the shader. Bind the vertex array object before each draw.

	// view/projection transformations
	glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    glm::mat4 view = camera.GetViewMatrix();
    pyramidShader.setMat4("projection", projection);
    pyramidShader.setMat4("view", view);
    // world transformation
    glm::mat4 model = glm::mat4(1.0f);
    // model = glm::translate(model, glm::vec3(1.0f, 2.0f, 3.0f));
    pyramidShader.setMat4("model", model);

    // render the pyramid
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 18);

    /* -------------------------------------------------------------------------------------------- */

    // also draw the lamp object
    lightCubeShader.use();
    lightCubeShader.setMat4("projection", projection);
    lightCubeShader.setMat4("view", view);
    model = glm::mat4(1.0f);
    model = glm::translate(model, curr_lightPos);
    model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
    lightCubeShader.setMat4("model", model);

    glBindVertexArray(lightCubeVAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);

effect

The effect of selecting the light source to move to different positions is shown below.

Figure 1 Figure 2 Figure 3
Here are the effects of another material.
Figure 1 Figure 2 Figure 3

Posted by jase01 on Wed, 17 Nov 2021 08:12:54 -0800