OpenGL 紋理詳解

rmb_999發表於2023-11-06

1. 紋理

在OpenGL中,紋理是一種常用的技術,用於將影像或圖案對映到3D模型的表面上,以增加圖形的細節和真實感

2. 紋理座標

紋理座標在x和y軸上,範圍為0到1之間(注意我們使用的是2D紋理影像)。使用紋理座標獲取紋理顏色叫做取樣(Sampling)。紋理座標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。下面的圖片展示了我們是如何把紋理座標對映到三角形上的。

 

 當圖片繪製大小與圖片實際大小不一致時,勢必會涉及到縮放。此時而縮放時我們要採取什麼插值方式,是需要我們指定給GL的,也就是說,我們要指定取樣方式。

3. 取樣方式

環繞方式

之前我們有說過,紋理的座標區域是[0,1],且通常左下角為紋理座標的(0,0)點。現在我們設想一下下面的情況:

 

 

當我們給出頂點座標是橫縱左邊均是0.5的四個象限的點時,我們的渲染區域即是第一個圖中的粉色區域。

此時我們有一張影像,即是第四個圖樣子的圖片。

我們知道,紋理座標系是[0,1]的,如果我們與頂點座標系中我們指定的四個頂點一一對應起來的時候,那就應該是渲染出第二幅圖的樣子。

但是如果我們並不是想影像A充滿我們的粉色區域怎麼辦?我們想讓影像只有粉色區域的1/2大小,並且居中平鋪,這時候怎麼辦呢?

試想如果大小1/2且居中對齊,那麼我們紋理座標系的(1,1)點應該對應頂點座標系的(0.25,0.25)。這點沒有問題對吧。但實際我們應用只應用頂點座標,那麼我們要換算一下頂點座標系中(0.5,0.5)對應的是紋理座標系中的那個點呢?換算完成後應該是(1.5,1.5)。這裡如何換算可以結合第三幅圖的樣子考慮下。這樣我們換算完四個頂點座標分別對應的紋理座標值後傳個頂點著色器就好。

另外我們只會渲染出我們頂點資料所渲染的圖形,超出邊屆的將會被剪裁掉。

事實上,我們只是想繪製一個比頂點區域要小的圖片,至於平鋪是我們選擇的一種環繞方式而已。

然而GL實際為我們提供了四種環繞方式:

 那麼知道了這幾種環繞方式,在GL中我們要如何設定環繞方式呢?

 當然,如果我們指定邊緣顏色的環繞模式,我們還要指定邊緣顏色。

 4. 紋理過濾

 

渲染一個影像,我們不可能保證繪製的實際大小即是圖片的實際大小,事實上一般情況下,我們都需要進行縮放。我們知道,GL中我們只指定頂點資料,而中間點都是GL內部自己採用插值器進行計算的。那麼當進行縮放時,我們就要告訴GL應該採用的插值方式。指定插值方式,又叫做紋理過濾

 

那麼縮放就涉及到影像的放大和縮小。我們先想一下放大影像應該採取什麼紋理濾鏡。

 

這裡我們先討論兩個較為重要的紋理濾鏡:GL_NEARESTGL_LINEAR

 

GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL預設的紋理過濾方式。當設定為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個畫素。下圖中你可以看到四個畫素,加號代表紋理座標。左上角那個紋理畫素的中心距離紋理座標最近,所以它會被選擇為樣本顏色:

 GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理畫素,計算出一個插值,近似出這些紋理畫素之間的顏色。一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近畫素的混合色:

 那麼這兩種紋理過濾方式有怎樣的視覺效果呢?讓我們看看在一個很大的物體上應用一張低解析度的紋理會發生什麼吧(紋理被放大了,每個紋理畫素都能看到):

 5. 多級漸遠紋理

上述中,我們敘述了放大的紋理濾鏡,但是如果是縮小呢?我們當然也可以採取之前提到的兩種紋理濾鏡。但當我們縮小的倍數足夠小時,計算插值將會是一個耗時過程,此外縮小本身就會丟失很多細節,這時如果我們仍使用原解析度的紋理進行縮放並繪製,無疑在記憶體上也是浪費。

 

如何建立多級漸遠紋理呢?我們可以使用glGenerateMipmaps函式。

那麼多級漸遠紋理有幾種模式呢:

 像放大時使用的紋理濾鏡一樣,我們應該像下面這樣設定縮小的紋理濾鏡:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
6. 載入與建立紋理
  1 #include <glad/glad.h>
  2 #include <GLFW/glfw3.h>
  3 #include <math.h>
  4 #include <iostream>
  5 #define STB_IMAGE_IMPLEMENTATION
  6 #include "stb_image.h"
  7 
  8 void framebuffer_size_callback(GLFWwindow* window, int width, int height);
  9 void processInput(GLFWwindow *window);
 10 GLFWwindow * configOpenGL();
 11 void loadImg(const char * path,unsigned int * texture,unsigned int uniteLoc);
 12 void configVAO(unsigned int * VAO,unsigned int * VBO,unsigned int * EBO);
 13 void finishiRenderLoop();
 14 // settings
 15 const unsigned int SCR_WIDTH = 800;
 16 const unsigned int SCR_HEIGHT = 600;
 17 
 18 
 19 const char *vertexShaderSource = "#version 330 core\n"
 20 "layout (location = 0) in vec2 aPos;\n"
 21 "layout (location = 1) in vec3 aColor;\n"
 22 "layout (location = 2) in float show;\n"
 23 "layout (location = 3) in vec2 aTexCoord;\n"
 24 "out vec3 ourColor;\n"
 25 "out float Img;\n"
 26 "out vec2 TexCoord;\n"
 27 "void main()\n"
 28 "{\n"
 29 "   gl_Position = vec4(aPos,0.0, 1.0);\n"
 30 "   ourColor = aColor;\n"
 31 "   Img = show;\n"
 32 "   TexCoord = aTexCoord;\n"
 33 "}\0";
 34 const char *fragmentShaderSource = "#version 330 core\n"
 35 "out vec4 FragColor;\n"
 36 "in vec3 ourColor;\n"
 37 "in float Img;\n"
 38 "in vec2 TexCoord;\n"
 39 "uniform sampler2D ourTexture;\n"
 40 "uniform sampler2D avatarTexture;\n"
 41 "uniform float factor;\n"
 42 "void main()\n"
 43 "{\n"
 44 "if (Img == 1.0f) {\n"
 45 "FragColor = mix(texture(ourTexture, TexCoord),texture(avatarTexture, TexCoord),factor) * vec4(ourColor, 1.0);\n"
 46 "} else {\n"
 47 "FragColor = vec4(ourColor, 1.0);\n"
 48 "}\n"
 49 "}\n\0";
 50 
 51 int main()
 52 {
 53     GLFWwindow * window = configOpenGL();
 54     
 55     ///建立一個頂點著色器
 56     int vertexShader = glCreateShader(GL_VERTEX_SHADER);
 57     
 58     ///附著原始碼並編譯
 59     glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
 60     glCompileShader(vertexShader);
 61     
 62     ///檢查編譯是否成功
 63     int success;
 64     char infoLog[512];
 65     glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
 66     if (!success)
 67     {
 68         glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
 69         std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
 70     }
 71     
 72     ///建立一個片段著色器
 73     int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
 74     
 75     ///附著原始碼並編譯
 76     glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
 77     glCompileShader(fragmentShader);
 78     
 79     ///檢查編譯是否成功
 80     glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
 81     if (!success)
 82     {
 83         glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
 84         std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
 85     }
 86     
 87     ///建立著色器程式
 88     int shaderProgram = glCreateProgram();
 89     
 90     ///連結著色器
 91     glAttachShader(shaderProgram, vertexShader);
 92     glAttachShader(shaderProgram, fragmentShader);
 93     glLinkProgram(shaderProgram);
 94     
 95     ///檢查連結是否成功
 96     glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
 97     if (!success) {
 98         glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
 99         std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
100     }
101     
102     ///釋放著色器
103     glDeleteShader(vertexShader);
104     glDeleteShader(fragmentShader);
105 
106     unsigned int VAO,VBO,EBO;
107     
108     ///配置VAO
109     configVAO(&VAO,&VBO,&EBO);
110     
111     ///設定紋理單元的位置(想要設定著色器程式的值,必先啟用著色器程式)
112     glUseProgram(shaderProgram);
113     glUniform1i(glGetUniformLocation(shaderProgram,"ourTexture"),0);
114     glUniform1i(glGetUniformLocation(shaderProgram,"avatarTexture"),1);
115     
116     while (!glfwWindowShouldClose(window))
117     {
118         processInput(window);
119         
120         ///設定清屏顏色
121         glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
122         ///清屏
123         glClear(GL_COLOR_BUFFER_BIT);
124         
125         ///使用指定著色器程式(由於上面已經啟用過著色器程式,所以此處不用再次啟用)
126 //        glUseProgram(shaderProgram);
127         
128         ///改變
129         float timeValue = glfwGetTime();
130         float factor = sin(timeValue) / 2.0f + 0.5f;
131         glad_glUniform1f(glGetUniformLocation(shaderProgram,"factor"),factor);
132         
133         ///繫結定點陣列物件
134         glBindVertexArray(VAO);
135         ///以索引繪製頂點資料
136 //        glDrawArrays(GL_TRIANGLES, 0, 3);
137         glDrawElements(GL_TRIANGLES,30,GL_UNSIGNED_INT,0);
138         
139         ///交換顏色緩衝
140         glfwSwapBuffers(window);
141         ///拉取使用者事件
142         glfwPollEvents();
143     }
144     
145     ///釋放物件
146     glDeleteVertexArrays(1, &VAO);
147     glDeleteBuffers(1, &VBO);
148     glDeleteBuffers(1, &EBO);
149     
150     finishiRenderLoop();
151     
152     return 0;
153 }
154 
155 GLFWwindow* configOpenGL() {
156     ///初始化glfw
157     glfwInit();
158     
159     ///設定版本號
160     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
161     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
162     
163     ///設定核心模式
164     glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
165     
166 #ifdef __APPLE__
167     ///設定對Mac OS X的相容
168     glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
169 #endif
170     
171     ///建立window
172     GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
173     if (window == NULL) {
174         std::cout << "Failed to create GLFW window" << std::endl;
175         glfwTerminate();
176         return NULL;
177     }
178     ///將window設定成當前上下文
179     glfwMakeContextCurrent(window);
180     ///設定視窗事件更新觸發的回撥
181     glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
182     
183     ///初始化GLAD
184     if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
185         std::cout << "Failed to initialize GLAD" << std::endl;
186         return NULL;
187     }
188     return window;
189 }
190 
191 void configVAO(unsigned int * VAO,unsigned int * VBO,unsigned int * EBO) {
192     ///頂點資料
193     float vertices[] = {
194         //頂點座標-2 //顏色-3 //是否繪製圖片-1 //紋理座標-2
195         0.5f, 0.5f,1.0f,1.0f,0.0f,1.0f,1.5f,1.5f,    // 右上角
196         0.5f, -0.5f,0.0f,1.0f,1.0f,1.0f,1.5f,-0.5f,  // 右下角
197         -0.5f, -0.5f,1.0f,0.0f,1.0f,1.0f,-0.5f,-0.5f,  // 左下角
198         -0.5f, 0.5f,1.0f,1.0f,1.0f,1.0f,-0.5f,1.5f,    // 左上角
199         1.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,0.0f,
200         1.0f,-1.0f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,
201         -1.0f,-1.0f,0.0f,1.0f,0.0f,0.0f,0.0f,0.0f,
202         -1.0f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,
203     };
204     
205     ///索引資料
206     unsigned int indices[] = {
207         0,1,3,
208         1,2,3,
209         0,4,5,
210         0,1,5,
211         1,5,6,
212         1,2,6,
213         2,6,7,
214         2,3,7,
215         3,7,4,
216         3,0,4,
217     };
218     
219     ///建立頂點陣列物件
220     glGenVertexArrays(1, VAO);
221     
222     ///建立頂點緩衝物件
223     glGenBuffers(1, VBO);
224     ///建立索引緩衝物件
225     glGenBuffers(1, EBO);
226     
227     ///繫結定點陣列物件至上下文
228     glBindVertexArray(*VAO);
229     
230     ///繫結定點緩衝物件至上下文
231     glBindBuffer(GL_ARRAY_BUFFER, *VBO);
232     
233     ///把頂點陣列複製到頂點緩衝物件中
234     glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
235     glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
236     glEnableVertexAttribArray(0);
237     glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(2 * sizeof(float)));
238     glEnableVertexAttribArray(1);
239     glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(5 * sizeof(float)));
240     glEnableVertexAttribArray(2);
241     glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
242     glEnableVertexAttribArray(3);
243     ///繫結索引緩衝物件至上下文
244     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, *EBO);
245     ///把索引資料複製到索引緩衝物件中
246     glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
247     
248     ///載入圖片
249     unsigned int texture,avatar;
250     loadImg("/Users/momo/Desktop/Wicky/Learn\ OpenGL/入門/Demos/6.紋理/OpenGL_Template/container.jpg", &texture,0);
251     loadImg("/Users/momo/Desktop/Wicky/Learn\ OpenGL/入門/Demos/6.紋理/OpenGL_Template/avatar.jpeg", &avatar, 1);
252     
253     ///解除頂點陣列物件的繫結
254     glBindVertexArray(0);
255     ///解除頂點緩衝物件的繫結
256     glBindBuffer(GL_ARRAY_BUFFER, 0);
257     ///解除索引緩衝物件的繫結
258     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
259 }
260 
261 void loadImg(const char * path,unsigned int * texture,unsigned int uniteLoc) {
262     ///設定圖片載入時上下翻轉
263     stbi_set_flip_vertically_on_load(true);
264     
265     ///載入圖片
266     int width, height, nrChannels;
267     unsigned char *data = stbi_load(path, &width, &height, &nrChannels, 0);
268     
269     ///生成紋理物件並繫結至上下文中的2D紋理
270     glGenTextures(1, texture);
271     glActiveTexture(GL_TEXTURE0 + uniteLoc);
272     glBindTexture(GL_TEXTURE_2D, *texture);
273     
274     ///設定紋理環繞及過濾模式
275     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
276     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
277     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
278     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
279     
280     ///載入紋理資料並設定多級漸遠紋理
281     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
282     glGenerateMipmap(GL_TEXTURE_2D);
283     
284     ///釋放影像資料
285     stbi_image_free(data);
286 }
287 
288 void finishiRenderLoop() {
289     ///釋放視窗資源
290     glfwTerminate();
291 }
292 
293 ///處理輸入
294 void processInput(GLFWwindow *window)
295 {
296     if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
297         glfwSetWindowShouldClose(window, true);
298 }
299 
300 ///視窗事件更新回撥
301 void framebuffer_size_callback(GLFWwindow* window, int width, int height)
302 {
303     ///設定視口大小
304     glViewport(0, 0, width, height);
305 }

7. 載入影像

那麼,我們先考慮如何載入圖片資料。這裡我們使用std_image.h進行影像載入。
接下來我們用std_image為我們提供的函式載入影像資料:

 8. 生成紋理

接下來我們基本就是用GL統一的模式去建立物件了:

這裡我們單獨講一下glTexImage2D這個函式。

  • 第一個引數指定了紋理目標(Target)。設定為GL_TEXTURE_2D意味著會生成與當前繫結的紋理物件在同一個目標上的紋理(任何繫結到GL_TEXTURE_1D和GL_TEXTURE_3D的紋理不會受到影響)。
  • 第二個引數為紋理指定多級漸遠紋理的級別,如果你希望單獨手動設定每個多級漸遠紋理的級別的話。這裡我們填0,也就是基本級別。
  • 第三個引數告訴OpenGL我們希望把紋理儲存為何種格式。我們的影像只有RGB值,因此我們也把紋理儲存為RGB值。
  • 第四個和第五個引數設定最終的紋理的寬度和高度。我們之前載入影像的時候儲存了它們,所以我們使用對應的變數。
  • 下個引數應該總是被設為0(歷史遺留的問題)。
  • 第七第八個引數定義了源圖的格式和資料型別。我們使用RGB值載入這個影像,並把它們儲存為char(byte)陣列,我們將會傳入對應值。
  • 最後一個引數是真正的影像資料。

有了影像資料,我們還要指定紋理座標到頂點座標資料中。同時我們要修改頂點著色器和片段著色器。並設定頂點屬性。與前文中繪製三角形時設定的基本相同。這裡我們只介紹如何在片段著色器中使用我們的紋理。

9. 紋理單元
GL中,一個紋理的位置值被稱為一個紋理單元。而GL中預設的紋理單元是0,且這個紋理單元是GL中預設啟用的。所以上述程式碼中,我們繫結紋理的時候,並沒有指定紋理單元,就是使用的預設的0這個單元。所以在片段著色器中宣告的取樣器,預設也是對應的紋理單元0。所以我們取到的也就是這個預設的紋理單元。

當使用多個紋理時,首先我們要啟用對應的紋理單元,然後在紋理單元中繫結紋理。在真正開始渲染之前,即進入渲染迴圈之前,我們還要告訴片段著色器每一個取樣器對應的是哪個紋理單元。

所以我們使用紋理的程式碼大概是這個樣子的:

那麼我們在片段著色器中,如果想要進行混合的話應該使用mix函式。

FragColor = mix(texture(ourTexture, TexCoord),texture(avatarTexture, TexCoord),factor) * vec4(ourColor, 1.0);

 

相關文章