需要预先学习:


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