openGL兔兔大作業-面的滑鼠拾取

peter_819發表於2020-12-23

好久沒寫過GL了,正好來發發教程

要求

給出兔兔的頂點座標與三角面索引,需要實現:

  1. 繪製模型,馮氏光照
  2. 模型及視角移動
  3. 滑鼠點選高亮某三角面

總體思路 & 坑點

For 可以自己實現OpenGL編寫的朋友

  • 匯入模型 :給出的資料為頂點座標,沒有法線資訊,需要求解法線,在我的實現中對每一三角面根據三點座標求解了法線,沒有進行法線插值,這會使得兔兔表面不夠圓潤,並且需要 NumFace * 3 大小的VBO,較浪費空間。按道理可以對每一點求解法線,使用該點與鄰接點的向量進行加權平均,具體可以參考連結 Weighted Vertex Normals

  • 光照模型:沒什麼特別的,Blinn-Phong或者Phong的Shader

  • 模型及視角移動:也沒什麼特別的,取下幀間滑鼠Δ值和鍵盤按鍵變model view矩陣就行了

  • 點選高亮:這個還蠻有趣的,想了個辦法,應該不是最優解,用一個drawcall繪製一張每個面顏色都是該面索引值 / 總面數的RT,然後readBack一下滑鼠位置的顏色,拿到高亮面的頂點,這裡我直接再加了一個drawcall畫這三角,應該是多餘了。

實現細節

For 不怎麼熟悉OpenGL的朋友

配置OpenGL環境

  • 使用GLFW初始化視窗
GLFW is a lightweight utility library for use with OpenGL. GLFW stands for Graphics Library Framework. It provides programmers with the ability to create and manage windows and OpenGL contexts, as well as handle joystick, keyboard and mouse input.

一般使用GLFW作為跨平臺的視窗工具,在本例中就作為建立OpenGL繪製視窗,處理滑鼠鍵盤事件的API。

  • 使用GLAD連結OpenGL API

可以簡單認為,雖然各個平臺都支援OpenGL繪製,但一般都僅僅是提供了按OpenGL標準所實現的二進位制連結庫,如WIndows下預設靜態連結庫會有opengl32.lib,但仍然需要一個第三方庫去在執行時載入這一dll,提供一個符合標準的c++標頭檔案,並且將二進位制庫中的實現載入到對應的函式API上。

void Application::Run()
{
    //Initialize GLFW Window
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    window = glfwCreateWindow(800, 600, "Bunny", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
    }
    glfwMakeContextCurrent(window);
    //Initialize GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
    }
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    Init(); //Read Data From File & Initialize VAOs FBOs
    while (!glfwWindowShouldClose(window))
    {
        Render();   //Main Render Function
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    glfwTerminate();
}

處理輸入

  • 將所有點與索引輸入,按面順序排序,每個點有7個float的長度:
    • 位置:vec3
    • 法線:vec3
    • 面編號(歸一化):float
void Application::Init() {
    //Read From File
    std::ifstream fin("bunny_iH.ply2");
    if (!fin) {
        assert(false);
    }
    numVertices = 0;
    fin >> numVertices;
    fin >> numFaces;
    vertices = new float[(size_t)numVertices * 3];
    sortedVertices = new float[(size_t)numFaces * 3 * 7];

    for (int i = 0; i < numVertices; i++) {
        fin >> vertices[i * 3] >> vertices[i * 3 + 1] >> vertices[i * 3 + 2];
    }
    for (int i = 0; i < numFaces; i++) {
        int a, b, c, d;
        fin >> a >> b >> c >> d;    // a always equals to 3

        int v1 = i * 3;
        int v2 = i * 3 + 1;
        int v3 = i * 3 + 2;

        // position : vec3      [v*7, v*7+2]
        // normal : vec3        [v*7+3, v*7+5]
        // faceIndex : float    [v*7+6, v*7+6]

        sortedVertices[v1 * 7] = vertices[b * 3];
        sortedVertices[v1 * 7 + 1] = vertices[b * 3 + 1];
        sortedVertices[v1 * 7 + 2] = vertices[b * 3 + 2];

        sortedVertices[v2 * 7] = vertices[c * 3];
        sortedVertices[v2 * 7 + 1] = vertices[c * 3 + 1];
        sortedVertices[v2 * 7 + 2] = vertices[c * 3 + 2];

        sortedVertices[v3 * 7] = vertices[d * 3];
        sortedVertices[v3 * 7 + 1] = vertices[d * 3 + 1];
        sortedVertices[v3 * 7 + 2] = vertices[d * 3 + 2];

        float* normal = new float[3];
        calcNormal(&sortedVertices[v1 * 7], &sortedVertices[v2 * 7], &sortedVertices[v3 * 7], normal);

        sortedVertices[v1 * 7 + 3] = normal[0];
        sortedVertices[v1 * 7 + 4] = normal[1];
        sortedVertices[v1 * 7 + 5] = normal[2];
        sortedVertices[v1 * 7 + 6] = (float)i / numFaces;

        sortedVertices[v2 * 7 + 3] = normal[0];
        sortedVertices[v2 * 7 + 4] = normal[1];
        sortedVertices[v2 * 7 + 5] = normal[2];
        sortedVertices[v2 * 7 + 6] = (float)i / numFaces;

        sortedVertices[v3 * 7 + 3] = normal[0];
        sortedVertices[v3 * 7 + 4] = normal[1];
        sortedVertices[v3 * 7 + 5] = normal[2];
        sortedVertices[v3 * 7 + 6] = (float)i / numFaces;
    }
    fin.close();
  • 求面法線:求兩向量叉積,實現為化簡過後的過程。
void calcNormal(float* v1, float* v2, float* v3, float* nor) {
    float na = (v2[1] - v1[1]) * (v3[2] - v1[2]) - (v2[2] - v1[2]) * (v3[1] - v1[1]);
    float nb = (v2[2] - v1[2]) * (v3[0] - v1[0]) - (v2[0] - v1[0]) * (v3[2] - v1[2]);
    float nc = (v2[0] - v1[0]) * (v3[1] - v1[1]) - (v2[1] - v1[1]) * (v3[0] - v1[0]);

    nor[0] = na;
    nor[1] = nb;
    nor[2] = nc;
}

初始化Shader

  • 本實現中使用了三個Shader,分別用於繪製面索引,光照模型,高亮三角

    void Application::InitShader() {   
        mShaders["BlinnPhong"] = new Shader("BlinnPhong");
        mShaders["FaceIndex"] = new Shader("FaceIndex");
        mShaders["postprocess"] = new Shader("postprocess");
    }
    
  • 從檔案讀入Shader程式碼(這裡有個bug,在檔案尾會讀入幾個亂碼字元,這裡在shader最後手動加換行暫時刪掉了亂碼)

    Shader::Shader(const std::string& shaderName)
    {
        const std::string vertShaderName = shaderName + ".vert";
        const std::string fragShaderName = shaderName + ".frag";
        std::ifstream vertFile(vertShaderName);
        std::ifstream fragFile(fragShaderName);
        if (!vertFile || !fragFile) {
            assert(false);
        }
        vertFile.seekg(0, std::ios::end);
        int length = vertFile.tellg();
        GLchar* vertexShaderCode = new GLchar[length];
        vertFile.seekg(0, std::ios::beg);
        vertFile.read(vertexShaderCode, length);
        while (vertexShaderCode[length - 1] != '\n') {	//TODO
            vertexShaderCode[length - 1] = 0;
            length--;
        }
    
        fragFile.seekg(0, std::ios::end);
        length = fragFile.tellg();
        GLchar* fragmentShaderCode = new GLchar[length];
        fragFile.seekg(0, std::ios::beg);
        fragFile.read(fragmentShaderCode, length);
        while (fragmentShaderCode[length - 1] != '\n') { //TODO
            fragmentShaderCode[length - 1] = 0;
            length--;
        }
    
    
  • 編譯Shader程式碼

    GLuint VertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(VertexShader, 1, &vertexShaderCode, NULL);
    glCompileShader(VertexShader);

    GLuint FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(FragmentShader, 1, &fragmentShaderCode, NULL);
    glCompileShader(FragmentShader);


    mProgramID = glCreateProgram();
    glAttachShader(mProgramID, VertexShader);
    glAttachShader(mProgramID, FragmentShader);
    glLinkProgram(mProgramID);
    glUseProgram(mProgramID);

    GLint success;
    glGetProgramiv(mProgramID, GL_LINK_STATUS, &success);
    if (!success) {
        GLchar infoLog[1024];
        glGetProgramInfoLog(mProgramID, 512, NULL, infoLog);
        std::cout << infoLog << std::endl;
    }

    glDeleteShader(VertexShader);
    glDeleteShader(FragmentShader);
  • 傳入uniform 以及 繫結program
// Check location
#define CHECK_LOC(loc) \
    if(loc == -1){  \
        assert(false);\
    }

void Shader::SetMat(const std::string& name, const glm::mat4& mat)
{
    GLint loc = glGetUniformLocation(mProgramID, name.c_str());
    CHECK_LOC(loc);
    glUniformMatrix4fv(loc, 1, GL_FALSE, &mat[0][0]);
}

//setVec.......
//setFloat.....
//setInt.......

void Shader::Bind()
{
    glUseProgram(mProgramID);
}

初始化頂點緩衝

  • 主頂點緩衝,包含所有頂點資訊

    InitShader();

    glGenVertexArrays(1, &vao);	//Vertex Array Buffer
    glBindVertexArray(vao);

    GLuint vbo;
    glGenBuffers(1, &vbo);	//Vertex Buffer Object
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
	//Total size : Face Number * 3 * sizeof(Per Vertex) 
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * numFaces * 3 * 7, sortedVertices, GL_STATIC_DRAW); 

	//Set Per Vertex Data Format
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0);					//position
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float)));	//normal
    glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(6 * sizeof(float)));	//FaceIndex

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);
  • 高亮三角緩衝,僅三個點,使用Dynamic Draw宣告該資料會實時變化,令GPU對資料儲存進行優化
	glGenVertexArrays(1, &CoveredVAO);
    glBindVertexArray(CoveredVAO);

    glGenBuffers(1, &CoveredVBO);
    glBindBuffer(GL_ARRAY_BUFFER, CoveredVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 9, nullptr, GL_DYNAMIC_DRAW);	// A hint for GPU to optimize VBO

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

初始化FrameBuffers

  • 一個FrameBuffer表示一組Render Target(可以理解為繪製到的一張視窗大小的圖片)的集合,包含若干color attachment以及一個depth-stencil attachment,本例中為了繪製FaceIndex使用一個R32F的紋理作為RT繫結。
    glGenFramebuffers(1, &FaceIndexFBO);							// generate frame buffer
    glBindFramebuffer(GL_FRAMEBUFFER, FaceIndexFBO);				// bind frame buffer
    glGenTextures(1, &FaceIndexTex);								// generate the texture to storage FaceIndex
    glBindTexture(GL_TEXTURE_2D, FaceIndexTex);						// bind texture
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, 800, 600, 0, GL_RED, GL_FLOAT, NULL);	// Set texture Format to R32F
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, FaceIndexTex, 0);	// Bind as Color Attachment 0 of the FrameBuffer

主繪製流程

  • 處理滑鼠輸入
void Application::HandleMouse() {
    glfwGetCursorPos(window, &MouseX, &MouseY);		// Use GLFW API to get current Mouse Postion
    auto MouseDelta = glm::vec2(0, 0);				// Delta Vector between last and current
    if (LastMouseX >= 0 && LastMouseY >= 0) {		// if last pos is legal (in the window)
        MouseDelta = glm::vec2(MouseX - LastMouseX, MouseY - LastMouseY);
    }
    auto state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT);	// Get current left button state
    if (state == GLFW_PRESS) {	// if left buttton is pressed
        // rotate viewDir around vertical axis (Up)
        viewDir = glm::mat3(glm::rotate(MouseDelta.x * 0.0015f, viewUp)) * viewDir;
        // rotate viewDir around horizontal axis (ViewUp x ViewDir)
        viewDir = glm::mat3(glm::rotate(-MouseDelta.y * 0.003f, glm::cross(viewUp, viewDir))) * viewDir;

        LastMouseX = MouseX;
        LastMouseY = MouseY;
    }
    else {
        LastMouseX = -1.0f;
        LastMouseX = -1.0f;
    }
}

  • 繪製頂點索引

    void Application::Render() {
        HandleMouse();
        glEnable(GL_CULL_FACE);	// NOTICE : Must Enable Cull Face to ensure NOT rendering back face of the BUNNY !!!
        glEnable(GL_DEPTH_TEST);
        glBindVertexArray(vao);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
        glm::mat4 projection = glm::perspective((float)glm::radians(45.0), 800.0f / 600.0f, 0.1f, 100.0f);
        glm::mat4 model = glm::scale(glm::mat4(1.0f), glm::vec3(5.0f));
        glm::mat4 view = glm::lookAt(viewPos, viewPos + viewDir, viewUp);
    
        mShaders["FaceIndex"]->Bind();
        // set Uniforms
        mShaders["FaceIndex"]->SetMat("projection", projection);
        mShaders["FaceIndex"]->SetMat("model", model);
        mShaders["FaceIndex"]->SetMat("view", view);
    
        glBindFramebuffer(GL_FRAMEBUFFER, FaceIndexFBO);
    
        glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);
    
    
    • 頂點索引 Shader 程式碼

      //FaceIndex.vert
      
      #version 330 core
      layout(location = 0) in vec3 in_position;
      layout(location = 1) in vec3 in_normal;
      layout(location = 2) in float in_index;
      
      uniform mat4 model;
      uniform mat4 view;
      uniform mat4 projection;
      
      out vec3 thePosition;
      out vec3 theNormal;
      flat out float theIndex;	// NOTICE: Index should NOT be interpolated
      
      void main() {
          vec4 v = vec4(in_position,1.0);
          gl_Position = projection * view * model * v;
      
          thePosition = vec3(model * v);
          theNormal = normalize(vec3(model * vec4(in_normal,0)));
          theIndex = in_index;
      }
      
      
      //FaceIndex.frag
      
      #version 430
      
      out vec4 daColor;
      
      in vec3 thePosition;
      in vec3 theNormal;
      flat in float theIndex;
      
      void main()
      {
          daColor = vec4(theIndex,0,0,0);
      }
      
      
  • 回讀滑鼠位置的顏色(索引值)

     float res[4];
        GLint viewport[4];
        glGetIntegerv(GL_VIEWPORT, viewport);
        
        glReadPixels((GLint)MouseX, viewport[3] - MouseY, 1, 1, GL_RED, GL_FLOAT, &res);	
    	// y is flipped in OpenGL screen coordinate system
    	// res[0] is the face index picked by mouse
    
  • 繪製馮氏光照模型

        mShaders["BlinnPhong"]->Bind();
        mShaders["BlinnPhong"]->SetMat("projection", projection);
        mShaders["BlinnPhong"]->SetMat("model", model);
        mShaders["BlinnPhong"]->SetMat("view", view);
    
        mShaders["BlinnPhong"]->SetVec("CameraPosition", glm::vec3(0, 0, -20.0f));
        mShaders["BlinnPhong"]->SetVec("LightPosition", glm::vec3(-50.0f, 10.0f, -20.0f));
        glBindFramebuffer(GL_FRAMEBUFFER, 0);		// Bind the default framebuffer (render to screen)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);
    
    • 馮氏光照Shader

      //BlinnPhong.vert
      
      #version 330 core
      layout(location = 0) in vec3 in_position;
      layout(location = 1) in vec3 in_normal;
      layout(location = 2) in float in_index;
      
      uniform mat4 model;
      uniform mat4 view;
      uniform mat4 projection;
      
      out vec3 thePosition;
      out vec3 theNormal;
      
      void main() {
          vec4 v = vec4(in_position,1.0);
          gl_Position = projection * view * model * v;
      
          thePosition = vec3(model * v);
          theNormal = normalize(vec3(model * vec4(in_normal,0)));
      }
      
      
      #version 430
      
      out vec4 daColor;
      
      in vec3 thePosition;
      in vec3 theNormal;
      
      uniform vec3 CameraPosition;
      uniform vec3 LightPosition;
      
      void main()
      {
      	float finalBrightness = 0.0;
      
          float ambient = 0.05;
          //diiffuse
          vec3 lightDir = normalize(LightPosition - thePosition);
          float diff = max(dot(lightDir,theNormal),0.0);
              
          //specular
          vec3 viewDir = normalize(CameraPosition - thePosition);
          vec3 reflectDir = reflect(-lightDir,theNormal);
          float spec = 0.0;
          vec3 halfwayDir = normalize(lightDir + viewDir);
          spec = pow(max(dot(theNormal,halfwayDir),0.0),32.0);
      	float specular = 0.3 * spec;
          finalBrightness = ambient + diff + specular;
      	
      	vec3 f = vec3(finalBrightness);
      	vec3 test =  min(f,vec3(1.0));
      	daColor = vec4(test, 1.0);
      }
      
      
  • 繪製高亮三角形

    • 高亮面的編號為 res[0] * numFaces

          DrawCoverVertices(res[0] * numFaces, projection, model, view);
      
    • 獲取高亮面的三個點座標

      void Application::DrawCoverVertices(int index, glm::mat4& projection, glm::mat4& model, glm::mat4& view)
      {
          if (index < 0 || index >= numFaces) {
              return;
          }
          int a = index * 3 + 0;
          int b = index * 3 + 1;
          int c = index * 3 + 2;
          CoveredVertices[0] = sortedVertices[a * 7 + 0];
          CoveredVertices[1] = sortedVertices[a * 7 + 1];
          CoveredVertices[2] = sortedVertices[a * 7 + 2];
      
          CoveredVertices[3] = sortedVertices[b * 7 + 0];
          CoveredVertices[4] = sortedVertices[b * 7 + 1];
          CoveredVertices[5] = sortedVertices[b * 7 + 2];
      
          CoveredVertices[6] = sortedVertices[c * 7 + 0];
          CoveredVertices[7] = sortedVertices[c * 7 + 1];
          CoveredVertices[8] = sortedVertices[c * 7 + 2];
      
    • 繪製高亮面 (關閉depth test 從而與之前的有光照的兔兔疊加)

          glDisable(GL_DEPTH_TEST);	// NOTICE: Disable depth test to cover last result
          glDisable(GL_CULL_FACE);
          glBindVertexArray(CoveredVAO);
          glBufferSubData(GL_ARRAY_BUFFER, 0, 9 * sizeof(float), CoveredVertices);
      
          mShaders["postprocess"]->Bind();
          mShaders["postprocess"]->SetMat("projection", projection);
          mShaders["postprocess"]->SetMat("model", model);
          mShaders["postprocess"]->SetMat("view", view);
          glBindVertexArray(CoveredVAO);
          glDrawArrays(GL_TRIANGLES, 0, 9);
      
      • 繪製高亮面Shader

        //postprocess.vert
        #version 330 core
        layout(location = 0) in vec3 in_position;
        
        uniform mat4 model;
        uniform mat4 view;
        uniform mat4 projection;
        
        void main() {
            vec4 v = vec4(in_position,1.0);
            gl_Position = projection * view * model * v;
        }
        
        
        //postprocess.frag
        #version 430
        
        out vec4 daColor;
        
        void main()
        {
            daColor = vec4(1.0,0,0,1.0);
        }
        
        
  • 加一個簡單延遲,鎖定在三十幀左右(也可以直接用glfw的垂直同步)

    DrawCoverVertices(res[0] * numFaces, projection, model, view);
    Sleep(20);
}
  • 附上一個Application與Shader的宣告
class Application {
public:
	void Run();

private:
	void InitShader();
	void Init();
	void Render();
	void HandleMouse();
	void DrawCoverVertices(int index, glm::mat4& projection, glm::mat4& model, glm::mat4& view);
private:
	using ShaderLibrary = std::unordered_map<std::string,Shader*>;
	ShaderLibrary mShaders;

	float* vertices;
	float* sortedVertices;
	int numFaces;
	int numVertices;
	GLuint vao;

	GLuint FaceIndexTex;
	GLuint FaceIndexFBO;

	double MouseX, MouseY;
	double LastMouseX, LastMouseY;
	GLFWwindow* window;

	float CoveredVertices[9];
	GLuint CoveredVAO;
	GLuint CoveredVBO;

	glm::vec3 viewDir = glm::vec3(0.0f, 0.0f, 20.0f);
	glm::vec3 viewPos = glm::vec3(0.0f, 0.0f, -3.0f);
	glm::vec3 viewUp = glm::vec3(0.0f, 1.0f, 0.0f);

	float CoverVertices[9];
};
class Shader {
public:
	Shader(const std::string& shaderName);

	GLuint GetProgramID() const { return mProgramID; }
	void SetMat(const std::string& name, const glm::mat4& mat);
	void SetMat(const std::string& name, const glm::mat3& mat);
	void SetVec(const std::string& name, const glm::vec3& vec);
	void SetVec(const std::string& name, const glm::vec4& vec);
	void SetInt(const std::string& name, int val);
	void SetFloat(const std::string& name, float val);
	void Bind();
private:
	GLuint mProgramID;
};

實現效果

在這裡插入圖片描述

在這裡插入圖片描述

碎碎念

主要難點的地方還是實現拾取,這裡用了三個drawcall顯然應該是多餘的,可優化的地方還很多

給別人講圖形學是真的非常難,概念冗雜不說,還一環套一環,必須全部搞懂才能實現一個非常簡單的東西

相關文章