开篇的话:其实混合和面剔除这两章内容还是蛮简单的。
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们混合(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。
透明的物体可以是完全透明的(让所有的颜色穿过),或者是半透明的(它让颜色通过,同时也会显示自身的颜色)。一个物体的透明度是通过它颜色的aplha值来决定的。Alpha颜色值是颜色向量的第四个分量,你可能已经看到过它很多遍了。在这个教程之前我们都将这个第四个分量设置为1.0,让这个物体的透明度为0.0,而当alpha值为0.0时物体将会是完全透明的。当alpha值为0.5时,物体的颜色有50%是来自物体自身的颜色,50%来自背后物体的颜色。
我们目前一直使用的纹理有三个颜色分量:红、绿、蓝。但一些材质会有一个内嵌的alpha通道,对每个纹素(Texel)都包含了一个alpha值。这个alpha值精确地告诉我们纹理各个部分的透明度。比如说,下面这个窗户纹理中的玻璃部分的alpha值为0.25(它在一般情况下是完全的红色,但由于它有75%的透明度,能让很大一部分的网站背景颜色穿过,让它看起来不那么红了),边框的alpha值是0.0。
有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。比如说草,如果想不太费劲地创建草这种东西,你需要将一个草的纹理贴在一个2D四边形(Quad)上,然后将这个四边形放到场景中。然而,草的形状和2D四边形的形状并不完全相同,所以你只想显示草纹理的某些部分,而忽略剩下的部分。
当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要丢弃(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。在此之前,我们还要学习如何加载一个透明的纹理。
要想加载有alpha值的纹理,我们并不需要改很多东西,stb_image
在纹理有alpha通道的时候会自动加载,但我们仍要在纹理生成过程中告诉OpenGL,我们的纹理现在使用alpha通道了:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
保证你在片段着色器中获取了纹理的全部4个颜色分量,而不仅仅是RGB分量:
void main()
{
FragColor = texture(texture1, TexCoords);
}
我们会创建一个vector,向里面添加几个glm::vec3
变量来代表草的位置:
vector vegetation;
vegetation.push_back(glm::vec3(-1.5f, 0.0f, -0.48f));
vegetation.push_back(glm::vec3( 1.5f, 0.0f, 0.51f));
vegetation.push_back(glm::vec3( 0.0f, 0.0f, 0.7f));
vegetation.push_back(glm::vec3(-0.3f, 0.0f, -2.3f));
vegetation.push_back(glm::vec3( 0.5f, 0.0f, -0.6f));
(模板类vector类,是一种动态数组,push_back 在数组的最后添加一个数据)
因为草的纹理是添加到四边形对象上的,我们还需要创建另外一个VAO,填充VBO,设置正确的顶点属性指针。接下来,在绘制完地板和两个立方体后,我们将会绘制草:
glBindVertexArray(vegetationVAO);
glBindTexture(GL_TEXTURE_2D, grassTexture);
for(unsigned int i = 0; i < vegetation.size(); i++)
{
model = glm::mat4();
model = glm::translate(model, vegetation[i]);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
GLSL给了我们discard
命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。有了这个指令,我们就能够在片段着色器中检测一个片段的alpha值是否低于某个阈值,如果是的话,则丢弃这个片段,就好像它不存在一样:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
FragColor = texColor;
}
我们检测被采样的纹理颜色的alpha值是否低于0.1的阈值,如果是的话,则丢弃这个片段。片段着色器保证了它只会渲染不是(几乎)完全透明的片段。
采样纹理的边缘的时候,OpenGL会对边缘的值和纹理下一个重复的值进行插值(因为我们将它的环绕方式设置为了GL_REPEAT。这通常是没问题的,但是由于我们使用了透明值,纹理图像的顶部将会与底部边缘的纯色值进行插值。这样的结果是一个半透明的有色边框,你可能会看见它环绕着你的纹理四边形。要想避免这个,每当你alpha纹理的时候,请将纹理的环绕方式设置为GL_CLAMP_TO_EDGE:
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
虽然直接丢弃片段很好,但它不能让我们渲染半透明的图像。我们要么渲染一个片段,要么完全丢弃它。要想渲染有多个透明度级别的图像,我们需要启用混合(Blending)。和OpenGL大多数的功能一样,我们可以启用GL_BLEND来启用混合:
glEnable(GL_BLEND);
启用了混合之后,我们需要告诉OpenGL它该如何混合。
(混合的原理,这里省去······)
OpenGL中的混合是通过下面这个方程来实现的:
glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。OpenGL为我们定义了很多个选项,我们将在下面列出大部分最常用的选项。
选项 | 值 |
---|---|
GL_ZERO |
因子等于00 |
GL_ONE |
因子等于11 |
GL_SRC_COLOR |
因子等于源颜色向量C¯sourceC¯source |
GL_ONE_MINUS_SRC_COLOR |
因子等于1−C¯source1−C¯source |
GL_DST_COLOR |
因子等于目标颜色向量C¯destinationC¯destination |
GL_ONE_MINUS_DST_COLOR |
因子等于1−C¯destination1−C¯destination |
GL_SRC_ALPHA |
因子等于C¯sourceC¯source的alphaalpha分量 |
GL_ONE_MINUS_SRC_ALPHA |
因子等于1−1− C¯sourceC¯source的alphaalpha分量 |
GL_DST_ALPHA |
因子等于C¯destinationC¯destination的alphaalpha分量 |
GL_ONE_MINUS_DST_ALPHA |
因子等于1−1− C¯destinationC¯destination的alphaalpha分量 |
GL_CONSTANT_COLOR |
因子等于常数颜色向量C¯constantC¯constant |
GL_ONE_MINUS_CONSTANT_COLOR |
因子等于1−C¯constant1−C¯constant |
GL_CONSTANT_ALPHA |
因子等于C¯constantC¯constant的alphaalpha分量 |
GL_ONE_MINUS_CONSTANT_ALPHA |
因子等于1−1− C¯constantC¯constant的alphaalpha分量 |
OpenGL甚至给了我们更多的灵活性,允许我们改变方程中源和目标部分的运算符。当前源和目标是相加的,但如果愿意的话,我们也可以让它们相减。glBlendEquation(GLenum mode)允许我们设置运算符,它提供了三个选项:
通常我们都可以省略调用glBlendEquation,因为GL_FUNC_ADD对大部分的操作来说都是我们希望的混合方程。
我们将会在场景中添加几个半透明的窗户
初始化时我们启用混合,并设定相应的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
把片段着色器还原:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
此时查看会有bug(最前面的窗户的透明部分遮蔽了背后的窗户)
发生这一现象的原因是,深度测试和混合一起使用的话会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。结果就是窗户的整个四边形不论透明度都会进行深度测试。即使透明的部分应该显示背后的窗户,深度测试仍然丢弃了它们。
所以我们不能随意地决定如何渲染窗户,让深度缓冲解决所有的问题了。这也是混合变得有些麻烦的部分。要想保证窗户中能够显示它们背后的窗户,我们需要首先绘制背后的这部分窗户。这也就是说在绘制的时候,我们必须先手动将窗户按照最远到最近来排序,再按照顺序渲染。
注意,对于草这种全透明的物体,我们可以选择丢弃透明的片段而不是混合它们,这样就解决了这些头疼的问题(没有深度问题)。
要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:
(接下来是一些我也看不懂的代码·····)
std::map sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
for(std::map::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
排序透明物体的一个比较简单的实现,它能够修复之前的问题
源码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
unsigned int loadTexture(const char *path);
// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
float cubeVertices[] = {
// positions // texture Coords
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
float planeVertices[] = {
// positions // texture Coords (note we set these higher than 1 (together with GL_REPEAT as texture wrapping mode). this will cause the floor texture to repeat)
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, 5.0f, 0.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, -5.0f, 2.0f, 2.0f
};
float transparentVertices[] = {
// positions // texture Coords (swapped y coordinates because texture is flipped upside down)
0.0f, 0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f, 1.0f,
1.0f, -0.5f, 0.0f, 1.0f, 1.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f,
1.0f, -0.5f, 0.0f, 1.0f, 1.0f,
1.0f, 0.5f, 0.0f, 1.0f, 0.0f
};
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LightingMaps", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
//在调用这个函数之后,无论我们怎么去移动鼠标,光标都不会显示了,它也不会离开窗口。
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Shader ourShader("shader/blend_vertex.vs", "shader/blend_fragment.vs");
unsigned int cubeVAO, cubeVBO;
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &cubeVBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glBindVertexArray(0);
// plane VAO
unsigned int planeVAO, planeVBO;
glGenVertexArrays(1, &planeVAO);
glGenBuffers(1, &planeVBO);
glBindVertexArray(planeVAO);
glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), &planeVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glBindVertexArray(0);
//transparent VAO
unsigned int transparentVAO, transparentVBO;
glGenVertexArrays(1, &transparentVAO);
glGenBuffers(1, &transparentVBO);
glBindVertexArray(transparentVAO);
glBindBuffer(GL_ARRAY_BUFFER, transparentVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(transparentVertices), transparentVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glBindVertexArray(0);
unsigned int cubeTexture = loadTexture("container.jpg");
unsigned int floorTexture = loadTexture("container.jpg");
//unsigned int transparentTexture = loadTexture("grass.png");
unsigned int transparentTexture = loadTexture("blending_transparent_window.png");
vector windows
{
glm::vec3(-1.5f, 0.0f, -0.48f),
glm::vec3(1.5f, 0.0f, 0.51f),
glm::vec3(0.0f, 0.0f, 0.7f),
glm::vec3(-0.3f, 0.0f, -2.3f),
glm::vec3(0.5f, 0.0f, -0.6f)
};
ourShader.use();
ourShader.setInt("texture1", 0);
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// don't forget to enable shader before setting uniforms
ourShader.use();
// view/projection transformations
glm::mat4 model;
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
ourShader.setMat4("projection", projection);
ourShader.setMat4("view", view);
// cubes
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
model = glm::mat4();
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// floor
glBindVertexArray(planeVAO);
glBindTexture(GL_TEXTURE_2D, floorTexture);
ourShader.setMat4("model", glm::mat4());
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
// vegetation
glBindVertexArray(transparentVAO);
glBindTexture(GL_TEXTURE_2D, transparentTexture);
std::map sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
for (std::map::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &planeVAO);
glDeleteBuffers(1, &cubeVBO);
glDeleteBuffers(1, &planeVBO);
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
//glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
void mouse_callback(GLFWwindow*window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
unsigned int loadTexture(char const *path)
{
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
(这张比较好理解,我甚至没有敲,主要就是讲剔除正反面)
尝试在脑子中想象一个3D立方体,数数你从任意方向最多能同时看到几个面。如果你的想象力不是过于丰富了,你应该能得出最大的面数是3。你可以从任意位置和任意方向看向这个球体,但你永远不能看到3个以上的面。所以我们为什么要浪费时间绘制我们不能看见的那3个面呢?如果我们能够以某种方式丢弃这几个看不见的面,我们能省下超过50%的片段着色器执行数!
超过50%而不是50%,因为从特定角度来看的话只能看见2个甚至是1个面。在这种情况下,我们就能省下超过50%了。
我们仍有一个问题需要解决:我们如何知道一个物体的某一个面不能从观察者视角看到呢?
如果我们想象任何一个闭合形状,它的每一个面都有两侧,每一侧要么面向用户,要么背对用户。如果我们能够只绘制面向观察者的面呢?
这正是面剔除(Face Culling)所做的。OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用(它们的开销很大!)。但我们仍要告诉OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face)。OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)。
当我们定义一组三角形顶点时,我们会以特定的环绕顺序来定义它们,可能是顺时针(Clockwise)的,也可能是逆时针(Counter-clockwise)的。每个三角形由3个顶点所组成,我们会从三角形中间来看,为这3个顶点设定一个环绕顺序。
我们首先定义了顶点1,之后我们可以选择定义顶点2或者顶点3,这个选择将定义了这个三角形的环绕顺序。下面的代码展示了这点:
float vertices[] = {
// 顺时针
vertices[0], // 顶点1
vertices[1], // 顶点2
vertices[2], // 顶点3
// 逆时针
vertices[0], // 顶点1
vertices[2], // 顶点3
vertices[1] // 顶点2
};
每组组成三角形图元的三个顶点就包含了一个环绕顺序。OpenGL在渲染图元的时候将使用这个信息来决定一个三角形是一个正向三角形还是背向三角形。默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形。
(如果还记得法线是怎么算出来的,就是按逆时针做向量的叉乘,所以逆时针的为正向三角形)
当你定义顶点顺序的时候,你应该想象对应的三角形是面向你的,所以你定义的三角形从正面看去应该是逆时针的。这样定义顶点很棒的一点是,实际的环绕顺序是在光栅化阶段进行的,也就是顶点着色器运行之后。这些顶点就是从观察者视角所见的了。
观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形。下面这张图显示了这个效果:
在顶点数据中,我们将两个三角形都以逆时针顺序定义(正面的三角形是1、2、3,背面的三角形也是1、2、3(如果我们从正面看这个三角形的话))。然而,如果从观察者当前视角使用1、2、3的顺序来绘制的话,从观察者的方向来看,背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的,它现在是以顺时针顺序渲染的了。这正是我们想要剔除(Cull,丢弃)的不可见面了!
在顶点数据中,我们定义的是两个逆时针顺序的三角形。然而,从观察者的方面看,后面的三角形是顺时针的,如果我们仍以1、2、3的顺序以观察者当面的视野看的话。即使我们以逆时针顺序定义后面的三角形,它现在还是变为顺时针。它正是我们打算剔除(丢弃)的不可见的面!
要想启用面剔除,我们只需要启用OpenGL的GL_CULL_FACE选项:
glEnable(GL_CULL_FACE);
从这一句代码之后,所有背向面都将被丢弃(尝试飞进立方体内部,看看所有的内面是不是都被丢弃了)。
OpenGL允许我们改变需要剔除的面的类型。我们可以调用glCullFace来定义这一行为:
glCullFace(GL_FRONT);
glCullFace函数有三个可用的选项:
GL_BACK
:只剔除背向面。GL_FRONT
:只剔除正向面。GL_FRONT_AND_BACK
:剔除正向面和背向面。glCullFace的初始值是GL_BACK。除了需要剔除的面之外,我们也可以通过调用glFrontFace,告诉OpenGL我们希望将顺时针的面(而不是逆时针的面)定义为正向面:
glFrontFace(GL_CCW);
默认值是GL_CCW,它代表的是逆时针的环绕顺序,另一个选项是GL_CW,它(显然)代表的是顺时针顺序。
OK,这节内容差不多就这么多,也很好理解。