Linux 下的 OpenGL 之路(三):向 3D 世界邁出一小步


前言

OpenGL 的學習資料很多,一個是比較著名的 OpenGL 紅寶書《OpenGL 編程指南》,可以在這里 http://opengl-redbook.com/ 下載該書配套的源代碼;另一個是網絡上的在線教程LearnOpenGL。所以,我這里就不再啰啰嗦嗦地介紹 OpenGL 的基礎知識和計算機圖形學的基礎知識了,主要是簡單闡述一些我自己的理解,以及寫一些能跑起來、能看到效果的體驗性的小程序。

通過前一篇闡述,可以看到在 Linux 系統中學習 OpenGL 是多么的方便。使用 GLEW 和 GLFW 庫,使用一行簡單的 g++ 編譯命令,第一個簡單的 OpenGL 程序就跑起來了。而上面提到的參考資料,都有讓初學者頭痛的地方,比如紅寶書,它用到了比較少見的 gl3w 庫,還用到了 cmake 系統,即使下載了它的源代碼,程序也跑不起來。網絡教程LearnOpenGL使用的是 GLAD,反正我沒有使用成功過。最終,還是 GLEW 和 GLFW 搭配是最順手的。

前一篇的最后,我們跑起來了一個空的 OpenGL 程序,里面沒有渲染任何內容。在這一篇中,我們將向 3D 世界邁出一小步,了解 OpenGL 渲染管線的基本知識,並繪制一些簡單的 3D 內容。

我所理解的簡單的 3D 圖形學知識

要想描述一個 3D 世界,最簡單的辦法就是描述一些頂點的坐標,(當然還有其它的辦法,比如通過數學公式描述的曲線曲面之類的,所以我說描述頂點坐標是最簡單的辦法。),然后由頂點連接成三角形,再由三角形連接成面,最后,計算機再把這些數據處理成屏幕上像素的顏色,顯示成我們看到的圖像。

而 OpenGL 通過渲染管線完成這個操作,渲染管線由許多個階段組成,首先,OpenGL 把我們傳遞給它的頂點進行坐標變換,把 3D 的坐標對應成屏幕上的點,然后,把這些頂點組裝成三角形,然后,對這些三角形內部的像素進行插值,生成片元,最后,對這些片元進行計算,以生成在屏幕上顯示的圖像。很顯然,渲染管線需要使用 GPU 進行加速。

GPU 是典型的眾人拾柴火焰高的並行計算架構,它可以同時對成千上萬的頂點進行計算,也可以同時對成千上萬的片元進行計算。我的這台 XPS 9570,使用的顯卡是 GTX 1050Ti,它有 768 個 CUDA 核心,我准備等有錢了再上一個 RTX 3060 顯卡的筆記本,據說有 3840 個 CUDA 核心,性能可以一下子提升 5 倍, _。讓這些核心運行什么樣的任務,是由我們指定的,也就是說,我們要寫一些小程序,然后把這些小程序傳遞給 GPU 核心執行,這些小程序就叫做 Shader。在 OpenGL 的渲染管線中,每一個階段都對應一個 Shader。

通過上面的描述,我們的工作流程呼之欲出。要想使用 OpenGL 渲染一點有用的東西,我們需要做下面這些工作:1.准備一些頂點數據;2.寫一些 Shader 程序,並組裝成渲染管線;3.把頂點數據傳遞給 Shader,讓 Shader 處理。

特別需要注意的還有兩點:1. 頂點數據可以不僅僅只是坐標,還可以包含法線呀、顏色值呀、紋理坐標呀等數據,還可以包含其它任何我們自定義的數據;2. Shader 是並行執行的,每個 Shader 處理一個頂點的數據,不同的頂點其傳入的數據是不同的,可以認為這些數據是逐頂點變量,但是,也可以給所有的 Shader 傳遞一些統一的值,這些值在所有的 Shader 中是相同的,稱之為 Uniform 變量。

這些數據,通過一系列不同的 OpenGL 函數傳遞給 Shader。

下面開始實戰。

准備頂點數據

頂點數據怎么來,這是一個問題。我們可以直接手寫三個頂點坐標,在屏幕上渲染一個三角形,同樣的方法,也可以手寫八個頂點坐標及其顏色值渲染一個顏色立方體。對於一些更復雜的幾何圖形,比如球體、甜甜圈這樣的幾何體,只要會一點簡單的三角函數,我們自己也可以生成出來。另外,對於一些復雜的、經典的模型,也可以找到它們的出處。比如紅寶書中就有一個 armadillo 神獸的 3D 模型,可以在紅寶書源代碼的github倉庫中找到,只不過是作者自創的 vbm 格式,需要自己動手扒一下。再比如 freeglut 中經典的茶壺模型,也可以在freeglut的github倉庫中找到,只不過它使用的是貝塞爾曲面的描述方式,這個技術含量就高了那么一點點了。

對於頂點數據的每一個分量(如坐標分量、法向量分量、紋理坐標分量、顏色分量等),我們都可以使用 GLM 庫中的vec2、vec3、vec4格式表示,然后組織成 struct,再然后組織成 vector,這是很自然的事情。如果使用到頂點索引,則需要再准備一個索引數組的 vector。再然后,就是 OpenGL 的那一套 API 了,創建 VAO、VBO,如果有索引,再創建 EBO,然后綁定對象,創建 Buffer,綁定 Buffer,向 Buffer 中存入數據,最后使用 DrawArrays() 進行渲染,如果使用到了頂點索引,則使用 DrawElements() 進行渲染。

在這里,我先創建一個 Mesh 類用來對以上流程進行一個封裝,並創建 Mesh 類的幾個子類,分別生成平面、球體、甜甜圈這幾種基本圖形,每個頂點具有坐標、法向量、紋理坐標(只有1組紋理坐標)。我把這些結構和類都放到一個文件 mesh.hpp 中,內容如下:

#ifndef __MESH_H__
#define __MESH_H__

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <string>
#include <GL/glew.h>

struct Vertex{
    glm::vec4 position;
    glm::vec3 normal;
    glm::vec2 texCoord;
};

class Mesh{
    protected:
        std::vector<Vertex> vertices;
        std::vector<GLuint> indices;

        GLuint VAO, VBO, EBO;
               
    public:
        void generateMesh(int iSlices);

        void setup(){
            glCreateVertexArrays(1, &VAO);
            glBindVertexArray(VAO);
            glCreateBuffers(1, &VBO);
            glBindBuffer(GL_ARRAY_BUFFER, VBO);
            glNamedBufferStorage(VBO, sizeof(Vertex)*vertices.size(), &vertices[0], 0);
            glGenBuffers(1, &EBO);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(float)*indices.size(), &indices[0], GL_STATIC_DRAW);
            glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
            glEnableVertexAttribArray(2);
        }

        void render(){
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        }

};

class Plane: public Mesh{
    public:
        void generateMesh(int iSlices){
            int n = iSlices + 1;
            float s = 2.0f/(float)iSlices;
            Vertex temp_vertex;
            for(int i=0; i<n; i++){
                for(int j=0; j<n; j++){
                    temp_vertex.position = glm::vec4(s*j - 1.0f, s*i - 1.0f, 0.0f, 1.0f);
                    temp_vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f);
                    temp_vertex.texCoord = glm::vec2(1.0f/(float)iSlices * j, 1.0f/(float)iSlices * i);
                    vertices.push_back(temp_vertex);
                }
            }
            
            for(int i=0; i<iSlices; i++){
                for(int j=0; j<iSlices; j++){
                    indices.push_back(i*n + j);
                    indices.push_back((i+1)*n + j+1);
                    indices.push_back((i+1)*n + j);

                    indices.push_back(i*n + j);
                    indices.push_back(i*n + j + 1);
                    indices.push_back((i+1)*n + j+1);
                }
            }
        }
};

class Sphere: public Mesh{
    public:
        void generateMesh(int iSlices){
            int m = iSlices/2 + 1;
            int n = iSlices+1;
            float s = 360.0f/(float)iSlices;
            glm::vec4 up(0.0f, 1.0f, 0.0f, 1.0f);
            glm::mat4 I(1.0f);
            glm::vec3 X(1.0f, 0.0f, 0.0f);
            glm::vec3 Y(0.0f, 1.0f, 0.0f);
            glm::vec3 Z(0.0f, 0.0f, 1.0f);
            Vertex temp_vertex;
            for(int i=0; i<m; i++){
                for(int j=0; j<n; j++){
                    temp_vertex.position = glm::rotate(I, glm::radians(s*j), Y) * glm::rotate(I, glm::radians(s*i), Z) * up;
                    temp_vertex.normal = temp_vertex.position;
                    temp_vertex.texCoord = glm::vec2(1.0f/(float)(n-1) * j, 1.0f/(float)(m-1) * i);
                    vertices.push_back(temp_vertex);
                }
            }
            
            for(int i=0; i<m-1; i++){
                for(int j=0; j<n-1; j++){
                    indices.push_back(i*n + j);
                    indices.push_back((i+1)*n + j);
                    indices.push_back((i+1)*n + j+1);

                    indices.push_back(i*n + j);
                    indices.push_back((i+1)*n + j+1);
                    indices.push_back(i*n + j + 1);                   
                }
            }
        }
};

class Torus: public Mesh{
    public:
        void generateMesh(int iSlices){
            int n = iSlices + 1;
            float s = -360.0f/(float)iSlices;
            glm::vec4 top(0.0f, 0.2f, 0.0f, 1.0f);
            glm::vec4 normal_up(0.0f, 1.0f, 0.0f, 1.0f);
            glm::mat4 I(1.0f);
            glm::vec3 X(1.0f, 0.0f, 0.0f);
            glm::vec3 Y(0.0f, 1.0f, 0.0f);
            glm::vec3 Z(0.0f, 0.0f, 1.0f);
            Vertex temp_vertex;
            for(int i=0; i<n; i++){
                for(int j=0; j<n; j++){
                    temp_vertex.position = glm::rotate(I, glm::radians(s*j), Z) 
                                            * glm::translate(I, glm::vec3(0.0f, 0.8f, 0.0f))
                                            * glm::rotate(I, glm::radians(s*i), X) 
                                            * top;
                    temp_vertex.normal = glm::rotate(I, glm::radians(s*j), Z) 
                                            * glm::rotate(I, glm::radians(s*i), X) 
                                            * normal_up;
                    temp_vertex.texCoord = glm::vec2(1.0f/(float)iSlices * j * 4, 1.0f/(float)iSlices * i);
                    vertices.push_back(temp_vertex);
                }

            }
            for(int i=0; i<iSlices; i++){
                for(int j=0; j<iSlices; j++){
                    indices.push_back(i*n + j);
                    indices.push_back((i+1)*n + j+1);
                    indices.push_back((i+1)*n + j);

                    indices.push_back(i*n + j);
                    indices.push_back(i*n + j + 1);
                    indices.push_back((i+1)*n + j+1);
                }
            }
        }
};

#endif

編譯和連接 Shader

在 OpenGL 中編譯和連接 Shader 的流程是固定的,也就是那幾個 API,詳細的知識我就不贅述了,基本所有的教材都有,對於簡單的程序來說,一個 Vertex Shader 和一個 Fragment Shader 就可以了,至於幾何着色器、細分着色器這樣的高級知識,等用到的時候再說。關於着色器的編譯連接,我也寫了一個 Shader 類對它進行了封裝,放到了文件 shader.hpp 中,如下:

#ifndef __SHADER_HPP__
#define __SHADER_HPP__

#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>

struct ShaderInfo{
        GLenum type;
        std::string filename;
        GLuint shader_id;
};

class Shader{
    private:
        GLuint program_id;
    public:
        Shader(){
            program_id = 0;
        }
        
        Shader(ShaderInfo* shaders){
            if(shaders == nullptr){
                program_id = 0;
                return;
            }

            program_id = glCreateProgram();

            ShaderInfo* entry = shaders;
            //加載並編譯Shader
            while(entry->type != GL_NONE){
                GLuint shader_id = glCreateShader(entry->type);
                entry->shader_id = shader_id;

                std::ifstream fs(entry->filename);
                std::string content((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
                               
                if(content.empty()){ //只要有一個Shader文件打不開,就刪掉之前創建的所有Shader
                    std::cerr << "Unable to open file '" << entry->filename << "'" << std::endl;
                    for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
                        glDeleteShader( entry->shader_id );
                        entry->shader_id = 0;
                    }
                    return;
                }
                const GLchar* source = content.c_str();
                glShaderSource( shader_id, 1, &source, NULL );

                glCompileShader( shader_id );

                GLint compiled;
                glGetShaderiv( shader_id, GL_COMPILE_STATUS, &compiled );
                if ( !compiled ) {//如果Shader編譯失敗,輸出失敗原因,便於調試
                    GLsizei len;
                    glGetShaderiv( shader_id, GL_INFO_LOG_LENGTH, &len );

                    GLchar* log = new GLchar[len+1];
                    glGetShaderInfoLog( shader_id, len, &len, log );
                    std::cerr << entry->filename << "," << "Shader compilation failed: " << log << std::endl;
                    delete [] log;

                    //編譯失敗,也要刪除前面創建的所有Shader
                    for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
                        glDeleteShader( entry->shader_id );
                        entry->shader_id = 0;
                    }

                    return ;
                }

                glAttachShader( program_id, shader_id );
        
                ++entry;
                
            }
            
            //進入連接階段
            glLinkProgram( program_id );

            GLint linked;
            glGetProgramiv( program_id, GL_LINK_STATUS, &linked );
            if ( !linked ) {//如果連接失敗,則輸出調試信息,同樣刪除之前創建的所有Shader
                GLsizei len;
                glGetProgramiv( program_id, GL_INFO_LOG_LENGTH, &len );

                GLchar* log = new GLchar[len+1];
                glGetProgramInfoLog( program_id, len, &len, log );
                std::cerr << "Shader linking failed: " << log << std::endl;
                delete [] log;

                for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
                    glDeleteShader( entry->shader_id );
                    entry->shader_id = 0;
                }
        
                return;
            }
            
        }

        GLuint getId(){
            return program_id;
        }
        
        void setCurrent(){
            glUseProgram(program_id);
        }

        void setModelMatrix(glm::mat4 model_matrix){
            glUniformMatrix4fv(glGetUniformLocation(program_id, "model_matrix"), 1, GL_FALSE, glm::value_ptr(model_matrix));
        }

        void setViewMatrix(glm::mat4 view_matrix){
            glUniformMatrix4fv(glGetUniformLocation(program_id, "view_matrix"), 1, GL_FALSE, glm::value_ptr(view_matrix));
        }

        void setProjectionMatrix(glm::mat4 projection_matrix){
            glUniformMatrix4fv(glGetUniformLocation(program_id, "projection_matrix"), 1, GL_FALSE, glm::value_ptr(projection_matrix));
        }
};

#endif

該 Shader 類還提供了一些向 Shader 中傳遞 Uniform 變量的方法,最常見的 Uniform 變量就是模型矩陣、視圖矩陣和投影矩陣。

主程序

主程序文件為 SphereWorld.cpp,在該文件中,創建一個 App 類的字類,並在 init 方法中創建一個平面、一個球體、一個甜甜圈,並使用模型矩陣分別將它們進行平移、縮放和旋轉,以放到場景中的適當位置,另外,使用視圖矩陣將 Camera 放到適當的位置,最后,創建合適的透視投影矩陣,就可以看到非常逼真的三維圖像了。在創建和使用這些矩陣的時候,GLM 為我們提供了非常大的方便。目前還沒有涉及到光照和紋理,故將 OpenGL 設置為線框模式,這樣看起來更加立體。程序代碼如下:

#include "../include/app.hpp"
#include "../include/shader.hpp"
#include "../include/mesh.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

class MyApp : public App {
    private:
        const GLfloat clearColor[4] = {0.2f, 0.3f, 0.3f, 1.0f};
        Plane plane;
        Sphere sphere;
        Torus torus;
        Shader* simpleShader;

    public:
        void init(){
            
            ShaderInfo shaders[] = {
                {GL_VERTEX_SHADER, "simpleShader.vert"},
                {GL_FRAGMENT_SHADER, "simpleShader.frag"},
                {GL_NONE, ""}
            };
            simpleShader = new Shader(shaders);
            plane.generateMesh(20);
            plane.setup();

            sphere.generateMesh(60);
            sphere.setup();

            torus.generateMesh(60);
            torus.setup();
            
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LEQUAL);

            glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
        }

        void display(){
            glClearBufferfv(GL_COLOR, 0, clearColor);
            glClear(GL_DEPTH_BUFFER_BIT);

            glm::mat4 I(1.0f);
            glm::vec3 X(1.0f, 0.0f, 0.0f);
            glm::vec3 Y(0.0f, 1.0f, 0.0f);
            glm::vec3 Z(0.0f, 0.0f, 1.0f);
            float t = (float)glfwGetTime();

            glm::mat4 view_matrix = glm::translate(I, glm::vec3(0.0f, 0.0f, -5.0f))
                                        * glm::rotate(I, t, Y);

            glm::mat4 projection_matrix = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);

            glm::mat4 plane_model_matrix = glm::translate(I, glm::vec3(0.0f, -1.0f, 0.0f)) 
                                        * glm::rotate(I, glm::radians(-90.0f), X)
                                        * glm::scale(I, glm::vec3(50.0f, 50.0f, 50.0f));
            
            simpleShader->setModelMatrix(plane_model_matrix);
            simpleShader->setViewMatrix(view_matrix);
            simpleShader->setProjectionMatrix(projection_matrix);
            simpleShader->setCurrent();
            plane.render();

            glm::mat4 sphere_model_matrix = glm::translate(I, glm::vec3(1.0f, 0.3f, 0.0f))
                                                * glm::scale(I, glm::vec3(0.8f, 0.8f, 0.8f));
            simpleShader->setModelMatrix(sphere_model_matrix);
            sphere.render();

            glm::mat4 torus_model_matrix = glm::translate(I, glm::vec3(-1.0f, 0.3f, 0.0f))
                                                * glm::rotate(I, glm::radians(90.0f), Y)
                                                * glm::scale(I, glm::vec3(1.3f, 1.3f, 1.3f));
            simpleShader->setModelMatrix(torus_model_matrix);
            torus.render();
        }

        ~MyApp(){
            if(simpleShader != NULL){
                delete simpleShader;
            }
        }

};


DECLARE_MAIN(MyApp)

我們暫時還沒有實現在場景中漫游的功能,所以只能讓它自己旋轉,以便於我們全方位地觀察。在這里,是通過 glfwGetTime() 獲取程序運行的時間,並根據時間值來生成視圖矩陣來實現這個功能的。從上面的程序可以看到,我們的 Shader 程序取名叫 simpleShader,這個 Shader 和簡單,也很通用,只比什么都不做的 Pass Through Shader 復雜一點點,那就是對頂點進行模型、視圖、投影變換,也就是把頂點坐標和傳入的模型、視圖、投影矩陣相乘。得益於 GLM 庫的強大功能,我們生成模型、視圖、投影矩陣都特別簡單,從上面的代碼中可以看到。

編寫 Shader 程序

這里的 Shader 程序是 simpleShader.vert 和 simpleShader.frag,前者是頂點着色器程序,后者是片元着色器程序。都很簡單。

simpleShader.vert 文件的內容如下:

#version 460

uniform mat4 model_matrix;
uniform mat4 projection_matrix;
uniform mat4 view_matrix;

layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 1) in vec2 vTexCoord;

out vec4 fColor;
out vec3 fNormal;
out vec2 fTexCoord;
out vec4 fPosition;

void main(void)
{
    mat4 MV_matrix = view_matrix * model_matrix;
    gl_Position = projection_matrix * view_matrix * model_matrix * vPosition;
    fPosition =  MV_matrix * vPosition;
    fNormal = normalize(transpose(inverse(mat3(MV_matrix))) * vNormal);
    fTexCoord = vTexCoord;
    fColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

該 Shader 很簡單,就是使用程序傳入的模型、視圖、投影矩陣對頂點坐標、法線進行適當的變換,貼圖坐標都不用特殊處理,直接傳給片元着色器。這些值在傳遞的過程中,會自動進行插值。

simpleShader.frag 文件內容如下:

#version 460

layout (location = 0) out vec4 color;

in vec4 fColor;
in vec3 fNormal;
in vec2 fTexCoord;
in vec4 fPosition;

void main(void)
{
    color = fColor;
}

最終效果

編譯,運行,就可以看到最終的效果了。如下圖:

我使用的是 Visual Studio Code。從截圖中可以看到我所有文件的組織情況。好了,今天就到這里。

版權申明

該隨筆由京山游俠在2021年02月07日發布於博客園,引用請注明出處,轉載或出版請聯系博主。QQ郵箱:1841079@qq.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM