文章目錄
LearnGL - 學習筆記目錄
本人才疏學淺,如有什麼錯誤,望不吝指出。
上一篇:LearnGL - 04.2 - 封裝 ShaderProgram 類,對 Shader、Program 封裝了類,便於後續使用。
這一篇:繼續我們的 OpenGL 學習主線內容:紋理(Texture)。紋理的內容挺多的,這裏只講到部分的內容。
爲何要有紋理?
想讓我們的幾何體(模型)看起來更豐富,除了之前我們說過的 添加頂點(由 三角形 到 四邊形,再多一些就甚至可以是圓形,甚至是各種幾何體,只要頂點數量足夠多)、然後給頂點添加顏色,最終都控制如何顯示 成最後的屏幕像素,讓它看起來更豐富的內容。
一些色彩豐富的物體,如一些畫家畫的抽象畫,什麼鬼顏色都有,如果我們只用添加頂點的方式,讓每一塊相同的顏色,都弄一個幾何體(一堆頂點),並添加頂點顏色來表示的話,那估計建模的同學會瘋掉。
所以聰明的人類、聰明的圖形先輩們想到了一個方法:紋理。
紋理這玩意兒,有點像我們在一個氣球上畫一些圖像,或是寫一些字,然後我們將氣球吹鼓。
再將氣球捏來捏去的,改變它的形狀,你會發現氣球上的圖案都很好的映射在你畫在對應位置上的氣球表面位置上;最後你將氣球泄氣會原型後,你會發現畫在氣球上的圖像內容都還原樣的保留着(除非你使勁兒地把畫上去的顏料戳沒了。-_-!!!)。
所以我們的 紋理 也是類型這麼個映射。這個方法可就很好的替代了上面添加N個無窮無盡的表面頂點來豐富畫面了。工作量上可是省了 N 多的量。建模同學是個幸運兒,都沒經歷這麼多痛的假設工作量,都給圖形先輩們提前想到並解決了。
紋理 的整體系統的思路很簡單,先提供一些保存着圖像內容的 圖片(紋理貼圖 texture map),這些圖片都有一些紋理座標,就想我們畫的一個二維的笛卡爾座標一樣,也有個座標。然後在頂點屬性上添加一個映射在紋理採樣的座標,這樣不管我的頂點變化到哪去,我都讓該頂點映射紋理座標的位置不變。頂點的位置變化能引起圖像映射的內容伸縮,但這正是我們想要的效果,就想上面提到的氣球的例子一樣。而上面的頂點座標中映射了紋理座標,我們通常叫:紋理映射(Texture-Mapping)。
紋理座標
前面簡單的簡述了紋理的概念,也提到了 紋理座標 的概念。
紋理系統中的 紋理貼圖(texture map) 就是張圖片,而這張 圖片的座標 通常是下面的方式:
但是我們的 紋理座標 可不一樣:(0,0)在左下角,即:Y軸的增量方向是朝上的。
加載圖片數據到內存
選用現成庫
有好幾種方法
- 一種是使用:http://www.opengl-redbook.com,(紅寶書)中的源碼:
vglLoadImage
、vglUnloadIImage
、vglLoadTexture
、等方法。(如果要全面一些的加載也可以使用這個) - 一種是:SOIL(Simple OpenGL Image Library,簡要的 OpenGL 圖像庫),
unsinged char* image = SIO_load_image("filename.ext", &width, &height, 0 SOIL_LOAD_RGB);
(爲了做學習用,可以使用這個) - 一種是:stb_image.h。(這個庫使用比較簡單,我是推薦使用這個)
我使用的是也 stb_image.h 的加載方式,使用方式可以參考:stb_image.h,除了參考文中說明的使用方式,大家也可以查查此頭文件中的前面的註釋,也有很詳細的英文說明。
將此文件放在我們的 include path(包含目錄)
在源碼中包含此頭文件之前,先要定義宏:
#define STB_IMAGE_IMPLEMENTATION
就像這樣:
#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"
加載圖片:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
使用 stb_image.h
上面確定使用了 stb_image.h
,下面我們列出我們使用到的加載方式。
要將圖片數據加載到紋理中,首先我們先將圖片數據加載到內存
// loading texture here...
// 加載紋理需要用的圖片數據
char img_path[MAX_PATH];
g_GetPicturePathCallback(img_path, "my_tex.png"); // 獲取圖片目錄
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軸增量方向不同,所以需要翻轉垂直方向的行數數據
// loading texture here...
// when loading complete.
// free image data here
stbi_image_free(img_data);
unsigned char* img_data = stbi_load(img_path, &img_w, &img_h, &img_channels, 0);
根據img_path路徑加載圖片數據,返回確定寬、高、通道數量、每個分量要多少字節,返回加載的數據- 注意:圖像垂直方向圖像翻轉,前面我們介紹過,因爲 圖片座標 與 紋理座標 的垂直向量增量方向不同,所以需要翻轉垂直方向行數的數據
stbi_set_flip_vertically_on_load(1);
也可以在加載前設置加載時翻轉的變量stbi__vertical_flip(img_data, img_w, img_h, 4);
如果不設置前面stbi_set_flip_vertically_on_load(1)
,也可以在這手動去翻轉
刪除加載的圖片數據
上面代碼最後一句,可以看到有個:回收內存函數,這樣就可以回收掉之前分配的內存。
只要我們的數據上傳到了GPU緩存(顯存)就回收。
stbi_image_free(img_data); // 紋理已經上傳到了顯存,內存中的數據可以刪除了
紋理對象
有了圖片數據我們就要開創建紋理。
就像之前介紹的:VAO(頂點緩存數組對象),VBO(頂點緩存對象),EBO/IBO(元素/索引緩存對象),Shader Object(着色器對象),Shader Program Object(着色器程序對象),等對象。
所以紋理也有一個封裝的對象:紋理對象(Texture Object)。
在使用紋理之前,需要先創建一個 紋理對象。
創建紋理對象
紋理對象的生成/創建可以使用: glCreateTextures。
而 glCreateTextures 是 OpenGL 4.0+ 的規範定義的API。
如果要在 OpenGL 4.0-之前的版本創建紋理對象的話,使用:glGenTextures。
OpenGL4.0+的glCreateTextures 和 OpenGL4.0-的glGenTextures 稍微有丟丟不同:
glCreateTextures
void glCreateTextures( GLenum target,
GLsizei n,
GLuint *textures);
glGenTextures
void glGenTextures( GLsizei n,
GLuint * textures);
OpenGL4.0+的glCreateTextures 的多了第一個參數 GLenum target
。後面兩參數 n
是創建的 紋理對象數量, textures
紋理對象 都一樣的意思。
紋理目標類型:glCreateTextures
- target:是要綁定的紋理目標。它能傳入的類型有:
目標(GL_TEXTURE_*) | 採樣器類型 | 維度 |
---|---|---|
1D | sampler1D | 一維 |
1D_ARRAY | sampler1DArray | 一維數組 |
2D | sampler2D | 二維 |
2D_Array | sampler2DArray | 二維數組 |
2D_MULTISAMPLE | sampler2DMS | 二維多重採樣 |
2D_MULTISAMPLE_ARRAY | sampler2DMSArray | 二維多重採樣數組 |
3D | sampler3D | 三維 |
CUBE | samplerCube | 立方體映射紋理 |
ARRAY | samplerCubeArray | 立方體映射紋理數組 |
RECTANGLE | samplerRect | 二維長方形 |
BUFFER | samplerBuffer | 一維緩存 |
傳入對應的紋理類型,對應到着色器中的 採樣器類型 也是一一對應的。
glIsTexture 可以判斷一個對象ID是否 紋理對象。
glCreateTextures(GL_TEXTURE_2D, 1, &texture); // 創建紋理對象
設置爲當前操作的紋理對象
//glActiveTexture(GL_TEXTURE0); // 默認第 0 個紋理單元是激活的,可以不用設置
glBindTextureUnit(0, texture); // 綁定第 0 索引的紋理單元,OpenGL 4.0+建議用這個,與 glCreateTextures 配對。 OpenGL4.0-可用 glBindTexture
刪除紋理對象
如果某個紋理對象不再使用了,可以使用 glDeleteTextures 刪除這個紋理對象:
glDeleteTextures(1, &texture); // 刪除紋理對象
設置紋理對象內部格式以及傳入圖像數據
上面創建好了 紋理對象 後,紋理對象裏還是空的:配置是默認的,紋理數據的空的。在給紋理對象設置圖像數據之前,先配置它的數據存儲的是什麼內部格式。
內部 格式中的 內部 指的是在 OpenGL 環境中的數據(着色器中的採樣出來的數據)格式。
有 內部 就會有相對的 外部,外部格式是我們的圖片的數據格式,代表它在 傳入到 OpenGL 環境前 的 外部 格式,後面會有說明如何指定我們紋理數據的外部格式。
設置紋理對象的內部格式有:glTextureStorage1D,glTextureStorage2D,glTextureStorage3D 三個API。
這三個 API 是 OpenGL 4.5+(包含4.5)纔有的,在 glad.c OpenGL 4.5 版本的文件可以看到它的讀取。
目前:docs.gl上沒找到對應的 API。
在 OpenGLS 4.5-(不確定是否4.5-)之前,可以使用其他的 API :glTexImage1D、glTexImage1D、glTexImage1D。
glTextureStorage1D
void glTextureStorage1D(GLuint texture,
GLsizei levels,
GLenum internalformat,
GLsizei width);
texture
是指定的紋理對象leves
是指定的 mipmaps 的分層有幾級internalFormat
就是我們上面說的的 內部各式 下面的列表有信息,有三個表(Base/Sized/Compressed Internal Format)width
用在這 1D,2D,3D 函數中的紋理的寬度
Base Internal Format(內部格式)
Base Internal Format(基礎的內部格式) | RGBA, Depth and Stencil Values(RGB,深度和模板值) | Internal Components(內部分量) |
---|---|---|
GL_DEPTH_COMPONENT | Depth | D |
GL_DEPTH_STENCIL | Depth, Stencil | D, S |
GL_RED | Red | R |
GL_RG | Red, Green | R, G |
GL_RGB | Red, Green, Blue | R, G, B |
GL_RGBA | Red, Green, Blue, Alpha | R, G, B, A |
Sized Internal Format
Sized Internal Format | Base Internal Format | Red Bits | Green Bits | Blue Bits | Alpha Bits | Shared Bits |
---|---|---|---|---|---|---|
GL_R8 | GL_RED | 8 | ||||
GL_R8_SNORM | GL_RED | s8 | ||||
GL_R16 | GL_RED | 16 | ||||
GL_R16_SNORM | GL_RED | s16 | ||||
GL_RG8 | GL_RG | 8 | 8 | |||
GL_RG8_SNORM | GL_RG | s8 | s8 | |||
GL_RG16 | GL_RG | 16 | 16 | |||
GL_RG16_SNORM | GL_RG | s16 | s16 | |||
GL_R3_G3_B2 | GL_RGB | 3 | 3 | 2 | ||
GL_RGB4 | GL_RGB | 4 | 4 | 4 | ||
GL_RGB5 | GL_RGB | 5 | 5 | 5 | ||
GL_RGB8 | GL_RGB | 8 | 8 | 8 | ||
GL_RGB8_SNORM | GL_RGB | s8 | s8 | s8 | ||
GL_RGB10 | GL_RGB | 10 | 10 | 10 | ||
GL_RGB12 | GL_RGB | 12 | 12 | 12 | ||
GL_RGB16_SNORM | GL_RGB | 16 | 16 | 16 | ||
GL_RGBA2 | GL_RGB | 2 | 2 | 2 | 2 | |
GL_RGBA4 | GL_RGB | 4 | 4 | 4 | 4 | |
GL_RGB5_A1 | GL_RGBA | 5 | 5 | 5 | 1 | |
GL_RGBA8 | GL_RGBA | 8 | 8 | 8 | 8 | |
GL_RGBA8_SNORM | GL_RGBA | s8 | s8 | s8 | s8 | |
GL_RGB10_A2 | GL_RGBA | 10 | 10 | 10 | 2 | |
GL_RGB10_A2UI | GL_RGBA | ui10 | ui10 | ui10 | ui2 | |
GL_RGBA12 | GL_RGBA | 12 | 12 | 12 | 12 | |
GL_RGBA16 | GL_RGBA | 16 | 16 | 16 | 16 | |
GL_SRGB8 | GL_RGB | 8 | 8 | 8 | ||
GL_SRGB8_ALPHA8 | GL_RGBA | 8 | 8 | 8 | 8 | |
GL_R16F | GL_RED | f16 | ||||
GL_RG16F | GL_RG | f16 | f16 | |||
GL_RGB16F | GL_RGB | f16 | f16 | f16 | ||
GL_RGBA16F | GL_RGBA | f16 | f16 | f16 | f16 | |
GL_R32F | GL_RED | f32 | ||||
GL_RG32F | GL_RG | f32 | f32 | |||
GL_RGB32F | GL_RGB | f32 | f32 | f32 | ||
GL_RGBA32F | GL_RGBA | f32 | f32 | f32 | f32 | |
GL_R11F_G11F_B10F | GL_RGB | f11 | f11 | f10 | ||
GL_RGB9_E5 | GL_RGB | 9 | 9 | 9 | 5 | |
GL_R8I | GL_RED | i8 | ||||
GL_R8UI | GL_RED | ui8 | ||||
GL_R16I | GL_RED | i16 | ||||
GL_R16UI | GL_RED | ui16 | ||||
GL_R32I | GL_RED | i32 | ||||
GL_R32UI | GL_RED | ui32 | ||||
GL_RG8I | GL_RG | i8 | i8 | |||
GL_RG8UI | GL_RG | ui8 | ui8 | |||
GL_RG16I | GL_RG | i16 | i16 | |||
GL_RG16UI | GL_RG | ui16 | ui16 | |||
GL_RG32I | GL_RG | i32 | i32 | |||
GL_RG32UI | GL_RG | ui32 | ui32 | |||
GL_RGB8I | GL_RGB | i8 | i8 | i8 | ||
GL_RGB8UI | GL_RGB | ui8 | ui8 | ui8 | ||
GL_RGB16I | GL_RGB | i16 | i16 | i16 | ||
GL_RGB16UI | GL_RGB | ui16 | ui16 | ui16 | ||
GL_RGB32I | GL_RGB | i32 | i32 | i32 | ||
GL_RGB32UI | GL_RGB | ui32 | ui32 | ui32 | ||
GL_RGBA8I | GL_RGBA | i8 | i8 | i8 | i8 | |
GL_RGBA8UI | GL_RGBA | ui8 | ui8 | ui8 | ui8 | |
GL_RGBA16I | GL_RGBA | i16 | i16 | i16 | i16 | |
GL_RGBA16UI | GL_RGBA | ui16 | ui16 | ui16 | ui16 | |
GL_RGBA32I | GL_RGBA | i32 | i32 | i32 | i32 | |
GL_RGBA32UI | GL_RGBA | ui32 | ui32 | ui32 | ui32 |
Compressed Internal Format
Compressed Internal Format | Base Internal Format | Type |
---|---|---|
GL_COMPRESSED_RED | GL_RED | Generic |
GL_COMPRESSED_RG | GL_RG | Generic |
GL_COMPRESSED_RGB | GL_RGB | Generic |
GL_COMPRESSED_RGBA | GL_RGBA | Generic |
GL_COMPRESSED_SRGB | GL_RGB | Generic |
GL_COMPRESSED_SRGB_ALPHA | GL_RGBA | Generic |
GL_COMPRESSED_RED_RGTC1 | GL_RED | Specific |
GL_COMPRESSED_SIGNED_RED_RGTC1 | GL_RED | Specific |
GL_COMPRESSED_RG_RGTC2 | GL_RG | Specific |
GL_COMPRESSED_SIGNED_RG_RGTC2 | GL_RG | Specific |
GL_COMPRESSED_RGBA_BPTC_UNORM | GL_RGBA | Specific |
GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM | GL_RGBA | Specific |
GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT | GL_RGB | Specific |
GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT | GL_RGB | Specific |
glTextureStorage2D
比 1D 的多了個 height
void glTextureStorage2D(前面的參數與1D一樣, GLsizei height);
glTextureStorage3D
比 2D 的多了個 depth
void glTextureStorage3D(前面的參數與1D一樣, GLsizei depth);
一般 2D 可以當作 1D 的數組,height 作爲 1D 的數據的行;3D 可以當作 2D 的數據,depth 作爲 2D 的切片層數。
glTextureStorage1D 執行後就像是下面的代碼處理:
for (i = 0; i < levels; i++) {
glTexImage1D(target, i, internalformat, width, 0, format, type, NULL);
width = max(1, (width / 2));
}
glTextureStorage2D 執行後等價於下面代碼:
for (i = 0; i < levels; i++) {
glTexImage2D(target, i, internalformat, width, height, 0, format, type, NULL);
width = max(1, (width / 2));
height = max(1, (height / 2));
}
// When target is GL_TEXTURE_CUBE_MAP, glTexStorage2D is equivalent to:
for (i = 0; i < levels; i++) {
for (face in (+X, -X, +Y, -Y, +Z, -Z)) {
glTexImage2D(face, i, internalformat, width, height, 0, format, type, NULL);
}
width = max(1, (width / 2));
height = max(1, (height / 2));
}
// When target is GL_TEXTURE_1D_ARRAY or GL_PROXY_TEXTURE_1D_ARRAY, glTexStorage2D is equivalent to:
for (i = 0; i < levels; i++) {
glTexImage2D(target, i, internalformat, width, height, 0, format, type, NULL);
width = max(1, (width / 2));
}
glTextureStorage3D 執行後等價於下面代碼:
for (i = 0; i < levels; i++) {
glTexImage3D(target, i, internalformat, width, height, depth, 0, format, type, NULL);
width = max(1, (width / 2));
height = max(1, (height / 2));
depth = max(1, (depth / 2));
}
// When target is GL_TEXTURE_2D_ARRAY, GL_PROXY_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP_ARRAY, or GL_PROXY_TEXTURE_CUBE_MAP_ARRAY, glTexStorage3D is equivalent to:
for (i = 0; i < levels; i++) {
glTexImage3D(target, i, internalformat, width, height, depth, 0, format, type, NULL);
width = max(1, (width / 2));
height = max(1, (height / 2));
}
其實就是根據要生成的 mipmaps 的 levels 值來遍歷設置對應 mipmaps 層級的數據、及格式。
注意:但是可以看到他們的等價代碼中會調用到glTexImage1D、glTexImage1D、glTexImage1D,並且最後一個 const void* data
數據傳入的是 NULL
,這裏特別說明一下,如果該參數傳入的數據爲 NULL
,並且當前 GL_PIXEL_UNPACK_BUFFER
綁定的像素緩存對象數據不爲 NULL
的話,那麼紋理對象中的數據將指向 GL_PIXEL_UNPACK_BUFFER
的緩存數據,它會在着色器程序讀取數據時再取讀取數據的。
在 OpenGL 4.5之前,上面的 mipmaps 的數據,我們也可以手動調用 glTexImage2D,再調用 glGenerateMipmap生成 mipmaps 數據:
//// 先填入第0 層 mipmaps 數據
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img_w, img_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data);
// 如果前面給這個紋理對象指定了多層級 mipmaps,那麼可以使用 glGenerateMipmap
// void glGenerateMipmap(GLenum target);
// void glGenerateTextureMipmap(GLuint texture);
// 來給紋理的0層之後的其他mipmaps層生成圖像數據
//glGenerateTextureMipmap(texture);// opengl 4.5 API,生成指定紋理對象的mipmaps
glGenerateMipmap(GL_TEXTURE_2D); // opengl 4.5之前的API,生成當前綁定紋理對象,且屬於 GL_TEXTURE_2D 類型的紋理對象的mipmaps
glTexImage1D 原型:
void glTexImage1D( GLenum target,
GLint level,
GLint internalformat,
GLsizei width,
GLint border,
GLenum format,
GLenum type,
const void * data);
target
紋理目標類型之間有列表羅列出來了各個枚舉與着色器中採樣器對應。level
當前要設置的 level 層級internalformat
內部格式,之前也有對應的表格width
單行像素的數量border
必須填0format
外部格式,指定需要用到的分量有哪些,下面講有對應表格說明type
外部格式,指定分量的類型
後面的 glTexImage2D/glTextureImage2D,glTexImage3D/glTextureImage3D 都只是對二維、三維的紋理處理的。多了個height
,depth
參數而已。類似的也可以使用 glTextureSubImage2D。
這裏提一下mipmaps
會每次縮小一倍的尺寸來保存另一個已經 linear 採樣來縮小過的圖片數據,會才圖元的某個片元的像素密集度到達一定程度時就會選擇使用對應的 mipmaps 數據,像素密集度越大,則使用 miplevels 中對應 level 越高的圖片數據來採樣。這樣既可以讓採樣性能更高,而且顯示質量也越好。
最終我們調用是:
使用 glTextureStorage2D 來 設置紋理對象內部格式 以及 使用 glTextureSubImage2D 傳入圖像數據
glTextureStorage2D( // 設置 texture 紋理對象的內部格式
texture, // 要設置的 texture 紋理對象
1, // mipmaps 的層數,只要1層 mipmaps 即可,至少要有1層,否則有錯誤。需要需要多層 mipmaps ,可以指定多層
GL_RGBA8, // 內部數據格式
img_w, // 圖像的寬
img_h // 圖像的高
);
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 // 外部圖片數據
);
之前 glTextureSubImage2D API的最後一個參數的作用有說明怎麼用,當 GL_PIXEL_UNPACK_BUFFER
緩存對象不爲NULL時, glTextureSubImage2D 的最後一個參數將作爲 GL_PIXEL_UNPACK_BUFFER
緩存對象數據的字節偏移值。
如下代碼,我寫了個宏,預處理分支 GET_IMG_DATA_TYPE
1 或是 2 的類型
- 1 : 直接在
glTextureSubImage2D
的最後一個參數設置數據 - 2 : 直接在
GL_PIXEL_UNPACK_BUFFER
中讀取
1 但是沒有問題的,就是 2 的時候有錯誤。具體暫時找不出什麼原因。(按 GL 提示的錯誤代碼是說的緩存數據提供的內容不符合紋理需要的格式,但是爲何 1 就可以呢?都是原始的字節流數據,除非 GL_PIXEL_UNPACK_BUFFER
緩存對象會修改原始字節的內容?)
// 1 : 直接在 glTextureSubImage2D 的最後一個參數設置數據
// 2 : 直接在 GL_PIXEL_UNPACK_BUFFER 中讀取
#define GET_IMG_DATA_TYPE 1 // 使用 2 類型的緩存對象方式來加載紋理對象數據會有錯誤
#if GET_IMG_DATA_TYPE == 2 // 如果需要從GL_PIXEL_UNPACK_BUFFER中讀取紋理數據的話
glCreateBuffers(1, &pixelBufObject); // 創建緩存對象
checkGLError();
glNamedBufferStorage( // 給指定緩存對象配置參數、設置數據
pixelBufObject, // 要配置的緩存對象
sizeof(img_data), // 要分配多少字節緩存大小(sizeof(img_data)的大小)
img_data, // 使用 img_data 初始化字節數據,如果填入 NULL,就是不使用數據初始化
0); // flag 標記位暫時填入0
checkGLError();
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelBufObject); // 將pixelBufObject緩存對象作爲 GL_PIXEL_UNPACK_BUFFER 目標緩存對象
checkGLError();
#endif
glTextureSubImage2D( // 給 texture 紋理對象設置對應 mipmap 層級的數據
texture, // 要設置的 texture 紋理對象
0, // mipmaps 的層級索引,從0開始,mipmaps 的
0, 0, // 要從 x,y 偏移多少開始,不要偏移所以都填0
img_w, img_h, // 要填入的行、列尺寸的像素數量
GL_RGBA, GL_UNSIGNED_BYTE, // 外部格式,指定要包含的分量數量 和 分量類型
#if GET_IMG_DATA_TYPE == 1
img_data // 外部圖片數據
#else
NULL // 先填充GL_PIXEL_UNPACK_BUFFER數據,這裏傳入的是NULL從GL_PIXEL_UNPACK_BUFFER緩存數據的字節偏移NULL就是0偏移
#endif
);
// 查看有無錯誤
checkGLError(); // GET_IMG_DATA_TYPE 2 時會有錯誤
之前我們有講到 內部格式,上面的代碼註釋中有講到 外部格式 它時告訴 OpenGL 傳入的數據是什麼格式,好讓它轉換爲 內部數據。
外部格式 有量個參數可以決定:format
,type
。
format 的枚舉值決定:
GL_RED
, GL_RG
, GL_RGB
, GL_BGR
, GL_RGBA
, GL_BGRA
, GL_DEPTH_COMPONENT
, 和 GL_STENCIL_INDEX
。
type 的枚舉值決定分量字節數據分佈:
GL_UNSIGNED_BYTE
, GL_BYTE
, GL_UNSIGNED_SHORT
, GL_SHORT
, GL_UNSIGNED_INT
, GL_INT
, GL_FLOAT
, GL_UNSIGNED_BYTE_3_3_2
, GL_UNSIGNED_BYTE_2_3_3_REV
, GL_UNSIGNED_SHORT_5_6_5
, GL_UNSIGNED_SHORT_5_6_5_REV
, GL_UNSIGNED_SHORT_4_4_4_4
, GL_UNSIGNED_SHORT_4_4_4_4_REV
, GL_UNSIGNED_SHORT_5_5_5_1
, GL_UNSIGNED_SHORT_1_5_5_5_REV
, GL_UNSIGNED_INT_8_8_8_8
, GL_UNSIGNED_INT_8_8_8_8_REV
, GL_UNSIGNED_INT_10_10_10_2
, 和 GL_UNSIGNED_INT_2_10_10_10_REV
。
也可以從紋理對象中讀取數據到內存
其實也可以從紋理對象中讀取圖像數據:
- OpenGL4.5- 使用 glGetTexImage
- OpenGL4.5+ 使用 glGetTexImage/glGetnTexImage/glGetTextureImage
紋理單元
我們的紋理對象的數據最終是要在着色器中被調用的,着色器調用是通過 綁定了紋理元的採樣器 對,這樣就可以採樣到 紋理單元上綁定的紋理對象 的數據了。
紋理單元(Texture Unit),你可以理解爲着色器程序中預先分配好的指向紋理指針對象。但是他們的數量是有限的。
如果獲取你本機設備上的 OpenGL 查看的紋理單元的支持最大數量,可以使用 glGetIntegerv
使用 GL_MAX_TEXTURE_IMAGE_UNITS
參數來獲取,如下:
// 打印着色器支持最大的紋理圖像單元的數量
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;
一般機器上都能支持 16 個紋理單元。
我的筆記本(2018年的頂配遊戲本)上運行上面的代碼輸出:
Maximun number of texture image units : 32
Maximun number of Combined texture image units : 192
紋理單元的支持最大數量 這個結果在不同的操作系統,不同的硬件的 OpenGL 實現,不同的 OpenGL 版本,都會有可能不一樣的結果。
先激活紋理單元
紋理單元要想正常使用,首先這個單元得先激活,可以使用:glActiveTexture
glActiveTexture(GL_TEXTURE0); // 默認第 0 個紋理單元是激活的,可以不用設置
激活紋理單元的用法與之前的 glBindxxxx 的很類似,因爲 OpenGL 底層實現就是一個狀態機,多數的操作對象都先要設置爲當前要操作的對象,後續的操作函數都會對之前激活、綁定的對象進行處理、配置、等。
注意:在有些代碼中,可能會沒看到對 GL_TEXTURE0
紋理單元的激活,因爲第 0 索引的紋理單元它默認是激活的。
它的參數 GL_TEXTURE[N]
中的 N
可以是 0
到 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1
的範圍。小於 0 或是超出最大支持數量都會出錯。
有些 GLAD 的頭文件的定義中,最大也就 GL_TEXTURE32
個。
但是,如果我們需要再着色器中 使用 對 多個紋理單元 的使用,就 需要激活多個對應的紋理單元。
創建紋理對象後,要在着色器中使用的話,需要綁定到 紋理單元 ,可使用 glBindTextureUnit:
glBindTextureUnit(0, texture); // 綁定第 0 索引的紋理單元,OpenGL 4.0+建議用這個,與 glCreateTextures 配對。 OpenGL4.0-可用 glBindTexture
如果綁定時的 紋理對象ID 是不存在的,可使用:glGetError 來獲取OpenGL 當前運行環境中的錯誤枚舉:返回的是:GLenum。通常是:
#define GL_INVALID_ENUM 0x0500
#define GL_INVALID_VALUE 0x0501
#define GL_INVALID_OPERATION 0x0502
下面是綁定一個不存在的紋理對象ID:
glBindTextureUnit(0, 99); // 第 0 索引的紋理單元,綁定一個不存在的紋理對象ID:99
GLenum error = glGetError();// 獲取 OpenGL 運行環境的錯誤
if (error != 0) {
std::cout << std::dec; // 以10進制顯示數值
std::cout << "glError : " << error;
std::cout << std::hex; // 以16進制顯示數值
std::cout << "(0x" << error << ")" << std::endl;
}
/* 輸出:
glError : 1282(0x502)
*/
採樣器
上述中,有題到 採樣器類型,其中 採樣器,是在着色器程序(Shader Program)中的子着色器程序(Shader)使用的。
一般來說,你在應用程序層創建的 紋理對象 的類型,要與 着色器程序中的 採樣器 的類型也是要一一對應的,否則會有問題。
上述的表格有列出了他們一一對應的關係表。
早期的 OpenGL4.5 前的都是使用紋理單元對應的紋理對象,與採樣器的類型匹配的方式來使用的,可讀性真的很差,我也不知道這麼個全球流行的開源渲染系統的設計規範,設計這麼拙劣(這也是導致學習成本比較高的原因之一,更高的原因是 API 名稱也不友好,很多API命名真的無語)。到了 OpenGL 4.5 的 API 設計上就好很多了,它可以創建採樣器,設置採樣器參數等,然後還可以綁定採樣器對應的採樣的紋理單元。
在 OpenGL 4.5 之前,就經過前面設置就可以在着色器添加對應的採樣器就可以運作了。
在 OpenGL 4.5 之後,雖然也可以兼容,但是它可以更清晰的設置那個紋理單元使用那個採樣器來採樣。
我們先對 頂點着色器 和 片元着色器 添加對應的 紋理座標 變量,後續再給應用層的頂點數據添加 紋理座標 數據。
首先我們對 着色器文件的目錄調整了:
- 在 Dependencies/Shaders/ 下添加了 TestingTexture 的目錄
- 並在 TestingTexture 目錄複製了之前的 Shaders/shader1.vert, Shaders/shader1.frag 的兩個文件,再調整內容。
testing_tex_shader.vert 的內容爲:
// jave.lin - testing_tex_shader.vert - 測試紋理的頂點着色器
#version 450 compatibility
uniform mat4 transformMat;
attribute vec3 vPos;
attribute vec3 vCol;
attribute vec2 vUV;
varying vec3 fCol;
varying vec2 fUV;
void main() {
gl_Position = transformMat * vec4(vPos, 1.0);
fCol = vCol;
fUV = vUV;
}
注意 頂點着色器 中我們添加了:
testing_tex_shader.frag 的內容爲:attribute vec2 vUV;
、varying vec2 fUV;
至於 attribute
、varying
之前有講過,前者是頂點着色器專用的屬性,後者是頂點着色器的數據在光柵化插值後再傳入片段着色器的。這裏就不再詳細說明了。
testing_tex_shader.frag 的內容爲:
// jave.lin - testing_tex_shader.frag - 測試紋理的片段着色器
#version 450 compatibility
varying vec3 fCol;
varying vec2 fUV;
uniform sampler2D tex;
void main() {
vec3 texCol = texture(tex, fUV).rgb;
gl_FragColor = vec4(fCol * texCol, 1.0);
}
varying vec2 fUV;
是頂點着色器傳過來的紋理座標數據。vec3 texCol = texture(tex, fUV).rgb;
中,texture
是 GLSL內置函數- 第一個參數 是 採樣器(這個 採樣器已綁定了 我們之前應用層設置的 紋理單元,而 紋理單元也綁定了 我們設置的 紋理對象,所以 該採樣器 最終會採樣 到我們綁定的 紋理對象中的圖像數據)
- 第二個參數 是 紋理座標,這個方法有多個重載,這裏我們只用到其中一個。
- 返回值 返回一個
vec4
類型,但是填充了數據的只會按我們之前設置的 外部格式,內部格式 來決定的,如果返回的是一個分量的vec4
,那麼vec4
中第一個分量中才是我們想要的數值,其他都是默認值。在這裏我們返回的是vec4
的3
個對我們的邏輯來說纔是有效的分量,所以我們採樣出來的vec4
,再用 swizzle 語言來獲取rgb
前3
個分量,並將結果賦值給 texCol 變量。
gl_FragColor = vec4(fCol * texCol, 1.0);
最後我們用頂點顏色與紋理顏色相乘混合了。
texture 的各重載原型定義:
gvec4 texture(gsampler1D tex, float P[, float bias]);
gvec4 texture(gsampler2D tex, vec2 P[, float bias]);
gvec4 texture(gsampler3D tex, vec3 P[, float bias]);
gvec4 texture(gsamplerCube tex, vec3 P[, float bias]);
gvec4 texture(gsampler1DArray tex, vec2 P[, float bias]);
gvec4 texture(gsampler2DArray tex, vec3 P[, float bias]);
gvec4 texture(gsampler2DRect tex, vec2 P[, float bias]);
gvec4 texture(gsamplerCubeArray tex, vec4 P[, float bias]);
從名爲 tex
的採樣器中採樣一個紋素,對應的紋理座標爲 P
。如果對象支持 mipmap
,並且設置了 bias
,那麼這個參數將用於 mipmap 細節層次(level-of-detail)的偏移量計算,來判斷採樣應當在哪一層進行。函數的返回值是一個包含了採樣後的紋理數據的向量。
這裏有一個專業術語上的解釋:對於很多 GLSL 函數的原型,我們都可以看到 gvec4
(或者其他維度的向量)這樣的定義。它實際上是一個“佔位符”,表示任何類型的一個向量。它可以用來表達 vec4
、ivec4
或uvec4
。同理,gsampler2D
也是一個這樣的佔位符,它可以表達 sampler2D
、isampler2D
或者usampler2D
類型。此外,我們在書寫函數參數的時候如果添加了方括號([和]),說明這個參數是可選的,可以忽略不計。
OpenGL 4.5 後,我們可以使用 glCreateSamplers 來創建我們自定義的採樣器。
但是因爲 OpenGL 才創建紋理對象時,會給他包含上一個默認配置的採樣器,所以我們一般可以不同區設置。
但一般什麼情況下會去使用呢?
因爲着色器中的採樣器對象/單元數量都是有限的,但你的紋理有很多個,多到超過了採樣器數量的上限。
這就可以通過抽象出多個紋理對象的相同採樣配置,來設置他們的採樣器位同一個採樣器即可。
可以通過 glBindSampler、glBindSamplers 來處理:
GLuint sampler_object;
...
glCreateSamplers(1, &sampler_object); // 創建採樣器,OpenGL 4.5+纔有的API,可讀性更高,因爲 OpenGL 默認的給每一個紋理對象都包含了一個默認配置的採樣器對象,沒必要使用,因爲有默認的,除非你想多給紋理單元都使用同一個採樣器來採樣時,就可以通過這種方式來指定
glBindSampler(0, sampler_object); // 將紋理單元0 的採樣器綁定爲 sampler_object的
我們現在就不用這個自定義的了。先暫時用着默認的。
紋理系統要區別好這幾個玩意兒:
- 紋理對象
- 紋理單元
- 採樣器對象/單元
這裏我引用一下 《OpenGL 編程指南》第9版說的(其實這是一本很權威的數據,畢竟三個作者都是非常厲害的,都是 OpenGL 開發核心人員,OpenGL 規範制定人員),但是,我看了一些內容,說得不夠透徹,該說的沒有說清楚。我反正是把它引用過來了,大家能否看懂是另一回事,我個人覺得有些說明前後上是後矛盾的。
我們可以通過着色器中帶有紋理單元信息的採樣器變量來讀取紋理,使用GLSL內置的函數從紋理圖像中讀取紋素。而紋素讀取的方式依賴於另一個對象中的參數,名爲採樣器對象(sampler object)。採樣器對象會綁定到採樣器單元,這類似於紋理對象綁定到紋理單元。爲了簡便起見,我們在每一個紋理對象中包含了一個默認內置的採樣器對象,如果沒有把採樣器對象綁定到專門的採樣器單元,這個對象可以用來從紋理對象中讀取數據。
應用層程序添加頂點屬性
GLfloat uvs[] = { // 頂點的 uv 座標
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
1.0f, 1.0f, // 右上角
0.0f, 1.0f, // 左上角
};
...
GLint vuv_location;
GLuint vertex_buffer[4]; // 原來是3的大小,現在改爲4,作爲uv用
// shader program init 5 - 根據shader源碼的相對路徑(變量),加載deps下的shader
char vs_path[MAX_PATH], fs_path[MAX_PATH];
g_GetShaderPathCallback(vs_path, "TestingTexture\\testing_tex_shader.vert");
g_GetShaderPathCallback(fs_path, "TestingTexture\\testing_tex_shader.frag");
if (!shaderProgram->initByPath(vs_path, fs_path)) {
std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 輸出shader program錯誤
exit(EXIT_FAILURE);
}
...
vuv_location = shaderProgram->getAttributeLoc("vUV"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
...
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[2]
glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW); // 設置 VBO uv數據
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[3],因爲後面要設置該 VBO 的uv格式
glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vUV 格式
sizeof(GLfloat) * 2, (GLvoid*)0);
glEnableVertexAttribArray(vuv_location); // 啓用 頂點緩存 location uv的屬性
設置好紋理座標的頂點數據後,現在的頂點數據就變成這樣的了:
運行效果
完整源碼
// 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.25f, -0.25f, 0.0f, // 第0個頂點,左下角
0.25f, -0.25f, 0.0f, // 第1個頂點,右下角
0.25f, 0.25f, 0.0f, // 第2個頂點,右上角
-0.25f, 0.25f, 0.0f, // 第3個頂點,左上角
};
GLfloat colors_1[] = { // 頂點顏色緩存數據1
1.0f, 0.0f, 0.0f, // 第0個頂點顏色
0.0f, 1.0f, 0.0f, // 第1個頂點顏色
1.0f, 1.0f, 0.0f, // 第2個頂點顏色
0.0f, 0.0f, 1.0f, // 第3個頂點顏色
};
GLfloat colors_2[] = { // 頂點顏色緩存數據2
1.0f, 1.0f, 0.0f, // 第0個頂點顏色
0.0f, 1.0f, 1.0f, // 第1個頂點顏色
1.0f, 1.0f, 1.0f, // 第2個頂點顏色
1.0f, 0.0f, 1.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
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_Texture", 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 mat_location;
GLint vpos_location, vcol_location, vuv_location;
GLuint vertex_buffer[4], index_buffer;
GLuint vertex_array_object[2];
GLuint sampler_object;
GLuint texture;
GLuint pixelBufObject;
GLint success, infoLogLen;
//glBindTextureUnit(0, 99); // 第 0 索引的紋理單元,綁定一個不存在的紋理對象ID:99
//checkGLError()
// 用 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;
};
// loading texture here...
// 加載紋理需要用的圖片數據
char img_path[MAX_PATH];
g_GetPicturePathCallback(img_path, "my_tex.png"); // 獲取圖片目錄
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軸增量方向不同,所以需要翻轉垂直方向的行數數據
glCreateTextures(GL_TEXTURE_2D, 1, &texture); // 創建紋理對象
//glActiveTexture(GL_TEXTURE0); // 默認第 0 個紋理單元是激活的,可以不用設置
glBindTextureUnit(0, texture); // 綁定第 0 索引的紋理單元,OpenGL 4.0+建議用這個,與 glCreateTextures 配對。 OpenGL4.0-可用 glBindTexture
// 查看有無錯誤
checkGLError();
//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範圍後的數值環繞方式,GL_REPEAT 是重複的
//glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT); // 設置 texture 紋理對象的 GL_TEXTURE_WRAP_T 參數,就是設置 uv 中的水平 v 座標超出0~1範圍後的數值環繞方式,GL_REPEAT 是重複的
//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();
// 1 : 直接在 glTextureSubImage2D 的最後一個參數設置數據
// 2 : 直接在 GL_PIXEL_UNPACK_BUFFER 中讀取
#define GET_IMG_DATA_TYPE 1 // 使用 2 類型的緩存對象方式來加載紋理對象數據會有錯誤
#if GET_IMG_DATA_TYPE == 2 // 如果需要從GL_PIXEL_UNPACK_BUFFER中讀取紋理數據的話
glCreateBuffers(1, &pixelBufObject); // 創建緩存對象
checkGLError();
glNamedBufferStorage( // 給指定緩存對象配置參數、設置數據
pixelBufObject, // 要配置的緩存對象
sizeof(img_data), // 要分配多少字節緩存大小(sizeof(img_data)的大小)
img_data, // 使用 img_data 初始化字節數據,如果填入 NULL,就是不使用數據初始化
0); // flag 標記位暫時填入0
checkGLError();
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelBufObject); // 將pixelBufObject緩存對象作爲 GL_PIXEL_UNPACK_BUFFER 目標緩存對象
checkGLError();
#endif
glTextureSubImage2D( // 給 texture 紋理對象設置對應 mipmap 層級的數據
texture, // 要設置的 texture 紋理對象
0, // mipmaps 的層級索引,從0開始,mipmaps 的
0, 0, // 要從 x,y 偏移多少開始,不要偏移所以都填0
img_w, img_h, // 要填入的行、列尺寸的像素數量
GL_RGBA, GL_UNSIGNED_BYTE, // 外部格式,指定要包含的分量數量 和 分量類型
#if GET_IMG_DATA_TYPE == 1
img_data // 外部圖片數據
#else
NULL // 先填充GL_PIXEL_UNPACK_BUFFER數據,這裏傳入的是NULL從GL_PIXEL_UNPACK_BUFFER緩存數據的字節偏移NULL就是0偏移
#endif
);
// 查看有無錯誤
checkGLError(); // GET_IMG_DATA_TYPE 2 時會有錯誤
//// 先填入第 0 層 mipmaps 數據
//glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img_w, img_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data);
// 如果前面給這個紋理對象指定了多層級 mipmaps,那麼可以使用 glGenerateMipmap
// void glGenerateMipmap(GLenum target);
// void glGenerateTextureMipmap(GLuint texture);
// 來給紋理的0層之後的其他mipmaps層生成圖像數據
//glGenerateMipmap(GL_TEXTURE_2D); // opengl 4.5之前的API,生成當前綁定紋理對象,且屬於 GL_TEXTURE_2D 類型的紋理對象的mipmaps
glGenerateTextureMipmap(texture);// opengl 4.5 API,生成指定紋理對象的mipmaps
// 查看有無錯誤
checkGLError();
// when loading complete.
// free image data here
stbi_image_free(img_data); // 紋理已經上傳到了顯存,內存中的數據可以刪除了
//glCreateSamplers(1, &sampler_object); // 創建採樣器,OpenGL 4.5+纔有的API,可讀性更高,因爲 OpenGL 默認的給每一個紋理對象都包含了一個默認配置的採樣器對象,沒必要使用,因爲有默認的,除非你想多給紋理單元都使用同一個採樣器來採樣時,就可以通過這種方式來指定
//// 查看有無錯誤
//checkGLError();
//glBindSampler(0, sampler_object); // 將紋理單元0 的採樣器綁定爲 sampler_object的
//// 查看有無錯誤
//checkGLError();
ShaderProgram* shaderProgram = new ShaderProgram;
//// shader program init 1 - 直接加載shader源碼方式
//if (!shaderProgram->initBySourceCode(vertex_shader_text, fragment_shader_text)) {
//// shader program init 2 - 加載shader源碼路徑方式,我真的是服了C++獲取當前運行目錄就這麼難嗎?
//char exeFullPath[512];
//char vs_path[512], fs_path[512];
//GetCurrentDirectoryA(1000, exeFullPath);
//sprintf_s(vs_path, "%s\\Debug\\%s", exeFullPath, "shader1.vert");
//sprintf_s(fs_path, "%s\\Debug\\%s", exeFullPath, "shader1.frag");
//if (!shaderProgram->initByPath(vs_path, fs_path)) {
//// shader program init 3 - 加載shader源碼的相對路徑,方面第二種方法的是絕對路徑
//if (!shaderProgram->initByPath("Debug\\shader1.vert", "Debug\\shader1.frag")) {
// std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 輸出shader program錯誤
// exit(EXIT_FAILURE);
//}
// // 這種宏定義只能處理常量路徑,所以如果要加載動態變量的路徑只能寫一個方法來處理
//#define GET_SHADER(name) "..\\..\\Dependencies\\Shaders\\"#name
// // shader program init 4 - 根據shader源碼的相對路徑(常量),加載deps下的shader
// if (!shaderProgram->initByPath(GET_SHADER(shader1.vert), GET_SHADER(shader1.frag))) {
// std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 輸出shader program錯誤
// exit(EXIT_FAILURE);
// }
// 用 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;
};
// shader program init 5 - 根據shader源碼的相對路徑(變量),加載deps下的shader
char vs_path[MAX_PATH], fs_path[MAX_PATH];
g_GetShaderPathCallback(vs_path, "TestingTexture\\testing_tex_shader.vert");
g_GetShaderPathCallback(fs_path, "TestingTexture\\testing_tex_shader.frag");
if (!shaderProgram->initByPath(vs_path, fs_path)) {
std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 輸出shader program錯誤
exit(EXIT_FAILURE);
}
mat_location = shaderProgram->getUniformLoc("transformMat"); // 獲取 着色器程序的 uniform 變量的 location
vpos_location = shaderProgram->getAttributeLoc("vPos"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
vcol_location = shaderProgram->getAttributeLoc("vCol"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
vuv_location = shaderProgram->getAttributeLoc("vUV"); // 獲取 頂點着色器中的頂點 attribute 屬性的 location
glGenVertexArrays(2, vertex_array_object); // 生成兩個 VAO
glGenBuffers(4, vertex_buffer); // 創建4個 VBO,這裏我們因爲有一個一樣的頂點座標,一個一樣的頂點UV,兩個不同的頂點顔色
glGenBuffers(1, &index_buffer); // 創建1個 EBO,因爲兩個 Quad 的頂點索引順序都是一樣的
//
// === VAO[0] ===
//
glBindVertexArray(vertex_array_object[0]); // 綁定 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(colors_1), colors_1, GL_STATIC_DRAW); // 設置 VBO 顏色數據
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[2]
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 的顏色格式
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vCol 格式
sizeof(GLfloat) * 3, (GLvoid*)0);
glEnableVertexAttribArray(vcol_location); // 啓用 頂點緩存 location uv的屬性
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[3],因爲後面要設置該 VBO 的uv格式
glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vUV 格式
sizeof(GLfloat) * 2, (GLvoid*)0);
glEnableVertexAttribArray(vuv_location); // 啓用 頂點緩存 location uv的屬性
//
// === VAO[1] ===
//
glBindVertexArray(vertex_array_object[1]); // 綁定 VAO[1],那麼之後的 vbo, ebo,的綁定指針都是指向該 VAO 中的,還有頂點格式(規範)都會保存在該 VAO
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]); // 綁定 VBO[1]
//glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 設置 VBO 座標數據,這裏不用再設置座標,因爲都一樣
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[2]); // 綁定 VBO[2]
glBufferData(GL_ARRAY_BUFFER, sizeof(colors_2), colors_2, GL_STATIC_DRAW); // 設置 VBO 顏色數據,顏色就需要重新設置了,因爲不一樣
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[2]
//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[2]); // 綁定 VBO[2],因爲後面要設置該 VBO 的顏色格式
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE, // 設置 頂點屬性 vCol 格式
sizeof(GLfloat) * 3, (GLvoid*)0);
glEnableVertexAttribArray(vcol_location); // 啓用 頂點緩存 location 位置的屬性
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]); // 綁定 VBO[3],因爲後面要設置該 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;
mat4x4 rMat, tMat, tranformMat; // 聲明定義一個 mat4x4 用的旋轉矩陣
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); // 清理顏色緩存
//glUseProgram(program); // 使用此着色器程序,兩個 VAO 的着色都一樣,設置一些 uniform 不一樣
shaderProgram->use();
glBindVertexArray(vertex_array_object[0]); // 先繪製 VAO[0] 的 VBO,EBO,VAF,ENABLED
mat4x4_identity(tMat); // 給矩陣單位化,消除之前的所有變換
mat4x4_translate(tMat, -0.5, 0.0f, 0.0f); // x軸位移-0.5,注意是NDC下的座標
//glUniformMatrix4fv(mat_location, 1, GL_FALSE, (const GLfloat*)tMat); // 設置, 着色器中 uniform mat4 rMat; 的矩陣數據
shaderProgram->setMatrix4x4(mat_location, (const GLfloat*)tMat); // 設置, 着色器中 uniform mat4 rMat; 的矩陣數據
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (GLvoid*)0); // 參數1:繪製三角圖元;參數2:取6個索引來繪製三角圖元(每個三角圖元需要3個,所以可以畫兩個三角圖元);參數3:將 GL_ELEMENT_ARRAY_BUFFER 每個元素視爲 uint 類型;參數4:設置索引緩存的字節偏移量。也可以設置爲另一個 緩存數據的指針,即:使用另一個數據。
glBindVertexArray(vertex_array_object[1]); // 先繪製 VAO[1] 的 VBO,EBO,VAF,ENABLED
mat4x4_identity(rMat); // 給矩陣單位化,消除之前的所有變換
mat4x4_rotate_Z(rMat, rMat, (float)glfwGetTime()); // 先旋轉,沿着 z 軸旋轉,旋轉量爲當前 glfw 啓用到現在的時間點(秒)
mat4x4_translate(tMat, +0.5, 0.0f, 0.0f); // 再位移
mat4x4_mul(tranformMat, tMat, rMat); // 將旋轉與位移的變換合併
//glUniformMatrix4fv(mat_location, 1, GL_FALSE, (const GLfloat*)tranformMat); // 設置, 着色器中 uniform mat4 rMat; 的矩陣數據
shaderProgram->setMatrix4x4(mat_location, (const GLfloat*)tranformMat); // 設置, 着色器中 uniform mat4 rMat; 的矩陣數據
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(); // 處理其他的系統消息
}
glDeleteSamplers(1, &sampler_object); // 測試刪除採樣器對象 SO
glDeleteBuffers(1, &pixelBufObject); // 測試刪除 BO
glDeleteBuffers(4, vertex_buffer); // 測試刪除 VBO
glDeleteBuffers(1, &index_buffer); // 測試刪除 EBO
glDeleteBuffers(2, vertex_array_object); // 測試刪除 VAO
glDeleteTextures(1, &texture); // 刪除紋理對象 TO
delete shaderProgram; // 銷燬 shader program
checkGLError(); // 最後再看看GL還有什麼錯誤
glfwDestroyWindow(window); // 銷燬之前創建的window對象
glfwTerminate(); // 清理glfw之前申請的資源
//checkGLError(); // 最後再看看GL還有什麼錯誤,從這句一直報錯可以看出:glfw 的銷燬與退出是有問題的
return 0;
} // int main() {
總結
紋理的使用簡述步驟爲:
- 創建紋理對象
- 設置紋理對象內部格式
- 加載圖片的圖像數據
- 設置外部格式數據加載到紋理對象
- 回收圖像數據
- 激活紋理單元(默認有激活第0個)
- 綁定紋理對象到指定的紋理單元上
- 添加頂點數據中的:紋理座標數據
- 創建紋理座標 vbo,設置指向頂點數據緩存對象
- 設置紋理座標 vbo 的格式(規範)
- 創建採樣器對象(這步可以不需要,默認紋理對象上有默認配置的採樣器)
- 綁定採樣器對象到指定的紋理單元(這步可以不需要,默認紋理對象上有默認配置的採樣器)
- 着色器添加採樣器(注意採樣器維度類型要與應用程序創建的紋理對象一直)
- 頂點着色器添加紋理座標 attribute、varying 變量
- 將 attribute 設置到 varying
- 片段着色器添加紋理座標 varying 變量
- 使用 紋理座標 varying 變量,來傳入 texture 內置函數來採樣紋理對象的數據(texture(sampler_tex_xxx, uv)