首先在OpenGL當中,所有東西都是在3D空間中,然而螢幕卻是個存取pixel的2D陣列。所以OpenGL主要的工作就是經過一系列的操作把3D的坐標系轉換成2D的坐標系。而這操作稱作「圖形渲染管線(Graphics Render Pipeline)」,圖形數據經過一個pipeline,中間經過各種轉換,最終輸出到螢幕上。
輸入 Vertex Data
Vertex Shader 頂點著色器
Shape Assembly 圖元裝配
Geometry Shader
Rasterization 光柵化
裁切(Clipping)
Fragment Shader 片段著色器
Test and Belending 測試與混合
在現代OpenGL中,我們必須定義至少一個Vertex Sahder
和一個Fragment Shader
(因為GPU中沒有預設的頂點/片段著色器)
在繪製圖形之前,必須給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x, y, z)。
OpenGL 只會處理 3D 座標在值在 $[-1.0, 1.0]$ 的座標,稱作 標準化設備座標 Normalized Device Coordinates (NDC),只有在此座標內的頂點最終才會顯示在螢幕上。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
我們將它頂點的z坐標設置為0.0。這樣子的話三角形每一點的深度(Depth)都是一樣的,從而使它看上去像是2D的。
有了頂點資料後,接著要把這些頂點資料放到「顯示記憶體」中,交給 Vertex Shader 處理。可以透過 Vertex Buffer Object (VBO) 來管理。
GL_ARRAY_BUFFER
表示產生、綁定、傳送資料
VBO
就跟其他OpenGL物件一樣,他需要獨一無二的ID
。
uint32_t VBO;
glGenBuffer(1, &VBO);
OpenGL
有很多種Buffer Object
,VBO
屬於GL_ARRAY_BUFFER
。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER
目標上的)Buffer
調用都會用來配置當前綁定的VBO
。然後我們可以調用glBufferData
函數,它會把之前定義的頂點數據複製到Buffer
的內存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertives), vertices, GL_STATIC_DRAW);
glBufferData
的一些參數說明:
glBufferData(GLenum target, GLsizeptr size, const GLvoid *data, GLenum usage)
target
: 目前緩衝物件綁定到的目標size
: 給定緩衝物件的大小data
: 要存入資料的 pointerusage
: 設定存入資料的使用方式Usage | 描述 |
---|---|
STATIC |
資料只被設定一次,但會被使用很多次 |
DYNAMIC |
資料被改變很多次,也被使用很多次 |
STREAM |
資料每次繪製都會改變 |
使用現在 OpenGL 至少需要一個以上的 Vertex Shader,以下是 Vertex Shader 的一個例子
#version 330 core // version
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
in
指得是輸入資料vec[1,4]
指得是向量,有 .x
, .y
, .z
, .w
這幾個 float
分量layout(location = 0
設定了這個變數的 index,在把資料傳入 shader (GPU)時會用到我們已經寫好了一個 Shader 的 code 了(通常儲存在字元陣列中),
為了讓 OpenGL 使用他,我們需要在 run-time 時編譯他。
我們要根據 type 創建 Shader 並拿到 shader id
uint32_t vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER)
之後把原始碼綁訂到該 Shader 上,然後編譯他
lShaderSource(vertexShader, 1, &vertexShader, nullptr);
glCompileShader(vertexShader);
如果要查看編譯的狀況要加入以下原始碼
int success;
char log[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(shader, 512, nullptr, log);
printf("Error Shader %s compile error\n%s\n",
type == GL_VERTEX_SHADER ? "Vertex" : "Fragment", log);
}
Fragment Shader 所做的是計算像素最後的顏色輸出
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
out
指定變數為輸出
FragColor
對應到的分量分別是 R, G, B, A
編譯跟 Vertex Shader 一樣
GL_FRAGMENT_SHADER
uint32_t fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
我們要把編譯好的 Shader Link 成一個 Shader Program Object
當我們渲染時啟用該 Shader Program,之後的渲染指令便會去使用該 Shader Program
首先,先建立 Shader Program
uint32_t program;
program = glCreateProgram();
接著將 Shader Attach 到 Program上
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
Link的狀況一樣可以檢查
glGetProgramiv(program, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(program, 512, nullptr, log);
printf("Error Shader Linking error\n%s\n", log);
}
接著我們可以啟用(激活)此 Program
glUseProgram(program);
到了這裡,我們已經把頂點資料存在 GPU 中,而且也指定了要怎麼處理這些資料(Shader),但是 OpenGL 還不知道要如何解析傳入的資料,以及該怎麼將頂點資料連接到 Shader 的參數上,我們指定給 OpenGL。
由於 OpenGL 沒有規定傳入頂點資料的格式,這意味著我們可以自己決定,但也必須要我們手動指定給 OpenGL。
根據我們上面訂出的頂點陣列 vertices[]
,有底下幾種屬性是必須告訴 OpenGL 的:
float
大小是 sizeof(float)
float
資料,分別是 x, y, z可以使用 glVertexAttribPointer
將頂點資料的資訊告訴 OpenGL 它該怎麼解析這些頂點資料:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer()
void glVertexAttribPointer(
GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);
所以到了這裡,我們已經有能力繪製東西在螢幕上了,你的 code 可能會長這樣:
// 建立 VBO 複製頂點資料
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 設定頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 使用 shader program
glUseProgram(shaderProgram);
// 畫東西
someOpenGLFunctionThatDrawsOurTriangle();
也許畫小東西看起來不多,但如果頂點屬性(Vertex Attribute)一多,或是有很多物體呢?
設定頂點屬性就會很麻煩,因此有 Vertex Array Object (VAO) 來將這些狀態都儲存起來,並可以透過綁定此物件來快速設定頂點屬性。
如果沒有綁定 VAO 則 OpenGL 可能不會畫出任何東西
頂點陣列物件 Vertex Array Object (VAO),就像 VAO 或是其他 OpenGL 的東西一樣可以被綁定,綁定後的任何 Vertex Attribute 設定都會儲存在此 VAO 中。這讓設定 Vertex Attribute 變得只要綁定不同的 VAO 就好,繁雜的 Vertex Attribute 就只要設定一次就好。
一個 VAO 會儲存以下狀態
glEnableVertexAttribArray()
/glDisableVertexAttribArray()
glVertexAttribPointer
設定的頂點屬性建立
uint32_t vao;
glGenVertexArrays(1, &vao);
綁定
glBindVertexArray(vao);
綁定 VAO 後,接著綁定與設定 VBO 的頂點屬性,之後解綁 VAO ,等到要繪製時再綁定 VAO 就好。
有了 VAO 後,整個流程看起來是這樣:
// 建立並綁定 VAO
glGenVertexArray(1, &VAO);
glBindVertexArray(VAO);
// 建立 VBO 複製頂點資料
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 設定頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// ...
// 繪製
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
一般當你打算繪製多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然後儲存它們供後面使用。當我們打算繪製物體的時候就拿出相應的VAO,綁定它,繪製完物體後,再解綁VAO。
要想繪製我們想要的物體,OpenGL給我們提供了glDrawArrays函數,它使用當前啟用(激活)的著色器,之前定義的頂點屬性配置,和VBO的頂點數據(通過VAO間接綁定)來繪製。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3); // 三角形,從0開始,畫3個
現在從上到下串連起來並執行你應該會看到
#include <iostream>
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
#include <glad/glad.h>
#include <SFML/OpenGL.hpp>
// Vertex Shader
const char *vertexShaderSource = R"glsl(
#version 450 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
)glsl";
// Fragment Shader
const char* fragmentShaderSoucre = R"glsl(
#version 450 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
)glsl";
uint32_t LoadShader(GLenum type, const char* src)
{
uint32_t shader;
shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
int success;
char log[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(shader, 512, nullptr, log);
printf("Error Shader %s compile error\n%s\n", type == GL_VERTEX_SHADER ? "Vertex" : "Fragment", log);
return 0;
}
return shader;
}
uint32_t LinkShaderProgram(uint32_t vertex, uint32_t fragment)
{
uint32_t program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
//
int success;
char log[512];
glGetProgramiv(program, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(program, 512, nullptr, log);
printf("Error Shader Linking error\n%s\n", log);
return 0;
}
return program;
}
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
int main()
{
sf::RenderWindow window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, sf::ContextSettings(
24, // depthBits
8, // stencilBits
4, // antialiasingLevel
4, // majorVersion
4 // minorVersion
));
window.setVerticalSyncEnabled(true);
// Load OpenGL functions using glad
if (!gladLoadGL())
{
printf("Something went wrong!\n");
exit(-1);
}
printf("OpenGL %s, GLSL %s\n", glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION));
window.setActive(true);
// Load shader
uint32_t vertexShader = LoadShader(GL_VERTEX_SHADER, vertexShaderSource);
uint32_t fragmentShader = LoadShader(GL_FRAGMENT_SHADER, fragmentShaderSoucre);
uint32_t program;
if(vertexShader && fragmentShader)
{
program = LinkShaderProgram(vertexShader, fragmentShader);
glUseProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
else
{
return 1;
}
// Load vertices
// VAO
uint32_t vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// VBO
uint32_t vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// glBindAttribLocation(program, 0, "aPos");
uint32_t aPos_attrib_index = glGetAttribLocation(program, "aPos");
printf("aPos location = %d\n", aPos_attrib_index);
glVertexAttribPointer(
aPos_attrib_index, // Location
3, // size
GL_FLOAT, // type
GL_FALSE, // normalize?
3 * sizeof(float), // stride
nullptr // offsets
);
glEnableVertexAttribArray(aPos_attrib_index);
bool running = true;
while (running)
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
running = false;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(program);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
window.display();
}
// release resources...
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteProgram(program);
return 0;
}
Element Buffer Object (EBO) 或 Index Buffer Object (IBO)
假設要畫一個矩形,用兩個三角形來組成一個矩形(OpenGL 主要處理三角形):
float vertices[] = {
// 第一個三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二個三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可以發現左上角與右下角被儲存了兩次,如此以來多了 50% 的額外開銷,這在有上千上萬個三角形的模型中會更糟糕。更好的方法是:儲存單獨的頂點,用另外一個陣列來表示頂點的順序。這正是 EBO 的功能。
EBO 就跟 VBO 一樣,它也是個 Buffer,但 EBO 專門儲存索引(Index)
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
uint32_t indices[] = {
{0, 1, 3}, // 第一個三角形
{1, 2, 3} // 第二個三角形
};
建立/綁定
// 建立
uint32_t ebo;
ebo = glGenBuffers(1, &ebo);
// 綁定
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
繪製
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glDrawElements(GL_TRIANGLES, // 形狀
6, // 頂點數量
GL_UNSIGNED_INT, // EBO 的 value type
0 // offset
);
glDrawElements()
從當前綁定的 GL_ELEMENT_ARRAY_BUFFER
EBO 中拿到 index加入 EBO 後,你的 OpenGL code 可能會長像這樣:
// 綁定 VAO
glGenVertexArray(1, &VAO);
glBindVertexArray(VAO);
// 建立 VBO 複製頂點資料
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 建立 EBO 並複製
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 設定頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// ...
// 繪製
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 三角形,畫6個,EBO的type,從0開始
我們的正方形就會出現了
加上glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
就可以看裸體的三角形喔
https://learnopengl.com/Getting-started/Hello-Triangle