title: OpenGL(5)之紋理初探
date: 2020-07-01 21:33
category: 圖形學
tags: opengl
鏈接:OpenGL(5)之紋理初探
1.概述
首先提出一個問題:什麼是紋理?
是由大塊的圖像數據組成的,可以用來繪製到物體的表面以增強其真實感。(紅寶書第六章本章目標中的內容)
再提一個問題:它能做什麼?
如上個問題所描述,既然是由大塊的圖像數據,那麼舉個例子,在片元處理階段,可以大量的使用到紋理,即對各個頂點着色的階段,就將原始的rgba顏色值轉換使用紋理相關數據進行上色,這樣一來節省了大量ragb顏色值爲了滿足複雜色值所帶來的開銷與操作難度。
紋理其實是一個2D圖片(也可以是1D或3D的),是由**紋素(texel)**組成,其中通常包含顏色數據信息。
紋理映射的概念
紋理好比一張繪製有磚塊的牆紙,然後粘在3D的房子上,這樣房子看起來就好像有了磚牆的外表,這就是紋理映射的概念。
將紋理映射到三角形上,需要指定三角形的每個頂點對應的紋理的哪個部分,每個頂點就會關聯一個紋理座標,該紋理座標用來表明從紋理圖像的哪個部分採樣(採集片段顏色),
之後片段着色器通過在這些頂點座標上進行插值即可。
紋理座標在x和y軸上,範圍爲0到1之間,使用紋理座標獲取紋理顏色叫做採樣(sampling)。
float texCoords[] ={
0.0f,0,0f,
1.0f,0,0f,
0.5f,1.0f
}
2.紋理環繞方式簡介
紋理座標的範圍是從(0,0)到(1,1)如果把紋理座標設置在範圍之外會發生什麼?
OpenGL默認的行爲是重複這個紋理圖像(比如座標點(0,2)
(1,2)這個範圍點是通過重複紋理圖像的形式來設置紋理的。)
環繞方式 | 描述 |
---|---|
GL_REPEAT | 對紋理的默認行爲,重複紋理圖像 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一樣,但每次重複圖片是鏡像放置的 |
GL_CLAMP_TO_EDGE | 紋理座標會被約束在0和1之間,超出部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出的座標爲用戶指定的邊緣顏色 |
當紋理座標超出默認範圍時,視覺效果如下:
關於設置上述環繞方式,可以使用glTexParameter*
函數來對單獨的一個座標軸進行設置(比如說紋理的座標軸爲s和t,(x,y)即你可以對其中的s或者t軸進行設置,設置爲針對於S軸進行上述環繞方式進行環繞);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,
GL_MIRRORED_REPEAT);
-
第一個參數指定了紋理座標,使用的是2D紋理
-
指定設置的選項與應用的紋理軸,人話就是以S或者T軸爲軸心進行環繞
-
設置環繞方式
如果使用了GL_CLAMP_TO_BORDER選項,即用戶爲超出範圍指定的邊緣的顏色,因此這裏選擇一個glTexParameter函數的fv後綴指定一個顏色。
float[] borderColor[] = {1.0f,1.0f,1.0f,0.0f};
glTexParameterfv(GL_TEXTURE_2D,GL_CLAMP_TO_BORDER,borderColor);
3.紋理過濾
紋理座標不依賴於分辨率,可以是任意浮點值,OpenGL需要知道怎麼樣將紋理像素映射到紋理座標上;
紋理座標是給模型頂點設置的數組,即與模型的頂點數據進行綁定;
紋理像素圖片中一個個的像素點;
OpenGL會通過模型頂點的紋理座標去查找紋理像素,然後通過採樣提取像素的顏色,那麼就完成了紋理像素通過紋理座標向紋理座標的映射。
當有一個很大的物體但是紋理分辨率很低的時候,我們就需要進行紋理的過濾操作,對於較遠的紋素,採用離中心點最接近紋理座標最近的那個像素進行處理,這就是一種紋理的過濾手段。
下面介紹的是過濾方式:
- GL_NEAREST(鄰近過濾),是OpenGL的一種默認紋理過濾方式,當設置爲這種方式的時候,OpenGL會選擇中心點最接近紋理座標的那個像素,
- GL_LINEAR(線性過濾),會基於紋理座標附近的紋理像素,計算出一個插值,近似這些紋理像素之間的顏色,這樣做的好處就是使得圖像看起來更加的平滑,視覺效果不會顯得突兀,比較順暢。
下圖中的圓圈代表紋理座標點,右邊方塊爲返回的顏色值。
何種情景會使用到這種紋理過濾方式呢?
當需要進行放大和縮小操作時。紋理被縮小的時候可以使用鄰近過濾,
當放大的時候使用線性過濾,因爲此時如果強行放大,不作處理導致圖片被拉伸,圖片顯得模糊,通過一種線性過濾的方式使得圖像更加平滑的過度。
使用glTexParameter*
函數爲放大和縮小指定過濾方式。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
多級漸遠紋理
紋理擁有與近處物體同樣高的分辨率,由於遠處的物體可能只產生較少的片段,因此OpenGL要從高分辨率紋理中爲較遠片段獲取正確的顏色值就很困難,因爲較遠處的顏色信息較少,且需要跨越紋理很大部分的片段採樣一個紋理顏色,在小物體上會產生不真實的感覺,再者說對於遠處片段使用高分辨率紋理浪費內存。
爲了解決上述遠處物體的採樣問題,OpenGL使用了一種多級漸遠紋理概念來解決這個問題,
具體如下:
就是通過一系列的紋理圖像,後一個紋理圖像是前一個的二分之一;
距離觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,也即是離的多遠該用多少分之一的紋理圖像,越遠紋理圖像更小(因爲是一直除以2嘛),可以看下圖:
其實上述的圖是手工畫的來模擬創建一系列的多級漸遠紋理,OpenGL提供了一個glGenerateMipmaps函數,在創建完一個紋理後,通過調用這個函數,OpenGL會幫助我們完成這些多級漸遠紋理的處理。
在渲染中切換多級漸遠紋理級別時候,會在不同級別紋理層產生不真實
的生硬邊界,因此通過不同多級漸遠紋理級別之間的過濾方式替代原有的過濾方式3.紋理過濾 介紹;
GL_XXX01_MIPMAP_XXX02;
使用XXX01方式插值進行採樣,
使用XXX02方式進行紋理處理;
if (XXX02.container(XXX_NEAREST))
{
cout<<"使用最鄰近多級漸遠紋理級別"<<endl;
}else{
cout<<"兩個鄰近多級漸遠紋理之間採取線性插值"<<endl;
}
過濾方式 | 描述| |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理來匹配像素大小,並使用鄰近插值進行紋理採樣 |
GL_LINEAR_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理級別,並使用線性插值進行採樣 |
GL_NEAREST_MIPMAP_LINEAR | 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行採樣 |
GL_LINEAR_MIPMAP_LINEAR | 在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行採樣 |
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
常見錯誤:將放大過濾的選項設置爲多級漸遠紋理過濾選項之一,這是一種錯誤的理念,因爲多級漸遠紋理主要是使用在紋理被縮小的情況下
4.加載與創建紋理
在使用紋理之前,需要將紋理加載到應用中,紋理圖像可能被存儲爲各種各樣的格式,每種都有自己的數據結構和排列,所以如何才能把這些圖像加載到應用中?
通用解決方案:
選一個需要的文件格式 比如.PNG,然後寫一個圖像加載器,把圖像轉化爲字節序列;
但是文件格式太多,不能都去寫支持的加載器。
更好的選擇:
使用一個支持多種流行格式的圖像加載庫來解決這個問題。
比如std_image.h庫
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
///......
通過定義STB_IMAGE_IMPLEMENTATION,預處理器會修改頭文件,
讓其只包含相關的函數定義源碼,等於是將stb_image.h頭文件變成了一個.cpp文件,只需要在你的程序中包含stb_image.h並且編譯就可以了。
使用stb_image.h加載圖片,需要使用stbi_load函數:
int width , height,nrChannels;
unsigned char* data = stbi_load("xxx.jpg",&width,&height,&nrChannels,0); // 將圖片轉換爲字符序列
5.生成紋理
- 紋理也是使用ID引用的,創建一個紋理對象;
unsigned int texture;
glGenTextures(1,&texture);
- 綁定紋理,是爲了之後的紋理指令配置得以生效
glBindTexture(GL_TEXTURE_2D,texture);
- 紋理綁定之後,通過載入的圖片數據生成一個紋理,
glTexImage2D(GL_TEXTURE_2D,//1
0,//2
GL_RGB,//3
width,//4
height,//5
0,//6
GL_RGB,//7
GL_UNSIGNED_BYTE,//8
data//9
);
glGenerateMipmap(GL_TEXTURE_2D);
-
第一個參數指定了紋理目標,設置爲GL_TEXTURE_2D意味着會生成與當前綁定的紋理對象是同一個目標上的紋理。(上述加黑步驟仔細體會)
-
第二個參數爲紋理指定多級漸遠紋理的級別,0爲基本級別
-
第三個參數告訴OpenGL希望把紋理存儲爲哪種格式,圖像只有RGB值,因此紋理存儲爲RGB值
-
第四個和第五個參數設置最終的紋理的寬度和高度,之前加載圖像的時候存儲了寬高,所以使用對應的變量。
-
第六個參數爲歷史遺留問題 總是爲0
-
第七個和第八個定義了源圖的格式和數據類型,使用RGB值加載這個圖像,並把它們存儲爲char(byte)數組;
-
最後一個參數爲真正的圖像數據。
上述調用之後,當前綁定的紋理對象就會被附加上紋理圖像,
如果要使用多級漸遠紋理,可以調用glGenerateMipmap,就會爲當前的紋理自動生成所有需要的多級漸遠紋理。
紋理生成流程:
unsigned int textureId;
glGenTextures(1,&textureId);
glBindTexture(GL_TEXTURE_2D,texture);
//configrure texture filter and rotate
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTExParamteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// load and generate texture
int width ,height,channels;
unsigned char * data = stbi_load("cc.jpg",&width,&height,&channels,0);
if(data){
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,
width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);
}else{
std::cout<<"Failed to load texture"<<std::endl;
}
stbi_image_free(data);
6.應用紋理
由於需要增加一組紋理座標導致頂點數組的數據越來越多了,這裏採用
EBO以及glDrawElements的方式來繪製,這樣一來就可以減少頂點數據,節省內存消耗。爲了OpenGL能夠採樣紋理,因此需要將紋理座標添加到頂點數據上,也就是將紋理座標與頂點數據進行一層映射;
float vertices[] = {
// -- 位置 --- -- 顏色 -- -- 紋理座標 --
0.5f,0.5f,0.0f, 1.0f,0.0f,0.0f, 1.0f,1.0f, //右上
-0.5f,0.5f,0.0f,0.0f,1.0f,0.0f, 0.0f,1.0f,//左上
0.5f,-0.5f,0.0f,0.0f,0.0f,1.0f,1.0f,0.0f,//右下
-0.5f,-0.5f,0.0f,1.0f,1.0f,0.0f,0.0f,0.0f//左下
};
添加了一個額外的頂點屬性,因此需要更新新的頂點格式,還有要同步更新屬性的步長信息,如下圖所示
glVertexAttribPointer(2,2,GL_FLOAT,8*sizeof(float),
(void*)(6*sizeof(float)));
glEnableVertexAttribArray(2);
相應的頂點着色器如下:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;
out vec3 shareColor;
out vec2 texCoord;
void main(){
gl_Position = vec4(aPos,1.0);
shareColor = aColor;
texCoord = aTexCoord;
}
相應的片段着色器如下:
#version 330 core
out vec4 FragColor;
in vec3 shareColor;
in vec2 texCoord;
uniform sampler2D shareTexture;
void main(){
FragColor = texture(shareTexture,texCoord)*vec4(shareColor,1.0);
}
上述的主要注意事項:sampler2D是一個GLSL針對紋理對象使用的內建數據類型,稱爲採樣器,通過這個數據類型將紋理添加到片段着色器。
還有就是通過GLSL的內建函數texture
來採樣紋理的顏色,第一個參數就是紋理採樣器,第二個參數就是對應的紋理座標。
7.紋理單元
紋理單元:一個紋理的位置值,可通過glUniformli函數給紋理採樣器分配一個位置值,如此,可以在一個片段着色器中設置多個紋理。
通過把紋理單元賦值給採樣器,可以一次性綁定多個紋理,通過後續的激活紋理手段就可以使用該紋理,多個紋理的綜合效果,就可以做一些比較有趣的事情了。
激活綁定紋理
glActiveTexture(GL_TEXTURE0);//GL_TEXTURE0 是默認的 比如只有一個
glBindTexture(GL_TEXTURE_2D,texture0);
// 兩個
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,texture1);
//....16
glActiveTexture(GL_TEXTURE16);
glBindTexture(GL_TEXTURE_2D,texture15);