LearnGL - 學習筆記目錄
本人才疏學淺,如有什麼錯誤,望不吝指出。
上一篇:LearnGL - 05.1 - Texture Wrap Mode,瞭解到可以使用紋理座標來滾動動畫。
這一篇:我們使用紋理座標滾動動畫 + 多紋理,實現類似 2D UI 流光動畫
實現過程與思路的參考可以查看我之前寫的:Unity Shader - 實現類似UI遮罩流光
(個人覺得 Unity 中的 ShaderLab 結合了 GLSL 與 HLSL 的有點,還是比 OpenGL GLSL有好很多的,而且還柔和了多種渲染狀態相關的設置在 ShaderLab 裏,提升了渲染狀態設置的內聚性,便於理解與維護)
思路
- 準備流光圖
- 準備流光遮罩圖
- 主要顯示紋理
流光圖(我用GIMP隨便畫的,還是PS好用)
流光遮罩圖
主紋理
着色器
// jave.lin - tex_2d_ui_flash_light.vert - 測試實現 2D UI 流光的頂點着色器
#version 450 compatibility
attribute vec3 vPos;
attribute vec2 vUV;
varying vec2 fUV;
void main() {
gl_Position = vec4(vPos, 1.0);
fUV = vUV;
}
// jave.lin - tex_2d_ui_flash_light.frag - 測試實現 2D UI 流光的片段着色器
#version 450 compatibility
varying vec2 fUV; // uv 座標
uniform sampler2D main_tex; // 主紋理
uniform sampler2D mask_tex; // 遮罩紋理
uniform sampler2D flash_light_tex; // 閃光/流光紋理
uniform float time; // 時間(秒)用於動畫
void main() {
vec3 mainCol = texture(main_tex, fUV).rgb;
float mask = texture(mask_tex, fUV).r;
vec4 flashCol = texture(flash_light_tex, fUV + vec2(-time, 0));
flashCol *= flashCol.a * mask;
mainCol = mainCol + flashCol.rgb;
gl_FragColor = vec4(mainCol, 1.0);
}
創建三個紋理對象
glCreateTextures(GL_TEXTURE_2D, 3, texture); // 創建 3 個紋理對象
綁定到紋理單元
glActiveTexture(GL_TEXTURE0); // 激活第 0 索引紋理單元
glBindTextureUnit(0, texture[0]); // 紋理單元 0 綁定:主紋理
glActiveTexture(GL_TEXTURE1); // 激活第 1 索引紋理單元
glBindTextureUnit(1, texture[1]); // 紋理單元 0 綁定:遮罩紋理
glActiveTexture(GL_TEXTURE2); // 激活第 2 索引紋理單元
glBindTextureUnit(2, texture[2]); // 紋理單元 0 綁定:閃光/流光紋理
加載三個紋理對象
loadTexture(texture[0], "my_tex.png"); // 加載紋理對象0:主紋理
loadTexture(texture[1], "my_tex_flash_mask.jpg"); // 加載紋理對象1:遮罩紋理
loadTexture(texture[2], "flash.png"); // 加載紋理對象2:閃光/流光紋理
設置採樣器採樣對應的紋理單元
shaderProgram->setInt("main_tex", 0); // 主 紋理設置採樣器採樣 0 索引紋理單元
shaderProgram->setInt("mask_tex", 1); // 遮罩 紋理設置採樣器採樣 1 索引紋理單元
shaderProgram->setInt("flash_light_tex", 2); // 閃光/流光 紋理設置採樣器採樣 2 索引紋理單元
傳入 time 時間控制 uv 紋理座標動畫
shaderProgram->setFloat("time", (float)glfwGetTime()); // 測試用就直接用字符串了,方便一些
運行效果
完整代碼
// jave.lin
#include"glad/glad.h"
#include"GLFW/glfw3.h"
//#include"linmath.h"
// 把linmath.h 放在 iostream 之前include會有錯誤,所以放到iostream 後include就好了
// 而這個錯誤正式 xkeycheck.h 文件內 #error 提示的,所以可以使用 #define _XKEYCHECK_H 這個頭文件的引用標記宏
// 就可以避免對 xkeycheck.h 頭文件的 include 了。
#include<iostream>
#include"linmath.h"
#include"shader.h"
// 使用 stb_image.h 的加載庫
// github 源碼:https://github.com/nothings/stb/blob/master/stb_image.h
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// 將之前的打印版本信息代碼包含一下
#include"print_gl_version_info.h"
GLfloat vertices[] = {
// x, y, z
// 直接放4個頂點
-0.5f, -0.5f, 0.0f, // 第0個頂點,左下角
0.5f, -0.5f, 0.0f, // 第1個頂點,右下角
0.5f, 0.5f, 0.0f, // 第2個頂點,右上角
-0.5f, 0.5f, 0.0f, // 第3個頂點,左上角
};
GLfloat uvs[] = { // 頂點的 uv 座標
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
1.0f, 1.0f, // 右上角
0.0f, 1.0f, // 左上角
};
GLuint indices[] = { // 注意索引從0開始!通過索引緩存來指定 圖元 組成 用的 頂點有哪些
0, 1, 3, // 放置頂點的索引,第一個三角形
1, 2, 3 // 放置頂點的索引,第二個三角形
};
// 定義:獲取 Shader 目錄的回調函數原型
typedef char* (__stdcall* GetShaderPathCallback)(char*, const char*);
GetShaderPathCallback g_GetShaderPathCallback = NULL;
// 定義:獲取 Pic 目錄的回調函數原型
typedef char* (__stdcall* GetPicturePathCallback)(char*, const char*);
GetPicturePathCallback g_GetPicturePathCallback = NULL;
static void error_callback(int error, const char* description) {
fprintf(stderr, "ErrorCode : %d(0x%08x), Error: %s\n", error, error, description);
}
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { // 當鍵盤按鍵ESCAPE按下時,設置該window爲:需要關閉
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
// 時候開啓檢測GL的錯誤
#define CHECK_GL_ERROR
#ifdef CHECK_GL_ERROR
// 檢測如果有GL的錯誤,則提示並退出程序
#define checkGLError() \
{\
GLenum errorCode = glGetError(); \
if (errorCode != 0) { \
std::cout << "Line:" << __LINE__ << " "; \
std::cout << std::dec; \
std::cout << "glError : " << errorCode; \
std::cout << std::hex; \
std::cout << "(0x" << errorCode << ")" << std::endl; \
exit(EXIT_FAILURE); \
}\
}
#else
#define checkGLError()
#endif
static void loadTexture(GLuint &texture, const char* textureFileName) {
// loading texture here...
// 加載紋理需要用的圖片數據
char img_path[MAX_PATH];
//g_GetPicturePathCallback(img_path, "\\Noise\\image1.png"); // 獲取圖片目錄
g_GetPicturePathCallback(img_path, textureFileName); // 獲取圖片目錄
int img_w, img_h, img_channels;
stbi_set_flip_vertically_on_load(1); // 也可以在加載前設置加載時翻轉的變量
unsigned char* img_data = stbi_load(img_path, &img_w, &img_h, &img_channels, 4); // 加載圖片數據,返回確定寬、高、通道數量、每個分量要多少字節
if (img_data == NULL) { // 如果加載圖片失敗
std::cout << "Loading Image File : " << img_path << " FAILURE : " << stbi_failure_reason() << std::endl;
exit(EXIT_FAILURE);
}
//stbi__vertical_flip(img_data, img_w, img_h, 4); // 如果不設置前面stbi_set_flip_vertically_on_load(1),也可以在這手動去翻轉,因爲圖片座標與紋理座標的Y軸增量方向不同,所以需要翻轉垂直方向的行數數據
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // 使用 OpenGL 4.5+ 的 API 會更清晰:glTextureParameteri,因爲這個是更具 target 類型,與當前 bind 的 紋理對象來確定設置那個紋理對象的,從可讀性來說 4.5+ 版本的可讀性高很多
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_REPEAT); // 設置 texture 紋理對象的 GL_TEXTURE_WRAP_S 參數,就是設置 uv 中的水平 u 座標超出0~1範圍後的數值環繞方式
glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT); // 設置 texture 紋理對象的 GL_TEXTURE_WRAP_T 參數,就是設置 uv 中的水平 v 座標超出0~1範圍後的數值環繞方式
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 設置 texture 紋理對象的 GL_TEXTURE_MIN_FILTER 在像素縮小時的濾波方式
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 設置 texture 紋理對象的 GL_TEXTURE_MAG_FILTER 在像素放大時的濾波方式
checkGLError();
glTextureStorage2D( // 設置 texture 紋理對象的內部格式
texture, // 要設置的 texture 紋理對象
1, // mipmaps 的層數,只要1層 mipmaps 即可,至少要有1層,否則有錯誤。需要需要多層 mipmaps ,可以指定多層
GL_RGBA8, // 內部數據格式
img_w, // 圖像的寬
img_h // 圖像的高
);
checkGLError();
glTextureSubImage2D( // 給 texture 紋理對象設置對應 mipmap 層級的數據
texture, // 要設置的 texture 紋理對象
0, // mipmaps 的層級索引,從0開始,mipmaps 的
0, 0, // 要從 x,y 偏移多少開始,不要偏移所以都填0
img_w, img_h, // 要填入的行、列尺寸的像素數量
GL_RGBA, GL_UNSIGNED_BYTE, // 外部格式,指定要包含的分量數量 和 分量類型
img_data // 外部圖片數據
);
checkGLError(); // GET_IMG_DATA_TYPE 2 時會有錯誤
//glGenerateTextureMipmap(texture);// opengl 4.5 API,生成指定紋理對象的mipmaps
//checkGLError();
// when loading complete.
// free image data here
stbi_image_free(img_data); // 紋理已經上傳到了顯存,內存中的數據可以刪除了
}
int main() {
glfwSetErrorCallback(error_callback); // 安裝glfw內部錯誤時的回調
if (!glfwInit()) { // 初始化glfw
std::cout << "glfwInit FAILURE" << std::endl; // 初始化失敗
exit(EXIT_FAILURE);
}
// 設置最低的openGL 版本,major:主版本號,minor:次版本號
// openGl 太低版本的話是不支持CORE Profile模式的
// 會報錯:ErrorCode: 65540(0x00010004), Error : Context profiles are only defined for OpenGL version 3.2 and above
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// 根據上面的錯誤提示,至少使用3.2纔行,這裏我們使用4.5
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// core profile 下運行有問題,不顯示任何內容,但不會報錯。
// 着色器編譯、着色器程序鏈接都沒有錯誤日誌信息。
// 很有可能是因爲我參考的學習網站使用的API相對比較老,使用的是3.3的。
//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 所以這裏我們不設置 major, minor的版本,默認使用本計算機能用的最高版本
// 使用 compatibility profile 就有內容出現了。
int width = 600;
int height = 600;
// 使用glfw創建窗體
GLFWwindow* window = glfwCreateWindow(width, height, "jave.lin - Learning OpenGL - 05_02_Texture_2D_UI_FlashLight", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl; // 構建窗體失敗
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, key_callback); // 安裝glfw內部鍵盤按鍵的回調
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { // 裝載OpenGL的C函數庫
std::cout << "Failed to initialize OpenGL context" << std::endl; // 裝載報錯
glfwTerminate();
exit(EXIT_FAILURE);
}
// 打印版本信息
print_infos(window);
// 打印支持最大的頂點支持的數量
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum number of vertex attributes supported : " << nrAttributes << std::endl;
// 打印着色器支持最大的紋理圖像單元的數量
int maxTexUnit;
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTexUnit);
std::cout << "Maximun number of texture image units : " << maxTexUnit << std::endl;
// 打印着色器支持最大的所有組合的紋理圖像單元的數量
int maxCombinedTexUnit;
glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxCombinedTexUnit);
std::cout << "Maximun number of Combined texture image units : " << maxCombinedTexUnit << std::endl;
GLint vpos_location, vuv_location;
GLuint vertex_buffer[2], index_buffer;
GLuint vertex_array_object;
GLuint texture[3];
GLuint pixelBufObject;
GLint success, infoLogLen;
// 用 lambda 設置,獲取 pic 目錄的回調,後面在封裝
g_GetPicturePathCallback = [](char* receiveBuff, const char* file)->char* {
char buf[MAX_PATH];
sprintf_s(buf, "..\\..\\Dependencies\\Pic\\%s", file);
strcpy_s(receiveBuff, MAX_PATH, buf);
return receiveBuff;
};
// 用 lambda 設置,獲取 shader 目錄的回調,後面在封裝
g_GetShaderPathCallback = [](char* receiveBuff, const char* file)->char* {
char buf[MAX_PATH];
sprintf_s(buf, "..\\..\\Dependencies\\Shaders\\%s", file);
strcpy_s(receiveBuff, MAX_PATH, buf);
return receiveBuff;
};
ShaderProgram* shaderProgram = new ShaderProgram;
// shader program init 5 - 根據shader源碼的相對路徑(變量),加載deps下的shader
char vs_path[MAX_PATH], fs_path[MAX_PATH];
g_GetShaderPathCallback(vs_path, "TestingTexture\\tex_2d_ui_flash_light.vert");
g_GetShaderPathCallback(fs_path, "TestingTexture\\tex_2d_ui_flash_light.frag");
if (!shaderProgram->initByPath(vs_path, fs_path)) {
std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 輸出shader program錯誤
exit(EXIT_FAILURE);
}
glCreateTextures(GL_TEXTURE_2D, 3, texture); // 創建 3 個紋理對象
glActiveTexture(GL_TEXTURE0); // 激活第 0 索引紋理單元
glBindTextureUnit(0, texture[0]); // 紋理單元 0 綁定:主紋理
glActiveTexture(GL_TEXTURE1); // 激活第 1 索引紋理單元
glBindTextureUnit(1, texture[1]); // 紋理單元 0 綁定:遮罩紋理
glActiveTexture(GL_TEXTURE2); // 激活第 2 索引紋理單元
glBindTextureUnit(2, texture[2]); // 紋理單元 0 綁定:閃光/流光紋理
checkGLError();
loadTexture(texture[0], "my_tex.png"); // 加載紋理對象0:主紋理
loadTexture(texture[1], "my_tex_flash_mask.jpg"); // 加載紋理對象1:遮罩紋理
loadTexture(texture[2], "flash.png"); // 加載紋理對象2:閃光/流光紋理
checkGLError();
vpos_location = shaderProgram->getAttributeLoc("vPos"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
vuv_location = shaderProgram->getAttributeLoc("vUV"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
glGenVertexArrays(1, &vertex_array_object); // 生成1個 VAO
glGenBuffers(2, vertex_buffer); // 創建2個 VBO,這裏我們因爲有一個一樣的頂點座標,一個一樣的頂點UV
glGenBuffers(1, &index_buffer); // 創建1個 EBO,因爲兩個 Quad 的頂點索引順序都是一樣的
//
// === VAO[0] ===
//
glBindVertexArray(vertex_array_object); // 綁定 VAO[0],那麼之後的 vbo, ebo,的綁定指針都是指向該 VAO 中的,還有頂點格式(規範)都會保存在該 VAO
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]); // 綁定 VBO[0]
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 設置 VBO 座標數據
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[1]); // 綁定 VBO[1]
glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW); // 設置 VBO uv數據
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer); // 綁定 EBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 設置 EBO 數據
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]); // 綁定 VBO[0],因爲後面要設置該 VBO 的座標格式
glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vPos 格式
sizeof(GLfloat) * 3, (GLvoid*)0);
glEnableVertexAttribArray(vpos_location); // 啓用 頂點緩存 location 位置的屬性
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[1]); // 綁定 VBO[1],因爲後面要設置該 VBO 的uv格式
glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vUV 格式
sizeof(GLfloat) * 2, (GLvoid*)0);
glEnableVertexAttribArray(vuv_location); // 啓用 頂點緩存 location uv的屬性
//glEnable(GL_CULL_FACE); // 開啓面向剔除
//glCullFace(GL_BACK); // 設置剔除背面
GLboolean cf = glIsEnabled(GL_CULL_FACE); // 查看是否啓用面向剔除
std::cout << "cull face enabled : " << (cf ? "true" : "false") << std::endl;
//glFrontFace(GL_CW); // 順時針
//glFrontFace(GL_CCW); // 逆時針(默認的)ClockWise
GLint facing;
glGetIntegerv(GL_FRONT_FACE, &facing); // 獲取正面的順逆時針 : CW(ClockWise - 順時針), CCW(Counter ClockWise - 逆時針)
std::cout << "facing : " << (facing == GL_CW ? "CW" : "CCW") << std::endl;
while (!glfwWindowShouldClose(window)) { // 檢測是否需要關閉窗體
glfwGetFramebufferSize(window, &width, &height); // 獲取窗口大小
glViewport(0, 0, width, height); // 設置Viewport
glClearColor(0.1f, 0.2f, 0.1f, 0.f); // 設置清理顏色緩存時,填充顏色值
glClear(GL_COLOR_BUFFER_BIT); // 清理顏色緩存
shaderProgram->use(); // 使用此着色器程序
shaderProgram->setInt("main_tex", 0); // 主 紋理設置採樣器採樣 0 索引紋理單元
shaderProgram->setInt("mask_tex", 1); // 遮罩 紋理設置採樣器採樣 1 索引紋理單元
shaderProgram->setInt("flash_light_tex", 2); // 閃光/流光 紋理設置採樣器採樣 2 索引紋理單元
shaderProgram->setFloat("time", (float)glfwGetTime()); // 測試用就直接用字符串了,方便一些
glBindVertexArray(vertex_array_object); // 先繪製 VAO[0] 的 VBO,EBO,VAF,ENABLED
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (GLvoid*)0); // 參數1:繪製三角圖元;參數2:取6個索引來繪製三角圖元(每個三角圖元需要3個,所以可以畫兩個三角圖元);參數3:將 GL_ELEMENT_ARRAY_BUFFER 每個元素視爲 uint 類型;參數4:設置索引緩存的字節偏移量。也可以設置爲另一個 緩存數據的指針,即:使用另一個數據。
glfwSwapBuffers(window); // swap buffer, from backbuffer to front buffer
glfwPollEvents(); // 處理其他的系統消息
}
glDeleteBuffers(1, &pixelBufObject); // 測試刪除 BO
glDeleteBuffers(2, vertex_buffer); // 測試刪除 VBO
glDeleteBuffers(1, &index_buffer); // 測試刪除 EBO
glDeleteBuffers(1, &vertex_array_object); // 測試刪除 VAO
glDeleteTextures(3, texture); // 刪除紋理對象 TO
delete shaderProgram; // 銷燬 shader program
checkGLError(); // 最後再看看GL還有什麼錯誤
glfwDestroyWindow(window); // 銷燬之前創建的window對象
glfwTerminate(); // 清理glfw之前申請的資源
//checkGLError(); // 最後再看看GL還有什麼錯誤,從這句一直報錯可以看出:glfw 的銷燬與退出是有問題的
return 0;
} // int main() {