openGLES中每个程序对象必须连接到一个顶点着色器和一个片段着色器。程序对象被链接为用于渲染的最后“可执行程序”。获得连接后的着色器对象的一般过程包括六个步骤:
- 创建一个顶点着色器对象和一个片段着色器对象;
- 将源代码连接到每个着色器对象;
- 编译着色器对象;
- 创建一个程序对象;
- 将编译后的着色器对象连接到程序对象;
- 链接程序对象
1. 创建和编译一个着色器
GLuint compileShader(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
// 检查编译状态
int success;
char infoLog[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cerr << "Shader Compile Error (" << (type == GL_VERTEX_SHADER ? "Vertex" : "Fragment") << "):\n"
<< infoLog << std::endl;
glDeleteShader( shader );
return 0;
}
return shader;
}
2. 创建和链接程序
const char* vertexShaderSource = R"glsl(...)glsl"; // 替换为上述顶点着色器代码
const char* fragmentShaderSource = R"glsl(...)glsl"; // 替换为上述片段着色器代码
GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
// 链接着色器程序
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 检查链接状态
int success;
char infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
std::cerr << "Program Link Error:\n" << infoLog << std::endl;
}
// 清理着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// ......执行着色器程序
glUseProgram(shaderProgram);
3. 程序二进制码
可以预先将shader编译好的二进制码保存起来,使用时直接加载,跳过在线编译过程。
void SaveShaderBinary(const std::string& filePath, GLuint program) {
GLint length = 0;
glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &length); // 获取二进制数据长度[1,6](@ref)
std::vector<GLubyte> buffer(length);
GLenum format = 0;
glGetProgramBinary(program, length, nullptr, &format, buffer.data()); // 获取二进制数据[2,6](@ref)
std::ofstream out(filePath, std::ios::binary);
out.write(reinterpret_cast<char*>(&format), sizeof(GLenum)); // 先写入格式标识[1](@ref)
out.write(reinterpret_cast<char*>(buffer.data()), length); // 再写入二进制数据
out.close();
}
GLuint LoadShaderBinary(const std::string& filePath) {
std::ifstream in(filePath, std::ios::binary);
if (!in) return 0;
GLenum format;
in.read(reinterpret_cast<char*>(&format), sizeof(GLenum)); // 读取格式标识
std::vector<GLubyte> buffer(
(std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>()
);
in.close();
GLuint program = glCreateProgram();
glProgramBinary(program, format, buffer.data(), buffer.size()); // 加载二进制[2,6](@ref)
// 验证加载结果
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glDeleteProgram(program);
return 0;
}
return program;
}
4. 统一变量和属性
4.1 动态获取 Uniform 变量的名称、类型、大小等信息
在 OpenGL/OpenGL ES 中,glGetActiveUniform 是一个关键的函数,用于查询着色器程序中已激活的 Uniform 变量的详细信息。它的核心作用是帮助开发者动态获取 Uniform 变量的名称、类型、大小等信息,尤其适用于需要动态处理着色器变量的场景(如通用材质系统或动态着色器加载)。以下是详细解析和具体用法:
void glGetActiveUniform(
GLuint program, // 着色器程序对象ID
GLuint index, // Uniform变量的索引(从0到glGetProgramiv返回的GL_ACTIVE_UNIFORMS-1)
GLsizei bufSize, // 名称缓冲区的最大长度
GLsizei *length, // 实际返回的名称长度(可传NULL)
GLint *size, // 返回数组长度(非数组变量为1)
GLenum *type, // 返回变量类型(如GL_FLOAT_VEC4)
GLchar *name // 存储变量名称的缓冲区
);
//---------------------------------------
GLuint program = ...; // 已链接的着色器程序
GLint numUniforms = 0;
glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);
for (GLuint i = 0; i < numUniforms; ++i) {
GLchar name[64];
GLint size;
GLenum type;
glGetActiveUniform(program, i, sizeof(name), NULL, &size, &type, name);
// 示例输出:u_MVP (type=GL_FLOAT_MAT4, size=1)
printf("Uniform %d: %s (type=0x%X, size=%d)\n", i, name, type, size);
}
4.2 Uniform的高级用法
// 获取Uniform位置(需在glUseProgram之后调用)
GLint uColorLoc = glGetUniformLocation(program, "u_Color");
glUniform4f(uColorLoc, 1.0f, 0.0f, 0.0f, 1.0f); // 设置红色
// 获取Attribute位置(OpenGL ES 2.0必须获取,3.0+可用layout指定)
GLint aPosLoc = glGetAttribLocation(program, "a_Position");
glEnableVertexAttribArray(aPosLoc);
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, GL_FALSE, stride, offset);
在 OpenGL/GLSL 中,显式指定 layout(location = X) 主要用于 顶点属性(Attribute) 的定位,而 Uniform 变量 的绑定通常通过 glGetUniformLocation 动态获取位置。但 OpenGL 4.3+ 或 GLSL 330+ 也支持显式为 Uniform 指定位置(需结合 Uniform Buffer Object, UBO)。以下是具体使用方法:
#version 430 core
layout(location = 0) uniform mat4 u_Model; // 显式指定位置为 0
layout(location = 1) uniform vec3 u_Color; // 显式指定位置为 1
// 在代码中直接通过位置设置 Uniform 值,无需调用 glGetUniformLocation:
glUniformMatrix4fv(0, 1, GL_FALSE, glm::value_ptr(modelMatrix)); // 直接使用位置 0
glUniform3f(1, 1.0f, 0.0f, 0.0f); // 直接使用位置 1
5. 统一变量缓冲区对象
UBO是一种GPU缓冲区,用于存储着色器中声明的Uniform变量集合(称为Uniform Block)。它允许将多个Uniform变量(如变换矩阵、光照参数)打包成一个数据块,一次性传输到GPU,避免逐变量设置的开销
5.1着色器中定义uniform block
#version 330 core
layout(std140) uniform Matrices { // 使用std140布局
mat4 u_ViewProj;
vec3 u_LightDir;
float u_Intensity;
};
在C/C++中定义UBO数据时,可使用 alignas 或 #pragma pack 强制对齐:
// C++示例
struct alignas(16) Data { // 强制16B对齐
glm::vec3 b; // 12B + 4B填充
glm::mat2 c; // 16B
float a;
};
或通过编译器指令调整:
#pragma pack(push, 16) // 设置16B对齐
struct Data { /* 成员定义 */ };
#pragma pack(pop)
5.2 创建并绑定UBO
GLuint ubo;
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, sizeof(MatricesData), &matricesData, GL_DYNAMIC_DRAW);
5.3 将UBO绑定到特定索引,与着色器建立关联
GLuint blockIndex = glGetUniformBlockIndex(program, "Matrices"); // 获取Uniform Block的位置
glUniformBlockBinding(program, blockIndex, 0); // 绑定到索引0,索引0是自定义的绑定点(类似插槽编号),后续操作都通过这个索引访问数据。
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo); // UBO绑定到索引0
5.4 动态更新数据
glMapBuffer会返回一个指针(ptr),这个指针直接指向UBO在GPU中的内存。glMapBuffer的操作目标由当前绑定的缓冲区类型决定,而非通过索引指定。当调用glMapBuffer时,OpenGL会直接映射当前通过glBindBuffer绑定的缓冲区对象。例如:
void* ptr = glMapBuffer(GL_UNIFORM_BUFFER, GL_WRITE_ONLY);
memcpy(ptr, &newData, sizeof(newData));
glUnmapBuffer(GL_UNIFORM_BUFFER); //告诉OpenGL:“我改完数据了,你可以收回指针权限了。”解除后,ptr指针会失效,不能再使用