一、简介
本文介绍了 OpenGL 中 Transform Feedback 方法的基本概念和代码示例。
二、Transform Feedback 介绍
1. Transform Feedback 简介
根据 OpenGL-wiki,Transform Feedback 是捕获由顶点处理步骤(vertex shader 和 geometry shader)生成的图元(Primitives)的过程,将这些图元的数据记录到缓冲区对象(Buffer Objects)中。这样可以保留物体的变换后渲染状态,(在GPU中)多次重新提交这些数据。
简单来讲,使用 Transform Feedback 可以将 vertex shader 和 geometry shader 处理后的数据存储到指定的 Buffer Objects 中,而不继续进行后续的 Clipper、Rasterizaer 和 Test & Blending 阶段。
Transform Feedback Buffer 在渲染管线中所处的位置如下图所示:
2. 使用 Transform feedback 可实现的功能
- 实现粒子系统:
在粒子系统中,Transform Feedback 可以用来捕获粒子的状态信息(如位置、速度、颜色等),然后将这些数据存储到缓冲区中。在每一帧中,系统可以通过 Transform Feedback 来更新粒子的状态,从而避免在CPU中重新计算整个粒子系统。这种方式能有效地避免 CPU 参与数据更新,提高系统的性能。 - 进行GPU并行运算:
使用 Transform Feedback,可以在 vertex shader 中执行并行计算任务,将计算结果直接存储在 Transform Feedback 对应的 buffer 中 (GPU 内存中)。
例如,可以在 vertex shader 中执行物理计算(如模拟重力或其他力学运动),并将计算结果(例如新的顶点位置)存储在 buffer 中,作为新渲染 shader program 的输入,或者直接作为结果传输到 CPU 上。而不需要每次都在 CPU 计算再传输到 GPU 中。
三、使用示例
1. 使用 Transform Feedback 与 vertex shader
在本例中,使用 Transform feedback 方法在 GPU (vertex shader) 中进行并行运算,将输入的数据开平方后存储到 Transform feedback 对应的 buffer 中再传到 CPU 中进行打印输出。
1.1. 步骤
- 初始化 glfw, glad, 窗口
- 构建,编译 vertex shader
- 指定 Transform feedback object 的接收变量
- 链接 shader program
- 准备输入数据, VAO 和 VBO
- 设置绑定到 Transform feedback object 上的 buffer,用于接收输出数据
- 运行 shader 程序
7.1 use shader program
7.2 disable rasterizer
7.3 begin transform feedback
7.4 bind VAO
7.5 draw
7.6 end transform feedback - 将 Transform feedback buffer 中的数据传输到 CPU
- 释放资源
1.2. 代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);
// 指定窗口默认width和height像素大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
/****** vertex shader 代码 ******/
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in float inputValue;
out float outputValue;
void main()
{
outputValue = sqrt(inputValue);
}
)";
/************************************/
int main()
{
/****** 1.初始化glfw, glad, 窗口 *******/
// glfw 初始化 + 配置 glfw 参数
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw 生成窗口
// (由于我们只使用 Transform feedback 进行并行运算,因此实际上不生成窗口也可以)
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
// 检查是否成功生成窗口,如果没有成功打印出错信息并且退出
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// 设置窗口window的上下文
glfwMakeContextCurrent(window);
// 配置window变化时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 使用 glad 加载 OpenGL 中的各种函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
/************************************/
/****** 2.构建,编译 vertex shader ******/
// vertex shader
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 检查是否成功编译 vertex shader
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// 生成 shader 程序
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
/****** 3. 指定 Transform feedback object 的接收变量 ******/
// 该步骤必须在 glLinkProgram() 之前进行
// 因为使用 Transform feedback 会改变 shader 程序的渲染流程,因此需要先指定 Transform feedback 的接收变量,告诉
// OpenGL 应该如何链接、编译 shader 程序,再进行 glLinkProgram()
const char *feedbackVaryings[] = {"outputValue"}; // 我们希望接收 vertex shader 的输出变量 out float outputValue
// glTransformFeedbackVaryings() 函数的参数解释:
// (1). 目标 shader program
// (2). 目标接收变量的个数,即 feedbackVaryings 数组中 字符串的个数
// (3). 目标接收的变量名字符串数组
// (4). 接收变量的存储目标. GL_INTERLEAVED_ATTRIBS, 表示所有输出变量都输出到 一个 buffer 中,
// GL_SEPARATE_ATTRIBS 表示 输出变量输出到不同的 buffer 中. 此处我们选择将输出存储到 一个 buffer 中
glTransformFeedbackVaryings(shaderProgram, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
/************************************/
/****** 4. 链接 shader program ******/
glLinkProgram(shaderProgram); // 链接 shader program
// 检查是否成功链接 vertex shader 和 fragment shader
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
/************************************/
/****** 5.准备输入数据, VAO 和 VBO *******/
float inputData[] = {0.0f, 0.5f, 1.0f};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
glBindVertexArray(VAO); // 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(inputData), inputData,
GL_STATIC_DRAW); // 将vertices中的数据复制到刚刚绑定的VBO buffer中去,VBO buffer是GPU内存上的一块区域
// glVertexAttribPointer() 用于指定顶点属性如何从绑定的 VBO 中读取数据,
// 此时绑定的 buffer 是 VBO,因此location=0 处的顶点属性就从VBO中读数据 glVertexAttribPointer()
// 需要6个参数,每个参数的含义如下:
// (1). 指定要配置的顶点属性, 即 shader 中指定 数据 location 值,对于position部分,此处填入 0
// (2). 指定顶点属性的大小,shader中我们将VBO中的数据传给了一个 float 类型的变量,
// 每个顶点数据只有一个 float, 因此此处填 1
// (3). 指定数据的类型,用于使用的是浮点型,因此此处填入 GL_FLOAT
// (4). 指定是否自动对数据进行归一化,我们不需要自动诡异化,因此此处填入 GL_FALSE
// (5). 指定VBO中顶点数据组之间的间隔,可以手动设置间隔,例如 sizeof(float),
// 也可以填 0 让 Opengl 自动设置
// (6). 指定读取VBO数据起始位置偏移,单位为字节,这里填入 (void *)(0*sizeof(float)) 即可
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0,
(void *)(0 * sizeof(float))); // 设置如何读取VBO中数据
glEnableVertexAttribArray(0); // 启用 location = 0 处的顶点属性
glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑VBO
/************************************/
/****** 6. 设置绑定到 Transform feedback object 上的 buffer,用于接收输出数据 ******/
// 新建一个 buffer 用于作为 Transform feedback object 的 buffer,用来接收 计算后的输出结果
GLuint tbo;
glGenBuffers(1, &tbo);
glBindBuffer(GL_ARRAY_BUFFER, tbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(inputData), nullptr, GL_STATIC_READ);
// 将 tbo 绑定到 TFO 的 0号卡槽上, 此处不对卡槽机制进行过多的讲解
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
/************************************/
/****** 7. 运行 shader 程序 ******/
// 7.1 use shader program
glUseProgram(shaderProgram);
// 关闭 光栅化阶段, 只使用 vertex shader 和 geometry shader (如果有的话) 阶段
// 7.2 disable rasterizer
glEnable(GL_RASTERIZER_DISCARD);
// 开始绘制 Transform feedback
// 此处只有 vertex shader,因此 glBeginTransformFeedback() 中指定的 图元 需要跟 glDrawArray() 中的相同
// 7.3 begin transform feedback
glBeginTransformFeedback(GL_POINTS);
// 7.4 bind VAO
glBindVertexArray(VAO); // 绑定VAO,指定当前使用的VAO对象
// 7.5 draw
glDrawArrays(GL_POINTS, 0,
3); // 输入数据数据只有三个 float,因此可以将每个数据分配给一个 point ,在 vertex shader 中进行计算处理
// 7.6 end transform feedback
glEndTransformFeedback(); // 结束 Transform feedback
// 开启 光栅化阶段
glDisable(GL_RASTERIZER_DISCARD);
glBindVertexArray(0); // 解绑 VAO
glFlush(); // 刷新 gl 命令 buffer 保证前面的命令都运行完成
/************************************/
/****** 8. 将 Transform feedback buffer 中的数据传输到 CPU ******/
// 接收 Transform feedback 对应的 buffer (tbo) 中的数据
GLfloat feedback[3];
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
for (int i = 0; i < 3; i++)
{
printf("%f\n", feedback[i]);
}
/************************************/
/****** 9.释放资源 ******/
// 释放之前申请的 VBO, VAO 资源和 shader 程序
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// glfw 释放 glfw使用的所有资源
glfwTerminate();
/************************************/
return 0;
}
// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{
// 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
1.3. 运行结果
0.000000
0.707107
1.000000
2. 使用 Transform Feedback 与 vertex + geometry shader
在本例中,使用 Transform feedback 方法在 vertex shader 和 geometry shader 中进行并行运算,在 vertex shader 中将输入的数据开平方,然后在 geometry shader 中将一份数据分别加0, 加1和加2,变为三份数据。最后存储到 Transform feedback 对应的 buffer 中再传到 CPU 中进行打印输出。
2.1. 步骤
- 初始化 glfw, glad, 窗口
- 构建,编译 vertex shader , geometry shader
- 指定 Transform feedback object 的接收变量
- 链接 shader program
- 准备输入数据, VAO 和 VBO
- 设置绑定到 Transform feedback object 上的 buffer,用于接收输出数据
- 运行 shader 程序
7.1 use shader program
7.2 disable rasterizer
7.3 begin transform feedback
7.4 bind VAO
7.5 draw
7.6 end transform feedback - 将 Transform feedback buffer 中的数据传输到 CPU
- 释放资源
2.2 代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);
// 指定窗口默认width和height像素大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
/****** vertex shader 代码 ******/
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in float inputValue;
out float geoValue;
void main()
{
geoValue = sqrt(inputValue);
}
)";
/****** geometry shader 代码 ******/
const char *geometryShaderSource = R"(
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices=3) out;
in float[] geoValue;
out float outputValue;
void main()
{
for(int i=0; i<3; i++){
outputValue = geoValue[0] + i;
EmitVertex();
}
EndPrimitive();
}
)";
/************************************/
int main()
{
/****** 1.初始化glfw, glad, 窗口 *******/
// glfw 初始化 + 配置 glfw 参数
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw 生成窗口
// (由于我们只使用 Transform feedback 进行并行运算,因此实际上不生成窗口也可以)
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
// 检查是否成功生成窗口,如果没有成功打印出错信息并且退出
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// 设置窗口window的上下文
glfwMakeContextCurrent(window);
// 配置window变化时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 使用 glad 加载 OpenGL 中的各种函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
/************************************/
/****** 2.构建,编译 vertex shader, geometry shader ******/
// vertex shader
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 检查是否成功编译 vertex shader
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// geometry shader
unsigned int geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &geometryShaderSource, NULL);
glCompileShader(geometryShader);
// 检查是否成功编译 geometry shader
glGetShaderiv(geometryShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(geometryShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::GEOMETRY::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// 生成 shader 程序
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, geometryShader);
/****** 3. 指定 Transform feedback object 的接收变量 ******/
// 该步骤必须在 glLinkProgram() 之前进行
// 因为使用 Transform feedback 会改变 shader 程序的渲染流程,因此需要先指定 Transform feedback 的接收变量,告诉
// OpenGL 应该如何链接、编译 shader 程序,再进行 glLinkProgram()
const char *feedbackVaryings[] = {"outputValue"}; // 我们希望接收 vertex shader 的输出变量 out float outputValue
// glTransformFeedbackVaryings() 函数的参数解释:
// (1). 目标 shader program
// (2). 目标接收变量的个数,即 feedbackVaryings 数组中 字符串的个数
// (3). 目标接收的变量名字符串数组
// (4). 接收变量的存储目标. GL_INTERLEAVED_ATTRIBS, 表示所有输出变量都输出到 一个 buffer 中,
// GL_SEPARATE_ATTRIBS 表示 输出变量输出到不同的 buffer 中. 此处我们选择将输出存储到 一个 buffer 中
glTransformFeedbackVaryings(shaderProgram, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
/************************************/
/****** 4. 链接 shader program ******/
glLinkProgram(shaderProgram); // 链接 shader program
// 检查是否成功链接 vertex shader 和 fragment shader
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(geometryShader);
/************************************/
/****** 5.准备输入数据, VAO 和 VBO *******/
float inputData[] = {0.0f, 0.5f, 1.0f};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
glBindVertexArray(VAO); // 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(inputData), inputData,
GL_STATIC_DRAW); // 将vertices中的数据复制到刚刚绑定的VBO buffer中去,VBO buffer是GPU内存上的一块区域
// glVertexAttribPointer() 用于指定顶点属性如何从绑定的 VBO 中读取数据,
// 此时绑定的 buffer 是 VBO,因此location=0 处的顶点属性就从VBO中读数据 glVertexAttribPointer()
// 需要6个参数,每个参数的含义如下:
// (1). 指定要配置的顶点属性, 即 shader 中指定 数据 location 值,对于position部分,此处填入 0
// (2). 指定顶点属性的大小,shader中我们将VBO中的数据传给了一个 float 类型的变量,
// 每个顶点数据只有一个 float, 因此此处填 1
// (3). 指定数据的类型,用于使用的是浮点型,因此此处填入 GL_FLOAT
// (4). 指定是否自动对数据进行归一化,我们不需要自动诡异化,因此此处填入 GL_FALSE
// (5). 指定VBO中顶点数据组之间的间隔,可以手动设置间隔,例如 sizeof(float),
// 也可以填 0 让 Opengl 自动设置
// (6). 指定读取VBO数据起始位置偏移,单位为字节,这里填入 (void *)(0*sizeof(float)) 即可
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0,
(void *)(0 * sizeof(float))); // 设置如何读取VBO中数据
glEnableVertexAttribArray(0); // 启用 location = 0 处的顶点属性
glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑VBO
/************************************/
/****** 6. 设置绑定到 Transform feedback object 上的 buffer,用于接收输出数据 ******/
// 新建一个 buffer 用于作为 Transform feedback object 的 buffer,用来接收 计算后的输出结果
GLuint tbo;
glGenBuffers(1, &tbo);
glBindBuffer(GL_ARRAY_BUFFER, tbo);
// 注意!!! 由于我们使用 geometry shader 将一个输入变量(geoValue)变为三个输出(outputValue),
// 因此 Transform feedback object 中用于接收数据的 buffer 大小应该为输入数据的 3 倍大
glBufferData(GL_ARRAY_BUFFER, sizeof(inputData) * 3, nullptr, GL_STATIC_READ);
// 将 tbo 绑定到 TFO 的 0号卡槽上, 此处不对卡槽机制进行过多的讲解
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
/************************************/
/****** 7. 运行 shader 程序 ******/
// 7.1 use shader program
glUseProgram(shaderProgram);
// 7.2 disable rasterizer
// 关闭 光栅化阶段, 只使用 vertex shader 和 geometry shader (如果有的话) 阶段
glEnable(GL_RASTERIZER_DISCARD);
// 7.3 begin transform feedback
// 开始绘制 Transform feedback
// 注意!!! 由于此时使用了 geometry shader, glBeginTransformFeedback() 中的图元应该与 geometry shader 的输出一致,
// 尽管 geometry shader 中的 out 图元为 triangle_strip,
// 但是 transform feedback 依旧独立地存储每个 triangle 的顶点数据,
// 而不是使用 strip 对数据进行压缩
glBeginTransformFeedback(GL_TRIANGLES);
// 7.4 bind VAO
glBindVertexArray(VAO); // 绑定VAO,指定当前使用的VAO对象
// 7.5 draw
glDrawArrays(GL_POINTS, 0,
3); // 输入数据数据只有三个 float,因此可以将每个数据分配给一个 point ,在 vertex shader 中进行计算处理
// 7.6 end transform feedback
glEndTransformFeedback(); // 结束 Transform feedback
// 开启 光栅化阶段
glDisable(GL_RASTERIZER_DISCARD);
glBindVertexArray(0); // 解绑 VAO
glFlush(); // 刷新 gl 命令 buffer 保证前面的命令都运行完成
/************************************/
/****** 8. 将 Transform feedback buffer 中的数据传输到 CPU ******/
// 接收 Transform feedback 对应的 buffer (tbo) 中的数据
GLfloat feedback[9];
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
for (int i = 0; i < 9; i++)
{
printf("%f\n", feedback[i]);
}
/************************************/
/****** 9.释放资源 ******/
// 释放之前申请的 VBO, VAO 资源和 shader 程序
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// glfw 释放 glfw使用的所有资源
glfwTerminate();
/************************************/
return 0;
}
// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{
// 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
2.3 结果
0.000000
1.000000
2.000000
0.707107
1.707107
2.707107
1.000000
2.000000
3.000000
四、参考
[1.] OpenGL-Transform Feedback
[2.] OpenGL–Transform feedback示例解析
[3.] OpenGL-wiki-Transform Feedback