OpenGL學習歷程

說明

  • OpenGL學習中文網
  • 學習OpenGL3.3核心模式(因爲早期的立即渲染模式,也稱固定渲染管線,這個模式雖然繪圖方便,但靈活性不強,OpenGL3.2開始,規範文檔開始廢棄立即渲染模式,並鼓勵開發者在OpenGL的核心模式(Core-profile)下進行開發,爲什麼要使用3.3版本,因爲所有OpenGL的更高的版本都是在3.3的基礎上,引入了額外的功能,並沒有改動核心架構)
  • 開發環境Win10,VS2017

OpenGL開發環境搭建

GLFW配置

  • GLFW是一個專門針對OpenGL的C語言庫,它提供了一些渲染物體所需的最低限度的接口。它允許用戶創建OpenGL上下文,定義窗口參數以及處理用戶輸入
  • GLFW源碼包下載,請下載32位的版本,64位版本大部分人反映有BUG。
  • CMake生成項目,並使用VistualStudio編譯之後在src/Debug文件夾內可找到glfw3.lib
    在這裏插入圖片描述
  • 創建文件夾ThirdParty/glfw3存放include和lib
  • ThirdParty
    • opengl
      • include
        • GLFW
          • glfw3.h
          • glfw3native.h
      • lib
        • glfw3.lib
  • 在項目引用頭文件的方法
#include <GLFW\glfw3.h>

GLAD配置

  • GLAD是一個開源的庫,GLAD使用了一個在線服務
  • 選擇對應選項後,點擊生成(Generate)按鈕來生成庫文件。
    在這裏插入圖片描述
  • GLAD現在應該提供給你了一個zip壓縮文件,包含兩個頭文件目錄,和一個glad.c文件。將兩個頭文件目錄(glad和KHR)複製到你的include文件夾中,並添加glad.c文件到你的工程中。
    在這裏插入圖片描述
  • 在項目引用頭文件的方法
#include <glad/glad.h>

VS項目引入第三方庫

  • 創建VC++控制檯項目
  • 拷貝glad.c文件到項目中
  • 項目設置中添加包含目錄:ThirdParty/opengl/include
  • 添加庫目錄:ThirdParty/opengl/lib
  • 添加庫:opengl32.lib;glfw3.lib;

知識理論

圖形渲染管線(Graphics Pipeline)

  • 圖形渲染管線,指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程。
  • 圖形渲染管線可以被劃分爲兩個主要部分:第一部分把你的3D座標轉換爲2D座標,第二部分是把2D座標轉變爲實際的有顏色的像素。
  • 圖形渲染管線階段
    1. 頂點着色器(Vertex Shader)
    它把一個單獨的頂點作爲輸入。頂點着色器主要的目的是把3D座標轉爲另一種3D座標,同時頂點着色器允許我們對頂點屬性進行一些基本處理。
    2. 圖元裝配(Primitive Assembly)
    頂點着色器輸出的所有頂點作爲輸入(如果是GL_POINTS,那麼就是一個頂點),並所有的點裝配成指定圖元的形狀。
    3. 幾何着色器(Geometry Shader)
    幾何着色器把圖元形式的一系列頂點的集合作爲輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。
    4. 光柵化階段(Rasterization Stage)
    這裏它會把圖元映射爲最終屏幕上相應的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
    OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據。
    下圖是圖形渲染管線的每個階段的抽象展示。
    要注意藍色部分代表的是我們可以注入自定義的着色器的部分
    

在這裏插入圖片描述

關鍵詞

  • 頂點數組對象:Vertex Array Object,VAO
  • 頂點緩衝對象:Vertex Buffer Object,VBO
  • 索引緩衝對象:Element Buffer Object,EBO或Index Buffer Object,IBO

GLSL(OpenGL着色器語言)

  • 着色器是使用一種叫GLSL的類C語言寫成的。GLSL是爲圖形計算量身定製的,它包含一些針對向量和矩陣操作的有用特性。
  • 典型的着色器結構
    #version version_number
    in type in_variable_name;
    in type in_variable_name;
    
    out type out_variable_name;
    
    uniform type uniform_name;
    
    int main()
    {
      // 處理輸入並進行一些圖形操作
      ...
      // 輸出處理過的結果到輸出變量
      out_variable_name = weird_stuff_we_processed;
    }
    
  • 默認基礎數據類型
    GLSL中包含C等其它語言大部分的默認基礎數據類型:int、float、double、uint和bool。GLSL也有兩種容器類型,它們會在這個教程中使用很多,分別是向量(Vector)和矩陣(Matrix)
  • 着色器的輸入與輸出(數據傳遞)
  1. in 和 out
  2. uniform

紋理(Texture)

  • 紋理是一個2D圖片(甚至也有1D和3D的紋理)
  • 紋理座標(Texture Coordinate),爲了能夠把紋理映射到三角形上,三角形每個頂點關聯着一個紋理座標,用來標明該從紋理圖像的哪個部分採樣(採集片段顏色),紋理座標在x和y軸上,範圍爲0到1之間(2D紋理圖像),使用紋理座標獲取紋理顏色叫做採樣(Sampling)。下面的圖片展示了我們是如何把紋理座標映射到三角形上的。在這裏插入圖片描述
    float texCoords[] = {
        0.0f, 0.0f, // 左下角
        1.0f, 0.0f, // 右下角
        0.5f, 1.0f // 上中
    };
    
  • 紋理環繞方式
    | 環繞方式 | 描述 |
    | -------- | ------------- |
    | GL_REPEAT | 對紋理的默認行爲。重複紋理圖像。 |
    | GL_MIRRORED_REPEAT | 和GL_REPEAT一樣,但每次重複圖片是鏡像放置的。 |
    | GL_CLAMP_TO_EDGE | 紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果。 |
    | GL_CLAMP_TO_BORDER | 超出的座標爲用戶指定的邊緣顏色。 |
    在這裏插入圖片描述
    // 可以使用glTexParameter*函數對單獨的一個座標軸設置(紋理座標軸s、t、r等價於x、y、z)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
    
    //選擇GL_CLAMP_TO_BORDER選項,我們還需要指定一個邊緣的顏色。
    float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    
  • 紋理過濾(Texture Filtering)
    紋理過濾有很多個選項,但是現在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。
  1. GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認的紋理過濾方式。當設置爲GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個像素。下圖中你可以看到四個像素,加號代表紋理座標。左上角那個紋理像素的中心距離紋理座標最近,所以它會被選擇爲樣本顏色:
    在這裏插入圖片描述
  2. GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理座標越近,那麼這個紋理像素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近像素的混合色:
    在這裏插入圖片描述
    那麼這兩種紋理過濾方式有怎樣的視覺效果呢?
    在這裏插入圖片描述
    //我們需要使用glTexParameter*函數爲放大和縮小指定過濾方式。
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
  • 多級漸遠紋理(Mipmap)
    簡單來說就是一系列的紋理圖像,後一個紋理圖像是前一個的二分之一。多級漸遠紋理背後的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,即最適合物體的距離的那個。
    在這裏插入圖片描述
    手工爲每個紋理圖像創建一系列多級漸遠紋理很麻煩,幸好OpenGL有一個glGenerateMipmaps函數,在創建完一個紋理後調用它OpenGL就會承擔接下來的所有工作了。
  • 加載與創建紋理
    紋理圖像可能被儲存爲各種各樣的格式,每種都有自己的數據結構和排列,所以我們如何才能把這些圖像加載到應用中呢?自己寫一個圖像加載器,把圖像轉化爲字節序列很麻煩,用另一種解決方案stb_image.h庫,stb_image下載
    下載這一個頭文件,將它以stb_image.h的名字加入你的工程,並另創建一個新的C++文件,輸入以下代碼:
    #define STB_IMAGE_IMPLEMENTATION
    #include "stb_image.h"
    

變換

  • 向量
  1. 向量與標量運算
  2. 向量取反
  3. 向量加減
  4. 長度
  5. 向量相乘
    • 點乘(Dot Product),記作v¯⋅k¯
      兩個向量的點乘等於它們的數乘結果乘以兩個向量之間夾角的餘弦值。
      公式:v¯⋅k¯=||v¯||⋅||k¯||⋅cosθ
      乘是通過將對應分量逐個相乘,然後再把所得積相加來計算的。兩個單位向量點乘會像是這樣:在這裏插入圖片描述
      要計算兩個單位向量間的夾角,我們可以使用反餘弦函數cos−1 ,可得結果是143.1度。
    • 另一個是叉乘(Cross Product),記作v¯×k¯。
      叉乘只在3D空間中有定義,它需要兩個不平行向量作爲輸入,生成一個正交於兩個輸入向量的第三個向量。下面的圖片展示了3D空間中叉乘的樣子:
      在這裏插入圖片描述
      兩個正交向量A和B叉積:
      在這裏插入圖片描述
  • 矩陣
  1. 矩陣加減
  2. 矩陣數乘
  3. 矩陣相乘
    • 只有當左側矩陣的列數與右側矩陣的行數相等,兩個矩陣才能相乘。
    • 矩陣相乘不遵守交換律(Commutative),也就是說A⋅B≠B⋅A。
      在這裏插入圖片描述
  4. 單位矩陣
    單位矩陣是一個除了對角線以外都是0的N×N矩陣。
    在這裏插入圖片描述
  5. 縮放
    如果我們把縮放變量表示爲(S1,S2,S3)我們可以爲任意向量(x,y,z)定義一個縮放矩陣:
    在這裏插入圖片描述
  6. 位移
    如果我們把位移向量表示爲(Tx,Ty,Tz),我們就能把位移矩陣定義爲:
    在這裏插入圖片描述
  7. 旋轉
    2D或3D空間中的旋轉用角(Angle)來表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。
    //大多數旋轉函數需要用弧度制的角,但幸運的是角度制的角也可以很容易地轉化爲弧度制的:
    弧度轉角度:角度 = 弧度 * (180.0f / PI)
    角度轉弧度:弧度 = 角度 * (PI / 180.0f)
    
    旋轉矩陣在3D空間中每個單位軸都有不同定義,旋轉角度用θ表示:
    沿x軸旋轉:
    在這裏插入圖片描述
    沿y軸旋轉:
    在這裏插入圖片描述
    沿z軸旋轉:
    在這裏插入圖片描述
  8. 矩陣的組合
    假設我們有一個頂點(x, y, z),我們希望將其縮放2倍,然後位移(1, 2, 3)個單位。我們需要一個位移和縮放矩陣來完成這些變換。
    在這裏插入圖片描述
    建議您在組合矩陣時,先進行縮放操作,然後是旋轉,最後纔是位移。比如,如果你先位移再縮放,位移的向量也會同樣被縮放。
    最終的變換矩陣左乘我們的向量會得到以下結果:
    在這裏插入圖片描述
    不錯!向量先縮放2倍,然後位移了(1, 2, 3)個單位。
  • GLM
    GLM是OpenGL Mathematics的縮寫,它是一個只有頭文件的庫,不用鏈接和編譯。
    GLM下載
    //我們需要的GLM的大多數功能都可以從下面這3個頭文件中找到:
    #include <glm/glm.hpp>
    #include <glm/gtc/matrix_transform.hpp>
    #include <glm/gtc/type_ptr.hpp>
    

項目案例

創建窗口

#include <iostream>

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

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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);

	// 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);

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

	// render loop
	while (!glfwWindowShouldClose(window))
	{
		//input
		processInput(window);

		//render
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	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);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章