计算机图形学编程(使用OpenGL和C++)(第2版)学习笔记 02.OpenGL图像管线

1. OpenGL图像管线

OpenGL(Open Graphics Library)是一个跨平台的、功能强大的图形渲染API,用于开发2D和3D图形应用程序。它由Khronos Group维护,广泛应用于游戏开发、图形设计、虚拟现实等领域。

1.0.1. OpenGL的特点:

  1. 跨平台:支持Windows、macOS、Linux等多个操作系统。 注:macOS上对OpenGL的支持不如Windows。
  2. 硬件加速:利用GPU进行高效的图形渲染。
  3. 开放标准:由Khronos Group管理,提供灵活的扩展机制。
  4. 实时渲染:适用于需要高性能图形渲染的应用。

1.0.2. OpenGL的主要功能:

  1. 图形绘制:支持点、线、三角形等基本图元的绘制。
  2. 着色器支持:通过GLSL(OpenGL Shading Language)实现自定义的顶点和片段着色器。
  3. 纹理映射:加载和应用纹理以增强图形的视觉效果。
  4. 变换与投影:支持模型变换、视图变换和投影变换。
  5. 光照与阴影:实现逼真的光照和阴影效果。

OpenGL的灵活性和高性能使其成为图形开发的核心工具之一。

2. OpenGL 与 Direct3D 开发语言上的对比

特性OpenGLDirect3D
语言支持支持多种语言,包括 C、C++、Python、Java 等主要支持 C 和 C++,部分支持 .NET 语言
跨平台性跨平台,支持 Windows、macOS、Linux 等仅支持 Windows 和 Xbox 平台
API 风格面向过程的函数调用,简单直接面向对象的设计,使用 COM 接口
着色器语言使用 GLSL(OpenGL Shading Language)使用 HLSL(High-Level Shading Language)
开发工具链工具链灵活,支持多种第三方工具和库集成在 Visual Studio,工具链较为封闭
学习曲线API 简单,适合初学者,但高级功能较复杂面向对象设计,初学者需要适应 COM 模型
社区支持开源社区活跃,文档和教程丰富由微软主导,官方文档和支持较完善

2.1. 总结

  • OpenGL 更加灵活,适合跨平台开发,支持多种语言,适合需要兼容多平台的项目。
  • Direct3D 更适合 Windows 和 Xbox 平台开发,工具链与 Visual Studio 集成,适合微软生态系统的开发者。

OpenGL 与普通的软件不同,其是运行在GPU上的。

2.2. 管线概览

以下是OpenGL图像管线的主要阶段:

在实践中,OpenGL图像管线经常涉及的阶段:

  1. 顶点处理:处理顶点数据,包括顶点坐标、颜色、纹理坐标等。
  2. 光栅化:将顶点数据转换为片段,进行光栅化处理,生成片段。
  3. 片段处理:处理片段数据,包括颜色、深度、模板等。

其中,顶点处理和片段处理是核心阶段,它们分别负责处理顶点和片段数据。光栅化阶段则负责将顶点数据转换为片段,进行光栅化处理。
我们需要编写顶点着色器和片段着色器来处理顶点和片段数据,光栅化阶段则由OpenGL自动完成。

2.3. 第一个OpenGL程序

我们不采用顶点着色器和片段着色器,而是直接使用OpenGL函数将屏幕绘制为全红。 以下是运行结果 :

代码:

/*
* 第2章 OpenGL图形管线示例程序
* 这个程序演示了OpenGL的基本设置和渲染循环
* 创建一个600x600像素的窗口并将其背景设置为红色
*/

// GLEW是OpenGL的扩展加载库
#include <GL\glew.h>
// GLFW提供了创建窗口和处理用户输入的功能
#include <GLFW\glfw3.h>
// 用于输出错误信息
#include <iostream>

using namespace std;

/**
 * 初始化OpenGL的设置
 * @param window 目标窗口的指针
 */
void init(GLFWwindow* window) { }

/**
 * 渲染函数,负责实际的绘制操作
 * @param window 目标窗口的指针
 * @param currentTime 当前时间,可用于动画
 */
void display(GLFWwindow* window, double currentTime) {
    // 设置清除缓冲区时要使用的颜色(红色)
    glClearColor(1.0, 0.0, 0.0, 1.0);
    // 使用上面设置的颜色清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);
}

/**
 * 主函数:程序入口点
 * 负责初始化GLFW和GLEW,创建窗口,并进入渲染循环
 */
int main(void) {
    // 初始化GLFW库,如果失败则退出
    if (!glfwInit()) { exit(EXIT_FAILURE); }
    
    // 设置OpenGL版本为4.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    
    // 创建一个600x600像素的窗口
    GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 1", NULL, NULL);
    
    // 将新创建的窗口设置为当前OpenGL上下文
    glfwMakeContextCurrent(window);
    
    // 初始化GLEW库,用于管理OpenGL扩展,如果失败则退出
    if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
    
    // 启用垂直同步,防止画面撕裂
    glfwSwapInterval(1);

    // 调用自定义的初始化函数
    init(window);

    // 渲染循环:直到窗口被关闭才退出
    while (!glfwWindowShouldClose(window)) {
        // 调用显示函数进行渲染,传入当前时间用于动画
        display(window, glfwGetTime());
        // 交换前后缓冲区,将渲染结果显示到屏幕上
        glfwSwapBuffers(window);
        // 处理窗口事件(如键盘输入、鼠标移动等)
        glfwPollEvents();
    }

    // 清理资源并退出
    glfwDestroyWindow(window);
    glfwTerminate();
    exit(EXIT_SUCCESS);
}

整个程序的流程如下:

  1. 初始化GLFW库,如果失败则退出。
  2. 设置OpenGL版本为4.3。
  3. 创建一个600x600像素的窗口。
  4. 将新创建的窗口设置为当前OpenGL上下文。
  5. 初始化GLEW库,用于管理OpenGL扩展,如果失败则退出。
  6. 启用垂直同步,防止画面撕裂。
  7. 调用自定义的初始化函数。
  8. 渲染循环:直到窗口被关闭才退出。
  9. 调用显示函数进行渲染,传入当前时间用于动画。
  10. 交换前后缓冲区,将渲染结果显示到屏幕上。
  11. 处理窗口事件(如键盘输入、鼠标移动等)。
  12. 清理资源并退出。

重点关注:

  1. init():自定义的初始化函数,负责初始化OpenGL的设置。目前为空
  2. display():自定义的渲染函数,负责实际的绘制操作。目前将屏幕设置为红色。该函数会不断被调用,直到窗口被关闭才退出。

2.4. 常用函数

函数名称描述示例
glfwInit()初始化GLFW库if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint()设置窗口提示glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwCreateWindow()创建一个窗口GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 1", NULL, NULL);
glfwMakeContextCurrent()将窗口的上下文设置为当前线程的上下文glfwMakeContextCurrent(window);
glewInit()初始化GLEW库if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval()设置交换间隔,用于垂直同步glfwSwapInterval(1);
glfwDestroyWindow()销毁窗口glfwDestroyWindow(window);
glfwTerminate()终止GLFW库glfwTerminate();
exit()退出程序exit(EXIT_SUCCESS);
glClearColor()设置清空屏幕所用的颜色glClearColor(1.0, 0.0, 0.0, 1.0);
glClear()清空屏幕glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers()交换前后缓冲区glfwSwapBuffers(window);
glfwPollEvents()处理所有待处理的事件glfwPollEvents();
glfwWindowShouldClose()检查窗口是否应该关闭while (!glfwWindowShouldClose(window)) { /* ... */ }

2.5. 第一个着色器程序

我们通过一个简单的着色器程序来演示OpenGL的图形管线。下图是在屏幕上显示一个点:

如果控制台的输出为乱码,在程序中添加以下代码:

 system("chcp 65001 > nul"); // 设置为 UTF-8 编码

我们在上一节的基础上进行添加,软件思路:

  1. 创建、编译和链接着色器程序
  2. 创建、绑定和配置顶点数组对象
  3. 调用显示函数进行渲染
  4. 调用glPointSize()设置点的大小,由于OpenGL默认的点大小为1.0,只显示一个点,会看不到效果,所以需要设置一个较大的点大小
  5. 调用glDrawArrays()绘制点,传入参数为GL_POINTS,表示绘制点,传入参数为0,表示从第0个顶点开始绘制,传入参数为1,表示绘制1个点

GLSL 与普通编码的过程不同,GLSL编译发生在C++运行时,实际运行在GPU中,我们不太好观察其编译错误。因此,我们使用辅助函数来打印编译错误,详见代码中的以下函数 :

  1. checkOpenGLError():检查OpenGL错误
  2. printShaderLog():打印着色器的编译错误
  3. printProgramLog():打印着色器程序的链接错误

顶点着色器代码

// 指定GLSL版本为4.30
#version 430

/**
 * 顶点着色器的主函数
 * 负责设置顶点的位置
 */
void main(void)
{
    // 设置顶点位置
    // vec4参数分别表示:
    // x=0.0:在x轴上居中
    // y=0.0:在y轴上居中
    // z=0.5:在z轴上的深度值
    // w=1.0:齐次坐标的w分量
    gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
}

以下是OpenGL顶点着色器中常见的变量,无需定义,可以直接使用:

变量名称类型描述
gl_Positionvec4输出的顶点位置,在裁剪空间中定义。
gl_PointSizefloat输出的点的大小(像素),仅在点图元时使用。
gl_VertexIDint输入的顶点ID,表示当前顶点的索引。
gl_InstanceIDint输入的实例ID,表示当前实例的索引(如果启用了实例化渲染)。
gl_PrimitiveIDint输入的图元ID,表示当前图元的索引(在几何着色器中使用)。
gl_Layerint输出的图层ID,用于分层渲染(在几何着色器中使用)。
gl_ViewportIndexint输出的视口索引,用于多视口渲染(在几何着色器中使用)。
in 变量自定义类型从顶点缓冲对象(VBO)传递的输入顶点属性,如位置、颜色、法线等。
out 变量自定义类型传递到片段着色器的输出变量,如颜色、纹理坐标等。

2.5.1. 说明

  • inout 变量的具体类型和名称由开发者根据需求定义。
  • gl_ 前缀的变量是OpenGL内置的变量,具有特定的用途。

片段着色器代码

// 指定GLSL版本为4.30
#version 430

// 定义输出变量color,表示最终的像素颜色
out vec4 color;

/**
 * 片段着色器的主函数
 * 负责设置每个片段(像素)的颜色
 */
void main(void)
{
    // 设置输出颜色
    // vec4参数分别表示:
    // r=0.0:红色分量为0
    // g=0.0:绿色分量为0
    // b=1.0:蓝色分量为1(纯蓝色)
    // a=1.0:完全不透明
    color = vec4(0.0, 0.0, 1.0, 1.0);
}

注意:

  1. out vec4 color 表示输出变量,其类型为vec4,表示一个4分量的向量,用于存储颜色值。
  2. 在渲染过程中,片段(或片元)代表的是经过光栅化处理的潜在像素。它们可能最终成为屏幕上的像素,但在某些情况下(如深度测试或模板测试失败),某些片段可能不会被显示出来。因此,片段是处于渲染管线更早期的阶段,而像素则是最终呈现在屏幕上的元素。

完整cpp代码

/*
* 第2章 OpenGL绘制点示例程序
* 这个程序演示了如何使用OpenGL的着色器程序绘制一个点
* 包含了着色器程序的加载、编译和链接过程
*/

// OpenGL相关库
#include <GL\glew.h>      // GLEW用于加载OpenGL函数
#include <GLFW\glfw3.h>   // GLFW用于创建窗口和处理输入

// 标准库
#include <iostream>        // 用于控制台输出
#include <string>         // 用于字符串处理
#include <fstream>        // 用于文件读取
#include <filesystem>     // 用于文件路径处理

using namespace std;

// 定义顶点数组对象(VAO)的数量
#define numVAOs 1

// 全局变量
GLuint renderingProgram;  // 存储编译后的着色器程序
GLuint vao[numVAOs];     // 存储顶点数组对象


/**
* 打印着色器编译日志
* 用于调试着色器编译错误
* @param shader 着色器对象的ID
*/
void printShaderLog(GLuint shader) {
   int len = 0;          // 日志长度
   int chWrittn = 0;     // 实际写入的字符数
   char *log;            // 日志内容
   // 获取日志长度
   glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
   if (len > 0) {
       log = (char *)malloc(len);
       // 获取实际的日志信息
       glGetShaderInfoLog(shader, len, &chWrittn, log);
       cout << "着色器编译日志: " << log << endl;
       free(log);
   }
}

/**
* 打印着色器程序链接日志
* 用于调试着色器程序链接错误
* @param prog 着色器程序的ID
*/
void printProgramLog(int prog) {
   int len = 0;          // 日志长度
   int chWrittn = 0;     // 实际写入的字符数
   char *log;            // 日志内容
   // 获取日志长度
   glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &len);
   if (len > 0) {
       log = (char *)malloc(len);
       // 获取实际的日志信息
       glGetProgramInfoLog(prog, len, &chWrittn, log);
       cout << "程序链接日志: " << log << endl;
       free(log);
   }
}

/**
* 检查OpenGL错误
* 遍历所有待处理的错误并打印
* @return 如果发现错误返回true,否则返回false
*/
bool checkOpenGLError() {
   bool foundError = false;
   int glErr = glGetError();
   while (glErr != GL_NO_ERROR) {
       cout << "OpenGL错误代码: " << glErr << endl;
       foundError = true;
       glErr = glGetError();
   }
   return foundError;
}




/**
* 从文件中读取着色器源代码
* @param filePath 着色器文件的路径
* @return 包含着色器源代码的字符串
* 
* 这个函数负责:
* 1. 打开并读取着色器源文件
* 2. 将文件内容转换为字符串
* 3. 处理文件打开失败的情况
*/
string readFile(const char *filePath) {
   string content;
   ifstream fileStream(filePath, ios::in);
   string line = "";
   
   // 检查文件是否成功打开
   if (!fileStream.is_open()) {
       cout << "无法打开文件: " << filePath << endl;
       return content;
   }
   // 逐行读取文件内容
   while (getline(fileStream, line)) {
       content.append(line + "\n");
   }
   fileStream.close();
   return content;
}
/**
* 创建、编译和链接着色器程序
* @return 返回编译和链接成功的着色器程序ID
* 
* 这个函数完成以下步骤:
* 1. 创建着色器对象
* 2. 读取并编译顶点着色器和片段着色器
* 3. 创建着色器程序并链接着色器
* 4. 进行错误检查和状态报告
*/
GLuint createShaderProgram() {
   // 编译状态变量
   GLint vertCompiled;    // 顶点着色器编译状态
   GLint fragCompiled;    // 片段着色器编译状态
   GLint linked;          // 程序链接状态

   // 创建着色器和程序对象
   GLuint vShader = glCreateShader(GL_VERTEX_SHADER);      // 创建顶点着色器
   GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);    // 创建片段着色器
   GLuint vfprogram = glCreateProgram();                   // 创建着色器程序

   // 读取着色器源代码
   string vertShaderStr = readFile("./shader/vertShader.glsl");
   string fragShaderStr = readFile("./shader/fragShader.glsl");
   const char *vertShaderSrc = vertShaderStr.c_str();
   const char *fragShaderSrc = fragShaderStr.c_str();

   // 将源代码加载到着色器对象中
   glShaderSource(vShader, 1, &vertShaderSrc, NULL);
   glShaderSource(fShader, 1, &fragShaderSrc, NULL);

   // 编译顶点着色器
   glCompileShader(vShader);
   checkOpenGLError();    // 检查OpenGL错误
   glGetShaderiv(vShader, GL_COMPILE_STATUS, &vertCompiled);
   if (vertCompiled == 1) {
        cout << "顶点着色器编译成功" << endl;
   }
   else {
       cout << "顶点着色器编译失败" << endl;
       printShaderLog(vShader);
   }

   // 编译片段着色器
   glCompileShader(fShader);
   checkOpenGLError();    // 检查OpenGL错误
   glGetShaderiv(fShader, GL_COMPILE_STATUS, &fragCompiled);
   if (fragCompiled == 1) {
       cout << "片段着色器编译成功" << endl;
   }
   else {
       cout << "片段着色器编译失败" << endl;
       printShaderLog(fShader);
   }

   // 将着色器附加到程序对象并链接
   glAttachShader(vfprogram, vShader);    // 附加顶点着色器
   glAttachShader(vfprogram, fShader);    // 附加片段着色器
   glLinkProgram(vfprogram);              // 链接程序

   // 检查链接状态
   checkOpenGLError();    // 检查OpenGL错误
   glGetProgramiv(vfprogram, GL_LINK_STATUS, &linked);
   if (linked == 1) {
       cout << "着色器程序链接成功" << endl;
   }
   else {
       cout << "着色器程序链接失败" << endl;
       printProgramLog(vfprogram);
   }
   cout << "着色器程序创建完成,ID: " << vfprogram << endl;
   return vfprogram;
}


/**
* 初始化OpenGL的设置
* @param window 目标窗口的指针
*/
void init(GLFWwindow* window) { 
   
   renderingProgram = createShaderProgram();
   glGenVertexArrays(numVAOs, vao);
   glBindVertexArray(vao[0]);
   
}

/**
* 渲染函数,负责实际的绘制操作
* @param window 目标窗口的指针
* @param currentTime 当前时间,可用于动画
*/
void display(GLFWwindow* window, double currentTime) {
   glUseProgram(renderingProgram);
   glPointSize(30.0f);
   glDrawArrays(GL_POINTS, 0, 1);
}

/**
* 主函数:程序入口点
* 负责初始化GLFW和GLEW,创建窗口,并进入渲染循环
*/
int main(void) {
   // 获取当前工作目录的路径
   std::filesystem::path currentPath = std::filesystem::current_path();

   // 获取绝对路径
   std::filesystem::path absolutePath = std::filesystem::absolute(currentPath);

   // 提取目录名称
   std::string directoryName = currentPath.filename().string();

   std::cout << "absolute_path: " << absolutePath.string() << std::endl;

   system("chcp 65001 > nul"); // 设置为 UTF-8 编码
   
   // 初始化GLFW库,如果失败则退出
   if (!glfwInit()) { exit(EXIT_FAILURE); }
   
   // 设置OpenGL版本为4.3
   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
   
   // 创建一个600x600像素的窗口
   GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 2", NULL, NULL);
   
   // 将新创建的窗口设置为当前OpenGL上下文
   glfwMakeContextCurrent(window);
   
   // 初始化GLEW库,用于管理OpenGL扩展,如果失败则退出
   if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
   
   // 启用垂直同步,防止画面撕裂
   glfwSwapInterval(1);

   // 调用自定义的初始化函数
   init(window);

   // 渲染循环:直到窗口被关闭才退出
   while (!glfwWindowShouldClose(window)) {
       // 调用显示函数进行渲染,传入当前时间用于动画
       display(window, glfwGetTime());
       // 交换前后缓冲区,将渲染结果显示到屏幕上
       glfwSwapBuffers(window);
       // 处理窗口事件(如键盘输入、鼠标移动等)
       glfwPollEvents();
   }

   // 清理资源并退出
   glfwDestroyWindow(window);
   glfwTerminate();
   exit(EXIT_SUCCESS);
}

2.6. 曲面细分着色器

曲面细分着色器(Tessellation Shader)是OpenGL中的一个着色器阶段,用于增加渲染模型的细节。它通过在几何图元(如三角形或四边形)的表面生成更多的顶点来细分这些图元,从而提高渲染的细节水平。曲面细分着色器通常由两个主要部分组成:

  1. 曲面细分控制着色器(Tessellation Control Shader,TCS):这个着色器阶段决定了每个输入图元将被细分为多少个更小的图元。它还可以设置每个新顶点的属性。

  2. 曲面细分评估着色器(Tessellation Evaluation Shader,TES):在这个阶段,新的顶点位置被计算出来,并且可以应用自定义的数学函数来生成复杂的几何形状。TES还负责设置每个新顶点的最终属性。

曲面细分着色器通常用于需要动态调整模型细节的场景,例如地形渲染、水面模拟或任何需要根据摄像机距离调整细节水平的对象。


对于地面,我们通常用两个三角形(四个顶点)来表示,但是通过曲面细分,我们可以用更多的三角形(更多顶点)来表示,从而提高渲染的细节。

目前阶段我们不会深入研究曲面细分着色器的细节。

2.7. 几何着色器

几何着色器是OpenGL渲染管线中的一个可选阶段,位于顶点着色器和片段着色器之间。它的主要功能是处理图元(如点、线、三角形)并生成额外的图元。这使得开发者可以在渲染过程中动态地修改或创建新的几何形状。

顶点着色器及片段着色器只能处理单个顶点或片段,而几何着色器可以操作图元(如点、线、三角形),并生成新的图元。这使得几何着色器可以用于创建复杂的几何形状,如网格、粒子系统或地形。
比如拉伸、压缩、旋转、扭曲、删除等操作。

比如下图左边是环面模型,右边是几何着色器修改后的模型。
通过这种方式,可以减少模型顶点数量,提高渲染效率。

2.8. 光栅化阶段

光栅化的本质是将每对顶点进行插值,生成一系列的顶点。如下图所示,将三角形的三个顶点进行插值,生成三条边。

到此时,呈现出来的图像将会是线框模型

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

而如果想要呈现出实心的三角形,需要将线框模式改为填充模式 ,这也是默认的模式,即完全光栅化

glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

2.9. 绘制三角形

我们在顶点着色器中通过gl_VertexID来获取当前正在处理的顶点编号,设置顶点坐标,从而实现绘制三角形。 gl_VertexID是OpenGL内置的变量,表示当前正在处理的顶点编号,从0开始计数。

2.9.1. 实心三角形

顶点着色器

// 指定GLSL版本为4.30
#version 430

/**
 * 顶点着色器的主函数
 * 负责设置顶点的位置
 */
void main(void)
{
    // 设置顶点位置
    // vec4参数分别表示:
    // x=0.0:在x轴上居中
    // y=0.0:在y轴上居中
    // z=0.5:在z轴上的深度值
    // w=1.0:齐次坐标的w分量
    //gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
    if (gl_VertexID == 0)
    {
        gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
    }
    else if (gl_VertexID == 1)
    {
        gl_Position = vec4(0.5, 0.0, 0.5, 1.0);
    }
    else if (gl_VertexID == 2)
    {
        gl_Position = vec4(0.5, 0.5, 0.5, 1.0);
    }
    
}

片段着色器
不用修改

main.cpp

void display(GLFWwindow *window, double currentTime)
{
	// 使用名为 renderingProgram 的着色器程序
	glUseProgram(renderingProgram);

	// 设置要绘制的点的大小为 1 像素
	glPointSize(1.0f);

    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 设置绘制模式为填充
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	// 绘制点图元,从数组的第 0 个元素开始,绘制 3 个点
	glDrawArrays(GL_TRIANGLES, 0, 3);
}

2.9.2. 线框三角形

只需要 将填充模式改为线框模式即可

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

2.10. 让三角形动起来

1

我们只需要不断改变顶点的位置,就可以让三角形动起来。
由于display()函数每秒会持续不断执行,因此我们只需要在每次执行时,将顶点的位置稍微移动一点,就可以让三角形看起来在动。
我们可以在c++代码中,增加一个变量offset,在每次执行display()函数时,将offset的值进行改变,同时将offset的值传递给顶点着色器,从而实现让三角形动起来的效果。
在顶点着色器中,通过uniform变量offsetX来接收offset的值,从而实现顶点位置的改变。

main.cpp

void display(GLFWwindow *window, double currentTime)
{
    // 清除颜色缓冲区和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    static float offset=0.0f; // 偏移量,用于动画效果
    static float delta=0.01f; // 偏移增量
    offset+=delta; // 更新偏移量

    if(offset>0.5f) delta=-0.01f;
    if(offset<-0.5f) delta=0.01f; // 如果偏移量超过范围,反转增量

	// 使用名为 renderingProgram 的着色器程序
	glUseProgram(renderingProgram);
	// 设置要绘制的点的大小为 1 像素
	glPointSize(1.0f);
    //glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 设置绘制模式为填充
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	// 绘制点图元,从数组的第 0 个元素开始,绘制 3 个点
    GLuint offsetLocation = glGetUniformLocation(renderingProgram, "offsetX"); // 获取偏移量uniform变量的位置
    glUniform1f(offsetLocation, offset); // 设置偏移量uniform变量的值
	glDrawArrays(GL_TRIANGLES, 0, 3);
}

顶点着色器

// 指定GLSL版本为4.30
#version 430

uniform float offsetX; // x轴偏移量
/**
 * 顶点着色器的主函数
 * 负责设置顶点的位置
 */
void main(void)
{
    // 设置顶点位置
    // vec4参数分别表示:
    // x=0.0:在x轴上居中
    // y=0.0:在y轴上居中
    // z=0.5:在z轴上的深度值
    // w=1.0:齐次坐标的w分量
    //gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
    if (gl_VertexID == 0)
    {
        gl_Position = vec4(-0.3+offsetX, 0.0, 0.5, 1.0);
    }
    else if (gl_VertexID == 1)
    {
        gl_Position = vec4(0.2+offsetX, 0.0, 0.5, 1.0);
    }
    else if (gl_VertexID == 2)
    {
        gl_Position = vec4(0.2+offsetX, 0.5, 0.5, 1.0);
    }
    
}

注意:
我们在display()函数中,需要调用以下代码

// 清除颜色缓冲区和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

否则,我们无法看到三角形在动,且会看到一些奇怪的图形,这是因为图形没有清空,导致图形叠加在一起。

学习笔记完整源代码下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值