着色器基础
着色器是运行在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
in 和 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;
}
// 片段着色器
#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用于着色器间通信
下一章我们将学习 顶点缓冲对象,了解如何高效管理顶点数据。
着色器开发建议
- 始终检查编译和链接错误
- 使用有意义的变量名
- 添加注释说明计算逻辑
- 将着色器代码放在单独文件中便于管理