跳到主要内容

着色器基础

着色器是运行在GPU上的小程序,用于控制渲染管线的可编程阶段。GLSL(OpenGL Shading Language)是OpenGL的着色器语言,语法类似C语言,但专门为图形计算设计。

GLSL基础语法

数据类型

GLSL提供了丰富的数据类型:

标量类型

int intValue = 42;        // 整数
float floatValue = 3.14; // 浮点数
bool boolValue = true; // 布尔值
uint uintValue = 42u; // 无符号整数
double doubleValue = 3.14159265359; // 双精度浮点

向量类型

vec2 v2 = vec2(1.0, 2.0);           // 2分量浮点向量
vec3 v3 = vec3(1.0, 2.0, 3.0); // 3分量浮点向量
vec4 v4 = vec4(1.0, 2.0, 3.0, 4.0); // 4分量浮点向量

ivec2 iv2 = ivec2(1, 2); // 2分量整数向量
bvec3 bv3 = bvec3(true, false, true); // 3分量布尔向量

向量分量可以通过多种方式访问:

vec4 color = vec4(1.0, 0.5, 0.0, 1.0);

// 使用xyzw(位置)
float x = color.x;
float y = color.y;

// 使用rgba(颜色)
float r = color.r;
float g = color.g;

// 使用stpq(纹理坐标)
float s = color.s;
float t = color.t;

// 重组(Swizzling)
vec3 rgb = color.rgb; // 取rgb分量
vec2 xy = color.xy; // 取xy分量
vec4 newColor = color.abgr; // 反转分量顺序

矩阵类型

mat2 m2 = mat2(1.0);  // 2x2单位矩阵
mat3 m3 = mat3(1.0); // 3x3单位矩阵
mat4 m4 = mat4(1.0); // 4x4单位矩阵

// 自定义矩阵
mat4 transform = mat4(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
1.0, 2.0, 3.0, 1.0
);

采样器类型

sampler1D   // 1D纹理
sampler2D // 2D纹理
sampler3D // 3D纹理
samplerCube // 立方体贴图

运算符

GLSL支持常见的运算符:

// 算术运算
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);
vec3 c = a + b; // (5.0, 7.0, 9.0)
vec3 d = a * 2.0; // (2.0, 4.0, 6.0)
vec3 e = a * b; // 分量相乘 (4.0, 10.0, 18.0)

// 矩阵运算
mat4 m = mat4(1.0);
vec4 v = vec4(1.0, 0.0, 0.0, 1.0);
vec4 result = m * v; // 矩阵乘向量

内置函数

GLSL提供了大量内置函数:

// 数学函数
float x = abs(-1.0); // 绝对值:1.0
float y = sqrt(4.0); // 平方根:2.0
float z = pow(2.0, 3.0); // 幂运算:8.0
float w = sin(3.14159); // 正弦函数

// 常用函数
float minVal = min(1.0, 2.0); // 最小值:1.0
float maxVal = max(1.0, 2.0); // 最大值:2.0
float clamped = clamp(1.5, 0.0, 1.0); // 限制范围:1.0
float mixed = mix(0.0, 1.0, 0.5); // 线性插值:0.5

// 向量函数
vec3 v1 = vec3(1.0, 0.0, 0.0);
vec3 v2 = vec3(0.0, 1.0, 0.0);
float d = dot(v1, v2); // 点积:0.0
vec3 crossProduct = cross(v1, v2); // 叉积:(0,0,1)
float len = length(v1); // 向量长度:1.0
vec3 normalized = normalize(v1); // 归一化

变量存储限定符

GLSL使用限定符来指定变量的用途和作用域:

in / out

inout 用于着色器之间的数据传递:

// 顶点着色器
#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;
}
// 片段着色器
#version 330 core
in vec3 vertexColor; // 从顶点着色器接收

out vec4 FragColor; // 输出:最终颜色

void main() {
FragColor = vec4(vertexColor, 1.0);
}

uniform

uniform 用于存储全局数据,在整个绘制调用中保持不变:

// 顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}

在C++中设置uniform值:

// 获取uniform位置
int modelLoc = glGetUniformLocation(shaderProgram, "model");

// 设置uniform值
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 0.0f, 1.0f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

const

const 用于声明常量:

const float PI = 3.14159265359;
const vec3 LIGHT_DIR = vec3(0.0, 1.0, 0.0);

顶点着色器

顶点着色器处理每个顶点,主要任务是变换顶点坐标。

基本结构

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
// 计算片段在世界空间中的位置
FragPos = vec3(model * vec4(aPos, 1.0));

// 变换法线(注意使用法线矩阵)
Normal = mat3(transpose(inverse(model))) * aNormal;

// 传递纹理坐标
TexCoord = aTexCoord;

// 计算裁剪空间坐标
gl_Position = projection * view * vec4(FragPos, 1.0);
}

内置输入变量

顶点着色器有以下内置输入:

  • gl_VertexID:当前顶点的索引
  • gl_InstanceID:当前实例的索引(用于实例化渲染)

内置输出变量

  • gl_Position:裁剪空间中的顶点位置(必须设置)
  • gl_PointSize:点的大小(绘制点时使用)

片段着色器

片段着色器计算每个片段的颜色。

基本结构

#version 330 core

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

out vec4 FragColor;

uniform sampler2D diffuseMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;

void main() {
// 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

// 镜面反射
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

// 最终颜色
vec3 result = (ambient + diffuse + specular) * texture(diffuseMap, TexCoord).rgb;
FragColor = vec4(result, 1.0);
}

内置输入变量

  • gl_FragCoord:片段的窗口坐标
  • gl_FrontFacing:是否为正面
  • gl_PointCoord:点精灵的纹理坐标

内置输出变量

  • gl_FragDepth:自定义深度值

着色器编译

在C++中,着色器需要编译和链接才能使用。

编译着色器

// 顶点着色器源码
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
)";

// 创建并编译顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

// 检查编译错误
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cerr << "Vertex shader compilation failed:\n" << infoLog << std::endl;
}

// 同样处理片段着色器
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

链接着色器程序

// 创建程序对象
GLuint shaderProgram = glCreateProgram();

// 附加着色器
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);

// 链接程序
glLinkProgram(shaderProgram);

// 检查链接错误
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cerr << "Shader program linking failed:\n" << infoLog << std::endl;
}

// 删除着色器对象(已链接到程序中)
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

使用着色器程序

glUseProgram(shaderProgram);

封装着色器类

为了方便使用,可以封装一个Shader类:

// Shader.h
#pragma once

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader {
public:
GLuint ID;

Shader(const char* vertexPath, const char* fragmentPath) {
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;

vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

try {
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;

vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();

vShaderFile.close();
fShaderFile.close();

vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
} catch (std::ifstream::failure& e) {
std::cerr << "Shader file read error: " << e.what() << std::endl;
}

const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();

GLuint vertex, fragment;

vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");

fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");

ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");

glDeleteShader(vertex);
glDeleteShader(fragment);
}

void use() {
glUseProgram(ID);
}

void setBool(const std::string& name, bool value) const {
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}

void setInt(const std::string& name, int value) const {
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void setFloat(const std::string& name, float value) const {
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

void setVec3(const std::string& name, const glm::vec3& value) const {
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}

void setMat4(const std::string& name, const glm::mat4& mat) const {
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}

private:
void checkCompileErrors(GLuint shader, std::string type) {
GLint success;
GLchar infoLog[1024];
if (type != "PROGRAM") {
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cerr << "Shader compilation error: " << type << "\n" << infoLog << std::endl;
}
} else {
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cerr << "Program linking error: " << type << "\n" << infoLog << std::endl;
}
}
}
};

使用示例:

Shader ourShader("vertex.glsl", "fragment.glsl");

ourShader.use();
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
ourShader.setVec3("lightPos", lightPos);

从文件加载着色器

将着色器代码放在单独的文件中更便于管理:

vertex.glsl

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = aTexCoord;
}

fragment.glsl

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;

void main() {
FragColor = texture(texture1, TexCoord);
}

调试着色器

着色器调试比普通程序更困难,因为它们运行在GPU上。以下是一些调试技巧:

1. 输出中间值

// 将中间值作为颜色输出进行可视化
FragColor = vec4(normalize(Normal), 1.0); // 可视化法线
FragColor = vec4(vec3(gl_FragCoord.z), 1.0); // 可视化深度

2. 使用着色器编译日志

GLint success;
GLchar infoLog[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(shader, 512, NULL, infoLog);
std::cout << "Error: " << infoLog << std::endl;
}

3. 使用图形调试工具

RenderDoc、NVIDIA Nsight等工具可以捕获帧并检查着色器变量值。

小结

着色器是OpenGL编程的核心:

  • GLSL是专门为图形计算设计的语言
  • 顶点着色器处理顶点变换
  • 片段着色器计算像素颜色
  • uniform用于传递全局数据
  • in/out用于着色器间通信

下一章我们将学习 顶点缓冲对象,了解如何高效管理顶点数据。

着色器开发建议
  1. 始终检查编译和链接错误
  2. 使用有意义的变量名
  3. 添加注释说明计算逻辑
  4. 将着色器代码放在单独文件中便于管理