1762 字
9 分钟
Cull Face

面剔除基本原理#

三角形顶点的定义环绕顺序,决定 GPU 判断正面、背面的规则。OpenGL 默认规则:顶点逆时针(CCW)环绕构成的面为正向面,正常保留渲染;顺时针环绕的背向面会被自动剔除,不参与后续渲染流程。

使用场景#

适用场景#

适用于立方体等封闭立体模型。封闭模型内部背面永远不会被看到,无需渲染,可安全开启面剔除。

禁用场景#

不适用于草面片、广告牌、面片模型等非封闭单面结构。这类模型需要正面、背面均可正常显示,必须禁用面剔除,否则会出现模型镂空、缺失问题。

性能优势#

片元着色器是渲染管线中开销最大的阶段,面剔除可在早期直接丢弃背面面片,使其完全不进入片元着色器计算,大幅节省性能。

相关OpenGL 指令#

开启面剔除#

// 默认:开启剔除,自动剔除背向面
glEnable(GL_CULL_FACE);

设置剔除面类型glCullFace#

用于指定需要被剔除的面片类型:

  • **GL_BACK:**剔除背向面(默认配置)
  • **GL_FRONT:**剔除正向面
  • **GL_FRONT_AND_BACK:**同时剔除正面、背面(模型完全不渲染)

重定义正向面规则 glFrontFace#

  • **GL_CCW:**逆时针环绕为正向面(默认)
  • **GL_CW:**顺时针环绕为正向面

正面规则与剔除逻辑组合效果#

顶点顺序分为 CCW(逆时针)CW(顺时针) 两种,通过搭配 glFrontFaceglCullFace 可灵活控制渲染面片:

  • 设置顺时针(GL_CW)为正面 + 剔除背面:仅保留背对摄像机的面
  • 设置逆时针(GL_CCW)为正面 + 剔除正面:仅保留背对摄像机的面

**顶点顺序(CCW / CW)**定义面的朝向属性,glFrontFace 判定何为正面,glCullFace 决定哪一类面需要被剔除。

example1: 尝试设置面剔除的相关指令#

main.cpp
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <myShader.h>
#include <myCamera.h>
#include <Model.h>
#include <iostream>
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 400;
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 firstCamera = true;
float deltaTime = 0.0f;
float lastFrame = 0.0f;
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE)) {
glfwSetWindowShouldClose(window, true);
}
if (glfwGetKey(window, GLFW_KEY_W)) {
camera.ProcessKeyboard(FORWARD, deltaTime);
}
if (glfwGetKey(window, GLFW_KEY_S)) {
camera.ProcessKeyboard(BACKWARD, deltaTime);
}
if (glfwGetKey(window, GLFW_KEY_A)) {
camera.ProcessKeyboard(LEFT, deltaTime);
}
if (glfwGetKey(window, GLFW_KEY_D)) {
camera.ProcessKeyboard(RIGHT, deltaTime);
}
}
void processMovement(GLFWwindow* window, double xpos, double ypos) {
if (firstCamera) {
lastX = static_cast<float>(xpos);
lastY = static_cast<float>(ypos);
firstCamera = false;
}
float xoffset = static_cast<float>(xpos) - lastX;
float yoffset = lastY - static_cast<float>(ypos);
lastX = static_cast<float>(xpos);
lastY = static_cast<float>(ypos);
camera.ProcessMouseMovement(xoffset, yoffset);
}
void processScroll(GLFWwindow* window, double xoffset, double yoffset) {
camera.ProcessScroll(static_cast<float>(yoffset));
}
unsigned int loadTexture(const char* path) {
unsigned int texture;
glGenTextures(1, &texture);
int width, height, nrChannels;
unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);
if (data) {
GLenum format;
if (nrChannels == 1) {
format = GL_RED;
}
else if (nrChannels == 3) {
format = GL_RGB;
}
else if (nrChannels == 4) {
format = GL_RGBA;
}
glBindTexture(GL_TEXTURE_2D, texture);
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, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
else {
cout << "failed to load tetxure" << endl;
}
stbi_image_free(data);
return texture;
}
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, "learnOpenGL", NULL, NULL);
if (window == NULL) {
cout << "failed to create GLFW window" << endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, processMovement);
glfwSetScrollCallback(window, processScroll);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
if (!gladLoadGLLoader(GLADloadproc(glfwGetProcAddress))) {
cout << "failed to initialize glad" << endl;
glfwTerminate();
return -1;
}
stbi_set_flip_vertically_on_load(true);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
glFrontFace(GL_CW);
Shader shader("src/Shader/vertexShader.txt", "src/Shader/fragmentShader.txt");
float cubeVertices[] = {
// Back face
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, // Bottom-left
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // top-right
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, // bottom-left
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, // top-left
// Front face
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, // bottom-left
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, 0.5f, 1.0f, 1.0f, // top-right
0.5f, 0.5f, 0.5f, 1.0f, 1.0f, // top-right
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, // top-left
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, // bottom-left
// Left face
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // top-right
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // top-left
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // bottom-left
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // bottom-left
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, // bottom-right
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // top-right
// Right face
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // top-left
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // bottom-right
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // bottom-right
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // top-left
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, // bottom-left
// Bottom face
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, // top-left
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, // bottom-left
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, // bottom-left
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, // bottom-right
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // top-right
// Top face
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, // top-left
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // top-right
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, // bottom-right
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, // top-left
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f // bottom-left
};
float planeVertices[] = {
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[] = {
0.0f, 0.5f, 0.0f, 0.0f, 1.0f,//左上
0.0f, -0.5f, 0.0f, 0.0f, 0.0f,//左下
1.0f, -0.5f, 0.0f, 1.0f, 0.0f,//右下
0.0f, 0.5f, 0.0f, 0.0f, 1.0f,//左上
1.0f, -0.5f, 0.0f, 1.0f, 0.0f,//右下
1.0f, 0.5f, 0.0f, 1.0f, 1.0f//右上
};
vector<glm::vec3> windowQuad{
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)
};
map<float, glm::vec3> sorted;
//map会自动根据键值即这里的float进行从小到大的排序,那么以distance作为key,从后往前取map中的value,就能得到从远到近的quad的位置
for (int i = 0; i < windowQuad.size(); i++) {
float distance = glm::length(camera.Position - windowQuad[i]);
sorted[distance] = windowQuad[i];
}
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);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
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);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
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);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
unsigned int cubeTexture = loadTexture("resources/textures/marble.jpg");
unsigned int floorTexture = loadTexture("resources/textures/metal.jpg");
unsigned int transparentTexture = loadTexture("resources/textures/blending_transparent_window.png");
shader.use();
shader.setInt("texture1", 0);
shader.setInt("texture2", 1);
shader.setInt("texture3", 2);
while (!glfwWindowShouldClose(window)) {
float currentFrame = (float)glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
processInput(window);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetCameraView();
glm::mat4 project = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
shader.setMatrix("view", view);
shader.setMatrix("project", project);
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
shader.setInt("useTex", 1);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
shader.setMatrix("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
shader.setMatrix("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glBindVertexArray(planeVAO);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, floorTexture);
shader.setInt("useTex", 2);
shader.setMatrix("model", glm::mat4(1.0f));
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glBindVertexArray(transparentVAO);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, transparentTexture);
shader.setInt("useTex", 3);
for (map<float, glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); it++) {
model = glm::mat4(1.0f);
model = glm::translate(model, it->second);
shader.setMatrix("model", model);
shader.setMatrix("view", view);
shader.setMatrix("project", project);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glBindVertexArray(0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &planeVAO);
glDeleteBuffers(1, &cubeVBO);
glDeleteBuffers(1, &planeVBO);
glfwTerminate();
return 0;
}

homework1#

你能够重新定义顶点数据,将每个三角形设置为顺时针顺序,并将顺时针的三角形设置为正向面,仍将场景渲染出来吗? 思路:三角形设置为顺时针,且顺时针为正向,要想保留靠近我们的面,剔除远离我们的面。从观察者的视角看背对我们的面是逆时针渲染的,是背向面,那么应该剔除背向面。

main.cpp
glCullFace(GL_BACK);
glFrontFace(GL_CW);

同时,在这种定义下,之前的透明窗户正面不再可见。因为这些面片顶点的环绕顺序为逆时针,且默认逆时针为正向,现在定义顺时针为正向,则朝向我们的面由于是逆时针定义,属于背向面,被剔除,背对我们的面被保留。

Cull Face
https://fuwari.vercel.app/posts/notes/opengl/cullface/
作者
Ruby
发布于
2026-06-27
许可协议
CC BY-NC-SA 4.0