着色器
需要预先学习:
1. GLSL 语言与着色器
着色器(shader)是运行在 GPU 上的小程序,它接管渲染管线中某些阶段的处理工作。在前一篇笔记中,你把顶点数据放进了 VBO,但 GPU 还不知道怎么处理这些数据——着色器就是告诉 GPU “该做什么” 的程序。
OpenGL 的着色器使用 GLSL(OpenGL Shading Language)编写,它是一种类 C 语言的着色器语言。一个着色器程序的入口点是 main 函数,和 C++ 一样。
1.1 数据传递
着色器之间通过特定的变量关键字来传递数据:
in:从上一个阶段接收数据out:向下一个阶段发送数据uniform:从 CPU 端传入的全局变量,对所有顶点或片元都相同
1.2 数据类型
GLSL 有 C 语言风格的类型系统,并提供了面向图形的内置类型:
| 类别 | 类型 |
|---|---|
| 基础类型 | int、float、bool、double |
| 向量 | vec2、vec3、vec4(float 向量)、ivec2~ivec4(int 向量)、bvec2~bvec4(bool 向量) |
| 矩阵 | mat2、mat3、mat4 |
向量支持分量选择(swizzling),可以用点号加分量名来访问或重新组合向量的部分元素:
vec3 color = vec3(1.0, 0.5, 0.2);
float r = color.r; // 等价于 color.x
vec2 rg = color.rg; // vec2(1.0, 0.5)
color.bgr = color.rgb; // 交换 r 和 b
分量名有两套等价的写法:x, y, z, w 和 r, g, b, a,前者常用于位置,后者常用于颜色。
1.3 简单示例
下面是一个最基本的顶点着色器和片段着色器,直接以 C++ 原始字符串字面量的形式硬编码在代码中:
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos, 1.0);
}
)";
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";
注:
R"(...)"是 C++11 的原始字符串字面量(raw string literal),反斜杠和引号不需要转义,非常适合嵌入着色器代码。#version 330 core声明了使用 GLSL 3.30 版本的核心模式。
2. 顶点着色器与片段着色器
2.1 顶点着色器
顶点着色器(vertex shader)对每个顶点运行一次。它的核心职责是确定顶点在屏幕上的最终位置,通过设置内置变量 gl_Position 来完成。
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos, 1.0);
}
layout (location = 0)指定这个属性的 location 为 0,与 C++ 端glVertexAttribPointer(0, ...)的第一个参数对应in vec3 aPos从 VBO 中接收 3 个 float 分量的顶点数据vec4(aPos, 1.0)把vec3扩展为齐次坐标(homogeneous coordinates)vec4,第四个分量w = 1.0表示这是一个点而非方向
顶点着色器还可以通过 out 变量把数据传递给片段着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 vertexColor; // 传递给片段着色器
void main()
{
gl_Position = vec4(aPos, 1.0);
vertexColor = aColor;
}
2.2 片段着色器
片段着色器(fragment shader)对每个片元(fragment,光栅化后产生的候选像素)运行一次。它的核心职责是计算片元的最终颜色。
#version 330 core
in vec3 vertexColor; // 从顶点着色器接收
out vec4 FragColor; // 输出颜色
void main()
{
FragColor = vec4(vertexColor, 1.0);
}
out vec4 FragColor声明一个输出变量,它的值会写入帧缓冲区- 从顶点着色器传来的
vertexColor在光栅化阶段会被插值(interpolation),所以相邻片元的颜色是平滑过渡的
注:在缓冲对象与顶点中提到过,顶点数据对 GPU 来说只是一串
float,它本身并不知道哪些数字代表位置、哪些代表颜色。着色器中的in变量配合layout(location)才真正赋予了这些数据语义——location 0 是位置、location 1 是颜色,这完全由你的着色器代码决定。
2.3 编译着色器
写好 GLSL 源码后,需要把它编译成着色器对象。流程是:创建 → 附加源码 → 编译 → 检查错误。
#include <iostream>
// 编译顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
// 检查编译是否成功
GLint success;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
char infoLog[512];
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
std::cout << "顶点着色器编译失败: " << infoLog << std::endl;
}
// 编译片段着色器(流程相同)
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
glCreateShader 的参数指定着色器类型:GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER。glShaderSource 把 C++ 字符串传递给着色器对象,第二个参数 1 表示只有一段源码。
注意:着色器编译失败不会导致 C++ 程序崩溃,但渲染结果会不正确。养成检查编译状态的习惯可以节省大量调试时间。
2.4 着色器程序
单个着色器不能独立工作,你需要把顶点着色器和片段着色器链接(link)到一个着色器程序(shader program)中。着色器程序是最终在绘制时使用的对象。
// 创建着色器程序
GLuint shaderProgram = glCreateProgram();
// 附加着色器
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
// 链接
glLinkProgram(shaderProgram);
// 检查链接是否成功
GLint success;
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
char infoLog[512];
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
std::cout << "着色器程序链接失败: " << infoLog << std::endl;
}
// 链接成功后,着色器对象已经不再需要了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
之后在绘制循环中使用这个程序:
glUseProgram(shaderProgram); // 激活着色器程序
// 绑定 VAO 并绘制
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
注:着色器链接到程序之后,各个着色器对象就可以删除了——它们的代码已经被复制到了程序内部。
glUseProgram必须在绘制调用之前执行,之后的绘制都会使用这个着色器程序,直到你切换到另一个程序。
3. 着色器的 C++ 封装
// Shader.hpp
#pragma once
#include <string>
#include <glad/glad.h>
#include <unordered_map>
namespace MY::OpenGL
{
class Shader
{
public:
Shader() = delete;
Shader(const std::string& path);
~Shader();
GLuint get_shader_id() const { return m_shader_id; }
private:
GLuint m_shader_id = 0;
};
class ShaderProgram
{
public:
ShaderProgram() = delete;
~ShaderProgram();
template<typename... Shaders>
ShaderProgram(Shaders&&... shaders)
{
(attach(shaders), ...);
link();
}
// 禁止拷贝
ShaderProgram(const ShaderProgram&) = delete;
ShaderProgram& operator=(const ShaderProgram&) = delete;
// 允许移动
ShaderProgram(ShaderProgram&& other) noexcept;
ShaderProgram& operator=(ShaderProgram&& other) noexcept;
public:
GLuint get_program_id() const { return m_program_id; }
/*
* 获取 uniform 变量的位置
* 首次查询会调用 glGetUniformLocation 并缓存结果,后续查询会直接从缓存中获取
*/
GLint get_uniform_location(const std::string& name) const;
private:
GLuint m_program_id = 0;
mutable std::unordered_map<std::string, GLint> m_uniform_location_cache;
/**
* @brief 将一个 Shader 对象附加到当前 ShaderProgram 对象上
*/
void attach(const Shader& shader);
/**
* @brief 链接当前 ShaderProgram 对象
*/
void link();
};
}
// Shader.cpp
#include "Shader.hpp"
#include <fstream>
#include <sstream>
#include <iostream>
#include <unordered_map>
namespace MY::OpenGL
{
namespace
{
/**
* @brief 根据着色器文件的扩展名推断着色器类型
*/
static GLenum infer_shader_type(const std::string& path)
{
static const std::unordered_map<std::string, GLenum> ext_map = {
{ ".vs", GL_VERTEX_SHADER },
{ ".vert", GL_VERTEX_SHADER },
{ ".fs", GL_FRAGMENT_SHADER },
{ ".frag", GL_FRAGMENT_SHADER },
{ ".gs", GL_GEOMETRY_SHADER },
{ ".geom", GL_GEOMETRY_SHADER },
{ ".cs", GL_COMPUTE_SHADER },
{ ".comp", GL_COMPUTE_SHADER },
{ ".tcs", GL_TESS_CONTROL_SHADER },
{ ".tes", GL_TESS_EVALUATION_SHADER },
};
auto dot = path.rfind('.');
if (dot == std::string::npos)
return GL_NONE;
std::string ext = path.substr(dot);
auto it = ext_map.find(ext);
return (it != ext_map.end()) ? it->second : GL_NONE;
}
/**
* @brief 读取文件内容到字符串
*/
static std::string read_file(const std::string& path)
{
std::ifstream ifs(path, std::ios::in | std::ios::binary);
if (!ifs)
return {};
std::ostringstream oss;
oss << ifs.rdbuf();
return oss.str();
}
} // 匿名命名空间
Shader::Shader(const std::string& path)
{
GLenum type = infer_shader_type(path);
if (type == GL_NONE)
{
std::cerr << "[Shader] 无法识别的着色器文件扩展名: " << path << std::endl;
return;
}
std::string source = read_file(path);
if (source.empty())
{
std::cerr << "[Shader] 读取着色器文件失败或文件为空: " << path << std::endl;
return;
}
const char* src_ptr = source.c_str();
m_shader_id = glCreateShader(type);
glShaderSource(m_shader_id, 1, &src_ptr, nullptr);
glCompileShader(m_shader_id);
// 检查编译错误
GLint success = 0;
glGetShaderiv(m_shader_id, GL_COMPILE_STATUS, &success);
if (!success)
{
GLchar info_log[512];
glGetShaderInfoLog(m_shader_id, sizeof(info_log), nullptr, info_log);
std::cerr << "[Shader] 编译失败 (" << path << "):\n" << info_log << std::endl;
glDeleteShader(m_shader_id);
m_shader_id = 0;
}
}
Shader::~Shader()
{
glDeleteShader(m_shader_id);
}
ShaderProgram::~ShaderProgram()
{
glDeleteProgram(m_program_id);
}
ShaderProgram::ShaderProgram(ShaderProgram&& other) noexcept
: m_program_id(other.m_program_id),
m_uniform_location_cache(std::move(other.m_uniform_location_cache))
{
other.m_program_id = 0;
other.m_uniform_location_cache.clear();
}
ShaderProgram& ShaderProgram::operator=(ShaderProgram&& other) noexcept
{
if (this != &other)
{
glDeleteProgram(m_program_id);
m_program_id = other.m_program_id;
m_uniform_location_cache = std::move(other.m_uniform_location_cache);
other.m_program_id = 0;
other.m_uniform_location_cache.clear();
}
return *this;
}
void ShaderProgram::attach(const Shader& shader)
{
glAttachShader(m_program_id, shader.get_shader_id());
}
void ShaderProgram::link()
{
glLinkProgram(m_program_id);
GLint success = 0;
glGetProgramiv(m_program_id, GL_LINK_STATUS, &success);
if (!success)
{
GLchar info_log[512];
glGetProgramInfoLog(m_program_id, sizeof(info_log), nullptr, info_log);
std::cerr << "[ShaderProgram] 链接失败:\n" << info_log << std::endl;
}
}
GLint ShaderProgram::get_uniform_location(const std::string& name) const
{
auto it = m_uniform_location_cache.find(name);
if (it != m_uniform_location_cache.end())
return it->second;
GLint location = glGetUniformLocation(m_program_id, name.c_str());
m_uniform_location_cache[name] = location;
return location;
}
} // namespace MY::OpenGL



