OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing

版本記錄

版本號 時間
V1.0 2018.01.11

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試

模板測試

當片段著色器處理完一個片段之后,模板測試(Stencil Test)會開始執行,和深度測試一樣,它也可能會丟棄片段。接下來,被保留的片段會進入深度測試,它可能會丟棄更多的片段。深度測試是根據又一個緩沖來進行的,它叫做模板緩沖(Stencil Buffer),我們可以在渲染的時候更新它來獲得一些很有意思的效果。

一個模板緩沖中,(通常)每個模板值(Stencil Value)是8位的。所以每個像素/片段一共能有256種不同的模板值。我們可以將這些模板值設置為我們想要的值,然后當某一個片段有某一個模板值的時候,我們就可以選擇丟棄或是保留這個片段了。

每個窗口庫都需要為你配置一個模板緩沖。GLFW自動做了這件事,所以我們不需要告訴GLFW來創建一個,但其它的窗口庫可能不會默認給你創建一個模板庫,所以記得要查看庫的文檔。

模板緩沖的一個簡單的例子如下:

模板緩沖首先會被清除為0,之后在模板緩沖中使用1填充了一個空心矩形。場景中的片段將會只在片段的模板值為1的時候會被渲染(其它的都被丟棄了)。

模板緩沖操作允許我們在渲染片段時將模板緩沖設定為一個特定的值。通過在渲染時修改模板緩沖的內容,我們寫入了模板緩沖。在同一個(或者接下來的)渲染迭代中,我們可以讀取這些值,來決定丟棄還是保留某個片段。使用模板緩沖的時候你可以盡情發揮,但大體的步驟如下:

  • 啟用模板緩沖的寫入。
  • 渲染物體,更新模板緩沖的內容。
  • 禁用模板緩沖的寫入。
  • 渲染(其它)物體,這次根據模板緩沖的內容丟棄特定的片段。

所以,通過使用模板緩沖,我們可以根據場景中已繪制的其它物體的片段,來決定是否丟棄特定的片段。

你可以啟用GL_STENCIL_TEST來啟用模板測試。在這一行代碼之后,所有的渲染調用都會以某種方式影響著模板緩沖。

glEnable(GL_STENCIL_TEST);

注意,和顏色和深度緩沖一樣,你也需要在每次迭代之前清除模板緩沖。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

和深度測試的glDepthMask函數一樣,模板緩沖也有一個類似的函數。glStencilMask允許我們設置一個位掩碼(Bitmask),它會與將要寫入緩沖的模板值進行與(AND)運算。默認情況下設置的位掩碼所有位都為1,不影響輸出,但如果我們將它設置為0x00,寫入緩沖的所有模板值最后都會變成0.這與深度測試中的glDepthMask(GL_FALSE)是等價的。

glStencilMask(0xFF); // 每一位寫入模板緩沖時都保持原樣
glStencilMask(0x00); // 每一位在寫入模板緩沖時都會變成0(禁用寫入)

大部分情況下你都只會使用0x00或者0xFF作為模板掩碼(Stencil Mask),但是知道有選項可以設置自定義的位掩碼總是好的。


模板函數

和深度測試一樣,我們對模板緩沖應該通過還是失敗,以及它應該如何影響模板緩沖,也是有一定控制的。一共有兩個函數能夠用來配置模板測試:glStencilFuncglStencilOp

glStencilFunc(GLenum func, GLint ref, GLuint mask)一共包含三個參數:

  • func:設置模板測試函數(Stencil Test Function)。這個測試函數將會應用到已儲存的模板值上和glStencilFunc函數的ref值上。可用的選項有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它們的語義和深度緩沖的函數類似。
  • ref:設置了模板測試的參考值(Reference Value)。模板緩沖的內容將會與這個值進行比較。
  • mask:設置一個掩碼,它將會與參考值和儲存的模板值在測試比較它們之前進行與(AND)運算。初始情況下所有位都為1。

在一開始的那個簡單的模板例子中,函數被設置為:

glStencilFunc(GL_EQUAL, 1, 0xFF)

這會告訴OpenGL,只要一個片段的模板值等于(GL_EQUAL)參考值1,片段將會通過測試并被繪制,否則會被丟棄。

但是glStencilFunc僅僅描述了OpenGL應該對模板緩沖內容做什么,而不是我們應該如何更新緩沖。這就需要glStencilOp這個函數了。

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三個選項,我們能夠設定每個選項應該采取的行為:

  • sfail:模板測試失敗時采取的行為。
  • dpfail:模板測試通過,但深度測試失敗時采取的行為。
  • dppass:模板測試和深度測試都通過時采取的行為。

每個選項都可以選用以下的其中一種行為:

默認情況下glStencilOp是設置為(GL_KEEP, GL_KEEP, GL_KEEP)的,所以不論任何測試的結果是如何,模板緩沖都會保留它的值。默認的行為不會更新模板緩沖,所以如果你想寫入模板緩沖的話,你需要至少對其中一個選項設置不同的值。

所以,通過使用glStencilFuncglStencilOp,我們可以精確地指定更新模板緩沖的時機與行為了,我們也可以指定什么時候該讓模板緩沖通過,即什么時候片段需要被丟棄。


物理輪廓

僅僅看了前面的部分你還是不太可能能夠完全理解模板測試的工作原理,所以我們將會展示一個使用模板測試就可以完成的有用特性,它叫做物體輪廓(Object Outlining)

物體輪廓所能做的事情正如它名字所描述的那樣。我們將會為每個(或者一個)物體在它的周圍創建一個很小的有色邊框。當你想要在策略游戲中選中一個單位進行操作的,想要告訴玩家選中的是哪個單位的時候,這個效果就非常有用了。為物體創建輪廓的步驟如下:

    1. 在繪制(需要添加輪廓的)物體之前,將模板函數設置為GL_ALWAYS,每當物體的片段被渲染時,將模板緩沖更新為1。
    1. 渲染物體。
    1. 禁用模板寫入以及深度測試。
    1. 將每個物體縮放一點點。
    1. 使用一個不同的片段著色器,輸出一個單獨的(邊框)顏色。
    1. 再次繪制物體,但只在它們片段的模板值不等于1時才繪制。
    1. 再次啟用模板寫入和深度測試。

這個過程將每個物體的片段處的深度緩沖設置為1,當我們想要繪制邊框的時候,我們基本上只是在繪制一個放大版本的物體,并且每當模板測試通過的時候,也就是物體的邊框的位置,放大的版本將會被繪制。我們基本上使用模板緩沖丟棄了放大版本物體中,位于原本物體片段處的,所有片段。

所以我們首先來創建一個很簡單的片段著色器,它會輸出一個邊框顏色。我們簡單地給它設置一個硬編碼的顏色值,將這個著色器命名為shaderSingleColor

void main()
{
    FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}

我們只想給那兩個箱子加上邊框,所以我們讓地板不參與這個過程。我們希望首先繪制地板,再繪制兩個箱子(并寫入模板緩沖),之后繪制放大的箱子(并丟棄覆蓋了之前繪制的箱子片段的那些片段)。

我們首先啟用模板測試,并設置測試通過或失敗時的行為:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

如果其中的一個測試失敗了,我們什么都不做,我們僅僅保留當前儲存在模板緩沖中的值。如果模板測試和深度測試都通過了,那么我們希望將儲存的模板值設置為參考值,參考值能夠通過glStencilFunc來設置,我們之后會設置為1。

我們將模板緩沖清除為0,對箱子中所有繪制的片段,將模板值更新為1:

glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都應該更新模板緩沖
glStencilMask(0xFF); // 啟用模板緩沖寫入
normalShader.use();
DrawTwoContainers();

通過使用GL_ALWAYS模板測試函數,我們保證了箱子的每個片段都會將模板緩沖的模板值更新為1。因為片段永遠會通過模板測試,在繪制片段的地方,模板緩沖會被更新為參考值。

現在模板緩沖在箱子被繪制的地方都更新為1了,我們將要繪制放大的箱子,但這次要禁用模板緩沖的寫入:

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止模板緩沖的寫入
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();

我們將模板函數設置為GL_NOTEQUAL,它會保證我們只繪制箱子上模板值不為1的部分,即只繪制箱子在之前繪制的箱子之外的部分。注意我們也禁用了深度測試,讓放大的箱子,即邊框,不會被地板所覆蓋。

記得要在完成之后重新啟用深度緩沖。

場景中物體輪廓的完整步驟會看起來像這樣:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

glStencilMask(0x00); // 記得保證我們在繪制地板的時候不會更新模板緩沖
normalShader.use();
DrawFloor()  

glStencilFunc(GL_ALWAYS, 1, 0xFF); 
glStencilMask(0xFF); 
DrawTwoContainers();

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); 
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);  

只要你理解了模板緩沖背后的大體思路,這個代碼片段就不是那么難理解了。如果還是不能理解的話,嘗試再次仔細閱讀之前的部分,并嘗試通過上面使用的范例,完全理解每個函數的功能。

深度測試小節的場景中,這個輪廓算法的結果看起來會像是這樣的:

可以在這里查看源代碼,看看物體輪廓算法的完整代碼。

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <learnopengl/shader_m.h>
#include <learnopengl/camera.h>
#include <learnopengl/model.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
unsigned int loadTexture(const char *path);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", 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);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glEnable(GL_STENCIL_TEST);
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

    // build and compile shaders
    // -------------------------
    Shader shader("2.stencil_testing.vs", "2.stencil_testing.fs");
    Shader shaderSingleColor("2.stencil_testing.vs", "2.stencil_single_color.fs");

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float cubeVertices[] = {
        // positions          // texture Coords
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    float planeVertices[] = {
        // positions          // texture Coords (note we set these higher than 1 (together with GL_REPEAT as texture wrapping mode). this will cause the floor texture to repeat)
         5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
        -5.0f, -0.5f,  5.0f,  0.0f, 0.0f,
        -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,

         5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
        -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,
         5.0f, -0.5f, -5.0f,  2.0f, 2.0f
    };
    // cube VAO
    unsigned int cubeVAO, cubeVBO;
    glGenVertexArrays(1, &cubeVAO);
    glGenBuffers(1, &cubeVBO);
    glBindVertexArray(cubeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glBindVertexArray(0);
    // plane VAO
    unsigned int planeVAO, planeVBO;
    glGenVertexArrays(1, &planeVAO);
    glGenBuffers(1, &planeVBO);
    glBindVertexArray(planeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), &planeVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glBindVertexArray(0);

    // load textures
    // -------------
    unsigned int cubeTexture = loadTexture(FileSystem::getPath("resources/textures/marble.jpg").c_str());
    unsigned int floorTexture = loadTexture(FileSystem::getPath("resources/textures/metal.png").c_str());

    // shader configuration
    // --------------------
    shader.use();
    shader.setInt("texture1", 0);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // don't forget to clear the stencil buffer!

        // set uniforms
        shaderSingleColor.use();
        glm::mat4 model;
        glm::mat4 view = camera.GetViewMatrix();
        glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        shaderSingleColor.setMat4("view", view);
        shaderSingleColor.setMat4("projection", projection);

        shader.use();
        shader.setMat4("view", view);
        shader.setMat4("projection", projection);

        // draw floor as normal, but don't write the floor to the stencil buffer, we only care about the containers. We set its mask to 0x00 to not write to the stencil buffer.
        glStencilMask(0x00);
        // floor
        glBindVertexArray(planeVAO);
        glBindTexture(GL_TEXTURE_2D, floorTexture);
        shader.setMat4("model", glm::mat4());
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);

        // 1st. render pass, draw objects as normal, writing to the stencil buffer
        // --------------------------------------------------------------------
        glStencilFunc(GL_ALWAYS, 1, 0xFF);
        glStencilMask(0xFF);
        // cubes
        glBindVertexArray(cubeVAO);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, cubeTexture);
        model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        model = glm::mat4();
        model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        // 2nd. render pass: now draw slightly scaled versions of the objects, this time disabling stencil writing.
        // Because the stencil buffer is now filled with several 1s. The parts of the buffer that are 1 are not drawn, thus only drawing 
        // the objects' size differences, making it look like borders.
        // -----------------------------------------------------------------------------------------------------------------------------
        glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
        glStencilMask(0x00);
        glDisable(GL_DEPTH_TEST);
        shaderSingleColor.use();
        float scale = 1.1;
        // cubes
        glBindVertexArray(cubeVAO);
        glBindTexture(GL_TEXTURE_2D, cubeTexture);
        model = glm::mat4();
        model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
        model = glm::scale(model, glm::vec3(scale, scale, scale));
        shaderSingleColor.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        model = glm::mat4();
        model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
        model = glm::scale(model, glm::vec3(scale, scale, scale));
        shaderSingleColor.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);
        glStencilMask(0xFF);
        glEnable(GL_DEPTH_TEST);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &cubeVAO);
    glDeleteVertexArrays(1, &planeVAO);
    glDeleteBuffers(1, &cubeVBO);
    glDeleteBuffers(1, &planeVBO);

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

// utility function for loading a 2D texture from file
// ---------------------------------------------------
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

你可以看到這兩個箱子的邊框重合了,這通常都是我們想要的結果(想想策略游戲中,我們希望選擇10個單位,合并邊框通常是我們想需要的結果)。如果你想讓每個物體都有一個完整的邊框,你需要對每個物體都清空模板緩沖,并有創意地利用深度緩沖。

你看到的物體輪廓算法在需要顯示選中物體的游戲(想想策略游戲)中非常常見。這樣的算法能夠在一個模型類中輕松實現。你可以在模型類中設置一個boolean標記,來設置需不需要繪制邊框。如果你有創造力的話,你也可以使用后期處理濾鏡(Filter),像是高斯模糊(Gaussian Blur),讓邊框看起來更自然。

除了物體輪廓之外,模板測試還有很多用途,比如在一個后視鏡中繪制紋理,讓它能夠繪制到鏡子形狀中,或者使用一個叫做陰影體積(Shadow Volume)的模板緩沖技術渲染實時陰影。模板緩沖為我們已經很豐富的OpenGL工具箱又提供了一個很好的工具。

后記

未完,待續~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容